From 932156c4b31a09b438f899180ddb467de1a2a316 Mon Sep 17 00:00:00 2001 From: satan Date: Wed, 29 Mar 2023 09:23:10 +0200 Subject: [PATCH 001/151] Drafting --- core/Cargo.toml | 3 + core/src/ledger/governance/parameters.rs | 2 +- core/src/ledger/ibc/actions.rs | 6 +- core/src/ledger/parameters/mod.rs | 2 +- core/src/ledger/storage/masp_conversions.rs | 2 +- core/src/proto/types.rs | 2 +- core/src/types/governance.rs | 6 +- core/src/types/mod.rs | 1 + core/src/types/token.rs | 352 +++++++++++++------- core/src/types/uint.rs | 119 +++++++ 10 files changed, 364 insertions(+), 131 deletions(-) create mode 100644 core/src/types/uint.rs diff --git a/core/Cargo.toml b/core/Cargo.toml index 2519cafa87..32ab5a970d 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -68,6 +68,7 @@ chrono = {version = "0.4.22", default-features = false, features = ["clock", "st data-encoding = "2.3.2" derivative = "2.2.0" ed25519-consensus = "1.2.0" +ethabi = "18.0.0" ferveo = {optional = true, git = "https://github.com/anoma/ferveo"} ferveo-common = {git = "https://github.com/anoma/ferveo"} tpke = {package = "group-threshold-cryptography", optional = true, git = "https://github.com/anoma/ferveo"} @@ -77,6 +78,7 @@ ibc-proto = {version = "0.17.1", default-features = false, optional = true} ibc-abcipp = {package = "ibc", git = "https://github.com/heliaxdev/ibc-rs", rev = "9fcc1c8c19db6af50806ffe5b2f6c214adcbfd5d", default-features = false, optional = true} ibc-proto-abcipp = {package = "ibc-proto", git = "https://github.com/heliaxdev/ibc-rs", rev = "9fcc1c8c19db6af50806ffe5b2f6c214adcbfd5d", default-features = false, optional = true} ics23 = "0.7.0" +impl-num-traits = "0.1.2" index-set = {git = "https://github.com/heliaxdev/index-set", tag = "v0.7.1", features = ["serialize-borsh", "serialize-serde"]} itertools = "0.10.0" libsecp256k1 = {git = "https://github.com/heliaxdev/libsecp256k1", rev = "bbb3bd44a49db361f21d9db80f9a087c194c0ae9", default-features = false, features = ["std", "static-context"]} @@ -99,6 +101,7 @@ tendermint-proto-abcipp = {package = "tendermint-proto", git = "https://github.c thiserror = "1.0.30" tracing = "0.1.30" zeroize = {version = "1.5.5", features = ["zeroize_derive"]} +uint = "0.9.5" [dev-dependencies] namada_tests = {path = "../tests", default-features = false, features = ["wasm-runtime"]} diff --git a/core/src/ledger/governance/parameters.rs b/core/src/ledger/governance/parameters.rs index 9ae820d96c..2d247bc24f 100644 --- a/core/src/ledger/governance/parameters.rs +++ b/core/src/ledger/governance/parameters.rs @@ -79,7 +79,7 @@ impl GovParams { } = self; let min_proposal_fund_key = gov_storage::get_min_proposal_fund_key(); - let amount = Amount::whole(*min_proposal_fund); + let amount = Amount::native_whole(*min_proposal_fund); storage.write(&min_proposal_fund_key, amount)?; let max_proposal_code_size_key = diff --git a/core/src/ledger/ibc/actions.rs b/core/src/ledger/ibc/actions.rs index 4e09f269c2..e925a98d92 100644 --- a/core/src/ledger/ibc/actions.rs +++ b/core/src/ledger/ibc/actions.rs @@ -956,7 +956,7 @@ pub trait IbcActions { data.denom = denom.to_string(); } let token = storage::token(&data.denom).map_err(Error::IbcStorage)?; - let amount = Amount::from_str(&data.amount).map_err(|e| { + let amount = Amount::from_str(&data.amount, 0).map_err(|e| { Error::SendingToken(format!( "Invalid amount: amount {}, error {}", data.amount, e @@ -1037,7 +1037,7 @@ pub trait IbcActions { data: &FungibleTokenPacketData, ) -> std::result::Result<(), Self::Error> { let token = storage::token(&data.denom).map_err(Error::IbcStorage)?; - let amount = Amount::from_str(&data.amount).map_err(|e| { + let amount = Amount::from_str(&data.amount, 0).map_err(|e| { Error::ReceivingToken(format!( "Invalid amount: amount {}, error {}", data.amount, e @@ -1122,7 +1122,7 @@ pub trait IbcActions { data: &FungibleTokenPacketData, ) -> std::result::Result<(), Self::Error> { let token = storage::token(&data.denom).map_err(Error::IbcStorage)?; - let amount = Amount::from_str(&data.amount).map_err(|e| { + let amount = Amount::from_str(&data.amount, 0).map_err(|e| { Error::ReceivingToken(format!( "Invalid amount: amount {}, error {}", data.amount, e diff --git a/core/src/ledger/parameters/mod.rs b/core/src/ledger/parameters/mod.rs index 442a614d68..6b1ff7adb4 100644 --- a/core/src/ledger/parameters/mod.rs +++ b/core/src/ledger/parameters/mod.rs @@ -187,7 +187,7 @@ impl Parameters { { let wrapper_tx_fees_key = storage::get_wrapper_tx_fees_key(); let wrapper_tx_fees = - wrapper_tx_fees.unwrap_or(token::Amount::whole(100)); + wrapper_tx_fees.unwrap_or(token::Amount::native_whole(100)); storage.write(&wrapper_tx_fees_key, wrapper_tx_fees)?; } Ok(()) diff --git a/core/src/ledger/storage/masp_conversions.rs b/core/src/ledger/storage/masp_conversions.rs index 0834d7bb53..7b5e788801 100644 --- a/core/src/ledger/storage/masp_conversions.rs +++ b/core/src/ledger/storage/masp_conversions.rs @@ -49,7 +49,7 @@ where let masp_rewards = address::masp_rewards(); // The total transparent value of the rewards being distributed - let mut total_reward = token::Amount::from(0); + let mut total_reward = token::Amount::native_whole(0); // Construct MASP asset type for rewards. Always timestamp reward tokens // with the zeroth epoch to minimize the number of convert notes clients diff --git a/core/src/proto/types.rs b/core/src/proto/types.rs index 40e343d1bf..cd5f73dc2e 100644 --- a/core/src/proto/types.rs +++ b/core/src/proto/types.rs @@ -321,7 +321,7 @@ impl From for ResponseDeliverTx { EventAttribute { key: encode_str("amount"), value: encode_string( - transfer.amount.to_string(), + transfer.amount.to_string_precise(), ), index: true, }, diff --git a/core/src/types/governance.rs b/core/src/types/governance.rs index 438017a370..d58f4b489d 100644 --- a/core/src/types/governance.rs +++ b/core/src/types/governance.rs @@ -14,7 +14,7 @@ use crate::types::hash::Hash; use crate::types::key::common::{self, Signature}; use crate::types::key::SigScheme; use crate::types::storage::Epoch; -use crate::types::token::SCALE; +use crate::types::token::NATIVE_SCALE; /// Type alias for vote power pub type VotePower = u128; @@ -111,8 +111,8 @@ impl Display for ProposalResult { f, "{} with {} yay votes over {} ({:.2}%)", self.result, - self.total_yay_power / SCALE as u128, - self.total_voting_power / SCALE as u128, + self.total_yay_power / NATIVE_SCALE as u128, + self.total_voting_power / NATIVE_SCALE as u128, percentage.checked_mul(100.into()).unwrap_or_default() ) } diff --git a/core/src/types/mod.rs b/core/src/types/mod.rs index 0550060498..b414efc89a 100644 --- a/core/src/types/mod.rs +++ b/core/src/types/mod.rs @@ -12,4 +12,5 @@ pub mod storage; pub mod time; pub mod token; pub mod transaction; +pub mod uint; pub mod validity_predicate; diff --git a/core/src/types/token.rs b/core/src/types/token.rs index d95d944b8c..4ef2f3677b 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -1,17 +1,18 @@ //! A basic fungible token -use std::fmt::Display; +//use std::fmt::Display; use std::ops::{Add, AddAssign, Mul, Sub, SubAssign}; use std::str::FromStr; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use masp_primitives::transaction::Transaction; -use rust_decimal::prelude::{Decimal, ToPrimitive}; +use rust_decimal::prelude::Decimal; use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::types::address::{masp, Address, DecodeError as AddressError}; use crate::types::storage::{DbKeySeg, Key, KeySeg}; +use crate::types::uint::{self, SignedUint, Uint}; /// Amount in micro units. For different granularity another representation /// might be more appropriate. @@ -30,77 +31,219 @@ use crate::types::storage::{DbKeySeg, Key, KeySeg}; Hash, )] pub struct Amount { - micro: u64, + raw: Uint, } -/// Maximum decimal places in a token [`Amount`] and [`Change`]. -pub const MAX_DECIMAL_PLACES: u32 = 6; -/// Decimal scale of token [`Amount`] and [`Change`]. -pub const SCALE: u64 = 1_000_000; +/// A number of decimal places for a token [`Amount`]. +pub type Denom = u8; -/// The largest value that can be represented by this integer type -pub const MAX_AMOUNT: Amount = Amount { micro: u64::MAX }; +/// Maximum decimal places in a native token [`Amount`] and [`Change`]. +/// For non-native (e.g. ERC20 tokens) one must read the `denom_key` storage +/// key. +pub const NATIVE_MAX_DECIMAL_PLACES: u8 = 6; -/// A change in tokens amount -pub type Change = i128; +/// Decimal scale of a native token [`Amount`] and [`Change`]. +/// For non-native (e.g. ERC20 tokens) one must read the `denom_key` storage +/// key. +pub const NATIVE_SCALE: u64 = 1_000_000; + +pub type Change = SignedUint; impl Amount { /// Get the amount as a [`Change`] pub fn change(&self) -> Change { - self.micro as Change + self.raw.try_into().unwrap() } /// Spend a given amount. - /// Panics when given `amount` > `self.micro` amount. + /// Panics when given `amount` > `self.raw` amount. pub fn spend(&mut self, amount: &Amount) { - self.micro = self.micro.checked_sub(amount.micro).unwrap(); + self.raw = self.raw.checked_sub(amount.raw).unwrap(); } /// Receive a given amount. - /// Panics on overflow. + /// Panics on overflow and when [`uint::MAX_VALUE`] is exceeded. pub fn receive(&mut self, amount: &Amount) { - self.micro = self.micro.checked_add(amount.micro).unwrap(); + self.raw = self.raw.checked_add(amount.raw).unwrap(); } - /// Create a new amount from whole number of tokens - pub const fn whole(amount: u64) -> Self { + /// Create a new amount of native token from whole number of tokens + pub fn native_whole(amount: u64) -> Self { Self { - micro: amount * SCALE, + raw: Uint::from(amount) * NATIVE_SCALE, } } /// Create a new amount with the maximum value pub fn max() -> Self { - Self { micro: u64::MAX } + Self { + raw: uint::MAX_VALUE, + } } - /// Checked addition. Returns `None` on overflow. + /// Checked addition. Returns `None` on overflow or if + /// the amount exceed [`uint::MAX_VALUE`] pub fn checked_add(&self, amount: Amount) -> Option { - self.micro - .checked_add(amount.micro) - .map(|result| Self { micro: result }) + self.raw.checked_add(amount.raw).and_then(|result| { + if result < uint::MAX_VALUE { + Some(Self { raw: result }) + } else { + None + } + }) } /// Checked subtraction. Returns `None` on underflow pub fn checked_sub(&self, amount: Amount) -> Option { - self.micro - .checked_sub(amount.micro) - .map(|result| Self { micro: result }) + self.raw + .checked_sub(amount.raw) + .map(|result| Self { raw: result }) } - /// Create amount from Change - /// - /// # Panics - /// - /// Panics if the change is negative or overflows `u64`. + /// Create amount from the absolute value of `Change`. pub fn from_change(change: Change) -> Self { - Self { - micro: change as u64, + Self { raw: change.abs() } + } + + /// Attempt to convert a `Decimal` to an `DenominatedAmount` with the + /// specified precision. + pub fn from_decimal( + decimal: Decimal, + denom: impl Into, + ) -> Result { + let denom = denom.into(); + if (denom as u32) < decimal.scale() { + Err(AmountParseError::ScaleTooLarge(decimal.scale(), denom)) + } else { + let value = Uint::from(decimal.mantissa().unsigned_abs()); + match Uint::from(10) + .checked_pow(Uint::from((denom as u32) - decimal.scale())) + .and_then(|scaling| scaling.checked_mul(value)) + { + Some(amount) => Ok(Self { raw: amount }), + None => Err(AmountParseError::ConvertToDecimal), + } + } + } + + /// Given a string and a denomination, parse an amount from string. + pub fn from_str( + string: impl AsRef, + denom: impl Into, + ) -> Result { + match Decimal::from_str(string.as_ref()) { + Ok(dec) => Ok(Self::from_decimal(dec, denom)?), + Err(err) => Err(AmountParseError::InvalidDecimal(err)), + } + } + + /// Attempt to convert a float to an `Erc20Amount` with the specified + /// precision. + pub fn from_float( + float: impl Into, + denom: impl Into, + ) -> Result { + match Decimal::try_from(float.into()) { + Err(e) => Err(AmountParseError::InvalidDecimal(e)), + Ok(decimal) => Self::from_decimal(decimal, denom), + } + } + + /// Attempt to convert an unsigned interger to an `Erc20Amount` with the + /// specified precision. + pub fn from_int( + uint: impl Into, + denom: impl Into, + ) -> Result { + Self::from_decimal(Decimal::try_from(uint.into()).unwrap(), denom) + } +} + +/// The number of decimal places in base 10 of an amount. +#[derive( + Debug, + Copy, + Clone, + Hash, + PartialEq, + Eq, + PartialOrd, + Ord, + BorshSerialize, + BorshDeserialize, + BorshSchema, +)] +pub struct Denomination(pub u8); + +impl From for Denomination { + fn from(denom: u8) -> Self { + Self(denom) + } +} + +impl From for u8 { + fn from(denom: Denomination) -> Self { + denom.0 + } +} + +/// An amount with its denomination. +#[derive( + Debug, + Copy, + Clone, + Hash, + PartialEq, + Eq, + PartialOrd, + Ord, + BorshSerialize, + BorshDeserialize, + BorshSchema, +)] +pub struct DenominatedAmount { + /// The mantissa + pub amount: Amount, + /// The number of decimal plces in base ten. + pub denom: Denomination, +} + +impl DenominatedAmount { + /// A precise string representation. The number of + /// decimal places in this string gives the denomination. + /// This not true of the string produced by the `Display` + /// trait. + pub fn to_string_precise(&self) -> String { + let decimals = self.denom.0 as usize; + let mut string = self.amount.raw.to_string(); + if string.len() > decimals { + string.insert(string.len() - decimals, '.'); + } else { + for _ in string.len()..decimals { + string.insert(0, '0'); + } + string.insert(0, '.'); + string.insert(0, '0'); } + string + } +} + +impl FromStr for DenominatedAmount { + type Err = AmountParseError; + + fn from_str(s: &str) -> Result { + let decimal = Decimal::from_str(s) + .or_else(|err| Err(AmountParseError::InvalidDecimal(err)))?; + let denom = Denomination(decimal.scale() as u8); + Ok(Self { + amount: Amount::from_decimal(decimal, denom)?, + denom, + }) } } -impl serde::Serialize for Amount { +impl serde::Serialize for DenominatedAmount { fn serialize( &self, serializer: S, @@ -108,12 +251,12 @@ impl serde::Serialize for Amount { where S: serde::Serializer, { - let amount_string = self.to_string(); + let amount_string = self.to_string_precise(); serde::Serialize::serialize(&amount_string, serializer) } } -impl<'de> serde::Deserialize<'de> for Amount { +impl<'de> serde::Deserialize<'de> for DenominatedAmount { fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, @@ -125,34 +268,22 @@ impl<'de> serde::Deserialize<'de> for Amount { } } -impl From for Decimal { - fn from(amount: Amount) -> Self { - Into::::into(amount.micro) / Into::::into(SCALE) - } -} - -impl From for Amount { - fn from(micro: Decimal) -> Self { - let res = (micro * Into::::into(SCALE)).to_u64().unwrap(); - Self { micro: res } - } -} +impl TryFrom for Decimal { + type Error = AmountParseError; -impl From for Amount { - fn from(micro: u64) -> Self { - Self { micro } - } -} - -impl From for u64 { - fn from(amount: Amount) -> Self { - amount.micro + fn try_from(amount: Amount) -> Result { + if amount.raw > Uint([u64::MAX, u64::MAX, 0, 0]) { + Err(AmountParseError::ConvertToDecimal) + } else { + Ok(Into::::into(amount.raw.as_u128()) + / Into::::into(NATIVE_SCALE)) + } } } impl From for u128 { fn from(amount: Amount) -> Self { - u128::from(amount.micro) + amount.raw.as_u128() } } @@ -160,7 +291,7 @@ impl Add for Amount { type Output = Amount; fn add(mut self, rhs: Self) -> Self::Output { - self.micro += rhs.micro; + self.raw += rhs.raw; self } } @@ -169,35 +300,28 @@ impl Mul for Amount { type Output = Amount; fn mul(mut self, rhs: u64) -> Self::Output { - self.micro *= rhs; + self.raw *= rhs; self } } /// A combination of Euclidean division and fractions: -/// x*(a,b) = (a*(x//b), x%b) +/// x*(a,b) = (a*(x//b), x%b). impl Mul<(u64, u64)> for Amount { type Output = (Amount, Amount); fn mul(mut self, rhs: (u64, u64)) -> Self::Output { - let ant = Amount::from((self.micro / rhs.1) * rhs.0); - self.micro %= rhs.1; - (ant, self) - } -} - -impl Mul for u64 { - type Output = Amount; - - fn mul(mut self, rhs: Amount) -> Self::Output { - self *= rhs.micro; - Self::Output::from(self) + let amt = Amount { + raw: (self.raw / rhs.1) * rhs.0, + }; + self.raw %= rhs.1; + (amt, self) } } impl AddAssign for Amount { fn add_assign(&mut self, rhs: Self) { - self.micro += rhs.micro + self.raw += rhs.raw } } @@ -205,14 +329,14 @@ impl Sub for Amount { type Output = Amount; fn sub(mut self, rhs: Self) -> Self::Output { - self.micro -= rhs.micro; + self.raw -= rhs.raw; self } } impl SubAssign for Amount { fn sub_assign(&mut self, rhs: Self) { - self.micro -= rhs.micro + self.raw -= rhs.raw } } @@ -221,16 +345,17 @@ impl KeySeg for Amount { where Self: Sized, { - let micro = u64::parse(string)?; - Ok(Self { micro }) + let raw = Uint::from_str(&string) + .map_err(|e| super::storage::Error::InvalidKeySeg(e.to_string()))?; + Ok(Self { raw }) } fn raw(&self) -> String { - self.micro.raw() + self.raw.to_string() } fn to_db_key(&self) -> DbKeySeg { - self.micro.to_db_key() + DbKeySeg::StringSeg(self.raw()) } } @@ -241,49 +366,34 @@ pub enum AmountParseError { InvalidDecimal(rust_decimal::Error), #[error( "Error decoding token amount, too many decimal places: {0}. Maximum \ - {MAX_DECIMAL_PLACES}" + {1}" + )] + ScaleTooLarge(u32, u8), + #[error( + "Error decoding token amount, the value is not within invalid range." )] - ScaleTooLarge(u32), - #[error("Error decoding token amount, the value is within invalid range.")] InvalidRange, + #[error("Error converting amount to decimal, number too large.")] + ConvertToDecimal, } -impl FromStr for Amount { - type Err = AmountParseError; +// impl Display for DenominatedAmount { +// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +// let string = self.to_string_precise(); +// let string = string.trim_end_matches(&['0', '.']); +// f.write_str(string) +// } +// } - fn from_str(s: &str) -> Result { - match rust_decimal::Decimal::from_str(s) { - Ok(decimal) => { - let scale = decimal.scale(); - if scale > MAX_DECIMAL_PLACES { - return Err(AmountParseError::ScaleTooLarge(scale)); - } - let whole = - decimal * rust_decimal::Decimal::new(SCALE as i64, 0); - let micro: u64 = - rust_decimal::prelude::ToPrimitive::to_u64(&whole) - .ok_or(AmountParseError::InvalidRange)?; - Ok(Self { micro }) - } - Err(err) => Err(AmountParseError::InvalidDecimal(err)), - } - } -} - -impl Display for Amount { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let decimal = rust_decimal::Decimal::from_i128_with_scale( - self.micro as i128, - MAX_DECIMAL_PLACES, - ) - .normalize(); - write!(f, "{}", decimal) +impl From for Change { + fn from(amount: Amount) -> Self { + amount.raw.try_into().unwrap() } } -impl From for Change { - fn from(amount: Amount) -> Self { - amount.micro as i128 +impl From for Amount { + fn from(amt: DenominatedAmount) -> Self { + amt.amount } } @@ -439,7 +549,7 @@ pub struct Transfer { /// Source token's sub prefix pub sub_prefix: Option, /// The amount of tokens - pub amount: Amount, + pub amount: DenominatedAmount, /// The unused storage location at which to place TxId pub key: Option, /// Shielded transaction part @@ -473,7 +583,7 @@ impl TryFrom for Transfer { let token = Address::decode(token_str).map_err(TransferError::Address)?; let amount = - Amount::from_str(&data.amount).map_err(TransferError::Amount)?; + DenominatedAmount::from_str(&data.amount).map_err(TransferError::Amount)?; Ok(Self { source, target, @@ -561,12 +671,12 @@ pub mod testing { /// Generate an arbitrary token amount pub fn arb_amount() -> impl Strategy { - any::().prop_map(Amount::from) + any::().prop_map(Amount::native_whole) } /// Generate an arbitrary token amount up to and including given `max` value pub fn arb_amount_ceiled(max: u64) -> impl Strategy { - (0..=max).prop_map(Amount::from) + (0..=max).prop_map(Amount::native_whole) } /// Generate an arbitrary non-zero token amount up to and including given @@ -574,6 +684,6 @@ pub mod testing { pub fn arb_amount_non_zero_ceiled( max: u64, ) -> impl Strategy { - (1..=max).prop_map(Amount::from) + (1..=max).prop_map(Amount::native_whole) } } diff --git a/core/src/types/uint.rs b/core/src/types/uint.rs new file mode 100644 index 0000000000..0fbcc42061 --- /dev/null +++ b/core/src/types/uint.rs @@ -0,0 +1,119 @@ +#![allow(clippy::assign_op_pattern)] +//! An unsigned 256 integer type. Used for, among other things, +//! the backing type of token amounts. +use std::cmp::Ordering; +use std::ops::{BitXor, Neg}; + +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use impl_num_traits::impl_uint_num_traits; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use uint::construct_uint; + +construct_uint! { + /// Namada native type to replace for unsigned 256 bit + /// integers. + #[derive( + Serialize, + Deserialize, + BorshSerialize, + BorshDeserialize, + BorshSchema, + )] + + pub struct Uint(4); +} + +impl_uint_num_traits!(Uint, 4); + +impl Uint { + /// Compute the two's complement of a number. + fn negate(&self) -> Option { + Self( + self.0 + .into_iter() + .map(|byte| byte.bitxor(u64::MAX)) + .try_collect() + .expect("This cannot fail"), + ) + .checked_add(Uint::from(1u64)) + } +} + +/// The maximum absolute value a [`SignedUint`] may have. +/// Note the the last digit is 2^63 - 1. We add this cap so +/// we can use two's complement. +pub const MAX_VALUE: Uint = + Uint([u64::MAX, u64::MAX, u64::MAX, 9223372036854775807]); + +/// A signed 256 big integer. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct SignedUint(Uint); + +impl SignedUint { + /// Check if the amount is not negative (greater + /// than or equal to zero) + pub fn non_negative(&self) -> bool { + self.0.0[3].leading_zeros() > 0 + } + + /// Get the absolute value + pub fn abs(&self) -> Uint { + if self.non_negative() { + self.0 + } else { + self.0.negate().unwrap() + } + } +} + +impl TryFrom for SignedUint { + type Error = Box; + + fn try_from(value: Uint) -> Result { + if value.0 <= MAX_VALUE.0 { + Ok(Self(value)) + } else { + Err("The given integer is too large to be represented asa \ + SignedUint" + .into()) + } + } +} + +impl Neg for SignedUint { + type Output = Self; + + fn neg(self) -> Self::Output { + Self( + self.0 + .into_iter() + .map(|byte| byte.bitxor(u64::MAX)) + .try_collect() + .expect("This cannot fail") + .0 + .checked_add(Uint::from(1u64)) + .unwrap(), + ) + } +} + +impl PartialOrd for SignedUint { + fn partial_cmp(&self, other: &Self) -> Option { + match (self.non_negative(), other.non_negative()) { + (true, false) => Some(Ordering::Greater), + (false, true) => Some(Ordering::Less), + _ => { + let this = self.abs(); + let that = other.abs(); + this.0.partial_cmp(&that.0) + } + } + } +} + +impl Ord for SignedUint { + fn cmp(&self, other: &Self) -> Ordering { + self.partial_cmp(other).unwrap() + } +} From 3aa2856a7027208282025764177744e4e70aec7e Mon Sep 17 00:00:00 2001 From: satan Date: Wed, 29 Mar 2023 10:03:23 +0200 Subject: [PATCH 002/151] Fixed errors in core --- core/src/types/token.rs | 34 +++++++++++++++++++++++---- core/src/types/transaction/wrapper.rs | 11 ++++----- core/src/types/uint.rs | 15 +++--------- 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 4ef2f3677b..a91b49af5c 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -47,6 +47,7 @@ pub const NATIVE_MAX_DECIMAL_PLACES: u8 = 6; /// key. pub const NATIVE_SCALE: u64 = 1_000_000; +/// A change in tokens amount pub type Change = SignedUint; impl Amount { @@ -137,7 +138,7 @@ impl Amount { } } - /// Attempt to convert a float to an `Erc20Amount` with the specified + /// Attempt to convert a float to an `Amount` with the specified /// precision. pub fn from_float( float: impl Into, @@ -149,7 +150,7 @@ impl Amount { } } - /// Attempt to convert an unsigned interger to an `Erc20Amount` with the + /// Attempt to convert an unsigned interger to an `Amount` with the /// specified precision. pub fn from_int( uint: impl Into, @@ -204,7 +205,7 @@ impl From for u8 { pub struct DenominatedAmount { /// The mantissa pub amount: Amount, - /// The number of decimal plces in base ten. + /// The number of decimal places in base ten. pub denom: Denomination, } @@ -234,7 +235,7 @@ impl FromStr for DenominatedAmount { fn from_str(s: &str) -> Result { let decimal = Decimal::from_str(s) - .or_else(|err| Err(AmountParseError::InvalidDecimal(err)))?; + .map_err(AmountParseError::InvalidDecimal)?; let denom = Denomination(decimal.scale() as u8); Ok(Self { amount: Amount::from_decimal(decimal, denom)?, @@ -243,6 +244,31 @@ impl FromStr for DenominatedAmount { } } +impl serde::Serialize for Amount { + fn serialize( + &self, + serializer: S, + ) -> std::result::Result + where + S: serde::Serializer, + { + let amount_string = self.raw.to_string(); + serde::Serialize::serialize(&amount_string, serializer) + } +} + +impl<'de> serde::Deserialize<'de> for Amount { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error; + let amount_string: String = + serde::Deserialize::deserialize(deserializer)?; + Ok(Self{raw: Uint::from_str(&amount_string).map_err(D::Error::custom)?}) + } +} + impl serde::Serialize for DenominatedAmount { fn serialize( &self, diff --git a/core/src/types/transaction/wrapper.rs b/core/src/types/transaction/wrapper.rs index 70ef2827bc..d3b3b5dce0 100644 --- a/core/src/types/transaction/wrapper.rs +++ b/core/src/types/transaction/wrapper.rs @@ -90,7 +90,7 @@ pub mod wrapper_tx { impl GasLimit { /// We refund unused gas up to GAS_LIMIT_RESOLUTION pub fn refund_amount(&self, used_gas: u64) -> Amount { - if used_gas < (u64::from(self) - GAS_LIMIT_RESOLUTION) { + Amount::native_whole(if used_gas < (u64::from(self) - GAS_LIMIT_RESOLUTION) { // we refund only up to GAS_LIMIT_RESOLUTION GAS_LIMIT_RESOLUTION } else if used_gas >= u64::from(self) { @@ -99,8 +99,7 @@ pub mod wrapper_tx { } else { // compute refund u64::from(self) - used_gas - } - .into() + }) } } @@ -108,8 +107,6 @@ pub mod wrapper_tx { /// of GAS_LIMIT_RESOLUTION impl From for GasLimit { fn from(amount: u64) -> GasLimit { - // we could use the ceiling function but this way avoids casts to - // floats if GAS_LIMIT_RESOLUTION * (amount / GAS_LIMIT_RESOLUTION) < amount { GasLimit { multiplier: (amount / GAS_LIMIT_RESOLUTION) + 1, @@ -126,7 +123,7 @@ pub mod wrapper_tx { /// of GAS_LIMIT_RESOLUTION impl From for GasLimit { fn from(amount: Amount) -> GasLimit { - GasLimit::from(u64::from(amount)) + GasLimit::from(u128::from(amount) as u64) } } @@ -147,7 +144,7 @@ pub mod wrapper_tx { /// Get back the gas limit as a raw number, viewed as an Amount impl From for Amount { fn from(limit: GasLimit) -> Amount { - Amount::from(limit.multiplier * GAS_LIMIT_RESOLUTION) + Amount::native_whole(limit.multiplier * GAS_LIMIT_RESOLUTION) } } diff --git a/core/src/types/uint.rs b/core/src/types/uint.rs index 0fbcc42061..079c20bd09 100644 --- a/core/src/types/uint.rs +++ b/core/src/types/uint.rs @@ -6,7 +6,6 @@ use std::ops::{BitXor, Neg}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use impl_num_traits::impl_uint_num_traits; -use itertools::Itertools; use serde::{Deserialize, Serialize}; use uint::construct_uint; @@ -33,7 +32,8 @@ impl Uint { self.0 .into_iter() .map(|byte| byte.bitxor(u64::MAX)) - .try_collect() + .collect::>() + .try_into() .expect("This cannot fail"), ) .checked_add(Uint::from(1u64)) @@ -85,16 +85,7 @@ impl Neg for SignedUint { type Output = Self; fn neg(self) -> Self::Output { - Self( - self.0 - .into_iter() - .map(|byte| byte.bitxor(u64::MAX)) - .try_collect() - .expect("This cannot fail") - .0 - .checked_add(Uint::from(1u64)) - .unwrap(), - ) + Self(self.0.negate().expect("This should not fail")) } } From d2d3665be3d193a7944262552a553af536fc898c Mon Sep 17 00:00:00 2001 From: satan Date: Sat, 1 Apr 2023 11:22:43 +0200 Subject: [PATCH 003/151] temp --- Cargo.lock | 279 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 268 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dfb800b157..8fb0dcf81e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -513,7 +513,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43473b34abc4b0b405efa0a250bac87eea888182b21687ee5c8115d279b0fda5" dependencies = [ - "bitvec", + "bitvec 0.22.3", "blake2s_simd 0.5.11", "byteorder", "crossbeam-channel 0.5.6", @@ -637,10 +637,22 @@ version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5237f00a8c86130a0cc317830e558b966dd7850d48a953d998c813f01a41b527" dependencies = [ - "funty", - "radium", + "funty 1.2.0", + "radium 0.6.2", "tap", - "wyz", + "wyz 0.4.0", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty 2.0.0", + "radium 0.7.0", + "tap", + "wyz 0.5.1", ] [[package]] @@ -819,7 +831,7 @@ source = "git+https://github.com/heliaxdev/borsh-rs.git?rev=cd5223e5103c4f139e0c dependencies = [ "borsh-derive-internal", "borsh-schema-derive-internal", - "proc-macro-crate", + "proc-macro-crate 0.1.5", "proc-macro2", "syn", ] @@ -861,6 +873,12 @@ version = "3.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" +[[package]] +name = "byte-slice-cast" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c" + [[package]] name = "byte-tools" version = "0.3.1" @@ -1901,6 +1919,50 @@ dependencies = [ "serde_json", ] +[[package]] +name = "ethabi" +version = "18.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7413c5f74cc903ea37386a8965a936cbeb334bd270862fdece542c1b2dcbc898" +dependencies = [ + "ethereum-types", + "hex", + "once_cell", + "regex", + "serde 1.0.145", + "serde_json", + "sha3 0.10.6", + "thiserror", + "uint", +] + +[[package]] +name = "ethbloom" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c22d4b5885b6aa2fe5e8b9329fb8d232bf739e434e6b87347c63bdd00c120f60" +dependencies = [ + "crunchy 0.2.2", + "fixed-hash", + "impl-rlp", + "impl-serde", + "tiny-keccak", +] + +[[package]] +name = "ethereum-types" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d215cbf040552efcbe99a38372fe80ab9d00268e20012b79fcd0f073edd8ee" +dependencies = [ + "ethbloom", + "fixed-hash", + "impl-rlp", + "impl-serde", + "primitive-types", + "uint", +] + [[package]] name = "event-listener" version = "2.5.3" @@ -2006,7 +2068,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "131655483be284720a17d74ff97592b8e76576dc25563148601df2d7c9080924" dependencies = [ - "bitvec", + "bitvec 0.22.3", "rand_core 0.6.4", "subtle", ] @@ -2046,6 +2108,18 @@ dependencies = [ "windows-sys 0.36.1", ] +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rand 0.8.5", + "rustc-hex", + "static_assertions", +] + [[package]] name = "fixedbitset" version = "0.4.2" @@ -2149,6 +2223,12 @@ name = "funty" version = "1.2.0" source = "git+https://github.com/bitvecto-rs/funty/?rev=7ef0d890fbcd8b3def1635ac1a877fc298488446#7ef0d890fbcd8b3def1635ac1a877fc298488446" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.1.31" @@ -2890,7 +2970,7 @@ dependencies = [ "prost", "ripemd160", "sha2 0.9.9", - "sha3", + "sha3 0.9.1", "sp-std", ] @@ -2921,6 +3001,55 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-num-traits" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951641f13f873bff03d4bf19ae8bec531935ac0ac2cc775f84d7edfdcfed3f17" +dependencies = [ + "integer-sqrt", + "num-traits 0.2.15", + "uint", +] + +[[package]] +name = "impl-rlp" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28220f89297a075ddc7245cd538076ee98b01f2a9c23a53a4f1105d5a322808" +dependencies = [ + "rlp", +] + +[[package]] +name = "impl-serde" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc88fc67028ae3db0c853baa36269d398d5f45b6982f95549ff5def78c935cd" +dependencies = [ + "serde 1.0.145", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d7a9f6330b71fea57921c9b61c47ee6e84f72d394754eff6163ae67e7395eb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "incrementalmerkletree" version = "0.2.0" @@ -2977,6 +3106,15 @@ dependencies = [ "web-sys", ] +[[package]] +name = "integer-sqrt" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770" +dependencies = [ + "num-traits 0.2.15", +] + [[package]] name = "iovec" version = "0.1.4" @@ -3031,7 +3169,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7baec19d4e83f9145d4891178101a604565edff9645770fc979804138b04c" dependencies = [ - "bitvec", + "bitvec 0.22.3", "bls12_381", "ff", "group", @@ -3343,7 +3481,7 @@ source = "git+https://github.com/anoma/masp?rev=bee40fc465f6afbd10558d12fe96eb17 dependencies = [ "aes", "bip0039", - "bitvec", + "bitvec 0.22.3", "blake2b_simd 1.0.0", "blake2s_simd 1.0.0", "bls12_381", @@ -3780,6 +3918,7 @@ dependencies = [ "data-encoding", "derivative", "ed25519-consensus", + "ethabi", "ferveo", "ferveo-common", "group-threshold-cryptography", @@ -3788,6 +3927,7 @@ dependencies = [ "ibc-proto 0.17.1 (git+https://github.com/heliaxdev/ibc-rs?rev=9fcc1c8c19db6af50806ffe5b2f6c214adcbfd5d)", "ibc-proto 0.17.1 (git+https://github.com/heliaxdev/ibc-rs.git?rev=f4703dfe2c1f25cc431279ab74f10f3e0f6827e2)", "ics23", + "impl-num-traits", "index-set", "itertools", "libsecp256k1", @@ -3816,6 +3956,7 @@ dependencies = [ "tonic-build", "tracing 0.1.37", "tracing-subscriber 0.3.16", + "uint", "zeroize", ] @@ -4282,7 +4423,7 @@ dependencies = [ "aes", "arrayvec 0.7.2", "bigint", - "bitvec", + "bitvec 0.22.3", "blake2b_simd 1.0.0", "ff", "fpe", @@ -4348,6 +4489,32 @@ dependencies = [ "group", ] +[[package]] +name = "parity-scale-codec" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "637935964ff85a605d114591d4d2c13c5d1ba2806dae97cea6bf180238a749ac" +dependencies = [ + "arrayvec 0.7.2", + "bitvec 1.0.1", + "byte-slice-cast", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "serde 1.0.145", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b26a931f824dd4eca30b3e43bb4f31cd5f0d3a403c5f5ff27106b805bfde7b" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "parity-wasm" version = "0.45.0" @@ -4643,6 +4810,19 @@ dependencies = [ "output_vt100", ] +[[package]] +name = "primitive-types" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f3486ccba82358b11a77516035647c34ba167dfa53312630de83b12bd4f3d66" +dependencies = [ + "fixed-hash", + "impl-codec", + "impl-rlp", + "impl-serde", + "uint", +] + [[package]] name = "proc-macro-crate" version = "0.1.5" @@ -4652,6 +4832,16 @@ dependencies = [ "toml", ] +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -4850,6 +5040,12 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.6.5" @@ -5303,6 +5499,16 @@ dependencies = [ "libc", ] +[[package]] +name = "rlp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" +dependencies = [ + "bytes 1.2.1", + "rustc-hex", +] + [[package]] name = "rocksdb" version = "0.19.0" @@ -5363,6 +5569,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + [[package]] name = "rustc_version" version = "0.2.3" @@ -5851,6 +6063,16 @@ dependencies = [ "opaque-debug 0.3.0", ] +[[package]] +name = "sha3" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdf0c33fae925bdc080598b84bc15c55e7b9a4a43b3c704da051f977469691c9" +dependencies = [ + "digest 0.10.5", + "keccak", +] + [[package]] name = "sharded-slab" version = "0.1.4" @@ -6745,6 +6967,23 @@ dependencies = [ "serde 1.0.145", ] +[[package]] +name = "toml_datetime" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" + +[[package]] +name = "toml_edit" +version = "0.19.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "tonic" version = "0.6.2" @@ -7990,6 +8229,15 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +[[package]] +name = "winnow" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.10.1" @@ -8018,6 +8266,15 @@ dependencies = [ "tap", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "xattr" version = "0.2.3" @@ -8075,7 +8332,7 @@ source = "git+https://github.com/zcash/librustzcash?rev=2425a08#2425a0869098e3b0 dependencies = [ "aes", "bip0039", - "bitvec", + "bitvec 0.22.3", "blake2b_simd 1.0.0", "blake2s_simd 1.0.0", "bls12_381", From faedfed893336a822ab89dd935a129c719ab41e5 Mon Sep 17 00:00:00 2001 From: satan Date: Mon, 3 Apr 2023 17:19:01 +0200 Subject: [PATCH 004/151] Some more fixes for amount types in the client code --- apps/src/lib/cli.rs | 4 +- apps/src/lib/client/rpc.rs | 112 ++++++---- apps/src/lib/client/tx.rs | 15 +- core/src/ledger/storage_api/token.rs | 13 ++ core/src/types/token.rs | 207 +++++++++++++++--- core/src/types/uint.rs | 62 +++++- proof_of_stake/src/lib.rs | 57 ++--- proof_of_stake/src/tests.rs | 2 +- proof_of_stake/src/types.rs | 30 ++- proof_of_stake/src/types/rev_order.rs | 8 +- shared/src/ledger/ibc/vp/token.rs | 13 +- .../src/ledger/native_vp/governance/utils.rs | 2 +- shared/src/ledger/pos/mod.rs | 2 +- shared/src/ledger/queries/router.rs | 10 +- shared/src/ledger/queries/shell.rs | 2 +- shared/src/ledger/queries/vp/mod.rs | 6 + shared/src/ledger/queries/vp/token.rs | 24 ++ tests/src/vm_host_env/ibc.rs | 2 +- tx_prelude/src/token.rs | 12 +- vp_prelude/src/token.rs | 11 +- 20 files changed, 437 insertions(+), 157 deletions(-) create mode 100644 shared/src/ledger/queries/vp/token.rs diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 9c23cadb46..dfa8fca8a2 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -1624,9 +1624,9 @@ pub mod args { const FORCE: ArgFlag = flag("force"); const DONT_PREFETCH_WASM: ArgFlag = flag("dont-prefetch-wasm"); const GAS_AMOUNT: ArgDefault = - arg_default("gas-amount", DefaultFn(|| token::Amount::from(0))); + arg_default("gas-amount", DefaultFn(|| token::Amount::default())); const GAS_LIMIT: ArgDefault = - arg_default("gas-limit", DefaultFn(|| token::Amount::from(0))); + arg_default("gas-limit", DefaultFn(|| token::Amount::default())); const GAS_TOKEN: ArgDefaultFromCtx = arg_default_from_ctx("gas-token", DefaultFn(|| "NAM".into())); const GENESIS_PATH: Arg = arg("genesis-path"); diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 4dd36ea2eb..4bc15cb45e 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -45,7 +45,7 @@ use namada::types::masp::{BalanceOwner, ExtendedViewingKey, PaymentAddress}; use namada::types::storage::{ BlockHeight, BlockResults, Epoch, Key, KeySeg, PrefixValue, TxIndex, }; -use namada::types::token::{balance_key, Transfer}; +use namada::types::token::{balance_key, DenominatedAmount, MaspDenom, Transfer}; use namada::types::transaction::{ process_tx, AffineCurve, DecryptedTx, EllipticCurve, PairingEngine, TxType, WrapperTx, @@ -239,21 +239,27 @@ pub async fn query_tx_deltas( // Describe how a Transfer simply subtracts from one // account and adds the same to another let mut delta = TransferDelta::default(); - let tfer_delta = Amount::from_nonnegative( - transfer.token.clone(), - u64::from(transfer.amount), - ) - .expect("invalid value for amount"); - delta.insert( - transfer.source, - Amount::zero() - &tfer_delta, - ); - delta.insert(transfer.target, tfer_delta); + for denom in MaspDenom::iter() { + let denominated = denom.denominate(&transfer.amount); + if denominated != 0 { + let tfer_delta = Amount::from_nonnegative( + (transfer.token.clone(), denom.into()), + denominated, + ) + .expect("invalid value for amount"); + delta.insert( + transfer.source.clone(), + Amount::zero() - &tfer_delta, + ); + delta.insert(transfer.target.clone(), tfer_delta); + } + } // No shielded accounts are affected by this Transfer transfers.insert( (height, idx), (epoch, delta, TransactionDelta::new()), ); + } } // An incomplete page signifies no more transactions @@ -322,8 +328,8 @@ pub async fn query_transfers(mut ctx: Context, args: args::QueryTransfers) { // Check if this transfer pertains to the supplied token relevant &= match &query_token { Some(token) => { - tfer_delta.values().any(|x| x[token] != 0) - || shielded_accounts.values().any(|x| x[token] != 0) + tfer_delta.values().zip(MaspDenom::iter()).any(|(x, denom)| x[&(token.clone(), denom)] != 0) + || shielded_accounts.values().zip(MaspDenom::iter()).any(|(x, denom)| x[&(token.clone(), denom)] != 0) } None => true, }; @@ -336,7 +342,7 @@ pub async fn query_transfers(mut ctx: Context, args: args::QueryTransfers) { for (account, amt) in tfer_delta { if account != masp() { print!(" {}:", account); - for (addr, val) in amt.components() { + for ((addr, denom), val) in amt.components() { let addr_enc = addr.encode(); let readable = tokens.get(addr).cloned().unwrap_or(addr_enc.as_str()); @@ -348,7 +354,7 @@ pub async fn query_transfers(mut ctx: Context, args: args::QueryTransfers) { print!( " {}{} {}", sign, - token::Amount::from(val.unsigned_abs()), + format_denominated_amount(&client, addr,token::Amount::from_masp_denominated(val.unsigned_abs(), *denom)).await, readable ); } @@ -360,7 +366,7 @@ pub async fn query_transfers(mut ctx: Context, args: args::QueryTransfers) { for (account, amt) in shielded_accounts { if fvk_map.contains_key(&account) { print!(" {}:", fvk_map[&account]); - for (addr, val) in amt.components() { + for ((addr, denom), val) in amt.components() { let addr_enc = addr.encode(); let readable = tokens.get(addr).cloned().unwrap_or(addr_enc.as_str()); @@ -372,7 +378,7 @@ pub async fn query_transfers(mut ctx: Context, args: args::QueryTransfers) { print!( " {}{} {}", sign, - token::Amount::from(val.unsigned_abs()), + format_denominated_amount(&client, addr, token::Amount::from_masp_denominated(val.unsigned_abs(), *denom)).await, readable ); } @@ -909,45 +915,48 @@ pub async fn query_shielded_balance( match (args.token, owner.is_some()) { // Here the user wants to know the balance for a specific token (Some(token), true) => { - // Query the multi-asset balance at the given spending key - let viewing_key = - ExtendedFullViewingKey::from(viewing_keys[0]).fvk.vk; - let balance: Amount = if no_conversions { - ctx.shielded - .compute_shielded_balance(&viewing_key) - .expect("context should contain viewing key") - } else { - ctx.shielded - .compute_exchanged_balance( - client.clone(), - &viewing_key, - epoch, - ) - .await - .expect("context should contain viewing key") - }; - // Compute the unique asset identifier from the token address + let mut total_balance = token::Amount::default(); let token = ctx.get(&token); - let asset_type = AssetType::new( - (token.clone(), epoch.0) - .try_to_vec() - .expect("token addresses should serialize") - .as_ref(), - ) - .unwrap(); + for denom in MaspDenom::iter() { + // Query the multi-asset balance at the given spending key + let viewing_key = + ExtendedFullViewingKey::from(viewing_keys[0]).fvk.vk; + let balance: Amount = if no_conversions { + ctx.shielded + .compute_shielded_balance(&viewing_key) + .expect("context should contain viewing key") + } else { + ctx.shielded + .compute_exchanged_balance( + client.clone(), + &viewing_key, + epoch, + ) + .await + .expect("context should contain viewing key") + }; + // Compute the unique asset identifier from the token address + let asset_type = AssetType::new( + (token.clone(), denom, epoch.0) + .try_to_vec() + .expect("token addresses should serialize") + .as_ref(), + ) + .unwrap(); + total_balance += token::Amount::from_masp_denominated(balance[&asset_type] as u64, denom); + } + let currency_code = tokens .get(&token) .map(|c| Cow::Borrowed(*c)) .unwrap_or_else(|| Cow::Owned(token.to_string())); - if balance[&asset_type] == 0 { + if total_balance.is_zero() { println!( "No shielded {} balance found for given key", currency_code ); } else { - let asset_value = - token::Amount::from(balance[&asset_type] as u64); - println!("{}: {}", currency_code, asset_value); + println!("{}: {}", currency_code, format_denominated_amount(&client, &token, total_balance).await); } } // Here the user wants to know the balance of all tokens across users @@ -2612,3 +2621,14 @@ fn unwrap_client_response(response: Result) -> T { cli::safe_exit(1) }) } + +/// Look up the denomination of a token in order to format it +/// correctly as a string. +async fn format_denominated_amount(client: &HttpClient, token: &Address, amount: token::Amount) -> String { + let denom = unwrap_client_response( RPC.vp().token().denomination(client, token).await) + .unwrap_or_else(|| { + println!("No denomination found for token: {token}, defaulting to zero decimal places"); + 0.into() + }); + DenominatedAmount{ amount, denom }.to_string() +} diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 0014833ca8..921f0eaa1e 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -50,9 +50,7 @@ use namada::types::storage::{ BlockHeight, Epoch, Key, KeySeg, TxIndex, RESERVED_ADDRESS_PREFIX, }; use namada::types::time::DateTimeUtc; -use namada::types::token::{ - Transfer, HEAD_TX_KEY, PIN_KEY_PREFIX, TX_KEY_PREFIX, -}; +use namada::types::token::{Transfer, HEAD_TX_KEY, PIN_KEY_PREFIX, TX_KEY_PREFIX, MaspDenom}; use namada::types::transaction::governance::{ InitProposalData, VoteProposalData, }; @@ -473,11 +471,14 @@ pub enum PinnedBalanceError { pub type Conversions = HashMap, i64)>; +/// Represents an amount that is +pub type MaspDenominatedAmount = Amount<(Address, MaspDenom)>; + /// Represents the changes that were made to a list of transparent accounts -pub type TransferDelta = HashMap>; +pub type TransferDelta = HashMap; /// Represents the changes that were made to a list of shielded accounts -pub type TransactionDelta = HashMap; +pub type TransactionDelta = HashMap; /// Represents the current state of the shielded pool from the perspective of /// the chosen viewing keys. @@ -1003,7 +1004,7 @@ impl ShieldedContext { pub async fn compute_exchanged_amount( &mut self, client: HttpClient, - mut input: Amount, + mut input: MaspDenominatedAmount, target_epoch: Epoch, mut conversions: Conversions, ) -> (Amount, Conversions) { @@ -1252,7 +1253,7 @@ impl ShieldedContext { client: HttpClient, amt: Amount, target_epoch: Epoch, - ) -> Amount
{ + ) -> MaspDenominatedAmount { let mut res = Amount::zero(); for (asset_type, val) in amt.components() { // Decode the asset type diff --git a/core/src/ledger/storage_api/token.rs b/core/src/ledger/storage_api/token.rs index c1e6573a21..fe6348fbc4 100644 --- a/core/src/ledger/storage_api/token.rs +++ b/core/src/ledger/storage_api/token.rs @@ -20,6 +20,19 @@ where Ok(balance) } +/// Read the denomination of a given token, if any. +pub fn read_denom( + storage: &S, + token: &Address, +) -> storage_api::Result> + where + S: StorageRead, +{ + let key = token::denom_key(token); + let denom = storage.read::(&key)?; + Ok(denom.map(Into::into)) +} + /// Transfer `token` from `src` to `dest`. Returns an `Err` if `src` has /// insufficient balance or if the transfer the `dest` would overflow (This can /// only happen if the total supply does't fit in `token::Amount`). diff --git a/core/src/types/token.rs b/core/src/types/token.rs index a91b49af5c..c245c0b366 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -1,16 +1,20 @@ //! A basic fungible token -//use std::fmt::Display; +use std::fmt::Display; use std::ops::{Add, AddAssign, Mul, Sub, SubAssign}; use std::str::FromStr; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use data_encoding::BASE32HEX_NOPAD; use masp_primitives::transaction::Transaction; use rust_decimal::prelude::Decimal; use serde::{Deserialize, Serialize}; use thiserror::Error; +use crate::ledger::storage_api::StorageRead; +use crate::ledger::storage_api::token::read_denom; use crate::types::address::{masp, Address, DecodeError as AddressError}; +use crate::types::storage; use crate::types::storage::{DbKeySeg, Key, KeySeg}; use crate::types::uint::{self, SignedUint, Uint}; @@ -34,9 +38,6 @@ pub struct Amount { raw: Uint, } -/// A number of decimal places for a token [`Amount`]. -pub type Denom = u8; - /// Maximum decimal places in a native token [`Amount`] and [`Change`]. /// For non-native (e.g. ERC20 tokens) one must read the `denom_key` storage /// key. @@ -63,7 +64,7 @@ impl Amount { } /// Receive a given amount. - /// Panics on overflow and when [`uint::MAX_VALUE`] is exceeded. + /// Panics on overflow and when [`uint::MAX_SIGNED_VALUE`] is exceeded. pub fn receive(&mut self, amount: &Amount) { self.raw = self.raw.checked_add(amount.raw).unwrap(); } @@ -82,11 +83,23 @@ impl Amount { } } + /// Create a new amount with the maximum signed value + pub fn max_signed() -> Self { + Self { + raw: uint::MAX_SIGNED_VALUE, + } + } + + /// Check if [`Amount`] is zero. + pub fn is_zero(&self) -> bool { + self.raw == Uint::from(0) + } + /// Checked addition. Returns `None` on overflow or if - /// the amount exceed [`uint::MAX_VALUE`] + /// the amount exceed [`uint::MAX_SIGNED_VALUE`] pub fn checked_add(&self, amount: Amount) -> Option { self.raw.checked_add(amount.raw).and_then(|result| { - if result < uint::MAX_VALUE { + if result < uint::MAX_SIGNED_VALUE { Some(Self { raw: result }) } else { None @@ -150,17 +163,60 @@ impl Amount { } } - /// Attempt to convert an unsigned interger to an `Amount` with the + /// Attempt to convert an unsigned integer to an `Amount` with the /// specified precision. - pub fn from_int( - uint: impl Into, + pub fn from_uint( + uint: impl Into, denom: impl Into, ) -> Result { - Self::from_decimal(Decimal::try_from(uint.into()).unwrap(), denom) + let denom = denom.into(); + match Uint::from(10) + .checked_pow(Uint::from(denom)) + .and_then(|scaling| scaling.checked_mul(uint.into())) + { + Some(amount) => Ok(Self { raw: amount }), + None => Err(AmountParseError::ConvertToDecimal), + } + } + + /// Given a u64 and [`MaspDenom`], construct the corresponding + /// amount. + pub fn from_masp_denominated(val: u64, denom: MaspDenom) -> Self { + let val = Uint::from(val); + let denom = Uint::from(denom as u64); + let scaling = Uint::from(2).pow(denom); + Self { raw: val * scaling } + } + + /// Get a string representation of a native token amount. + pub fn to_string_native(&self) -> String { + DenominatedAmount { + amount: *self, + denom: 6.into(), + }.to_string_precise() + } + + /// Add denomination info if it exists in storage. + pub fn denominated(&self, token: &Address, storage: &impl StorageRead) -> Option { + let denom = read_denom(storage, token).expect("Should be able to read storage"); + denom.map(|denom| + DenominatedAmount { + amount: *self, + denom, + }) + } + + /// Convert to an [`Amount`] under the assumption that the input + /// string encodes all necessary decimal places. + pub fn from_string_precise(string: &str) -> Result { + DenominatedAmount::from_str(string).map(|den| den.amount) } + } -/// The number of decimal places in base 10 of an amount. +/// Given a number represented as `M*B^D`, then +/// `M` is the matissa, `B` is the base and `D` +/// is the denomination, represented by this stuct. #[derive( Debug, Copy, @@ -230,6 +286,14 @@ impl DenominatedAmount { } } +impl Display for DenominatedAmount { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let string = self.to_string_precise(); + let string = string.trim_end_matches(&['0', '.']); + f.write_str(string) + } +} + impl FromStr for DenominatedAmount { type Err = AmountParseError; @@ -307,6 +371,18 @@ impl TryFrom for Decimal { } } +impl<'a> From<&'a DenominatedAmount> for &'a Amount { + fn from(denom: &'a DenominatedAmount) -> Self { + &denom.amount + } +} + +impl From for Amount { + fn from(denom: DenominatedAmount) -> Self { + denom.amount + } +} + impl From for u128 { fn from(amount: Amount) -> Self { amount.raw.as_u128() @@ -331,6 +407,24 @@ impl Mul for Amount { } } +impl Mul for Amount { + type Output = Amount; + + fn mul(mut self, rhs: Uint) -> Self::Output { + self.raw *= rhs; + self + } +} + +impl Mul for Amount { + type Output = Amount; + + fn mul(mut self, rhs: Amount) -> Self::Output { + self.raw *= rhs.raw; + self + } +} + /// A combination of Euclidean division and fractions: /// x*(a,b) = (a*(x//b), x%b). impl Mul<(u64, u64)> for Amount { @@ -371,13 +465,19 @@ impl KeySeg for Amount { where Self: Sized, { - let raw = Uint::from_str(&string) - .map_err(|e| super::storage::Error::InvalidKeySeg(e.to_string()))?; - Ok(Self { raw }) + let bytes = BASE32HEX_NOPAD.decode(string.as_ref()).map_err(|err| { + storage::Error::ParseKeySeg(format!( + "Failed parsing {} with {}", + string, err + )) + })?; + Ok(Amount{ raw: Uint::from_big_endian(&bytes)}) } fn raw(&self) -> String { - self.raw.to_string() + let mut buf = [0u8; 32]; + self.raw.to_big_endian(&mut buf); + BASE32HEX_NOPAD.encode(&buf) } fn to_db_key(&self) -> DbKeySeg { @@ -401,31 +501,64 @@ pub enum AmountParseError { InvalidRange, #[error("Error converting amount to decimal, number too large.")] ConvertToDecimal, + #[error("Could not convert from string, expected an unsigned 256-bit integer.")] + FromString, } -// impl Display for DenominatedAmount { -// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { -// let string = self.to_string_precise(); -// let string = string.trim_end_matches(&['0', '.']); -// f.write_str(string) -// } -// } - impl From for Change { fn from(amount: Amount) -> Self { amount.raw.try_into().unwrap() } } -impl From for Amount { - fn from(amt: DenominatedAmount) -> Self { - amt.amount +impl From for Amount { + fn from(change: Change) -> Self { + Amount{raw: change.abs()} + } +} + +/// The four possible u64 words in a [`Uint`]. +/// Used for converting to MASP amounts. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, BorshSerialize, BorshDeserialize)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum MaspDenom { + Zero = 0, + One, + Two, + Three, +} + +impl From for MaspDenom { + fn from(denom: u8) -> Self { + match denom { + 0 => Self::Zero, + 1 => Self::One, + 2 => Self::Two, + 3 => Self::Three, + _ => panic!("Possible MASP denominations must be between 0 and 3"), + } + } +} + +impl MaspDenom { + /// Iterator over the possible denominations + pub fn iter() -> impl Iterator{ + (0u8..3).into_iter().map(Self::from) + } + + /// Get the corresponding u64 word from the input uint256. + pub fn denominate<'a>(&self, amount: impl Into<&'a Amount>) -> u64 { + let amount = amount.into(); + amount.raw.0[*self as usize] } } /// Key segment for a balance key pub const BALANCE_STORAGE_KEY: &str = "balance"; -/// Key segment for head shielded transaction pointer key +/// Key segment for a denomination key +pub const DENOM_STORAGE_KEY: &str = "balance"; +/// Key segment for head shielded transaction pointer keys pub const HEAD_TX_KEY: &str = "head-tx"; /// Key segment prefix for shielded transaction key pub const TX_KEY_PREFIX: &str = "tx-"; @@ -496,6 +629,22 @@ pub fn is_any_token_balance_key(key: &Key) -> Option<&Address> { } } +/// Obtain a storage key denomination of a token. +pub fn denom_key(token_addr: &Address) -> Key { + Key::from(token_addr.to_db_key()) + .push(&DENOM_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Check if the given storage key is a denomination key for the given token. +pub fn is_denom_key(token_addr: &Address, key: &Key) -> bool { + matches!(&key.segments[..], + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(key), + ] if key == DENOM_STORAGE_KEY && addr == token_addr) +} + /// Check if the given storage key is a masp key pub fn is_masp_key(key: &Key) -> bool { matches!(&key.segments[..], @@ -646,7 +795,7 @@ mod tests { let max = Amount::from(u64::MAX); assert_eq!("18446744073709.551615", max.to_string()); - let whole = Amount::from(u64::MAX / SCALE * SCALE); + let whole = Amount::from(u64::MAX / NATIVE_SCALE * NATIVE_SCALE); assert_eq!("18446744073709", whole.to_string()); let trailing_zeroes = Amount::from(123000); diff --git a/core/src/types/uint.rs b/core/src/types/uint.rs index 079c20bd09..01d315829c 100644 --- a/core/src/types/uint.rs +++ b/core/src/types/uint.rs @@ -2,12 +2,13 @@ //! An unsigned 256 integer type. Used for, among other things, //! the backing type of token amounts. use std::cmp::Ordering; -use std::ops::{BitXor, Neg}; +use std::ops::{Add, AddAssign, BitXor, Neg, Sub}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use impl_num_traits::impl_uint_num_traits; use serde::{Deserialize, Serialize}; use uint::construct_uint; +use crate::types::token; construct_uint! { /// Namada native type to replace for unsigned 256 bit @@ -25,6 +26,9 @@ construct_uint! { impl_uint_num_traits!(Uint, 4); +/// The maximum 256 bit integer +pub const MAX_VALUE: Uint = Uint([u64::MAX; 4]); + impl Uint { /// Compute the two's complement of a number. fn negate(&self) -> Option { @@ -43,11 +47,11 @@ impl Uint { /// The maximum absolute value a [`SignedUint`] may have. /// Note the the last digit is 2^63 - 1. We add this cap so /// we can use two's complement. -pub const MAX_VALUE: Uint = +pub const MAX_SIGNED_VALUE: Uint = Uint([u64::MAX, u64::MAX, u64::MAX, 9223372036854775807]); /// A signed 256 big integer. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct SignedUint(Uint); impl SignedUint { @@ -65,13 +69,36 @@ impl SignedUint { self.0.negate().unwrap() } } + + /// Check if this value is zero + pub fn is_zero(&self) -> bool { + self.0 == Uint::zero() + } + + /// Get a string representation of `self` as a + /// native token amount. + pub fn to_string_native(&self) -> String { + let mut sign = if self.non_negative() { + String::from("-") + } else { + String::new() + }; + sign.push_str(&token::Amount::from(*self).to_string_native()); + sign + } +} + +impl From for SignedUint { + fn from(val: u64) -> Self { + SignedUint::try_from(Uint::from(val)).expect("A u64 will always fit in this type") + } } impl TryFrom for SignedUint { type Error = Box; fn try_from(value: Uint) -> Result { - if value.0 <= MAX_VALUE.0 { + if value.0 <= MAX_SIGNED_VALUE.0 { Ok(Self(value)) } else { Err("The given integer is too large to be represented asa \ @@ -108,3 +135,30 @@ impl Ord for SignedUint { self.partial_cmp(other).unwrap() } } + +impl Add for SignedUint { + type Output = Self; + + fn add(self, rhs: SignedUint) -> Self::Output { + match (self.non_negative(), rhs.non_negative()) { + (true, true) => Self(self.0 + rhs.0), + (false, false) => -Self(self.abs() + rhs.abs()), + (true, false) => Self(self.0 - rhs.abs()), + (false, true) => Self(rhs.0 - self.abs()), + } + } +} + +impl AddAssign for SignedUint { + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} + +impl Sub for SignedUint { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + self + (-rhs) + } +} \ No newline at end of file diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 315de87c6d..c65a539e72 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -19,8 +19,8 @@ pub mod storage; pub mod types; // pub mod validation; -#[cfg(test)] -mod tests; +//#[cfg(test)] +//mod tests; use core::fmt::Debug; use std::collections::{BTreeMap, HashMap, HashSet}; @@ -56,6 +56,7 @@ use storage::{ ReverseOrdTokenAmount, UnbondDetails, WeightedValidator, }; use thiserror::Error; +use namada_core::types::token::{Amount, NATIVE_MAX_DECIMAL_PLACES}; use types::{ BelowCapacityValidatorSet, BelowCapacityValidatorSets, Bonds, CommissionRates, ConsensusValidator, ConsensusValidatorSet, @@ -65,7 +66,7 @@ use types::{ ValidatorState, ValidatorStates, }; -use crate::types::{decimal_mult_i128, decimal_mult_u64, BondId}; +use crate::types::{decimal_mult_u128, BondId}; /// Address of the PoS account implemented as a native VP pub const ADDRESS: Address = Address::Internal(InternalAddress::PoS); @@ -117,7 +118,7 @@ pub enum UnbondError { #[error( "Trying to withdraw more tokens ({0}) than the amount bonded ({0})" )] - UnbondAmountGreaterThanBond(token::Amount, token::Amount), + UnbondAmountGreaterThanBond(String, String), #[error("No bonds found for the validator {0}")] ValidatorHasNoBonds(Address), #[error("Voting power not found for the validator {0}")] @@ -728,7 +729,7 @@ where S: StorageRead + StorageWrite, { let amount = amount.change(); - tracing::debug!("Bonding token amount {amount} at epoch {current_epoch}"); + tracing::debug!("Bonding token amount {} at epoch {current_epoch}", amount.to_string_native()); let params = read_pos_params(storage)?; let pipeline_epoch = current_epoch + params.pipeline_len; if let Some(source) = source { @@ -771,7 +772,7 @@ where tracing::debug!( "Bond remain at offset epoch {}: {}", current_epoch + offset, - cur_remain + token::Amount::from(cur_remain).to_string_native() ); bond_handle.set(storage, cur_remain + amount, current_epoch, offset)?; @@ -911,7 +912,7 @@ fn update_validator_set( where S: StorageRead + StorageWrite, { - if token_change == 0_i128 { + if token_change.is_zero() { return Ok(()); } let epoch = current_epoch + params.pipeline_len; @@ -1328,13 +1329,14 @@ where S: StorageRead + StorageWrite, { let amount = amount.change(); - tracing::debug!("Unbonding token amount {amount} at epoch {current_epoch}"); + tracing::debug!("Unbonding token amount {} at epoch {current_epoch}", amount.to_string_native()); let params = read_pos_params(storage)?; let pipeline_epoch = current_epoch + params.pipeline_len; tracing::debug!( "Current validator stake at pipeline: {}", read_validator_stake(storage, ¶ms, validator, pipeline_epoch)? .unwrap_or_default() + .to_string_native() ); if let Some(source) = source { @@ -1372,8 +1374,8 @@ where .unwrap_or_default(); if amount > remaining_at_pipeline { return Err(UnbondError::UnbondAmountGreaterThanBond( - token::Amount::from_change(amount), - token::Amount::from_change(remaining_at_pipeline), + token::Amount::from_change(amount).to_string_native(), + token::Amount::from_change(remaining_at_pipeline).to_string_native(), ) .into()); } @@ -1589,7 +1591,7 @@ where ) = unbond?; tracing::debug!( - "Unbond delta ({start_epoch}..{withdraw_epoch}), amount {amount}", + "Unbond delta ({start_epoch}..{withdraw_epoch}), amount {}", amount.to_string_native() ); // TODO: worry about updating this later after PR 740 perhaps @@ -1613,10 +1615,10 @@ where .unwrap_or_default() { let slash_rate = slash_type.get_slash_rate(¶ms); - let to_slash = token::Amount::from(decimal_mult_u64( + let to_slash = token::Amount::from_uint(decimal_mult_u128( slash_rate, - u64::from(amount), - )); + u128::from(amount), + ), NATIVE_MAX_DECIMAL_PLACES).expect("Amount out of bounds"); slashed += to_slash; } } @@ -1624,7 +1626,7 @@ where unbonds_to_remove.push((withdraw_epoch, start_epoch)); } withdrawable_amount -= slashed; - tracing::debug!("Withdrawing total {withdrawable_amount}"); + tracing::debug!("Withdrawing total {}", withdrawable_amount.to_string_native()); // Remove the unbond data from storage for (withdraw_epoch, start_epoch) in unbonds_to_remove { @@ -1723,7 +1725,10 @@ where let current_stake = read_validator_stake(storage, params, validator, current_epoch)? .unwrap_or_default(); - let slashed_amount = decimal_mult_u64(rate, u64::from(current_stake)); + let slashed_amount = Amount::from_uint( + decimal_mult_u128(rate, u128::from(current_stake)), + NATIVE_MAX_DECIMAL_PLACES + ).expect("Amount out of bounds"); let token_change = -token::Change::from(slashed_amount); // Update validator sets and deltas at the pipeline length @@ -1750,7 +1755,7 @@ where transfer_tokens( storage, &staking_token_address(), - token::Amount::from(slashed_amount), + slashed_amount, &ADDRESS, &SLASH_POOL_ADDRESS, )?; @@ -1779,8 +1784,8 @@ where tracing::error!( "PoS system transfer error, the source doesn't have \ sufficient balance. It has {}, but {} is required", - src_balance, - amount + src_balance.to_string_native(), + amount.to_string_native(), ); } src_balance.spend(&amount); @@ -1864,7 +1869,7 @@ where continue; } let current_slashed = - decimal_mult_i128(slash_type.get_slash_rate(params), delta); + mult_change_to_amount(slash_type.get_slash_rate(params), delta).change(); let delta = token::Amount::from_change(delta - current_slashed); total += delta; if bond_epoch <= epoch { @@ -1912,7 +1917,7 @@ where ) = validator.unwrap(); tracing::debug!( - "Consensus validator address {address}, stake {cur_stake}" + "Consensus validator address {address}, stake {}", cur_stake.to_string_native() ); // Check if the validator was consensus in the previous epoch with @@ -1932,13 +1937,13 @@ where .unwrap_or_default(); into_tm_voting_power( params.tm_votes_per_token, - prev_validator_stake, + u128::from(prev_validator_stake) as u64, ) }); let cur_tm_voting_power = Lazy::new(|| { into_tm_voting_power( params.tm_votes_per_token, - cur_stake, + u128::from(cur_stake) as u64, ) }); @@ -1975,7 +1980,7 @@ where ); Some(ValidatorSetUpdate::Consensus(ConsensusValidator { consensus_key, - bonded_stake: cur_stake.into(), + bonded_stake: u128::from(cur_stake) as u64, })) }); let cur_below_capacity_validators = @@ -1994,7 +1999,7 @@ where let cur_stake = token::Amount::from(cur_stake); tracing::debug!( - "Below-capacity validator address {address}, stake {cur_stake}" + "Below-capacity validator address {address}, stake {}", cur_stake.to_string_native() ); let prev_tm_voting_power = previous_epoch @@ -2007,7 +2012,7 @@ where .unwrap_or_default(); into_tm_voting_power( params.tm_votes_per_token, - prev_validator_stake, + u128::from(prev_validator_stake) as u64, ) }) .unwrap_or_default(); diff --git a/proof_of_stake/src/tests.rs b/proof_of_stake/src/tests.rs index bbbc9e1027..22149a191e 100644 --- a/proof_of_stake/src/tests.rs +++ b/proof_of_stake/src/tests.rs @@ -1599,7 +1599,7 @@ fn arb_genesis_validators( size: Range, ) -> impl Strategy> { let tokens: Vec<_> = (0..size.end) - .map(|_| (1..=10_u64).prop_map(token::Amount::from)) + .map(|_| (1..=10_u64).prop_map(token::Amount::native_whole)) .collect(); (size, tokens).prop_map(|(size, token_amounts)| { // use unique seeds to generate validators' address and consensus key diff --git a/proof_of_stake/src/types.rs b/proof_of_stake/src/types.rs index db0e697098..7a8af3eebb 100644 --- a/proof_of_stake/src/types.rs +++ b/proof_of_stake/src/types.rs @@ -21,6 +21,8 @@ use namada_core::types::storage::{Epoch, KeySeg}; use namada_core::types::token; pub use rev_order::ReverseOrdTokenAmount; use rust_decimal::prelude::{Decimal, ToPrimitive}; +use rust_decimal::RoundingStrategy; +use namada_core::types::token::{Amount, NATIVE_MAX_DECIMAL_PLACES}; use crate::parameters::PosParams; @@ -240,7 +242,7 @@ impl Display for WeightedValidator { write!( f, "{} with bonded stake {}", - self.address, self.bonded_stake + self.address, self.bonded_stake.to_string_native() ) } } @@ -445,10 +447,10 @@ impl Display for SlashType { /// Multiply a value of type Decimal with one of type u64 and then return the /// truncated u64 -pub fn decimal_mult_u64(dec: Decimal, int: u64) -> u64 { +pub fn decimal_mult_u128(dec: Decimal, int: u128) -> u128 { let prod = dec * Decimal::from(int); // truncate the number to the floor - prod.to_u64().expect("Product is out of bounds") + prod.to_u128().expect("Product is out of bounds") } /// Multiply a value of type Decimal with one of type i128 and then return the @@ -459,23 +461,29 @@ pub fn decimal_mult_i128(dec: Decimal, int: i128) -> i128 { prod.to_i128().expect("Product is out of bounds") } -/// Multiply a value of type Decimal with one of type i128 and then convert it +/// Multiply a value of type Decimal with one of type Uint and then convert it /// to an Amount type pub fn mult_change_to_amount( dec: Decimal, change: token::Change, ) -> token::Amount { - let prod = dec * Decimal::from(change); - // truncate the number to the floor - token::Amount::from(prod.to_u64().expect("Product is out of bounds")) + // this function is used for slashing calculations. We want to err + // on the side of slashing more, not less. + let dec = dec.round_dp_with_strategy(6, RoundingStrategy::ToPositiveInfinity); + Amount::from_decimal( + dec, + NATIVE_MAX_DECIMAL_PLACES + ).unwrap() * change.abs() } /// Multiply a value of type Decimal with one of type Amount and then return the /// truncated Amount pub fn mult_amount(dec: Decimal, amount: token::Amount) -> token::Amount { - let prod = dec * Decimal::from(amount); - // truncate the number to the floor - token::Amount::from(prod.to_u64().expect("Product is out of bounds")) + let dec = dec.round_dp_with_strategy(6, RoundingStrategy::ToPositiveInfinity); + Amount::from_decimal( + dec, + NATIVE_MAX_DECIMAL_PLACES + ).unwrap() * amount } /// Calculate voting power in the tendermint context (which is stored as i64) @@ -484,7 +492,7 @@ pub fn into_tm_voting_power( votes_per_token: Decimal, tokens: impl Into, ) -> i64 { - let prod = decimal_mult_u64(votes_per_token, tokens.into()); + let prod = decimal_mult_u128(votes_per_token, tokens.into() as u128); i64::try_from(prod).expect("Invalid voting power") } diff --git a/proof_of_stake/src/types/rev_order.rs b/proof_of_stake/src/types/rev_order.rs index 807795e10e..19749fabd4 100644 --- a/proof_of_stake/src/types/rev_order.rs +++ b/proof_of_stake/src/types/rev_order.rs @@ -23,7 +23,7 @@ impl From for ReverseOrdTokenAmount { /// Invert the token amount fn invert(amount: token::Amount) -> token::Amount { - token::MAX_AMOUNT - amount + token::Amount::max_signed() - amount } impl KeySeg for ReverseOrdTokenAmount { @@ -46,15 +46,15 @@ impl KeySeg for ReverseOrdTokenAmount { impl std::fmt::Display for ReverseOrdTokenAmount { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) + f.write_str(&self.0.to_string_native()) } } impl std::str::FromStr for ReverseOrdTokenAmount { - type Err = ::Err; + type Err = token::AmountParseError; fn from_str(s: &str) -> Result { - let amount = token::Amount::from_str(s)?; + let amount = token::Amount::from_str(s, token::NATIVE_MAX_DECIMAL_PLACES)?; Ok(Self(amount)) } } diff --git a/shared/src/ledger/ibc/vp/token.rs b/shared/src/ledger/ibc/vp/token.rs index 455a0a49d5..e460301330 100644 --- a/shared/src/ledger/ibc/vp/token.rs +++ b/shared/src/ledger/ibc/vp/token.rs @@ -1,7 +1,6 @@ //! IBC token transfer validation as a native validity predicate use std::collections::{BTreeSet, HashMap, HashSet}; -use std::str::FromStr; use borsh::BorshDeserialize; use thiserror::Error; @@ -123,7 +122,7 @@ where changes.insert(sub_prefix, change + this_change); } } - if changes.iter().all(|(_, c)| *c == 0) { + if changes.iter().all(|(_, c)| c.is_zero()) { return Ok(true); } else { return Err(Error::TokenTransfer( @@ -192,7 +191,7 @@ where } let token = ibc_storage::token(&data.denom) .map_err(|e| Error::Denom(e.to_string()))?; - let amount = Amount::from_str(&data.amount).map_err(Error::Amount)?; + let amount = Amount::from_string_precise(&data.amount).map_err(Error::Amount)?; let denom = if let Some(denom) = data .denom @@ -277,7 +276,7 @@ where .map_err(Error::DecodingPacketData)?; let token = ibc_storage::token(&data.denom) .map_err(|e| Error::Denom(e.to_string()))?; - let amount = Amount::from_str(&data.amount).map_err(Error::Amount)?; + let amount = Amount::from_string_precise(&data.amount).map_err(Error::Amount)?; let prefix = format!( "{}/{}/", @@ -316,7 +315,7 @@ where )? .unwrap_or_default(); // the previous balance of the mint address should be the maximum - Amount::max().change() - post.change() + Amount::max_signed().change() - post.change() }; if change == amount.change() { @@ -335,7 +334,7 @@ where .map_err(Error::DecodingPacketData)?; let token_str = data.denom.split('/').last().ok_or(Error::NoToken)?; let token = Address::decode(token_str).map_err(Error::Address)?; - let amount = Amount::from_str(&data.amount).map_err(Error::Amount)?; + let amount = Amount::from_string_precise(&data.amount).map_err(Error::Amount)?; // check the denom field let prefix = format!( @@ -359,7 +358,7 @@ where )? .unwrap_or_default(); // the previous balance of the mint address should be the maximum - Amount::max().change() - post.change() + Amount::max_signed().change() - post.change() } else { // source zone: unescrow the token for the refund let source_key = token::multitoken_balance_key( diff --git a/shared/src/ledger/native_vp/governance/utils.rs b/shared/src/ledger/native_vp/governance/utils.rs index a0337938ff..59456906da 100644 --- a/shared/src/ledger/native_vp/governance/utils.rs +++ b/shared/src/ledger/native_vp/governance/utils.rs @@ -86,7 +86,7 @@ where { let params = read_pos_params(storage)?; let total_stake = read_total_stake(storage, ¶ms, epoch)?; - let total_stake = VotePower::from(u64::from(total_stake)); + let total_stake = VotePower::from(total_stake); let Votes { yay_validators, diff --git a/shared/src/ledger/pos/mod.rs b/shared/src/ledger/pos/mod.rs index 5e4a1a064d..ee4492a187 100644 --- a/shared/src/ledger/pos/mod.rs +++ b/shared/src/ledger/pos/mod.rs @@ -29,7 +29,7 @@ pub fn into_tm_voting_power( votes_per_token: Decimal, tokens: impl Into, ) -> i64 { - let prod = decimal_mult_u64(votes_per_token, tokens.into()); + let prod = decimal_mult_u128(votes_per_token, tokens.into() as u128); i64::try_from(prod).expect("Invalid validator voting power (i64)") } diff --git a/shared/src/ledger/queries/router.rs b/shared/src/ledger/queries/router.rs index d65826ca68..db095a2c00 100644 --- a/shared/src/ledger/queries/router.rs +++ b/shared/src/ledger/queries/router.rs @@ -828,6 +828,7 @@ macro_rules! router { ); } +/* /// You can expand the `handlers!` macro invocation with e.g.: /// ```shell /// cargo expand ledger::queries::router::test_rpc_handlers --features "ferveo-tpke, ibc-mocks, testing, wasm-runtime, tendermint-rpc" --tests --lib @@ -1029,13 +1030,13 @@ mod test { let result = TEST_RPC.b1(&client).await.unwrap(); assert_eq!(result, "b1"); - let balance = token::Amount::from(123_000_000); + let balance = token::Amount::native_whole(123_000_000); let result = TEST_RPC.b2i(&client, &balance).await.unwrap(); assert_eq!(result, format!("b2i/{balance}")); - let a1 = token::Amount::from(345); - let a2 = token::Amount::from(123_000); - let a3 = token::Amount::from(1_000_999); + let a1 = token::Amount::native_whole(345); + let a2 = token::Amount::native_whole(123_000); + let a3 = token::Amount::native_whole(1_000_999); let result = TEST_RPC.b3(&client, &a1, &a2, &a3).await.unwrap(); assert_eq!(result, format!("b3/{a1}/{a2}/{a3}")); @@ -1088,3 +1089,4 @@ mod test { Ok(()) } } +*/ \ No newline at end of file diff --git a/shared/src/ledger/queries/shell.rs b/shared/src/ledger/queries/shell.rs index 43a51a1b0f..65ed48c3d1 100644 --- a/shared/src/ledger/queries/shell.rs +++ b/shared/src/ledger/queries/shell.rs @@ -426,7 +426,7 @@ mod test { assert!(!has_balance_key); // Then write some balance ... - let balance = token::Amount::from(1000); + let balance = token::Amount::native_whole(1000); StorageWrite::write(&mut client.wl_storage, &balance_key, balance)?; // It has to be committed to be visible in a query client.wl_storage.commit_tx(); diff --git a/shared/src/ledger/queries/vp/mod.rs b/shared/src/ledger/queries/vp/mod.rs index 5f5b5f6081..ed83a492ec 100644 --- a/shared/src/ledger/queries/vp/mod.rs +++ b/shared/src/ledger/queries/vp/mod.rs @@ -1,11 +1,17 @@ // Re-export to show in rustdoc! pub use pos::Pos; +pub use token::Token; + use pos::POS; +use token::TOKEN; + mod pos; +mod token; // Validity predicate queries router! {VP, ( "pos" ) = (sub POS), + ( "token" ) = (sub TOKEN), } #[cfg(any(test, feature = "async-client"))] diff --git a/shared/src/ledger/queries/vp/token.rs b/shared/src/ledger/queries/vp/token.rs new file mode 100644 index 0000000000..7f5e0a0b84 --- /dev/null +++ b/shared/src/ledger/queries/vp/token.rs @@ -0,0 +1,24 @@ +use namada_core::ledger::storage::{DB, DBIter, StorageHasher}; +use namada_core::ledger::storage_api; +use namada_core::ledger::storage_api::token::read_denom; +use namada_core::types::address::Address; +use namada_core::types::token; +use namada_core::types::token::Denomination; +use crate::ledger::queries::RequestCtx; + +router! {TOKEN, + ( "denomination" / [addr: Address] ) -> Option = denomination, +} + +/// Get the number of decimal places (in base 10) for a +/// token specified by `addr`. +fn denomination( + ctx: RequestCtx<'_, D, H>, + addr: Address, +) -> storage_api::Result> + where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + read_denom(ctx.wl_storage, &addr) +} \ No newline at end of file diff --git a/tests/src/vm_host_env/ibc.rs b/tests/src/vm_host_env/ibc.rs index 7755627c6b..23d59dfb01 100644 --- a/tests/src/vm_host_env/ibc.rs +++ b/tests/src/vm_host_env/ibc.rs @@ -202,7 +202,7 @@ pub fn init_storage() -> (Address, Address) { // initialize an account let account = tx::ctx().init_account(code).unwrap(); let key = token::balance_key(&token, &account); - let init_bal = Amount::from(1_000_000_000u64); + let init_bal = Amount::native_whole(1_000_000_000u64); tx::ctx().write(&key, init_bal).unwrap(); (token, account) } diff --git a/tx_prelude/src/token.rs b/tx_prelude/src/token.rs index 5ea8549554..753f666a30 100644 --- a/tx_prelude/src/token.rs +++ b/tx_prelude/src/token.rs @@ -14,11 +14,11 @@ pub fn transfer( dest: &Address, token: &Address, sub_prefix: Option, - amount: Amount, + amount: DenominatedAmount, key: &Option, shielded: &Option, ) -> TxResult { - if amount != Amount::default() { + if amount.amount != Amount::default() { let src_key = match &sub_prefix { Some(sub_prefix) => { let prefix = @@ -36,7 +36,7 @@ pub fn transfer( None => token::balance_key(token, dest), }; let src_bal: Option = match src { - Address::Internal(InternalAddress::IbcMint) => Some(Amount::max()), + Address::Internal(InternalAddress::IbcMint) => Some(Amount::max_signed()), Address::Internal(InternalAddress::IbcBurn) => { log_string("invalid transfer from the burn address"); unreachable!() @@ -47,7 +47,7 @@ pub fn transfer( log_string(format!("src {} has no balance", src_key)); unreachable!() }); - src_bal.spend(&amount); + src_bal.spend(&amount.amount); let mut dest_bal: Amount = match dest { Address::Internal(InternalAddress::IbcMint) => { log_string("invalid transfer to the mint address"); @@ -55,7 +55,7 @@ pub fn transfer( } _ => ctx.read(&dest_key)?.unwrap_or_default(), }; - dest_bal.receive(&amount); + dest_bal.receive(&amount.amount); if src != dest { match src { Address::Internal(InternalAddress::IbcMint) => { @@ -141,7 +141,7 @@ pub fn transfer_with_keys( let src_owner = is_any_multitoken_balance_key(src_key).map(|(_, o)| o); let src_bal: Option = match src_owner { Some(Address::Internal(InternalAddress::IbcMint)) => { - Some(Amount::max()) + Some(Amount::max_signed()) } Some(Address::Internal(InternalAddress::IbcBurn)) => { log_string("invalid transfer from the burn address"); diff --git a/vp_prelude/src/token.rs b/vp_prelude/src/token.rs index 0785fbf97d..dd54ab864f 100644 --- a/vp_prelude/src/token.rs +++ b/vp_prelude/src/token.rs @@ -18,7 +18,7 @@ pub fn vp( keys_changed: &BTreeSet, verifiers: &BTreeSet
, ) -> VpResult { - let mut change: Change = 0; + let mut change: Change = Change::default(); for key in keys_changed.iter() { let owner: Option<&Address> = match token::is_multitoken_balance_key(token, key) { @@ -37,7 +37,7 @@ pub fn vp( // accumulate the change let pre: Amount = match owner { Address::Internal(InternalAddress::IbcMint) => { - Amount::max() + Amount::max_signed() } Address::Internal(InternalAddress::IbcBurn) => { Amount::default() @@ -46,7 +46,7 @@ pub fn vp( }; let post: Amount = match owner { Address::Internal(InternalAddress::IbcMint) => { - ctx.read_temp(key)?.unwrap_or_else(Amount::max) + ctx.read_temp(key)?.unwrap_or_else(Amount::max_signed) } Address::Internal(InternalAddress::IbcBurn) => { ctx.read_temp(key)?.unwrap_or_default() @@ -56,13 +56,12 @@ pub fn vp( let this_change = post.change() - pre.change(); change += this_change; // make sure that the spender approved the transaction - if this_change < 0 - && !(verifiers.contains(owner) || *owner == address::masp()) + if !(this_change.non_negative() || verifiers.contains(owner) || *owner == address::masp()) { return reject(); } } } } - Ok(change == 0) + Ok(change.is_zero()) } From 0082f2b6b95a5ed7c3bde2009cda1da91538c118 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 4 Apr 2023 09:53:28 +0100 Subject: [PATCH 005/151] core/token: add `fn zero` --- apps/src/lib/cli.rs | 4 +- apps/src/lib/client/rpc.rs | 98 ++++++++++++++----- core/src/types/token.rs | 66 +++++++++---- .../src/ledger/native_vp/governance/utils.rs | 2 +- shared/src/ledger/queries/vp/pos.rs | 4 +- 5 files changed, 122 insertions(+), 52 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index dfa8fca8a2..c164c278e1 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -1624,9 +1624,9 @@ pub mod args { const FORCE: ArgFlag = flag("force"); const DONT_PREFETCH_WASM: ArgFlag = flag("dont-prefetch-wasm"); const GAS_AMOUNT: ArgDefault = - arg_default("gas-amount", DefaultFn(|| token::Amount::default())); + arg_default("gas-amount", DefaultFn(|| token::Amount::zero())); const GAS_LIMIT: ArgDefault = - arg_default("gas-limit", DefaultFn(|| token::Amount::default())); + arg_default("gas-limit", DefaultFn(|| token::Amount::zero())); const GAS_TOKEN: ArgDefaultFromCtx = arg_default_from_ctx("gas-token", DefaultFn(|| "NAM".into())); const GENESIS_PATH: Arg = arg("genesis-path"); diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 4bc15cb45e..d55b4d3bbf 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -45,7 +45,9 @@ use namada::types::masp::{BalanceOwner, ExtendedViewingKey, PaymentAddress}; use namada::types::storage::{ BlockHeight, BlockResults, Epoch, Key, KeySeg, PrefixValue, TxIndex, }; -use namada::types::token::{balance_key, DenominatedAmount, MaspDenom, Transfer}; +use namada::types::token::{ + balance_key, DenominatedAmount, MaspDenom, Transfer, +}; use namada::types::transaction::{ process_tx, AffineCurve, DecryptedTx, EllipticCurve, PairingEngine, TxType, WrapperTx, @@ -240,18 +242,22 @@ pub async fn query_tx_deltas( // account and adds the same to another let mut delta = TransferDelta::default(); for denom in MaspDenom::iter() { - let denominated = denom.denominate(&transfer.amount); - if denominated != 0 { + let denominated = + denom.denominate(&transfer.amount); + if denominated != 0 { let tfer_delta = Amount::from_nonnegative( (transfer.token.clone(), denom.into()), denominated, ) - .expect("invalid value for amount"); + .expect("invalid value for amount"); delta.insert( transfer.source.clone(), Amount::zero() - &tfer_delta, ); - delta.insert(transfer.target.clone(), tfer_delta); + delta.insert( + transfer.target.clone(), + tfer_delta, + ); } } // No shielded accounts are affected by this Transfer @@ -259,7 +265,6 @@ pub async fn query_tx_deltas( (height, idx), (epoch, delta, TransactionDelta::new()), ); - } } // An incomplete page signifies no more transactions @@ -328,8 +333,14 @@ pub async fn query_transfers(mut ctx: Context, args: args::QueryTransfers) { // Check if this transfer pertains to the supplied token relevant &= match &query_token { Some(token) => { - tfer_delta.values().zip(MaspDenom::iter()).any(|(x, denom)| x[&(token.clone(), denom)] != 0) - || shielded_accounts.values().zip(MaspDenom::iter()).any(|(x, denom)| x[&(token.clone(), denom)] != 0) + tfer_delta + .values() + .zip(MaspDenom::iter()) + .any(|(x, denom)| x[&(token.clone(), denom)] != 0) + || shielded_accounts + .values() + .zip(MaspDenom::iter()) + .any(|(x, denom)| x[&(token.clone(), denom)] != 0) } None => true, }; @@ -354,7 +365,15 @@ pub async fn query_transfers(mut ctx: Context, args: args::QueryTransfers) { print!( " {}{} {}", sign, - format_denominated_amount(&client, addr,token::Amount::from_masp_denominated(val.unsigned_abs(), *denom)).await, + format_denominated_amount( + &client, + addr, + token::Amount::from_masp_denominated( + val.unsigned_abs(), + *denom + ) + ) + .await, readable ); } @@ -378,7 +397,15 @@ pub async fn query_transfers(mut ctx: Context, args: args::QueryTransfers) { print!( " {}{} {}", sign, - format_denominated_amount(&client, addr, token::Amount::from_masp_denominated(val.unsigned_abs(), *denom)).await, + format_denominated_amount( + &client, + addr, + token::Amount::from_masp_denominated( + val.unsigned_abs(), + *denom + ) + ) + .await, readable ); } @@ -915,7 +942,7 @@ pub async fn query_shielded_balance( match (args.token, owner.is_some()) { // Here the user wants to know the balance for a specific token (Some(token), true) => { - let mut total_balance = token::Amount::default(); + let mut total_balance = token::Amount::zero(); let token = ctx.get(&token); for denom in MaspDenom::iter() { // Query the multi-asset balance at the given spending key @@ -943,7 +970,10 @@ pub async fn query_shielded_balance( .as_ref(), ) .unwrap(); - total_balance += token::Amount::from_masp_denominated(balance[&asset_type] as u64, denom); + total_balance += token::Amount::from_masp_denominated( + balance[&asset_type] as u64, + denom, + ); } let currency_code = tokens @@ -956,7 +986,12 @@ pub async fn query_shielded_balance( currency_code ); } else { - println!("{}: {}", currency_code, format_denominated_amount(&client, &token, total_balance).await); + println!( + "{}: {}", + currency_code, + format_denominated_amount(&client, &token, total_balance) + .await + ); } } // Here the user wants to know the balance of all tokens across users @@ -1416,8 +1451,8 @@ pub async fn query_and_print_unbonds( }); let total_withdrawable = withdrawable .into_iter() - .fold(token::Amount::default(), |acc, (_, amount)| acc + amount); - if total_withdrawable != token::Amount::default() { + .fold(token::Amount::zero(), |acc, (_, amount)| acc + amount); + if total_withdrawable != token::Amount::zero() { println!("Total withdrawable now: {total_withdrawable}."); } if !not_yet_withdrawable.is_empty() { @@ -1490,7 +1525,7 @@ pub async fn query_bonds(ctx: Context, args: args::QueryBonds) { total += bond.amount; total_slashed += bond.slashed_amount.unwrap_or_default(); } - if total_slashed != token::Amount::default() { + if total_slashed != token::Amount::zero() { writeln!( w, "Active (slashed) bonds total: {}", @@ -1502,7 +1537,7 @@ pub async fn query_bonds(ctx: Context, args: args::QueryBonds) { bonds_total += total; bonds_total_slashed += total_slashed; - let mut withdrawable = token::Amount::default(); + let mut withdrawable = token::Amount::zero(); if !details.unbonds.is_empty() { let mut total: token::Amount = 0.into(); let mut total_slashed: token::Amount = 0.into(); @@ -2345,7 +2380,7 @@ pub async fn get_proposal_offline_votes( }, ) in bonds_and_unbonds { - let mut delegated_amount = token::Amount::default(); + let mut delegated_amount = token::Amount::zero(); for delta in bonds { if delta.start <= proposal.tally_epoch { delegated_amount += delta.amount @@ -2400,7 +2435,7 @@ pub async fn get_proposal_offline_votes( // continue; // } else { // delta -= to_deduct; - // to_deduct = token::Amount::default(); + // to_deduct = token::Amount::zero(); // } // delta = apply_slashes( @@ -2540,7 +2575,7 @@ pub async fn get_total_staked_tokens( /// Get the total stake of a validator at the given epoch. The total stake is a /// sum of validator's self-bonds and delegations to their address. /// Returns `None` when the given address is not a validator address. For a -/// validator with `0` stake, this returns `Ok(token::Amount::default())`. +/// validator with `0` stake, this returns `Ok(token::Amount::zero())`. async fn get_validator_stake( client: &HttpClient, epoch: Epoch, @@ -2624,11 +2659,20 @@ fn unwrap_client_response(response: Result) -> T { /// Look up the denomination of a token in order to format it /// correctly as a string. -async fn format_denominated_amount(client: &HttpClient, token: &Address, amount: token::Amount) -> String { - let denom = unwrap_client_response( RPC.vp().token().denomination(client, token).await) - .unwrap_or_else(|| { - println!("No denomination found for token: {token}, defaulting to zero decimal places"); - 0.into() - }); - DenominatedAmount{ amount, denom }.to_string() +async fn format_denominated_amount( + client: &HttpClient, + token: &Address, + amount: token::Amount, +) -> String { + let denom = unwrap_client_response( + RPC.vp().token().denomination(client, token).await, + ) + .unwrap_or_else(|| { + println!( + "No denomination found for token: {token}, defaulting to zero \ + decimal places" + ); + 0.into() + }); + DenominatedAmount { amount, denom }.to_string() } diff --git a/core/src/types/token.rs b/core/src/types/token.rs index c245c0b366..87e423d3c8 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -11,8 +11,8 @@ use rust_decimal::prelude::Decimal; use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::ledger::storage_api::StorageRead; use crate::ledger::storage_api::token::read_denom; +use crate::ledger::storage_api::StorageRead; use crate::types::address::{masp, Address, DecodeError as AddressError}; use crate::types::storage; use crate::types::storage::{DbKeySeg, Key, KeySeg}; @@ -90,6 +90,11 @@ impl Amount { } } + /// Zero [`Amount`]. + pub fn zero() -> Self { + Self::default() + } + /// Check if [`Amount`] is zero. pub fn is_zero(&self) -> bool { self.raw == Uint::from(0) @@ -193,17 +198,22 @@ impl Amount { DenominatedAmount { amount: *self, denom: 6.into(), - }.to_string_precise() + } + .to_string_precise() } /// Add denomination info if it exists in storage. - pub fn denominated(&self, token: &Address, storage: &impl StorageRead) -> Option { - let denom = read_denom(storage, token).expect("Should be able to read storage"); - denom.map(|denom| - DenominatedAmount { - amount: *self, - denom, - }) + pub fn denominated( + &self, + token: &Address, + storage: &impl StorageRead, + ) -> Option { + let denom = + read_denom(storage, token).expect("Should be able to read storage"); + denom.map(|denom| DenominatedAmount { + amount: *self, + denom, + }) } /// Convert to an [`Amount`] under the assumption that the input @@ -211,7 +221,6 @@ impl Amount { pub fn from_string_precise(string: &str) -> Result { DenominatedAmount::from_str(string).map(|den| den.amount) } - } /// Given a number represented as `M*B^D`, then @@ -298,8 +307,8 @@ impl FromStr for DenominatedAmount { type Err = AmountParseError; fn from_str(s: &str) -> Result { - let decimal = Decimal::from_str(s) - .map_err(AmountParseError::InvalidDecimal)?; + let decimal = + Decimal::from_str(s).map_err(AmountParseError::InvalidDecimal)?; let denom = Denomination(decimal.scale() as u8); Ok(Self { amount: Amount::from_decimal(decimal, denom)?, @@ -329,7 +338,9 @@ impl<'de> serde::Deserialize<'de> for Amount { use serde::de::Error; let amount_string: String = serde::Deserialize::deserialize(deserializer)?; - Ok(Self{raw: Uint::from_str(&amount_string).map_err(D::Error::custom)?}) + Ok(Self { + raw: Uint::from_str(&amount_string).map_err(D::Error::custom)?, + }) } } @@ -471,7 +482,9 @@ impl KeySeg for Amount { string, err )) })?; - Ok(Amount{ raw: Uint::from_big_endian(&bytes)}) + Ok(Amount { + raw: Uint::from_big_endian(&bytes), + }) } fn raw(&self) -> String { @@ -501,7 +514,9 @@ pub enum AmountParseError { InvalidRange, #[error("Error converting amount to decimal, number too large.")] ConvertToDecimal, - #[error("Could not convert from string, expected an unsigned 256-bit integer.")] + #[error( + "Could not convert from string, expected an unsigned 256-bit integer." + )] FromString, } @@ -513,13 +528,24 @@ impl From for Change { impl From for Amount { fn from(change: Change) -> Self { - Amount{raw: change.abs()} + Amount { raw: change.abs() } } } /// The four possible u64 words in a [`Uint`]. /// Used for converting to MASP amounts. -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, BorshSerialize, BorshDeserialize)] +#[derive( + Copy, + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + BorshSerialize, + BorshDeserialize, +)] #[repr(u8)] #[allow(missing_docs)] pub enum MaspDenom { @@ -543,7 +569,7 @@ impl From for MaspDenom { impl MaspDenom { /// Iterator over the possible denominations - pub fn iter() -> impl Iterator{ + pub fn iter() -> impl Iterator { (0u8..3).into_iter().map(Self::from) } @@ -757,8 +783,8 @@ impl TryFrom for Transfer { data.denom.split('/').last().ok_or(TransferError::NoToken)?; let token = Address::decode(token_str).map_err(TransferError::Address)?; - let amount = - DenominatedAmount::from_str(&data.amount).map_err(TransferError::Amount)?; + let amount = DenominatedAmount::from_str(&data.amount) + .map_err(TransferError::Amount)?; Ok(Self { source, target, diff --git a/shared/src/ledger/native_vp/governance/utils.rs b/shared/src/ledger/native_vp/governance/utils.rs index 59456906da..30e6a48fb9 100644 --- a/shared/src/ledger/native_vp/governance/utils.rs +++ b/shared/src/ledger/native_vp/governance/utils.rs @@ -172,7 +172,7 @@ where bond_amount(storage, ¶ms, &bond_id, epoch)? .1; - if amount != token::Amount::default() { + if !amount.is_zero() { if vote.is_yay() { let entry = yay_delegators .entry(voter_address.to_owned()) diff --git a/shared/src/ledger/queries/vp/pos.rs b/shared/src/ledger/queries/vp/pos.rs index 8836dba0a9..ccb5f40f44 100644 --- a/shared/src/ledger/queries/vp/pos.rs +++ b/shared/src/ledger/queries/vp/pos.rs @@ -166,7 +166,7 @@ where /// `None`. The total stake is a sum of validator's self-bonds and delegations /// to their address. /// Returns `None` when the given address is not a validator address. For a -/// validator with `0` stake, this returns `Ok(token::Amount::default())`. +/// validator with `0` stake, this returns `Ok(token::Amount::zero())`. fn validator_stake( ctx: RequestCtx<'_, D, H>, validator: Address, @@ -379,7 +379,7 @@ where let epoch = epoch.unwrap_or(ctx.wl_storage.storage.last_epoch); let handle = unbond_handle(&source, &validator); - let mut total = token::Amount::default(); + let mut total = token::Amount::zero(); for result in handle.iter(ctx.wl_storage)? { let ( lazy_map::NestedSubKey::Data { From a468d49af2a3cfc7b516e0a74a55f76f0b3780c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 4 Apr 2023 09:58:36 +0100 Subject: [PATCH 006/151] core/token: add `write_denom` --- core/src/ledger/storage_api/token.rs | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/core/src/ledger/storage_api/token.rs b/core/src/ledger/storage_api/token.rs index fe6348fbc4..793cba632b 100644 --- a/core/src/ledger/storage_api/token.rs +++ b/core/src/ledger/storage_api/token.rs @@ -20,17 +20,31 @@ where Ok(balance) } -/// Read the denomination of a given token, if any. +/// Read the denomination of a given token, if any. Note that native +/// transparent tokens do not have this set and instead use the constant +/// [`token::NATIVE_MAX_DECIMAL_PLACES`]. pub fn read_denom( storage: &S, token: &Address, ) -> storage_api::Result> - where - S: StorageRead, +where + S: StorageRead, +{ + let key = token::denom_key(token); + storage.read(&key) +} + +/// Write the denomination of a given token. +pub fn write_denom( + storage: &mut S, + token: &Address, + denom: token::Denomination, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, { let key = token::denom_key(token); - let denom = storage.read::(&key)?; - Ok(denom.map(Into::into)) + storage.write(&key, denom) } /// Transfer `token` from `src` to `dest`. Returns an `Err` if `src` has From 6bb685d31fef7d8d5ca5d65e299e46dd8665529e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 4 Apr 2023 10:04:35 +0100 Subject: [PATCH 007/151] core/transaction/wrapper: add a todo for later --- core/src/types/transaction/wrapper.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/types/transaction/wrapper.rs b/core/src/types/transaction/wrapper.rs index d3b3b5dce0..d23d0ca2cb 100644 --- a/core/src/types/transaction/wrapper.rs +++ b/core/src/types/transaction/wrapper.rs @@ -123,7 +123,8 @@ pub mod wrapper_tx { /// of GAS_LIMIT_RESOLUTION impl From for GasLimit { fn from(amount: Amount) -> GasLimit { - GasLimit::from(u128::from(amount) as u64) + // TODO: this may panic. + GasLimit::from(u128::try_from(amount).unwrap() as u64) } } From f17e75eeb43b213bc3efb82c0c0fde1de7f1c325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 4 Apr 2023 10:17:12 +0100 Subject: [PATCH 008/151] app/shell: use token::Amount::native_whole for min fee conversion --- apps/src/lib/node/ledger/shell/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index ef7fe504a1..c79d65a384 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -731,7 +731,7 @@ where &self.wl_storage, ) .expect("Must be able to read wrapper tx fees parameter"); - fees.unwrap_or(token::Amount::whole(MIN_FEE)) + fees.unwrap_or(token::Amount::native_whole(MIN_FEE)) } #[cfg(not(feature = "mainnet"))] From 8a64466231cbc80fb1b162a3ad86dfb112669c0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 4 Apr 2023 10:17:44 +0100 Subject: [PATCH 009/151] core/token: make conv from Amount to u128 falliable --- core/src/types/token.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 87e423d3c8..818851a726 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -394,9 +394,20 @@ impl From for Amount { } } -impl From for u128 { - fn from(amount: Amount) -> Self { - amount.raw.as_u128() +impl TryFrom for u128 { + type Error = std::io::Error; + + fn try_from(value: Amount) -> Result { + let Uint(arr) = value.raw; + for i in 2..4 { + if arr[i] != 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Integer overflow when casting to u128", + )); + } + } + Ok(value.raw.low_u128()) } } From de172189876d3de9d6a6a325c6d2790bd2320475 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 4 Apr 2023 10:20:18 +0100 Subject: [PATCH 010/151] WIP: shared/pos: update token to TM voting power conv --- shared/src/ledger/pos/mod.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/shared/src/ledger/pos/mod.rs b/shared/src/ledger/pos/mod.rs index ee4492a187..4d915547fb 100644 --- a/shared/src/ledger/pos/mod.rs +++ b/shared/src/ledger/pos/mod.rs @@ -27,9 +27,12 @@ pub const SLASH_POOL_ADDRESS: Address = /// from the number of tokens pub fn into_tm_voting_power( votes_per_token: Decimal, - tokens: impl Into, + tokens: token::Amount, ) -> i64 { - let prod = decimal_mult_u128(votes_per_token, tokens.into() as u128); + let prod = decimal_mult_u128( + votes_per_token, + u128::try_from(tokens).expect("TODO(Tomas): handle overflow"), + ); i64::try_from(prod).expect("Invalid validator voting power (i64)") } From 819f319b725497ba3d7108ac4138aa2562e658b0 Mon Sep 17 00:00:00 2001 From: satan Date: Thu, 6 Apr 2023 22:11:50 +0200 Subject: [PATCH 011/151] Fixed most of the masp compile issues. May not be logically correct yet --- apps/src/lib/cli.rs | 154 +++++- apps/src/lib/client/rpc.rs | 471 ++++++++++------ apps/src/lib/client/signing.rs | 6 +- apps/src/lib/client/tx.rs | 467 +++++++++------- apps/src/lib/config/genesis.rs | 16 +- core/src/ledger/storage/masp_conversions.rs | 56 +- core/src/ledger/testnet_pow.rs | 2 +- core/src/types/address.rs | 17 +- core/src/types/token.rs | 120 +++- core/src/types/transaction/wrapper.rs | 60 +- core/src/types/uint.rs | 10 +- proof_of_stake/src/lib.rs | 72 ++- proof_of_stake/src/types.rs | 21 +- proof_of_stake/src/types/rev_order.rs | 3 +- shared/src/ledger/ibc/vp/token.rs | 11 +- .../src/ledger/native_vp/governance/utils.rs | 13 +- shared/src/ledger/queries/router.rs | 522 +++++++++--------- shared/src/ledger/queries/shell.rs | 21 +- shared/src/ledger/queries/vp/mod.rs | 3 +- shared/src/ledger/queries/vp/token.rs | 11 +- tx_prelude/src/token.rs | 4 +- vp_prelude/src/token.rs | 4 +- 22 files changed, 1288 insertions(+), 776 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index c164c278e1..fa99319ed1 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -1567,6 +1567,7 @@ pub mod args { use std::str::FromStr; use namada::ibc::core::ics24_host::identifier::{ChannelId, PortId}; + use namada::ledger::queries::RPC; use namada::types::address::Address; use namada::types::chain::{ChainId, ChainIdPrefix}; use namada::types::governance::ProposalVote; @@ -1575,8 +1576,10 @@ pub mod args { use namada::types::storage::{self, Epoch}; use namada::types::time::DateTimeUtc; use namada::types::token; + use namada::types::token::NATIVE_MAX_DECIMAL_PLACES; use namada::types::transaction::GasLimit; use rust_decimal::Decimal; + use tendermint_rpc::HttpClient; use super::context::*; use super::utils::*; @@ -1591,7 +1594,7 @@ pub mod args { const ALIAS_OPT: ArgOpt = ALIAS.opt(); const ALIAS: Arg = arg("alias"); const ALLOW_DUPLICATE_IP: ArgFlag = flag("allow-duplicate-ip"); - const AMOUNT: Arg = arg("amount"); + const AMOUNT: Arg = arg("amount"); const ARCHIVE_DIR: ArgOpt = arg_opt("archive-dir"); const BALANCE_OWNER: ArgOpt = arg_opt("owner"); const BASE_DIR: ArgDefault = arg_default( @@ -1623,10 +1626,20 @@ pub mod args { const EPOCH: ArgOpt = arg_opt("epoch"); const FORCE: ArgFlag = flag("force"); const DONT_PREFETCH_WASM: ArgFlag = flag("dont-prefetch-wasm"); - const GAS_AMOUNT: ArgDefault = - arg_default("gas-amount", DefaultFn(|| token::Amount::zero())); - const GAS_LIMIT: ArgDefault = - arg_default("gas-limit", DefaultFn(|| token::Amount::zero())); + const GAS_AMOUNT: ArgDefault = arg_default( + "gas-amount", + DefaultFn(|| token::DenominatedAmount { + amount: token::Amount::default(), + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }), + ); + const GAS_LIMIT: ArgDefault = arg_default( + "gas-limit", + DefaultFn(|| token::DenominatedAmount { + amount: token::Amount::default(), + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }), + ); const GAS_TOKEN: ArgDefaultFromCtx = arg_default_from_ctx("gas-token", DefaultFn(|| "NAM".into())); const GENESIS_PATH: Arg = arg("genesis-path"); @@ -1873,20 +1886,64 @@ pub mod args { /// Transferred token address pub sub_prefix: Option, /// Transferred token amount - pub amount: token::Amount, + pub amount: token::DenominatedAmount, } impl TxTransfer { - pub fn parse_from_context( - &self, + pub async fn parse_from_context( + &mut self, ctx: &mut Context, ) -> ParsedTxTransferArgs { + let token = ctx.get(&self.token); ParsedTxTransferArgs { - tx: self.tx.parse_from_context(ctx), + tx: self.tx.parse_from_context(ctx).await, source: ctx.get_cached(&self.source), target: ctx.get(&self.target), - token: ctx.get(&self.token), - amount: self.amount, + amount: self.validate_amount(&token).await, + token, + } + } + + /// Get the correct representation of the amount given the token type. + async fn validate_amount(&mut self, token: &Address) -> token::Amount { + let client = + HttpClient::new(self.tx.ledger_address.clone()).unwrap(); + let denom = RPC + .vp() + .token() + .denomination(&client, token) + .await + .unwrap_or_else(|err| { + eprintln!("Error in the query {}", err); + safe_exit(1) + }) + .unwrap_or_else(|| { + println!( + "No denomination found for token: {token}, the input \ + arguments could + not be parsed." + ); + safe_exit(1); + }); + let input_amount = self.amount.canonical(); + if denom < input_amount.denom { + println!( + "The input amount contained a higher precision than \ + allowed by {token}." + ); + safe_exit(1); + } else { + let validated = input_amount + .increase_precision(denom) + .unwrap_or_else(|| { + println!( + "The amount provided requires more the 256 bits \ + to represent." + ); + safe_exit(1); + }); + self.amount = validated; + self.amount.amount } } } @@ -1968,7 +2025,7 @@ pub mod args { receiver, token, sub_prefix, - amount, + amount: amount.amount, port_id, channel_id, timeout_height, @@ -2185,6 +2242,14 @@ pub mod args { let tx = Tx::parse(matches); let validator = VALIDATOR.parse(matches); let amount = AMOUNT.parse(matches); + let amount = amount + .canonical() + .increase_precision(NATIVE_MAX_DECIMAL_PLACES.into()) + .unwrap_or_else(|| { + println!("Could not parse bond amount"); + safe_exit(1); + }) + .amount; let source = SOURCE_OPT.parse(matches); Self { tx, @@ -2224,6 +2289,14 @@ pub mod args { let tx = Tx::parse(matches); let validator = VALIDATOR.parse(matches); let amount = AMOUNT.parse(matches); + let amount = amount + .canonical() + .increase_precision(NATIVE_MAX_DECIMAL_PLACES.into()) + .unwrap_or_else(|| { + println!("Could not parse bond amount"); + safe_exit(1); + }) + .amount; let source = SOURCE_OPT.parse(matches); Self { tx, @@ -2858,7 +2931,7 @@ pub mod args { /// save it in the wallet. pub initialized_account_alias: Option, /// The amount being payed to include the transaction - pub fee_amount: token::Amount, + pub fee_amount: token::DenominatedAmount, /// The token in which the fee is being paid pub fee_token: WalletAddress, /// The max amount of gas used to process tx @@ -2870,7 +2943,11 @@ pub mod args { } impl Tx { - pub fn parse_from_context(&self, ctx: &mut Context) -> ParsedTxArgs { + pub async fn parse_from_context( + &mut self, + ctx: &mut Context, + ) -> ParsedTxArgs { + let fee_token = ctx.get(&self.fee_token); ParsedTxArgs { dry_run: self.dry_run, dump_tx: self.dump_tx, @@ -2880,8 +2957,8 @@ pub mod args { initialized_account_alias: self .initialized_account_alias .clone(), - fee_amount: self.fee_amount, - fee_token: ctx.get(&self.fee_token), + fee_amount: self.validate_amount(&fee_token).await, + fee_token, gas_limit: self.gas_limit.clone(), signing_key: self .signing_key @@ -2890,6 +2967,49 @@ pub mod args { signer: self.signer.as_ref().map(|signer| ctx.get(signer)), } } + + /// Get the correct representation of the fee amount given the token + /// type. + async fn validate_amount(&mut self, token: &Address) -> token::Amount { + let client = HttpClient::new(self.ledger_address.clone()).unwrap(); + let denom = RPC + .vp() + .token() + .denomination(&client, token) + .await + .unwrap_or_else(|err| { + eprintln!("Error in the query {}", err); + safe_exit(1) + }) + .unwrap_or_else(|| { + println!( + "No denomination found for token: {token}, the input \ + arguments could + not be parsed." + ); + safe_exit(1); + }); + let input_amount = self.fee_amount.canonical(); + if denom < input_amount.denom { + println!( + "The input amount contained a higher precision than \ + allowed by {token}." + ); + safe_exit(1); + } else { + let validated = input_amount + .increase_precision(denom) + .unwrap_or_else(|| { + println!( + "The amount provided requires more the 256 bits \ + to represent." + ); + safe_exit(1); + }); + self.fee_amount = validated; + self.fee_amount.amount + } + } } impl Args for Tx { @@ -2953,7 +3073,7 @@ pub mod args { let initialized_account_alias = ALIAS_OPT.parse(matches); let fee_amount = GAS_AMOUNT.parse(matches); let fee_token = GAS_TOKEN.parse(matches); - let gas_limit = GAS_LIMIT.parse(matches).into(); + let gas_limit = GAS_LIMIT.parse(matches).amount.into(); let signing_key = SIGNING_KEY_OPT.parse(matches); let signer = SIGNER.parse(matches); diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index d55b4d3bbf..c156e4ed8b 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -58,7 +58,8 @@ use tokio::time::{Duration, Instant}; use crate::cli::{self, args, Context}; use crate::client::tendermint_rpc_types::TxResponse; use crate::client::tx::{ - Conversions, PinnedBalanceError, TransactionDelta, TransferDelta, + Conversions, MaspDenominatedAmount, PinnedBalanceError, TransactionDelta, + TransferDelta, }; use crate::facade::tendermint::merkle::proof::Proof; use crate::facade::tendermint_config::net::Address as TendermintAddress; @@ -132,6 +133,16 @@ pub async fn query_epoch(client: &HttpClient) -> Epoch { unwrap_client_response(RPC.shell().epoch(client).await) } +/// Query the epoch of the given block height, if it exists. +/// Will return none if the input block height is greater than +/// the latest committed block height. +pub async fn query_epoch_at_height( + client: &HttpClient, + height: BlockHeight, +) -> Option { + unwrap_client_response(RPC.shell().epoch_at_height(client, &height).await) +} + /// Query the last committed block pub async fn query_block( args: args::Query, @@ -213,6 +224,11 @@ pub async fn query_tx_deltas( .txs; for response_tx in txs { let height = BlockHeight(response_tx.height.value()); + let epoch = + query_epoch_at_height(&client, height).await.expect( + "This block height should not exceed that latest \ + committed block height", + ); let idx = TxIndex(response_tx.index); // Only process yet unprocessed transactions which have been // accepted by node VPs @@ -522,15 +538,20 @@ pub async fn query_transparent_balance( .map(|c| Cow::Borrowed(*c)) .unwrap_or_else(|| Cow::Owned(token.to_string())); match query_storage_value::(&client, &key).await { - Some(balance) => match &args.sub_prefix { - Some(sub_prefix) => { - println!( - "{} with {}: {}", - currency_code, sub_prefix, balance - ); + Some(balance) => { + let balance = + format_denominated_amount(&client, &token, balance) + .await; + match &args.sub_prefix { + Some(sub_prefix) => { + println!( + "{} with {}: {}", + currency_code, sub_prefix, balance + ); + } + None => println!("{}: {}", currency_code, balance), } - None => println!("{}: {}", currency_code, balance), - }, + } None => { println!("No {} balance found for {}", currency_code, owner) } @@ -546,10 +567,12 @@ pub async fn query_transparent_balance( if let Some(balances) = balances { print_balances( ctx, + &client, balances, &token, owner.address().as_ref(), - ); + ) + .await; } } } @@ -559,7 +582,7 @@ pub async fn query_transparent_balance( let balances = query_storage_prefix::(&client, &prefix).await; if let Some(balances) = balances { - print_balances(ctx, balances, &token, None); + print_balances(ctx, &client, balances, &token, None).await; } } (None, None) => { @@ -568,7 +591,7 @@ pub async fn query_transparent_balance( let balances = query_storage_prefix::(&client, &key).await; if let Some(balances) = balances { - print_balances(ctx, balances, &token, None); + print_balances(ctx, &client, balances, &token, None).await; } } } @@ -655,25 +678,37 @@ pub async fn query_pinned_balance(ctx: &mut Context, args: args::QueryBalance) { } (Ok((balance, epoch)), Some(token)) => { let token = ctx.get(token); - // Extract and print only the specified token from the total - let (_asset_type, balance) = - value_by_address(&balance, token.clone(), epoch); let currency_code = tokens .get(&token) .map(|c| Cow::Borrowed(*c)) .unwrap_or_else(|| Cow::Owned(token.to_string())); - if balance == 0 { + let mut total_balance = token::Amount::default(); + for denom in MaspDenom::iter() { + // Extract and print only the specified token from the total + let (_asset_type, value) = + value_by_address(&balance, token.clone(), denom, epoch); + total_balance += token::Amount::from_masp_denominated( + value as u64, + denom, + ); + } + if total_balance.is_zero() { println!( "Payment address {} was consumed during epoch {}. \ Received no shielded {}", owner, epoch, currency_code ); } else { - let asset_value = token::Amount::from(balance as u64); + let formatted = format_denominated_amount( + &client, + &token, + total_balance, + ) + .await; println!( "Payment address {} was consumed during epoch {}. \ Received {} {}", - owner, epoch, asset_value, currency_code + owner, epoch, formatted, currency_code ); } } @@ -684,8 +719,11 @@ pub async fn query_pinned_balance(ctx: &mut Context, args: args::QueryBalance) { .shielded .decode_amount(client.clone(), balance, epoch) .await; - for (addr, value) in balance.components() { - let asset_value = token::Amount::from(*value as u64); + for ((addr, denom), value) in balance.components() { + let asset_value = token::Amount::from_masp_denominated( + *value as u64, + *denom, + ); if !found_any { println!( "Payment address {} was consumed during epoch {}. \ @@ -695,10 +733,13 @@ pub async fn query_pinned_balance(ctx: &mut Context, args: args::QueryBalance) { found_any = true; } let addr_enc = addr.encode(); + let formatted = + format_denominated_amount(&client, addr, asset_value) + .await; println!( " {}: {}", tokens.get(addr).cloned().unwrap_or(addr_enc.as_str()), - asset_value, + formatted, ); } if !found_any { @@ -713,15 +754,15 @@ pub async fn query_pinned_balance(ctx: &mut Context, args: args::QueryBalance) { } } -fn print_balances( +async fn print_balances( ctx: &Context, + client: &HttpClient, balances: impl Iterator, token: &Address, target: Option<&Address>, ) { let stdout = io::stdout(); let mut w = stdout.lock(); - // Token let tokens = address::tokens(); let currency_code = tokens @@ -729,40 +770,42 @@ fn print_balances( .map(|c| Cow::Borrowed(*c)) .unwrap_or_else(|| Cow::Owned(token.to_string())); writeln!(w, "Token {}", currency_code).unwrap(); - - let print_num = balances - .filter_map( - |(key, balance)| match token::is_any_multitoken_balance_key(&key) { - Some((sub_prefix, owner)) => Some(( - owner.clone(), - format!( - "with {}: {}, owned by {}", - sub_prefix, - balance, - lookup_alias(ctx, owner) - ), - )), - None => token::is_any_token_balance_key(&key).map(|owner| { + let mut print_num = 0; + for (key, balance) in balances { + let (o, s) = match token::is_any_multitoken_balance_key(&key) { + Some((sub_prefix, owner)) => ( + owner.clone(), + format!( + "with {}: {}, owned by {}", + sub_prefix, + format_denominated_amount(&client, &token, balance).await, + lookup_alias(ctx, owner) + ), + ), + None => { + if let Some(owner) = token::is_any_token_balance_key(&key) { ( owner.clone(), format!( ": {}, owned by {}", - balance, + format_denominated_amount(&client, &token, balance) + .await, lookup_alias(ctx, owner) ), ) - }), - }, - ) - .filter_map(|(o, s)| match target { - Some(t) if o == *t => Some(s), - Some(_) => None, - None => Some(s), - }) - .map(|s| { - writeln!(w, "{}", s).unwrap(); - }) - .count(); + } else { + continue; + } + } + }; + let s = match target { + Some(t) if o == *t => s, + Some(_) => continue, + None => s, + }; + writeln!(w, "{}", s).unwrap(); + print_num += 1; + } if print_num == 0 { match target { @@ -891,11 +934,12 @@ pub async fn query_proposal(_ctx: Context, args: args::QueryProposal) { pub fn value_by_address( amt: &masp_primitives::transaction::components::Amount, token: Address, + denom: MaspDenom, epoch: Epoch, ) -> (AssetType, i64) { // Compute the unique asset identifier from the token address let asset_type = AssetType::new( - (token, epoch.0) + (token, denom, epoch.0) .try_to_vec() .expect("token addresses should serialize") .as_ref(), @@ -1033,7 +1077,9 @@ pub async fn query_shielded_balance( .decode_asset_type(client.clone(), asset_type) .await; match decoded { - Some((addr, asset_epoch)) if asset_epoch == epoch => { + Some((addr, denom, asset_epoch)) + if asset_epoch == epoch => + { // Only assets with the current timestamp count let addr_enc = addr.encode(); println!( @@ -1043,22 +1089,29 @@ pub async fn query_shielded_balance( .cloned() .unwrap_or(addr_enc.as_str()) ); - read_tokens.insert(addr); + read_tokens.insert(addr.clone()); + let mut found_any = false; + for (fvk, value) in balances { + let value = token::Amount::from_masp_denominated( + value as u64, + denom, + ); + let formatted = format_denominated_amount( + &client, &addr, value, + ) + .await; + println!(" {}, owned by {}", formatted, fvk); + found_any = true; + } + if !found_any { + println!( + "No shielded {} balance found for any wallet \ + key", + asset_type + ); + } } - _ => continue, - } - - let mut found_any = false; - for (fvk, value) in balances { - let value = token::Amount::from(value as u64); - println!(" {}, owned by {}", value, fvk); - found_any = true; - } - if !found_any { - println!( - "No shielded {} balance found for any wallet key", - asset_type - ); + _ => {} } } // Print zero balances for remaining assets @@ -1077,42 +1130,49 @@ pub async fn query_shielded_balance( (Some(token), false) => { // Compute the unique asset identifier from the token address let token = ctx.get(&token); - let asset_type = AssetType::new( - (token.clone(), epoch.0) - .try_to_vec() - .expect("token addresses should serialize") - .as_ref(), - ) - .unwrap(); + let mut found_any = false; let currency_code = tokens .get(&token) .map(|c| Cow::Borrowed(*c)) .unwrap_or_else(|| Cow::Owned(token.to_string())); println!("Shielded Token {}:", currency_code); - let mut found_any = false; for fvk in viewing_keys { - // Query the multi-asset balance at the given spending key - let viewing_key = ExtendedFullViewingKey::from(fvk).fvk.vk; - let balance = if no_conversions { - ctx.shielded - .compute_shielded_balance(&viewing_key) - .expect("context should contain viewing key") - } else { - ctx.shielded - .compute_exchanged_balance( - client.clone(), - &viewing_key, - epoch, - ) - .await - .expect("context should contain viewing key") - }; - if balance[&asset_type] != 0 { - let asset_value = - token::Amount::from(balance[&asset_type] as u64); - println!(" {}, owned by {}", asset_value, fvk); - found_any = true; + let mut balance = token::Amount::default(); + for denom in MaspDenom::iter() { + let asset_type = AssetType::new( + (token.clone(), denom, epoch.0) + .try_to_vec() + .expect("token addresses should serialize") + .as_ref(), + ) + .unwrap(); + // Query the multi-asset balance at the given spending key + let viewing_key = ExtendedFullViewingKey::from(fvk).fvk.vk; + let denom_balance = if no_conversions { + ctx.shielded + .compute_shielded_balance(&viewing_key) + .expect("context should contain viewing key") + } else { + ctx.shielded + .compute_exchanged_balance( + client.clone(), + &viewing_key, + epoch, + ) + .await + .expect("context should contain viewing key") + }; + if denom_balance[&asset_type] != 0 { + balance += token::Amount::from_masp_denominated( + denom_balance[&asset_type] as u64, + denom, + ); + found_any = true; + } } + let formatted = + format_denominated_amount(&client, &token, balance).await; + println!(" {}, owned by {}", formatted, fvk); } if !found_any { println!( @@ -1137,7 +1197,8 @@ pub async fn query_shielded_balance( .shielded .decode_all_amounts(client.clone(), balance) .await; - print_decoded_balance_with_epoch(decoded_balance); + print_decoded_balance_with_epoch(&client, decoded_balance) + .await; } else { balance = ctx .shielded @@ -1153,48 +1214,66 @@ pub async fn query_shielded_balance( .shielded .decode_amount(client.clone(), balance, epoch) .await; - print_decoded_balance(decoded_balance); + print_decoded_balance(&client, decoded_balance).await; } } } } -pub fn print_decoded_balance(decoded_balance: Amount
) { +pub async fn print_decoded_balance( + client: &HttpClient, + decoded_balance: MaspDenominatedAmount, +) { let tokens = address::tokens(); - let mut found_any = false; - for (addr, value) in decoded_balance.components() { - let asset_value = token::Amount::from(*value as u64); - let addr_enc = addr.encode(); - println!( - "{} : {}", - tokens.get(addr).cloned().unwrap_or(addr_enc.as_str()), - asset_value - ); - found_any = true; + let mut balances = HashMap::new(); + for ((addr, denom), value) in decoded_balance.components() { + let asset_value = + token::Amount::from_masp_denominated(*value as u64, *denom); + balances + .entry(addr) + .and_modify(|val| *val += asset_value) + .or_insert(asset_value); } - if !found_any { + if balances.is_empty() { println!("No shielded balance found for given key"); + } else { + for (addr, amount) in balances { + let addr_enc = addr.encode(); + println!( + "{} : {}", + tokens.get(addr).cloned().unwrap_or(addr_enc.as_str()), + format_denominated_amount(client, addr, amount).await, + ); + } } } -pub fn print_decoded_balance_with_epoch( - decoded_balance: Amount<(Address, Epoch)>, +pub async fn print_decoded_balance_with_epoch( + client: &HttpClient, + decoded_balance: Amount<(Address, MaspDenom, Epoch)>, ) { let tokens = address::tokens(); - let mut found_any = false; - for ((addr, epoch), value) in decoded_balance.components() { - let asset_value = token::Amount::from(*value as u64); - let addr_enc = addr.encode(); - println!( - "{} | {} : {}", - tokens.get(addr).cloned().unwrap_or(addr_enc.as_str()), - epoch, - asset_value - ); - found_any = true; + let mut balances = HashMap::new(); + for ((addr, denom, epoch), value) in decoded_balance.components() { + let asset_value = + token::Amount::from_masp_denominated(*value as u64, *denom); + balances + .entry((addr, epoch)) + .and_modify(|val| *val += asset_value) + .or_insert(asset_value); } - if !found_any { + if balances.is_empty() { println!("No shielded balance found for given key"); + } else { + for ((addr, epoch), amount) in balances { + let addr_enc = addr.encode(); + println!( + "{} | {} : {}", + tokens.get(addr).cloned().unwrap_or(addr_enc.as_str()), + epoch, + format_denominated_amount(client, addr, amount).await, + ); + } } } @@ -1453,15 +1532,18 @@ pub async fn query_and_print_unbonds( .into_iter() .fold(token::Amount::zero(), |acc, (_, amount)| acc + amount); if total_withdrawable != token::Amount::zero() { - println!("Total withdrawable now: {total_withdrawable}."); + println!( + "Total withdrawable now: {}.", + total_withdrawable.to_string_native() + ); } if !not_yet_withdrawable.is_empty() { println!("Current epoch: {current_epoch}.") } for ((_start_epoch, withdraw_epoch), amount) in not_yet_withdrawable { println!( - "Amount {amount} withdrawable starting from epoch \ - {withdraw_epoch}." + "Amount {} withdrawable starting from epoch {withdraw_epoch}.", + amount.to_string_native() ); } } @@ -1498,14 +1580,14 @@ pub async fn query_bonds(ctx: Context, args: args::QueryBonds) { .bonds_and_unbonds(&client, &source, &validator) .await, ); - let mut bonds_total: token::Amount = 0.into(); - let mut bonds_total_slashed: token::Amount = 0.into(); - let mut unbonds_total: token::Amount = 0.into(); - let mut unbonds_total_slashed: token::Amount = 0.into(); - let mut total_withdrawable: token::Amount = 0.into(); + let mut bonds_total = token::Amount::default(); + let mut bonds_total_slashed = token::Amount::default(); + let mut unbonds_total = token::Amount::default(); + let mut unbonds_total_slashed = token::Amount::default(); + let mut total_withdrawable = token::Amount::default(); for (bond_id, details) in bonds_and_unbonds { - let mut total: token::Amount = 0.into(); - let mut total_slashed: token::Amount = 0.into(); + let mut total = token::Amount::default(); + let mut total_slashed = token::Amount::default(); let bond_type = if bond_id.source == bond_id.validator { format!("Self-bonds from {}", bond_id.validator) } else { @@ -1519,7 +1601,8 @@ pub async fn query_bonds(ctx: Context, args: args::QueryBonds) { writeln!( w, " Remaining active bond from epoch {}: Δ {}", - bond.start, bond.amount + bond.start, + bond.amount.to_string_native() ) .unwrap(); total += bond.amount; @@ -1529,18 +1612,18 @@ pub async fn query_bonds(ctx: Context, args: args::QueryBonds) { writeln!( w, "Active (slashed) bonds total: {}", - total - total_slashed + (total - total_slashed).to_string_native() ) .unwrap(); } - writeln!(w, "Bonds total: {}", total).unwrap(); + writeln!(w, "Bonds total: {}", total.to_string_native()).unwrap(); bonds_total += total; bonds_total_slashed += total_slashed; let mut withdrawable = token::Amount::zero(); if !details.unbonds.is_empty() { - let mut total: token::Amount = 0.into(); - let mut total_slashed: token::Amount = 0.into(); + let mut total = token::Amount::default(); + let mut total_slashed = token::Amount::default(); let bond_type = if bond_id.source == bond_id.validator { format!("Unbonded self-bonds from {}", bond_id.validator) } else { @@ -1553,36 +1636,43 @@ pub async fn query_bonds(ctx: Context, args: args::QueryBonds) { writeln!( w, " Withdrawable from epoch {} (active from {}): Δ {}", - unbond.withdraw, unbond.start, unbond.amount + unbond.withdraw, + unbond.start, + unbond.amount.to_string_native() ) .unwrap(); } withdrawable = total - total_slashed; - writeln!(w, "Unbonded total: {}", total).unwrap(); + writeln!(w, "Unbonded total: {}", total.to_string_native()) + .unwrap(); unbonds_total += total; unbonds_total_slashed += total_slashed; total_withdrawable += withdrawable; } - writeln!(w, "Withdrawable total: {}", withdrawable).unwrap(); + writeln!(w, "Withdrawable total: {}", withdrawable.to_string_native()) + .unwrap(); println!(); } if bonds_total != bonds_total_slashed { println!( "All bonds total active: {}", - bonds_total - bonds_total_slashed + (bonds_total - bonds_total_slashed).to_string_native() ); } - println!("All bonds total: {}", bonds_total); + println!("All bonds total: {}", bonds_total.to_string_native()); if unbonds_total != unbonds_total_slashed { println!( "All unbonds total active: {}", - unbonds_total - unbonds_total_slashed + (unbonds_total - unbonds_total_slashed).to_string_native() ); } - println!("All unbonds total: {}", unbonds_total); - println!("All unbonds total withdrawable: {}", total_withdrawable); + println!("All unbonds total: {}", unbonds_total.to_string_native()); + println!( + "All unbonds total withdrawable: {}", + total_withdrawable.to_string_native() + ); } /// Query PoS bonded stake @@ -1602,7 +1692,10 @@ pub async fn query_bonded_stake(ctx: Context, args: args::QueryBondedStake) { Some(stake) => { // TODO: show if it's in consensus set, below capacity, or // below threshold set - println!("Bonded stake of validator {validator}: {stake}",) + println!( + "Bonded stake of validator {validator}: {}", + stake.to_string_native() + ) } None => { println!("No bonded stake found for {validator}") @@ -1629,8 +1722,13 @@ pub async fn query_bonded_stake(ctx: Context, args: args::QueryBondedStake) { writeln!(w, "Consensus validators:").unwrap(); for val in consensus { - writeln!(w, " {}: {}", val.address.encode(), val.bonded_stake) - .unwrap(); + writeln!( + w, + " {}: {}", + val.address.encode(), + val.bonded_stake.to_string_native() + ) + .unwrap(); } if !below_capacity.is_empty() { writeln!(w, "Below capacity validators:").unwrap(); @@ -1639,7 +1737,7 @@ pub async fn query_bonded_stake(ctx: Context, args: args::QueryBondedStake) { w, " {}: {}", val.address.encode(), - val.bonded_stake + val.bonded_stake.to_string_native() ) .unwrap(); } @@ -1648,7 +1746,10 @@ pub async fn query_bonded_stake(ctx: Context, args: args::QueryBondedStake) { } let total_staked_tokens = get_total_staked_tokens(&client, epoch).await; - println!("Total bonded stake: {total_staked_tokens}"); + println!( + "Total bonded stake: {}", + total_staked_tokens.to_string_native() + ); } /// Query and return validator's commission rate and max commission rate change @@ -1890,7 +1991,7 @@ pub async fn query_conversions(ctx: Context, args: args::QueryConversions) { .expect("Conversions should be defined"); // Track whether any non-sentinel conversions are found let mut conversions_found = false; - for (addr, epoch, conv, _) in conv_state.assets.values() { + for ((addr, _), epoch, conv, _) in conv_state.assets.values() { let amt: masp_primitives::transaction::components::Amount = conv.clone().into(); // If the user has specified any targets, then meet them @@ -1914,7 +2015,7 @@ pub async fn query_conversions(ctx: Context, args: args::QueryConversions) { for (asset_type, val) in amt.components() { // Look up the address and epoch of asset to facilitate pretty // printing - let (addr, epoch, _, _) = &conv_state.assets[asset_type]; + let ((addr, _), epoch, _, _) = &conv_state.assets[asset_type]; // Now print out this component of the conversion let addr_enc = addr.encode(); print!( @@ -1941,6 +2042,7 @@ pub async fn query_conversion( asset_type: AssetType, ) -> Option<( Address, + MaspDenom, Epoch, masp_primitives::transaction::components::Amount, MerklePath, @@ -2262,7 +2364,8 @@ pub async fn get_proposal_votes( get_validator_stake(client, epoch, &voter_address) .await .unwrap_or_default() - .into(); + .try_into() + .expect("Amount out of bounds"); yay_validators.insert(voter_address, amount); } else if !validators.contains(&voter_address) { let validator_address = @@ -2282,13 +2385,19 @@ pub async fn get_proposal_votes( if vote.is_yay() { let entry = yay_delegators.entry(voter_address).or_default(); - entry - .insert(validator_address, VotePower::from(amount)); + entry.insert( + validator_address, + VotePower::try_from(amount) + .expect("Amount out of bounds"), + ); } else { let entry = nay_delegators.entry(voter_address).or_default(); - entry - .insert(validator_address, VotePower::from(amount)); + entry.insert( + validator_address, + VotePower::try_from(amount) + .expect("Amount out of bounds"), + ); } } } @@ -2346,7 +2455,8 @@ pub async fn get_proposal_offline_votes( ) .await .unwrap_or_default() - .into(); + .try_into() + .expect("Amount out of bounds"); yay_validators.insert(proposal_vote.address, amount); } else if is_delegator_at( client, @@ -2391,12 +2501,20 @@ pub async fn get_proposal_offline_votes( let entry = yay_delegators .entry(proposal_vote.address.clone()) .or_default(); - entry.insert(validator, VotePower::from(delegated_amount)); + entry.insert( + validator, + VotePower::try_from(delegated_amount) + .expect("Amount out of bounds"), + ); } else { let entry = nay_delegators .entry(proposal_vote.address.clone()) .or_default(); - entry.insert(validator, VotePower::from(delegated_amount)); + entry.insert( + validator, + VotePower::try_from(delegated_amount) + .expect("Amount out of bounds"), + ); } } @@ -2487,8 +2605,10 @@ pub async fn compute_tally( epoch: Epoch, votes: Votes, ) -> ProposalResult { - let total_staked_tokens: VotePower = - get_total_staked_tokens(client, epoch).await.into(); + let total_staked_tokens: VotePower = get_total_staked_tokens(client, epoch) + .await + .try_into() + .expect("Amount out of bounds"); let Votes { yay_validators, @@ -2631,7 +2751,8 @@ pub async fn get_governance_parameters(client: &HttpClient) -> GovParams { .expect("Parameter should be definied."); GovParams { - min_proposal_fund: u64::from(min_proposal_fund), + min_proposal_fund: u128::try_from(min_proposal_fund) + .expect("Amount out of bounds") as u64, max_proposal_code_size, min_proposal_period, max_proposal_period, @@ -2650,7 +2771,9 @@ fn lookup_alias(ctx: &Context, addr: &Address) -> String { } /// A helper to unwrap client's response. Will shut down process on error. -fn unwrap_client_response(response: Result) -> T { +pub(super) fn unwrap_client_response( + response: Result, +) -> T { response.unwrap_or_else(|err| { eprintln!("Error in the query {}", err); cli::safe_exit(1) @@ -2659,7 +2782,7 @@ fn unwrap_client_response(response: Result) -> T { /// Look up the denomination of a token in order to format it /// correctly as a string. -async fn format_denominated_amount( +pub(super) async fn format_denominated_amount( client: &HttpClient, token: &Address, amount: token::Amount, @@ -2676,3 +2799,17 @@ async fn format_denominated_amount( }); DenominatedAmount { amount, denom }.to_string() } + +/// Make asset type corresponding to given address and epoch +pub fn make_asset_type( + epoch: Epoch, + token: &Address, + denom: MaspDenom, +) -> AssetType { + // Typestamp the chosen token with the current epoch + let token_bytes = (token, denom, epoch.0) + .try_to_vec() + .expect("token should serialize"); + // Generate the unique asset identifier from the unique token address + AssetType::new(token_bytes.as_ref()).expect("unable to create asset type") +} diff --git a/apps/src/lib/client/signing.rs b/apps/src/lib/client/signing.rs index 9b1a00b987..e37423d652 100644 --- a/apps/src/lib/client/signing.rs +++ b/apps/src/lib/client/signing.rs @@ -238,7 +238,7 @@ pub async fn sign_wrapper( let client = HttpClient::new(args.ledger_address.clone()).unwrap(); let fee_amount = if cfg!(feature = "mainnet") { - Amount::whole(MIN_FEE) + Amount::native_whole(MIN_FEE) } else { let wrapper_tx_fees_key = parameter_storage::get_wrapper_tx_fees_key(); rpc::query_storage_value::(&client, &wrapper_tx_fees_key) @@ -255,7 +255,9 @@ pub async fn sign_wrapper( if balance < fee_amount { eprintln!( "The wrapper transaction source doesn't have enough balance to \ - pay fee {fee_amount}, got {balance}." + pay fee {}, got {}.", + fee_amount.to_string_native(), + balance.to_string_native(), ); if !args.force && cfg!(feature = "mainnet") { cli::safe_exit(1); diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 921f0eaa1e..6f2575bb0d 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -30,6 +30,7 @@ use masp_primitives::transaction::components::{Amount, OutPoint, TxOut}; use masp_primitives::transaction::Transaction; use masp_primitives::zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}; use masp_proofs::prover::LocalTxProver; +use namada::core::types::uint::Uint; use namada::ibc::applications::ics20_fungible_token_transfer::msgs::transfer::MsgTransfer; use namada::ibc::signer::Signer; use namada::ibc::timestamp::Timestamp as IbcTimestamp; @@ -39,6 +40,7 @@ use namada::ibc_proto::cosmos::base::v1beta1::Coin; use namada::ledger::governance::storage as gov_storage; use namada::ledger::masp; use namada::ledger::pos::{CommissionPair, PosParams}; +use namada::ledger::queries::RPC; use namada::proto::Tx; use namada::types::address::{masp, masp_tx_key, Address}; use namada::types::governance::{ @@ -50,7 +52,10 @@ use namada::types::storage::{ BlockHeight, Epoch, Key, KeySeg, TxIndex, RESERVED_ADDRESS_PREFIX, }; use namada::types::time::DateTimeUtc; -use namada::types::token::{Transfer, HEAD_TX_KEY, PIN_KEY_PREFIX, TX_KEY_PREFIX, MaspDenom}; +use namada::types::token::{ + DenominatedAmount, MaspDenom, Transfer, HEAD_TX_KEY, PIN_KEY_PREFIX, + TX_KEY_PREFIX, +}; use namada::types::transaction::governance::{ InitProposalData, VoteProposalData, }; @@ -66,7 +71,10 @@ use super::rpc; use super::types::ShieldedTransferContext; use crate::cli::context::WalletAddress; use crate::cli::{args, safe_exit, Context}; -use crate::client::rpc::{query_conversion, query_storage_value}; +use crate::client::rpc::{ + format_denominated_amount, make_asset_type, query_conversion, + query_storage_value, unwrap_client_response, +}; use crate::client::signing::{find_keypair, sign_tx, tx_signer, TxSigningKey}; use crate::client::tendermint_rpc_types::{TxBroadcastData, TxResponse}; use crate::client::types::ParsedTxTransferArgs; @@ -475,10 +483,10 @@ pub type Conversions = pub type MaspDenominatedAmount = Amount<(Address, MaspDenom)>; /// Represents the changes that were made to a list of transparent accounts -pub type TransferDelta = HashMap; +pub type TransferDelta = HashMap>; /// Represents the changes that were made to a list of shielded accounts -pub type TransactionDelta = HashMap; +pub type TransactionDelta = HashMap; /// Represents the current state of the shielded pool from the perspective of /// the chosen viewing keys. @@ -511,7 +519,7 @@ pub struct ShieldedContext { /// The set of note positions that have been spent spents: HashSet, /// Maps asset types to their decodings - asset_types: HashMap, + asset_types: HashMap, /// Maps note positions to their corresponding viewing keys vk_map: HashMap, } @@ -836,19 +844,25 @@ impl ShieldedContext { .expect("found note with invalid value or asset type"); } } + // Record the changes to the transparent accounts - let transparent_delta = - Amount::from_nonnegative(tx.token.clone(), u64::from(tx.amount)) - .expect("invalid value for amount"); let mut transfer_delta = TransferDelta::new(); - transfer_delta - .insert(tx.source.clone(), Amount::zero() - &transparent_delta); - transfer_delta.insert(tx.target.clone(), transparent_delta); + for denom in MaspDenom::iter() { + let transparent_delta = + Amount::from_nonnegative((tx.token.clone(), denom), denom.denominate(&tx.amount.amount)) + .expect("invalid value for amount"); + if transparent_delta == Amount::zero() { + continue; + } + transfer_delta + .insert(tx.source.clone(), Amount::zero() - &transparent_delta); + transfer_delta.insert(tx.target.clone(), transparent_delta); + self.last_txidx += 1; + } self.delta_map.insert( (height, index), (epoch, transfer_delta, transaction_delta), ); - self.last_txidx += 1; } /// Summarize the effects on shielded and transparent accounts of each @@ -895,16 +909,22 @@ impl ShieldedContext { &mut self, client: HttpClient, asset_type: AssetType, - ) -> Option<(Address, Epoch)> { + ) -> Option<(Address, MaspDenom, Epoch)> { // 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, ep, _conv, _path): (Address, _, Amount, MerklePath) = - query_conversion(client, asset_type).await?; - self.asset_types.insert(asset_type, (addr.clone(), ep)); - Some((addr, ep)) + let (addr, denom, ep, _conv, _path): ( + Address, + MaspDenom, + _, + Amount, + MerklePath, + ) = query_conversion(client, asset_type).await?; + self.asset_types + .insert(asset_type, (addr.clone(), denom, ep)); + Some((addr, denom, ep)) } /// Query the ledger for the conversion that is allowed for the given asset @@ -919,9 +939,9 @@ impl ShieldedContext { Entry::Occupied(conv_entry) => Some(conv_entry.into_mut()), Entry::Vacant(conv_entry) => { // Query for the ID of the last accepted transaction - let (addr, ep, conv, path): (Address, _, _, _) = + let (addr, denom, ep, conv, path): (Address, _, _, _, _) = query_conversion(client, asset_type).await?; - self.asset_types.insert(asset_type, (addr, ep)); + self.asset_types.insert(asset_type, (addr, denom, ep)); // If the conversion is 0, then we just have a pure decoding if conv == Amount::zero() { None @@ -1004,7 +1024,7 @@ impl ShieldedContext { pub async fn compute_exchanged_amount( &mut self, client: HttpClient, - mut input: MaspDenominatedAmount, + mut input: Amount, target_epoch: Epoch, mut conversions: Conversions, ) -> (Amount, Conversions) { @@ -1017,7 +1037,9 @@ impl ShieldedContext { let target_asset_type = self .decode_asset_type(client.clone(), asset_type) .await - .map(|(addr, _epoch)| make_asset_type(target_epoch, &addr)) + .map(|(addr, denom, _epoch)| { + make_asset_type(target_epoch, &addr, denom) + }) .unwrap_or(asset_type); let at_target_asset_type = asset_type == target_asset_type; if let (Some((conv, _wit, usage)), false) = ( @@ -1261,8 +1283,8 @@ impl ShieldedContext { self.decode_asset_type(client.clone(), *asset_type).await; // Only assets with the target timestamp count match decoded { - Some((addr, epoch)) if epoch == target_epoch => { - res += &Amount::from_pair(addr, *val).unwrap() + Some((addr, denom, epoch)) if epoch == target_epoch => { + res += &Amount::from_pair((addr, denom), *val).unwrap() } _ => {} } @@ -1276,40 +1298,31 @@ impl ShieldedContext { &mut self, client: HttpClient, amt: Amount, - ) -> Amount<(Address, Epoch)> { + ) -> Amount<(Address, MaspDenom, Epoch)> { let mut res = Amount::zero(); for (asset_type, val) in amt.components() { // Decode the asset type let decoded = self.decode_asset_type(client.clone(), *asset_type).await; // Only assets with the target timestamp count - if let Some((addr, epoch)) = decoded { - res += &Amount::from_pair((addr, epoch), *val).unwrap() + if let Some((addr, denom, epoch)) = decoded { + res += &Amount::from_pair((addr, denom, epoch), *val).unwrap() } } res } } -/// Make asset type corresponding to given address and epoch -fn make_asset_type(epoch: Epoch, token: &Address) -> AssetType { - // Typestamp the chosen token with the current epoch - let token_bytes = (token, epoch.0) - .try_to_vec() - .expect("token should serialize"); - // Generate the unique asset identifier from the unique token address - AssetType::new(token_bytes.as_ref()).expect("unable to create asset type") -} - /// Convert Namada amount and token type to MASP equivalents fn convert_amount( epoch: Epoch, token: &Address, - val: token::Amount, + val: u64, + denom: MaspDenom, ) -> (AssetType, Amount) { - let asset_type = make_asset_type(epoch, token); + let asset_type = make_asset_type(epoch, token, denom); // Combine the value and unit into one amount - let amount = Amount::from_nonnegative(asset_type, u64::from(val)) + let amount = Amount::from_nonnegative(asset_type, val) .expect("invalid value for amount"); (asset_type, amount) } @@ -1335,106 +1348,13 @@ where let epoch = ctx.query_epoch(args.tx.ledger_address.clone()).await; // Context required for storing which notes are in the source's possesion let consensus_branch_id = BranchId::Sapling; - let amt: u64 = args.amount.into(); let memo: Option = None; - - // Now we build up the transaction within this object - let mut builder = Builder::::new(0u32); - // Convert transaction amount into MASP types - let (asset_type, amount) = convert_amount(epoch, &args.token, args.amount); - // Transactions with transparent input and shielded output // may be affected if constructed close to epoch boundary let mut epoch_sensitive: bool = false; - // If there are shielded inputs - if let Some(sk) = spending_key { - // Transaction fees need to match the amount in the wrapper Transfer - // when MASP source is used - let (_, fee) = - convert_amount(epoch, &args.tx.fee_token, args.tx.fee_amount); - builder.set_fee(fee.clone())?; - // If the gas is coming from the shielded pool, then our shielded inputs - // must also cover the gas fee - let required_amt = if shielded_gas { amount + fee } else { amount }; - // Locate unspent notes that can help us meet the transaction amount - let (_, unspent_notes, used_convs) = ctx - .collect_unspent_notes( - args.tx.ledger_address.clone(), - &to_viewing_key(&sk).vk, - required_amt, - epoch, - ) - .await; - // Commit the notes found to our transaction - for (diversifier, note, merkle_path) in unspent_notes { - builder.add_sapling_spend(sk, diversifier, note, merkle_path)?; - } - // Commit the conversion notes used during summation - for (conv, wit, value) in used_convs.values() { - if *value > 0 { - builder.add_convert( - conv.clone(), - *value as u64, - wit.clone(), - )?; - } - } - } else { - // No transfer fees come from the shielded transaction for non-MASP - // sources - builder.set_fee(Amount::zero())?; - // We add a dummy UTXO to our transaction, but only the source of the - // parent Transfer object is used to validate fund availability - let secp_sk = - secp256k1::SecretKey::from_slice(&[0xcd; 32]).expect("secret key"); - let secp_ctx = secp256k1::Secp256k1::::gen_new(); - let secp_pk = - secp256k1::PublicKey::from_secret_key(&secp_ctx, &secp_sk) - .serialize(); - let hash = - ripemd160::Ripemd160::digest(&sha2::Sha256::digest(&secp_pk)); - let script = TransparentAddress::PublicKey(hash.into()).script(); - epoch_sensitive = true; - builder.add_transparent_input( - secp_sk, - OutPoint::new([0u8; 32], 0), - TxOut { - asset_type, - value: amt, - script_pubkey: script, - }, - )?; - } - // Now handle the outputs of this transaction - // If there is a shielded output - if let Some(pa) = payment_address { - let ovk_opt = spending_key.map(|x| x.expsk.ovk); - builder.add_sapling_output( - ovk_opt, - pa.into(), - asset_type, - amt, - memo.clone(), - )?; - } else { - epoch_sensitive = false; - // Embed the transparent target address into the shielded transaction so - // that it can be signed - let target_enc = args - .target - .address() - .expect("target address should be transparent") - .try_to_vec() - .expect("target address encoding"); - let hash = ripemd160::Ripemd160::digest(&sha2::Sha256::digest( - target_enc.as_ref(), - )); - builder.add_transparent_output( - &TransparentAddress::PublicKey(hash.into()), - asset_type, - amt, - )?; - } + + // Now we build up the transaction within this object + let mut builder = Builder::::new(0u32); let prover = if let Ok(params_dir) = env::var(masp::ENV_VAR_MASP_PARAMS_DIR) { let params_dir = PathBuf::from(params_dir); @@ -1446,28 +1366,76 @@ where LocalTxProver::with_default_location() .expect("unable to load MASP Parameters") }; - // Build and return the constructed transaction - let mut tx = builder.build(consensus_branch_id, &prover); - - if epoch_sensitive { - let new_epoch = ctx.query_epoch(args.tx.ledger_address.clone()).await; - - // If epoch has changed, recalculate shielded outputs to match new epoch - if new_epoch != epoch { - // Hack: build new shielded transfer with updated outputs - let mut replay_builder = Builder::::new(0u32); - replay_builder.set_fee(Amount::zero())?; - let ovk_opt = spending_key.map(|x| x.expsk.ovk); - let (new_asset_type, _) = - convert_amount(new_epoch, &args.token, args.amount); - replay_builder.add_sapling_output( - ovk_opt, - payment_address.unwrap().into(), - new_asset_type, - amt, - memo, - )?; - + let fee = if spending_key.is_some() { + // Transaction fees need to match the amount in the wrapper Transfer + // when MASP source is used. This amount should be <= `u64::MAX`. + let (_, fee) = convert_amount( + epoch, + &args.tx.fee_token, + MaspDenom::Zero.denominate(&args.tx.fee_amount), + MaspDenom::Zero, + ); + builder.set_fee(fee.clone())?; + fee + } else { + // No transfer fees come from the shielded transaction for non-MASP + // sources + builder.set_fee(Amount::zero())?; + Amount::zero() + }; + let mut epoch_transitions = vec![]; + // break up a transfer into a number of transfers with suitable + // denominations + for denom in MaspDenom::iter() { + let denom_amount = denom.denominate(&args.amount); + if denom_amount == 0 { + continue; + } + // Convert transaction amount into MASP types + let (asset_type, amount) = + convert_amount(epoch, &args.token, denom_amount, denom); + + // If there are shielded inputs + if let Some(sk) = spending_key { + // If the gas is coming from the shielded pool, then our shielded + // inputs must also cover the gas fee + let required_amt = if shielded_gas { + amount + fee.clone() + } else { + amount + }; + // Locate unspent notes that can help us meet the transaction amount + let (_, unspent_notes, used_convs) = ctx + .collect_unspent_notes( + args.tx.ledger_address.clone(), + &to_viewing_key(&sk).vk, + required_amt, + epoch, + ) + .await; + // Commit the notes found to our transaction + for (diversifier, note, merkle_path) in unspent_notes { + builder.add_sapling_spend( + sk, + diversifier, + note, + merkle_path, + )?; + } + // Commit the conversion notes used during summation + for (conv, wit, value) in used_convs.values() { + if *value > 0 { + builder.add_convert( + conv.clone(), + *value as u64, + wit.clone(), + )?; + } + } + } else { + // We add a dummy UTXO to our transaction, but only the source of + // the parent Transfer object is used to validate fund + // availability let secp_sk = secp256k1::SecretKey::from_slice(&[0xcd; 32]) .expect("secret key"); let secp_ctx = @@ -1478,33 +1446,124 @@ where let hash = ripemd160::Ripemd160::digest(&sha2::Sha256::digest(&secp_pk)); let script = TransparentAddress::PublicKey(hash.into()).script(); - replay_builder.add_transparent_input( + epoch_sensitive = true; + builder.add_transparent_input( secp_sk, OutPoint::new([0u8; 32], 0), TxOut { - asset_type: new_asset_type, - value: amt, + asset_type, + value: denom_amount, script_pubkey: script, }, )?; + } - let (replay_tx, _) = - replay_builder.build(consensus_branch_id, &prover)?; - tx = tx.map(|(t, tm)| { - let mut temp = t.deref().clone(); - temp.shielded_outputs = replay_tx.shielded_outputs.clone(); - temp.value_balance = temp.value_balance.reject(asset_type) - - Amount::from_pair(new_asset_type, amt).unwrap(); - (temp.freeze().unwrap(), tm) - }); + // Now handle the outputs of this transaction + // If there is a shielded output + if let Some(pa) = payment_address { + let ovk_opt = spending_key.map(|x| x.expsk.ovk); + builder.add_sapling_output( + ovk_opt, + pa.into(), + asset_type, + denom_amount, + memo.clone(), + )?; + } else { + epoch_sensitive = false; + // Embed the transparent target address into the shielded + // transaction so that it can be signed + let target_enc = args + .target + .address() + .expect("target address should be transparent") + .try_to_vec() + .expect("target address encoding"); + let hash = ripemd160::Ripemd160::digest(&sha2::Sha256::digest( + target_enc.as_ref(), + )); + builder.add_transparent_output( + &TransparentAddress::PublicKey(hash.into()), + asset_type, + denom_amount, + )?; } - } + if epoch_sensitive { + let new_epoch = + ctx.query_epoch(args.tx.ledger_address.clone()).await; + + // If epoch has changed, recalculate shielded outputs to match new + // epoch + if new_epoch != epoch { + // Hack: build new shielded transfer with updated outputs + let mut replay_builder = + Builder::::new(0u32); + replay_builder.set_fee(Amount::zero())?; + let ovk_opt = spending_key.map(|x| x.expsk.ovk); + let (new_asset_type, _) = + convert_amount(new_epoch, &args.token, denom_amount, denom); + replay_builder.add_sapling_output( + ovk_opt, + payment_address.unwrap().into(), + new_asset_type, + denom_amount, + memo.clone(), + )?; + + let secp_sk = secp256k1::SecretKey::from_slice(&[0xcd; 32]) + .expect("secret key"); + let secp_ctx = + secp256k1::Secp256k1::::gen_new(); + let secp_pk = + secp256k1::PublicKey::from_secret_key(&secp_ctx, &secp_sk) + .serialize(); + let hash = ripemd160::Ripemd160::digest(&sha2::Sha256::digest( + &secp_pk, + )); + let script = + TransparentAddress::PublicKey(hash.into()).script(); + replay_builder.add_transparent_input( + secp_sk, + OutPoint::new([0u8; 32], 0), + TxOut { + asset_type: new_asset_type, + value: denom_amount, + script_pubkey: script, + }, + )?; + let (replay_tx, _) = + replay_builder.build(consensus_branch_id, &prover)?; + epoch_transitions.push(( + replay_tx, + asset_type, + new_asset_type, + denom_amount, + )); + } + } + } + // Build and return the constructed transaction + let mut tx = builder.build(consensus_branch_id, &prover); + tx = tx.map(|(t, tm)| { + let mut temp = t.deref().clone(); + let mut shielded_outputs = vec![]; + for (replay_tx, asset_type, new_asset_type, denom_amount) in + epoch_transitions + { + let mut replay_outputs = replay_tx.shielded_outputs.clone(); + shielded_outputs.append(&mut replay_outputs); + temp.value_balance = temp.value_balance.reject(asset_type) + - Amount::from_pair(new_asset_type, denom_amount).unwrap(); + } + temp.shielded_outputs = shielded_outputs; + (temp.freeze().unwrap(), tm) + }); tx.map(Some) } -pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { - let parsed_args = args.parse_from_context(&mut ctx); +pub async fn submit_transfer(mut ctx: Context, mut args: args::TxTransfer) { + let parsed_args = args.parse_from_context(&mut ctx).await; let source = parsed_args.source.effective_address(); let target = parsed_args.target.effective_address(); // Check that the source address exists on chain @@ -1557,12 +1616,18 @@ pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { match rpc::query_storage_value::(&client, &balance_key).await { Some(balance) => { - if balance < args.amount { + if balance < parsed_args.amount { + let balance_amount = format_denominated_amount( + &client, + &parsed_args.token, + balance, + ) + .await; eprintln!( "The balance of the source {} of token {} is lower than \ the amount to be transferred. Amount to transfer is {} \ and the balance is {}.", - source, parsed_args.token, args.amount, balance + source, parsed_args.token, args.amount, balance_amount ); if !args.tx.force { safe_exit(1) @@ -1590,19 +1655,19 @@ pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { // TODO Refactor me, we shouldn't rely on any specific token here. ( TxSigningKey::SecretKey(masp_tx_key()), - 0.into(), + token::Amount::default(), ctx.native_token.clone(), ) } else if source == masp_addr { ( TxSigningKey::SecretKey(masp_tx_key()), - args.amount, + parsed_args.amount, parsed_args.token.clone(), ) } else { ( TxSigningKey::WalletAddress(args.source.to_address()), - args.amount, + parsed_args.amount, parsed_args.token.clone(), ) }; @@ -1621,13 +1686,25 @@ pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { #[cfg(not(feature = "mainnet"))] let is_source_faucet = rpc::is_faucet_account(&source, args.tx.ledger_address.clone()).await; - + let denom = unwrap_client_response( + RPC.vp() + .token() + .denomination(&client, &parsed_args.token) + .await, + ) + .unwrap_or_else(|| { + println!( + "No denomination found for token: {token}, defaulting to zero \ + decimal places" + ); + 0.into() + }); let transfer = token::Transfer { source, target, token, sub_prefix, - amount, + amount: DenominatedAmount { amount, denom }, key, shielded: { let spending_key = parsed_args.source.spending_key(); @@ -1733,11 +1810,16 @@ pub async fn submit_ibc_transfer(ctx: Context, args: args::TxIbcTransfer) { { Some(balance) => { if balance < args.amount { + let formatted_amount = + format_denominated_amount(&client, &token, args.amount) + .await; + let formatted_balance = + format_denominated_amount(&client, &token, balance).await; eprintln!( "The balance of the source {} of token {} is lower than \ the amount to be transferred. Amount to transfer is {} \ and the balance is {}.", - source, token, args.amount, balance + source, token, formatted_amount, formatted_balance ); if !args.tx.force { safe_exit(1) @@ -1763,7 +1845,7 @@ pub async fn submit_ibc_transfer(ctx: Context, args: args::TxIbcTransfer) { }; let token = Some(Coin { denom, - amount: args.amount.to_string(), + amount: Uint::from(args.amount).to_string(), }); // this height should be that of the destination chain, not this chain @@ -1923,7 +2005,9 @@ pub async fn submit_init_proposal(mut ctx: Context, args: args::InitProposal) { .await .unwrap_or_default(); if balance - < token::Amount::from(governance_parameters.min_proposal_fund) + < token::Amount::native_whole( + governance_parameters.min_proposal_fund, + ) { eprintln!( "Address {} doesn't have enough funds.", @@ -2329,13 +2413,15 @@ pub async fn submit_bond(ctx: Context, args: args::Bond) { match rpc::query_storage_value::(&client, &balance_key).await { Some(balance) => { - println!("Found source balance {}", balance); + println!("Found source balance {}", balance.to_string_native()); if balance < args.amount { eprintln!( "The balance of the source {} is lower than the amount to \ be transferred. Amount to transfer is {} and the balance \ is {}.", - bond_source, args.amount, balance + bond_source, + args.amount.to_string_native(), + balance.to_string_native() ); if !args.tx.force { safe_exit(1) @@ -2392,13 +2478,13 @@ pub async fn submit_unbond(ctx: Context, args: args::Unbond) { let bond_source = source.clone().unwrap_or_else(|| validator.clone()); let bond_amount = rpc::query_bond(&client, &bond_source, &validator, None).await; - println!("BOND AMOUNT REMAINING IS {}", bond_amount); - if args.amount > bond_amount { eprintln!( "The total bonds of the source {} is lower than the amount to be \ unbonded. Amount to unbond is {} and the total bonds is {}.", - bond_source, args.amount, bond_amount + bond_source, + args.amount.to_string_native(), + bond_amount.to_string_native() ); if !args.tx.force { safe_exit(1) @@ -2459,7 +2545,7 @@ pub async fn submit_withdraw(ctx: Context, args: args::Withdraw) { Some(epoch), ) .await; - if tokens == 0.into() { + if tokens.is_zero() { eprintln!( "There are no unbonded bonds ready to withdraw in the current \ epoch {}.", @@ -2470,7 +2556,10 @@ pub async fn submit_withdraw(ctx: Context, args: args::Withdraw) { safe_exit(1) } } else { - println!("Found {tokens} tokens that can be withdrawn."); + println!( + "Found {} tokens that can be withdrawn.", + tokens.to_string_native() + ); println!("Submitting transaction to withdraw them..."); } diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index 0dbb385208..d072395e45 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -324,7 +324,7 @@ pub mod genesis_config { pos_data: GenesisValidator { address: Address::decode(config.address.as_ref().unwrap()) .unwrap(), - tokens: token::Amount::whole(config.tokens.unwrap_or_default()), + tokens: token::Amount::native_whole(config.tokens.unwrap_or_default()), consensus_key: config .consensus_public_key .as_ref() @@ -373,7 +373,7 @@ pub mod genesis_config { .unwrap() .to_dkg_public_key() .unwrap(), - non_staked_balance: token::Amount::whole( + non_staked_balance: token::Amount::native_whole( config.non_staked_balance.unwrap_or_default(), ), validator_vp_code_path: validator_vp_config.filename.to_owned(), @@ -463,7 +463,7 @@ pub mod genesis_config { } } }, - token::Amount::whole(*amount), + token::Amount::native_whole(*amount), ) }) .collect(), @@ -895,7 +895,7 @@ pub fn genesis() -> Genesis { let validator = Validator { pos_data: GenesisValidator { address, - tokens: token::Amount::whole(200_000), + tokens: token::Amount::native_whole(200_000), consensus_key: consensus_keypair.ref_to(), commission_rate: dec!(0.05), max_commission_rate_change: dec!(0.01), @@ -903,7 +903,7 @@ pub fn genesis() -> Genesis { account_key: account_keypair.ref_to(), protocol_key: protocol_keypair.ref_to(), dkg_public_key: dkg_keypair.public(), - non_staked_balance: token::Amount::whole(100_000), + non_staked_balance: token::Amount::native_whole(100_000), // TODO replace with https://github.com/anoma/namada/issues/25) validator_vp_code_path: vp_user_path.into(), validator_vp_sha256: Default::default(), @@ -925,7 +925,7 @@ pub fn genesis() -> Genesis { pos_gain_d: dec!(0.1), staked_ratio: dec!(0.0), pos_inflation_amount: 0, - wrapper_tx_fees: Some(token::Amount::whole(0)), + wrapper_tx_fees: Some(token::Amount::native_whole(0)), }; let albert = EstablishedAccount { address: wallet::defaults::albert_address(), @@ -958,8 +958,8 @@ pub fn genesis() -> Genesis { let implicit_accounts = vec![ImplicitAccount { public_key: wallet::defaults::daewon_keypair().ref_to(), }]; - let default_user_tokens = token::Amount::whole(1_000_000); - let default_key_tokens = token::Amount::whole(1_000); + let default_user_tokens = token::Amount::native_whole(1_000_000); + let default_key_tokens = token::Amount::native_whole(1_000); let balances: HashMap = HashMap::from_iter([ // established accounts' balances (wallet::defaults::albert_address(), default_user_tokens), diff --git a/core/src/ledger/storage/masp_conversions.rs b/core/src/ledger/storage/masp_conversions.rs index 7b5e788801..f6a9bb9641 100644 --- a/core/src/ledger/storage/masp_conversions.rs +++ b/core/src/ledger/storage/masp_conversions.rs @@ -10,6 +10,7 @@ use masp_primitives::sapling::Node; use crate::types::address::Address; use crate::types::storage::Epoch; +use crate::types::token::MaspDenom; /// A representation of the conversion state #[derive(Debug, Default, BorshSerialize, BorshDeserialize)] @@ -19,7 +20,10 @@ pub struct ConversionState { /// The tree currently containing all the conversions pub tree: FrozenCommitmentTree, /// Map assets to their latest conversion and position in Merkle tree - pub assets: BTreeMap, + pub assets: BTreeMap< + AssetType, + ((Address, MaspDenom), Epoch, AllowedConversion, usize), + >, } // This is only enabled when "wasm-runtime" is on, because we're using rayon @@ -55,16 +59,17 @@ where // with the zeroth epoch to minimize the number of convert notes clients // have to use. This trick works under the assumption that reward tokens // from different epochs are exactly equivalent. - let reward_asset_bytes = (address::nam(), 0u64) + let reward_asset_bytes = (address::nam(), MaspDenom::Zero, 0u64) .try_to_vec() .expect("unable to serialize address and epoch"); let reward_asset = AssetType::new(reward_asset_bytes.as_ref()) .expect("unable to derive asset identifier"); // Conversions from the previous to current asset for each address - let mut current_convs = BTreeMap::::new(); + let mut current_convs = + BTreeMap::<(Address, MaspDenom), AllowedConversion>::new(); // Reward all tokens according to above reward rates - for (addr, reward) in &masp_rewards { - // Dispence a transparent reward in parallel to the shielded rewards + for ((addr, denom), reward) in &masp_rewards { + // Dispense a transparent reward in parallel to the shielded rewards let addr_bal: token::Amount = wl_storage .read(&token::balance_key(addr, &masp_addr))? .unwrap_or_default(); @@ -76,12 +81,18 @@ where // 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 - let old_asset = - encode_asset_type(addr.clone(), wl_storage.storage.last_epoch); - let new_asset = - encode_asset_type(addr.clone(), wl_storage.storage.block.epoch); - current_convs.insert( + let old_asset = encode_asset_type( + addr.clone(), + *denom, + wl_storage.storage.last_epoch, + ); + let new_asset = encode_asset_type( addr.clone(), + *denom, + wl_storage.storage.block.epoch, + ); + current_convs.insert( + (addr.clone(), *denom), (MaspAmount::from_pair(old_asset, -(reward.1 as i64)).unwrap() + MaspAmount::from_pair(new_asset, reward.1).unwrap() + MaspAmount::from_pair(reward_asset, reward.0).unwrap()) @@ -91,7 +102,7 @@ where wl_storage.storage.conversion_state.assets.insert( old_asset, ( - addr.clone(), + (addr.clone(), *denom), wl_storage.storage.last_epoch, MaspAmount::zero().into(), 0, @@ -119,9 +130,9 @@ where .into_par_iter() .with_min_len(notes_per_thread_min) .with_max_len(notes_per_thread_max) - .map(|(idx, (addr, _epoch, conv, pos))| { + .map(|(idx, (asset, _epoch, conv, pos))| { // Use transitivity to update conversion - *conv += current_convs[addr].clone(); + *conv += current_convs[asset].clone(); // Update conversion position to leaf we are about to create *pos = idx; // The merkle tree need only provide the conversion commitment, @@ -162,15 +173,18 @@ 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_rewards.keys() { + for (addr, denom) in masp_rewards.keys() { // Add the decoding entry for the new asset type. An uncommited // node position is used since this is not a conversion. - let new_asset = - encode_asset_type(addr.clone(), wl_storage.storage.block.epoch); + let new_asset = encode_asset_type( + addr.clone(), + *denom, + wl_storage.storage.block.epoch, + ); wl_storage.storage.conversion_state.assets.insert( new_asset, ( - addr.clone(), + (addr.clone(), *denom), wl_storage.storage.block.epoch, MaspAmount::zero().into(), wl_storage.storage.conversion_state.tree.size(), @@ -195,8 +209,12 @@ where } /// Construct MASP asset type with given epoch for given token -pub fn encode_asset_type(addr: Address, epoch: Epoch) -> AssetType { - let new_asset_bytes = (addr, epoch.0) +pub fn encode_asset_type( + addr: Address, + denom: MaspDenom, + epoch: Epoch, +) -> AssetType { + let new_asset_bytes = (addr, denom, epoch.0) .try_to_vec() .expect("unable to serialize address and epoch"); AssetType::new(new_asset_bytes.as_ref()) diff --git a/core/src/ledger/testnet_pow.rs b/core/src/ledger/testnet_pow.rs index bed2273a70..88b3aac392 100644 --- a/core/src/ledger/testnet_pow.rs +++ b/core/src/ledger/testnet_pow.rs @@ -511,7 +511,7 @@ mod test_with_tx_and_vp_env { fn test_challenge_and_solution() -> storage_api::Result<()> { let faucet_address = address::testing::established_address_1(); let difficulty = Difficulty::try_new(1).unwrap(); - let withdrawal_limit = token::Amount::whole(1_000); + let withdrawal_limit = token::Amount::native_whole(1_000); let mut tx_env = TestTxEnv::default(); diff --git a/core/src/types/address.rs b/core/src/types/address.rs index ae96842f8c..9c3985ff81 100644 --- a/core/src/types/address.rs +++ b/core/src/types/address.rs @@ -14,6 +14,7 @@ use thiserror::Error; use crate::types::key; use crate::types::key::PublicKeyHash; +use crate::types::token::MaspDenom; /// The length of an established [`Address`] encoded with Borsh. pub const ESTABLISHED_ADDRESS_BYTES_LEN: usize = 45; @@ -575,15 +576,15 @@ pub fn tokens() -> HashMap { /// Temporary helper for testing, a hash map of tokens addresses with their /// MASP XAN incentive schedules. If the reward is (a, b) then a rewarded tokens /// are dispensed for every b possessed tokens. -pub fn masp_rewards() -> HashMap { +pub fn masp_rewards() -> HashMap<(Address, MaspDenom), (u64, u64)> { vec![ - (nam(), (0, 100)), - (btc(), (1, 100)), - (eth(), (2, 100)), - (dot(), (3, 100)), - (schnitzel(), (4, 100)), - (apfel(), (5, 100)), - (kartoffel(), (6, 100)), + ((nam(), MaspDenom::Zero), (0, 100)), + ((btc(), MaspDenom::Zero), (1, 100)), + ((eth(), MaspDenom::Zero), (2, 100)), + ((dot(), MaspDenom::Zero), (3, 100)), + ((schnitzel(), MaspDenom::Zero), (4, 100)), + ((apfel(), MaspDenom::Zero), (5, 100)), + ((kartoffel(), MaspDenom::Zero), (6, 100)), ] .into_iter() .collect() diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 818851a726..5a426f3013 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -293,6 +293,41 @@ impl DenominatedAmount { } string } + + /// Find the minimal precision that holds this value losslessly. + /// This equates to stripping trailing zeros after the decimal + /// place. + pub fn canonical(self) -> Self { + let mut value = self.amount.raw; + let ten = Uint::from(10); + let mut denom = self.denom.0; + for _ in 0..self.denom.0 { + let (div, rem) = value.div_mod(ten); + if rem == Uint::zero() { + value = div; + denom -= 1; + } + } + Self { + amount: Amount { raw: value }, + denom: denom.into(), + } + } + + /// Attempt to increase the precision of an amount. Can fail + /// if the resulting amount does not fit into 256 bits. + pub fn increase_precision(self, denom: Denomination) -> Option { + if denom.0 < self.denom.0 { + return None; + } + Uint::from(10) + .checked_pow(Uint::from(denom.0 - self.denom.0)) + .and_then(|scaling| self.amount.raw.checked_mul(scaling)) + .map(|amount| Self { + amount: Amount { raw: amount }, + denom, + }) + } } impl Display for DenominatedAmount { @@ -307,11 +342,41 @@ impl FromStr for DenominatedAmount { type Err = AmountParseError; fn from_str(s: &str) -> Result { - let decimal = - Decimal::from_str(s).map_err(AmountParseError::InvalidDecimal)?; - let denom = Denomination(decimal.scale() as u8); + let precision = s.find('.').map(|pos| s.len() - pos); + let digits = s + .chars() + .filter_map(|c| { + if c.is_numeric() { + c.to_digit(10).map(Uint::from) + } else { + None + } + }) + .rev() + .collect::>(); + if digits.len() != s.len() && precision.is_none() + || digits.len() != s.len() - 1 && precision.is_some() + { + return Err(AmountParseError::NotNumeric); + } + if digits.len() > 77 { + return Err(AmountParseError::ScaleTooLarge( + digits.len() as u32, + 77, + )); + } + let mut value = Uint::default(); + let ten = Uint::from(10); + for (pow, digit) in digits.into_iter().enumerate() { + value = ten + .checked_pow(Uint::from(pow)) + .and_then(|scaling| scaling.checked_mul(digit)) + .and_then(|scaled| value.checked_add(scaled)) + .ok_or(AmountParseError::InvalidRange)?; + } + let denom = Denomination(precision.unwrap_or_default() as u8); Ok(Self { - amount: Amount::from_decimal(decimal, denom)?, + amount: Amount { raw: value }, denom, }) } @@ -399,8 +464,8 @@ impl TryFrom for u128 { fn try_from(value: Amount) -> Result { let Uint(arr) = value.raw; - for i in 2..4 { - if arr[i] != 0 { + for word in arr.iter().skip(2) { + if *word != 0 { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, "Integer overflow when casting to u128", @@ -529,6 +594,8 @@ pub enum AmountParseError { "Could not convert from string, expected an unsigned 256-bit integer." )] FromString, + #[error("Could not parse string as a correctly formatted number.")] + NotNumeric, } impl From for Change { @@ -543,6 +610,12 @@ impl From for Amount { } } +impl From for Uint { + fn from(amount: Amount) -> Self { + amount.raw + } +} + /// The four possible u64 words in a [`Uint`]. /// Used for converting to MASP amounts. #[derive( @@ -819,27 +892,34 @@ mod tests { /// starting to lose precision. #[test] fn test_token_amount_decimal_conversion(raw_amount in 0..2_u64.pow(51)) { - let amount = Amount::from(raw_amount); + let amount = Amount::from_uint(raw_amount, NATIVE_MAX_DECIMAL_PLACES).expect("Test failed"); // A round-trip conversion to and from Decimal should be an identity - let decimal = Decimal::from(amount); - let identity = Amount::from(decimal); + let decimal = Decimal::from(raw_amount); + let identity = Amount::from_decimal(decimal, NATIVE_MAX_DECIMAL_PLACES).expect("Test failed"); assert_eq!(amount, identity); } } #[test] fn test_token_display() { - let max = Amount::from(u64::MAX); - assert_eq!("18446744073709.551615", max.to_string()); - - let whole = Amount::from(u64::MAX / NATIVE_SCALE * NATIVE_SCALE); - assert_eq!("18446744073709", whole.to_string()); - - let trailing_zeroes = Amount::from(123000); - assert_eq!("0.123", trailing_zeroes.to_string()); - - let zero = Amount::from(0); - assert_eq!("0", zero.to_string()); + let max = Amount::from_uint(u64::MAX, NATIVE_MAX_DECIMAL_PLACES) + .expect("Test failed"); + assert_eq!("18446744073709.551615", max.to_string_native()); + + let whole = Amount::from_uint( + u64::MAX / NATIVE_SCALE * NATIVE_SCALE, + NATIVE_MAX_DECIMAL_PLACES, + ) + .expect("Test failed"); + assert_eq!("18446744073709", whole.to_string_native()); + + let trailing_zeroes = + Amount::from_uint(123000, NATIVE_MAX_DECIMAL_PLACES) + .expect("Test failed"); + assert_eq!("0.123", trailing_zeroes.to_string_native()); + + let zero = Amount::default(); + assert_eq!("0", zero.to_string_native()); } #[test] diff --git a/core/src/types/transaction/wrapper.rs b/core/src/types/transaction/wrapper.rs index d23d0ca2cb..604dc1dd78 100644 --- a/core/src/types/transaction/wrapper.rs +++ b/core/src/types/transaction/wrapper.rs @@ -20,6 +20,7 @@ pub mod wrapper_tx { use crate::types::transaction::{ hash_tx, EncryptionKey, Hash, TxError, TxType, }; + use crate::types::uint::Uint; /// Minimum fee amount in micro NAMs pub const MIN_FEE: u64 = 100; @@ -81,39 +82,46 @@ pub mod wrapper_tx { BorshDeserialize, BorshSchema, )] - #[serde(from = "u64")] - #[serde(into = "u64")] + #[serde(from = "Uint")] + #[serde(into = "Uint")] pub struct GasLimit { - multiplier: u64, + multiplier: Uint, } impl GasLimit { /// We refund unused gas up to GAS_LIMIT_RESOLUTION - pub fn refund_amount(&self, used_gas: u64) -> Amount { - Amount::native_whole(if used_gas < (u64::from(self) - GAS_LIMIT_RESOLUTION) { - // we refund only up to GAS_LIMIT_RESOLUTION - GAS_LIMIT_RESOLUTION - } else if used_gas >= u64::from(self) { - // Gas limit was under estimated, no refund - 0 - } else { - // compute refund - u64::from(self) - used_gas - }) + pub fn refund_amount(&self, used_gas: Uint) -> Amount { + Amount::from_uint( + if used_gas + < (Uint::from(self) - Uint::from(GAS_LIMIT_RESOLUTION)) + { + // we refund only up to GAS_LIMIT_RESOLUTION + Uint::from(GAS_LIMIT_RESOLUTION) + } else if used_gas >= Uint::from(self) { + // Gas limit was under estimated, no refund + Uint::from(0) + } else { + // compute refund + Uint::from(self) - used_gas + }, + 0, + ) + .unwrap() } } /// Round the input number up to the next highest multiple /// of GAS_LIMIT_RESOLUTION - impl From for GasLimit { - fn from(amount: u64) -> GasLimit { - if GAS_LIMIT_RESOLUTION * (amount / GAS_LIMIT_RESOLUTION) < amount { + impl From for GasLimit { + fn from(amount: Uint) -> GasLimit { + let gas_limit_resolution = Uint::from(GAS_LIMIT_RESOLUTION); + if gas_limit_resolution * (amount / gas_limit_resolution) < amount { GasLimit { - multiplier: (amount / GAS_LIMIT_RESOLUTION) + 1, + multiplier: (amount / gas_limit_resolution) + 1, } } else { GasLimit { - multiplier: (amount / GAS_LIMIT_RESOLUTION), + multiplier: (amount / gas_limit_resolution), } } } @@ -123,21 +131,20 @@ pub mod wrapper_tx { /// of GAS_LIMIT_RESOLUTION impl From for GasLimit { fn from(amount: Amount) -> GasLimit { - // TODO: this may panic. - GasLimit::from(u128::try_from(amount).unwrap() as u64) + GasLimit::from(Uint::from(amount)) } } /// Get back the gas limit as a raw number - impl From<&GasLimit> for u64 { - fn from(limit: &GasLimit) -> u64 { + impl From<&GasLimit> for Uint { + fn from(limit: &GasLimit) -> Uint { limit.multiplier * GAS_LIMIT_RESOLUTION } } /// Get back the gas limit as a raw number - impl From for u64 { - fn from(limit: GasLimit) -> u64 { + impl From for Uint { + fn from(limit: GasLimit) -> Uint { limit.multiplier * GAS_LIMIT_RESOLUTION } } @@ -145,7 +152,8 @@ pub mod wrapper_tx { /// Get back the gas limit as a raw number, viewed as an Amount impl From for Amount { fn from(limit: GasLimit) -> Amount { - Amount::native_whole(limit.multiplier * GAS_LIMIT_RESOLUTION) + Amount::from_uint(limit.multiplier * GAS_LIMIT_RESOLUTION, 0) + .unwrap() } } diff --git a/core/src/types/uint.rs b/core/src/types/uint.rs index 01d315829c..4d0d19484b 100644 --- a/core/src/types/uint.rs +++ b/core/src/types/uint.rs @@ -8,6 +8,7 @@ use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use impl_num_traits::impl_uint_num_traits; use serde::{Deserialize, Serialize}; use uint::construct_uint; + use crate::types::token; construct_uint! { @@ -51,7 +52,9 @@ pub const MAX_SIGNED_VALUE: Uint = Uint([u64::MAX, u64::MAX, u64::MAX, 9223372036854775807]); /// A signed 256 big integer. -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +#[derive( + Copy, Clone, Debug, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize, +)] pub struct SignedUint(Uint); impl SignedUint { @@ -90,7 +93,8 @@ impl SignedUint { impl From for SignedUint { fn from(val: u64) -> Self { - SignedUint::try_from(Uint::from(val)).expect("A u64 will always fit in this type") + SignedUint::try_from(Uint::from(val)) + .expect("A u64 will always fit in this type") } } @@ -161,4 +165,4 @@ impl Sub for SignedUint { fn sub(self, rhs: Self) -> Self::Output { self + (-rhs) } -} \ No newline at end of file +} diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index c65a539e72..e3fdcde9f2 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -20,7 +20,7 @@ pub mod types; // pub mod validation; //#[cfg(test)] -//mod tests; +// mod tests; use core::fmt::Debug; use std::collections::{BTreeMap, HashMap, HashSet}; @@ -42,6 +42,7 @@ use namada_core::types::key::{ }; pub use namada_core::types::storage::Epoch; use namada_core::types::token; +use namada_core::types::token::{Amount, NATIVE_MAX_DECIMAL_PLACES}; use once_cell::unsync::Lazy; use parameters::PosParams; use rust_decimal::Decimal; @@ -56,7 +57,6 @@ use storage::{ ReverseOrdTokenAmount, UnbondDetails, WeightedValidator, }; use thiserror::Error; -use namada_core::types::token::{Amount, NATIVE_MAX_DECIMAL_PLACES}; use types::{ BelowCapacityValidatorSet, BelowCapacityValidatorSets, Bonds, CommissionRates, ConsensusValidator, ConsensusValidatorSet, @@ -729,7 +729,10 @@ where S: StorageRead + StorageWrite, { let amount = amount.change(); - tracing::debug!("Bonding token amount {} at epoch {current_epoch}", amount.to_string_native()); + tracing::debug!( + "Bonding token amount {} at epoch {current_epoch}", + amount.to_string_native() + ); let params = read_pos_params(storage)?; let pipeline_epoch = current_epoch + params.pipeline_len; if let Some(source) = source { @@ -1329,7 +1332,10 @@ where S: StorageRead + StorageWrite, { let amount = amount.change(); - tracing::debug!("Unbonding token amount {} at epoch {current_epoch}", amount.to_string_native()); + tracing::debug!( + "Unbonding token amount {} at epoch {current_epoch}", + amount.to_string_native() + ); let params = read_pos_params(storage)?; let pipeline_epoch = current_epoch + params.pipeline_len; tracing::debug!( @@ -1375,7 +1381,8 @@ where if amount > remaining_at_pipeline { return Err(UnbondError::UnbondAmountGreaterThanBond( token::Amount::from_change(amount).to_string_native(), - token::Amount::from_change(remaining_at_pipeline).to_string_native(), + token::Amount::from_change(remaining_at_pipeline) + .to_string_native(), ) .into()); } @@ -1591,7 +1598,8 @@ where ) = unbond?; tracing::debug!( - "Unbond delta ({start_epoch}..{withdraw_epoch}), amount {}", amount.to_string_native() + "Unbond delta ({start_epoch}..{withdraw_epoch}), amount {}", + amount.to_string_native() ); // TODO: worry about updating this later after PR 740 perhaps @@ -1615,10 +1623,14 @@ where .unwrap_or_default() { let slash_rate = slash_type.get_slash_rate(¶ms); - let to_slash = token::Amount::from_uint(decimal_mult_u128( - slash_rate, - u128::from(amount), - ), NATIVE_MAX_DECIMAL_PLACES).expect("Amount out of bounds"); + let to_slash = token::Amount::from_uint( + decimal_mult_u128( + slash_rate, + u128::try_from(amount).expect("Amount out of bounds"), + ), + NATIVE_MAX_DECIMAL_PLACES, + ) + .expect("Amount out of bounds"); slashed += to_slash; } } @@ -1626,7 +1638,10 @@ where unbonds_to_remove.push((withdraw_epoch, start_epoch)); } withdrawable_amount -= slashed; - tracing::debug!("Withdrawing total {}", withdrawable_amount.to_string_native()); + tracing::debug!( + "Withdrawing total {}", + withdrawable_amount.to_string_native() + ); // Remove the unbond data from storage for (withdraw_epoch, start_epoch) in unbonds_to_remove { @@ -1726,9 +1741,13 @@ where read_validator_stake(storage, params, validator, current_epoch)? .unwrap_or_default(); let slashed_amount = Amount::from_uint( - decimal_mult_u128(rate, u128::from(current_stake)), - NATIVE_MAX_DECIMAL_PLACES - ).expect("Amount out of bounds"); + decimal_mult_u128( + rate, + u128::try_from(current_stake).expect("Amount out of bounds"), + ), + NATIVE_MAX_DECIMAL_PLACES, + ) + .expect("Amount out of bounds"); let token_change = -token::Change::from(slashed_amount); // Update validator sets and deltas at the pipeline length @@ -1869,7 +1888,8 @@ where continue; } let current_slashed = - mult_change_to_amount(slash_type.get_slash_rate(params), delta).change(); + mult_change_to_amount(slash_type.get_slash_rate(params), delta) + .change(); let delta = token::Amount::from_change(delta - current_slashed); total += delta; if bond_epoch <= epoch { @@ -1917,7 +1937,8 @@ where ) = validator.unwrap(); tracing::debug!( - "Consensus validator address {address}, stake {}", cur_stake.to_string_native() + "Consensus validator address {address}, stake {}", + cur_stake.to_string_native() ); // Check if the validator was consensus in the previous epoch with @@ -1937,13 +1958,17 @@ where .unwrap_or_default(); into_tm_voting_power( params.tm_votes_per_token, - u128::from(prev_validator_stake) as u64, + u128::try_from(prev_validator_stake) + .expect("Amount out of bounds") + as u64, ) }); let cur_tm_voting_power = Lazy::new(|| { into_tm_voting_power( params.tm_votes_per_token, - u128::from(cur_stake) as u64, + u128::try_from(cur_stake) + .expect("Amount out of bounds") + as u64, ) }); @@ -1980,7 +2005,9 @@ where ); Some(ValidatorSetUpdate::Consensus(ConsensusValidator { consensus_key, - bonded_stake: u128::from(cur_stake) as u64, + bonded_stake: u128::try_from(cur_stake) + .expect("Amount out of bounds") + as u64, })) }); let cur_below_capacity_validators = @@ -1999,7 +2026,8 @@ where let cur_stake = token::Amount::from(cur_stake); tracing::debug!( - "Below-capacity validator address {address}, stake {}", cur_stake.to_string_native() + "Below-capacity validator address {address}, stake {}", + cur_stake.to_string_native() ); let prev_tm_voting_power = previous_epoch @@ -2012,7 +2040,9 @@ where .unwrap_or_default(); into_tm_voting_power( params.tm_votes_per_token, - u128::from(prev_validator_stake) as u64, + u128::try_from(prev_validator_stake) + .expect("Amount out of bounds") + as u64, ) }) .unwrap_or_default(); diff --git a/proof_of_stake/src/types.rs b/proof_of_stake/src/types.rs index 7a8af3eebb..70759250ed 100644 --- a/proof_of_stake/src/types.rs +++ b/proof_of_stake/src/types.rs @@ -19,10 +19,10 @@ use namada_core::types::address::Address; use namada_core::types::key::common; use namada_core::types::storage::{Epoch, KeySeg}; use namada_core::types::token; +use namada_core::types::token::{Amount, NATIVE_MAX_DECIMAL_PLACES}; pub use rev_order::ReverseOrdTokenAmount; use rust_decimal::prelude::{Decimal, ToPrimitive}; use rust_decimal::RoundingStrategy; -use namada_core::types::token::{Amount, NATIVE_MAX_DECIMAL_PLACES}; use crate::parameters::PosParams; @@ -242,7 +242,8 @@ impl Display for WeightedValidator { write!( f, "{} with bonded stake {}", - self.address, self.bonded_stake.to_string_native() + self.address, + self.bonded_stake.to_string_native() ) } } @@ -469,21 +470,17 @@ pub fn mult_change_to_amount( ) -> token::Amount { // this function is used for slashing calculations. We want to err // on the side of slashing more, not less. - let dec = dec.round_dp_with_strategy(6, RoundingStrategy::ToPositiveInfinity); - Amount::from_decimal( - dec, - NATIVE_MAX_DECIMAL_PLACES - ).unwrap() * change.abs() + let dec = + dec.round_dp_with_strategy(6, RoundingStrategy::ToPositiveInfinity); + Amount::from_decimal(dec, NATIVE_MAX_DECIMAL_PLACES).unwrap() * change.abs() } /// Multiply a value of type Decimal with one of type Amount and then return the /// truncated Amount pub fn mult_amount(dec: Decimal, amount: token::Amount) -> token::Amount { - let dec = dec.round_dp_with_strategy(6, RoundingStrategy::ToPositiveInfinity); - Amount::from_decimal( - dec, - NATIVE_MAX_DECIMAL_PLACES - ).unwrap() * amount + let dec = + dec.round_dp_with_strategy(6, RoundingStrategy::ToPositiveInfinity); + Amount::from_decimal(dec, NATIVE_MAX_DECIMAL_PLACES).unwrap() * amount } /// Calculate voting power in the tendermint context (which is stored as i64) diff --git a/proof_of_stake/src/types/rev_order.rs b/proof_of_stake/src/types/rev_order.rs index 19749fabd4..57619941d4 100644 --- a/proof_of_stake/src/types/rev_order.rs +++ b/proof_of_stake/src/types/rev_order.rs @@ -54,7 +54,8 @@ impl std::str::FromStr for ReverseOrdTokenAmount { type Err = token::AmountParseError; fn from_str(s: &str) -> Result { - let amount = token::Amount::from_str(s, token::NATIVE_MAX_DECIMAL_PLACES)?; + let amount = + token::Amount::from_str(s, token::NATIVE_MAX_DECIMAL_PLACES)?; Ok(Self(amount)) } } diff --git a/shared/src/ledger/ibc/vp/token.rs b/shared/src/ledger/ibc/vp/token.rs index e460301330..0f34024da4 100644 --- a/shared/src/ledger/ibc/vp/token.rs +++ b/shared/src/ledger/ibc/vp/token.rs @@ -122,7 +122,7 @@ where changes.insert(sub_prefix, change + this_change); } } - if changes.iter().all(|(_, c)| c.is_zero()) { + if changes.iter().all(|(_, c)| c.is_zero()) { return Ok(true); } else { return Err(Error::TokenTransfer( @@ -191,7 +191,8 @@ where } let token = ibc_storage::token(&data.denom) .map_err(|e| Error::Denom(e.to_string()))?; - let amount = Amount::from_string_precise(&data.amount).map_err(Error::Amount)?; + let amount = + Amount::from_string_precise(&data.amount).map_err(Error::Amount)?; let denom = if let Some(denom) = data .denom @@ -276,7 +277,8 @@ where .map_err(Error::DecodingPacketData)?; let token = ibc_storage::token(&data.denom) .map_err(|e| Error::Denom(e.to_string()))?; - let amount = Amount::from_string_precise(&data.amount).map_err(Error::Amount)?; + let amount = + Amount::from_string_precise(&data.amount).map_err(Error::Amount)?; let prefix = format!( "{}/{}/", @@ -334,7 +336,8 @@ where .map_err(Error::DecodingPacketData)?; let token_str = data.denom.split('/').last().ok_or(Error::NoToken)?; let token = Address::decode(token_str).map_err(Error::Address)?; - let amount = Amount::from_string_precise(&data.amount).map_err(Error::Amount)?; + let amount = + Amount::from_string_precise(&data.amount).map_err(Error::Amount)?; // check the denom field let prefix = format!( diff --git a/shared/src/ledger/native_vp/governance/utils.rs b/shared/src/ledger/native_vp/governance/utils.rs index 30e6a48fb9..f97e438763 100644 --- a/shared/src/ledger/native_vp/governance/utils.rs +++ b/shared/src/ledger/native_vp/governance/utils.rs @@ -15,7 +15,6 @@ use crate::ledger::storage_api; use crate::types::address::Address; use crate::types::governance::{ProposalVote, TallyResult, VotePower}; use crate::types::storage::Epoch; -use crate::types::token; /// Proposal structure holding votes information necessary to compute the /// outcome @@ -86,7 +85,8 @@ where { let params = read_pos_params(storage)?; let total_stake = read_total_stake(storage, ¶ms, epoch)?; - let total_stake = VotePower::from(total_stake); + let total_stake = + VotePower::try_from(total_stake).expect("Amount out of bounds"); let Votes { yay_validators, @@ -156,7 +156,8 @@ where epoch, )? .unwrap_or_default() - .into(); + .try_into() + .expect("Amount out of bounds"); yay_validators.insert(voter_address.clone(), amount); } else if !validators.contains(voter_address) { @@ -179,7 +180,8 @@ where .or_default(); entry.insert( validator.to_owned(), - VotePower::from(amount), + VotePower::try_from(amount) + .expect("Amount out of bounds"), ); } else { let entry = nay_delegators @@ -187,7 +189,8 @@ where .or_default(); entry.insert( validator.to_owned(), - VotePower::from(amount), + VotePower::try_from(amount) + .expect("Amount out of bounds"), ); } } diff --git a/shared/src/ledger/queries/router.rs b/shared/src/ledger/queries/router.rs index db095a2c00..f959a7a54c 100644 --- a/shared/src/ledger/queries/router.rs +++ b/shared/src/ledger/queries/router.rs @@ -828,265 +828,263 @@ macro_rules! router { ); } -/* -/// You can expand the `handlers!` macro invocation with e.g.: -/// ```shell -/// cargo expand ledger::queries::router::test_rpc_handlers --features "ferveo-tpke, ibc-mocks, testing, wasm-runtime, tendermint-rpc" --tests --lib -/// ``` -#[cfg(test)] -mod test_rpc_handlers { - use borsh::BorshSerialize; - - use crate::ledger::queries::{ - EncodedResponseQuery, RequestCtx, RequestQuery, ResponseQuery, - }; - use crate::ledger::storage::{DBIter, StorageHasher, DB}; - use crate::ledger::storage_api::{self, ResultExt}; - use crate::types::storage::Epoch; - use crate::types::token; - - /// A little macro to generate boilerplate for RPC handler functions. - /// These are implemented to return their name as a String, joined by - /// slashes with their argument values turned `to_string()`, if any. - macro_rules! handlers { - ( - // name and params, if any - $( $name:ident $( ( $( $param:ident: $param_ty:ty ),* ) )? ),* - // optional trailing comma - $(,)? ) => { - $( - pub fn $name( - _ctx: RequestCtx<'_, D, H>, - $( $( $param: $param_ty ),* )? - ) -> storage_api::Result - where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, - { - let data = stringify!($name).to_owned(); - $( $( - let data = format!("{data}/{}", $param); - )* )? - Ok(data) - } - )* - }; - } - - // Generate handler functions for the router below - handlers!( - a, - b0i, - b0ii, - b1, - b2i(balance: token::Amount), - b3(a1: token::Amount, a2: token::Amount, a3: token::Amount), - b3i(a1: token::Amount, a2: token::Amount, a3: token::Amount), - b3ii(a1: token::Amount, a2: token::Amount, a3: token::Amount), - x, - y(untyped_arg: &str), - z(untyped_arg: &str), - ); - - /// This handler is hand-written, because the test helper macro doesn't - /// support optional args. - pub fn b3iii( - _ctx: RequestCtx<'_, D, H>, - a1: token::Amount, - a2: token::Amount, - a3: Option, - ) -> storage_api::Result - where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, - { - let data = "b3iii".to_owned(); - let data = format!("{data}/{}", a1); - let data = format!("{data}/{}", a2); - let data = a3.map(|a3| format!("{data}/{}", a3)).unwrap_or(data); - Ok(data) - } - - /// This handler is hand-written, because the test helper macro doesn't - /// support optional args. - pub fn b3iiii( - _ctx: RequestCtx<'_, D, H>, - a1: token::Amount, - a2: token::Amount, - a3: Option, - a4: Option, - ) -> storage_api::Result - where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, - { - let data = "b3iiii".to_owned(); - let data = format!("{data}/{}", a1); - let data = format!("{data}/{}", a2); - let data = a3.map(|a3| format!("{data}/{}", a3)).unwrap_or(data); - let data = a4.map(|a4| format!("{data}/{}", a4)).unwrap_or(data); - Ok(data) - } - - /// This handler is hand-written, because the test helper macro doesn't - /// support handlers with `with_options`. - pub fn c( - _ctx: RequestCtx<'_, D, H>, - _request: &RequestQuery, - ) -> storage_api::Result - where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, - { - let data = "c".to_owned().try_to_vec().into_storage_result()?; - Ok(ResponseQuery { - data, - ..ResponseQuery::default() - }) - } -} - -/// You can expand the `router!` macro invocation with e.g.: -/// ```shell -/// cargo expand ledger::queries::router::test_rpc --features "ferveo-tpke, ibc-mocks, testing, wasm-runtime, tendermint-rpc" --tests --lib -/// ``` -#[cfg(test)] -mod test_rpc { - use super::test_rpc_handlers::*; - use crate::types::storage::Epoch; - use crate::types::token; - - // Setup an RPC router for testing - router! {TEST_RPC, - ( "sub" ) = (sub TEST_SUB_RPC), - ( "a" ) -> String = a, - ( "b" ) = { - ( "0" ) = { - ( "i" ) -> String = b0i, - ( "ii" ) -> String = b0ii, - }, - ( "1" ) -> String = b1, - ( "2" ) = { - ( "i" / [balance: token::Amount] ) -> String = b2i, - }, - ( "3" / [a1: token::Amount] / [a2: token::Amount] ) = { - ( "i" / [a3: token:: Amount] ) -> String = b3i, - ( [a3: token:: Amount] ) -> String = b3, - ( [a3: token:: Amount] / "ii" ) -> String = b3ii, - ( [a3: opt token::Amount] / "iii" ) -> String = b3iii, - ( "iiii" / [a3: opt token::Amount] / "xyz" / [a4: opt Epoch] ) -> String = b3iiii, - }, - }, - ( "c" ) -> String = (with_options c), - } - - router! {TEST_SUB_RPC, - ( "x" ) -> String = x, - ( "y" / [untyped_arg] ) -> String = y, - ( "z" / [untyped_arg] ) -> String = z, - } -} - -#[cfg(test)] -mod test { - use super::test_rpc::TEST_RPC; - use crate::ledger::queries::testing::TestClient; - use crate::ledger::queries::{RequestCtx, RequestQuery, Router}; - use crate::ledger::storage_api; - use crate::types::storage::Epoch; - use crate::types::token; - - /// Test all the possible paths in `TEST_RPC` router. - #[tokio::test] - async fn test_router_macro() -> storage_api::Result<()> { - let client = TestClient::new(TEST_RPC); - - // Test request with an invalid path - let request = RequestQuery { - path: "/invalid".to_owned(), - ..RequestQuery::default() - }; - let ctx = RequestCtx { - event_log: &client.event_log, - wl_storage: &client.wl_storage, - vp_wasm_cache: client.vp_wasm_cache.clone(), - tx_wasm_cache: client.tx_wasm_cache.clone(), - storage_read_past_height_limit: None, - }; - let result = TEST_RPC.handle(ctx, &request); - assert!(result.is_err()); - - // Test requests to valid paths using the router's methods - - let result = TEST_RPC.a(&client).await.unwrap(); - assert_eq!(result, "a"); - - let result = TEST_RPC.b0i(&client).await.unwrap(); - assert_eq!(result, "b0i"); - - let result = TEST_RPC.b0ii(&client).await.unwrap(); - assert_eq!(result, "b0ii"); - - let result = TEST_RPC.b1(&client).await.unwrap(); - assert_eq!(result, "b1"); - - let balance = token::Amount::native_whole(123_000_000); - let result = TEST_RPC.b2i(&client, &balance).await.unwrap(); - assert_eq!(result, format!("b2i/{balance}")); - - let a1 = token::Amount::native_whole(345); - let a2 = token::Amount::native_whole(123_000); - let a3 = token::Amount::native_whole(1_000_999); - let result = TEST_RPC.b3(&client, &a1, &a2, &a3).await.unwrap(); - assert_eq!(result, format!("b3/{a1}/{a2}/{a3}")); - - let result = TEST_RPC.b3i(&client, &a1, &a2, &a3).await.unwrap(); - assert_eq!(result, format!("b3i/{a1}/{a2}/{a3}")); - - let result = TEST_RPC.b3ii(&client, &a1, &a2, &a3).await.unwrap(); - assert_eq!(result, format!("b3ii/{a1}/{a2}/{a3}")); - - let result = - TEST_RPC.b3iii(&client, &a1, &a2, &Some(a3)).await.unwrap(); - assert_eq!(result, format!("b3iii/{a1}/{a2}/{a3}")); - - let result = TEST_RPC.b3iii(&client, &a1, &a2, &None).await.unwrap(); - assert_eq!(result, format!("b3iii/{a1}/{a2}")); - - let result = TEST_RPC - .b3iiii(&client, &a1, &a2, &Some(a3), &None) - .await - .unwrap(); - assert_eq!(result, format!("b3iiii/{a1}/{a2}/{a3}")); - - let a4 = Epoch::from(10); - let result = TEST_RPC - .b3iiii(&client, &a1, &a2, &Some(a3), &Some(a4)) - .await - .unwrap(); - assert_eq!(result, format!("b3iiii/{a1}/{a2}/{a3}/{a4}")); - - let result = TEST_RPC - .b3iiii(&client, &a1, &a2, &None, &None) - .await - .unwrap(); - assert_eq!(result, format!("b3iiii/{a1}/{a2}")); - - let result = TEST_RPC.c(&client, None, None, false).await.unwrap(); - assert_eq!(result.data, format!("c")); - - let result = TEST_RPC.test_sub_rpc().x(&client).await.unwrap(); - assert_eq!(result, format!("x")); - - let arg = "test123"; - let result = TEST_RPC.test_sub_rpc().y(&client, arg).await.unwrap(); - assert_eq!(result, format!("y/{arg}")); - - let arg = "test321"; - let result = TEST_RPC.test_sub_rpc().z(&client, arg).await.unwrap(); - assert_eq!(result, format!("z/{arg}")); - - Ok(()) - } -} -*/ \ No newline at end of file +// You can expand the `handlers!` macro invocation with e.g.: +// ```shell +// cargo expand ledger::queries::router::test_rpc_handlers --features "ferveo-tpke, ibc-mocks, testing, wasm-runtime, tendermint-rpc" --tests --lib +// ``` +// #[cfg(test)] +// mod test_rpc_handlers { +// use borsh::BorshSerialize; +// +// use crate::ledger::queries::{ +// EncodedResponseQuery, RequestCtx, RequestQuery, ResponseQuery, +// }; +// use crate::ledger::storage::{DBIter, StorageHasher, DB}; +// use crate::ledger::storage_api::{self, ResultExt}; +// use crate::types::storage::Epoch; +// use crate::types::token; +// +// A little macro to generate boilerplate for RPC handler functions. +// These are implemented to return their name as a String, joined by +// slashes with their argument values turned `to_string()`, if any. +// macro_rules! handlers { +// ( +// name and params, if any +// $( $name:ident $( ( $( $param:ident: $param_ty:ty ),* ) )? ),* +// optional trailing comma +// $(,)? ) => { +// $( +// pub fn $name( +// _ctx: RequestCtx<'_, D, H>, +// $( $( $param: $param_ty ),* )? +// ) -> storage_api::Result +// where +// D: 'static + DB + for<'iter> DBIter<'iter> + Sync, +// H: 'static + StorageHasher + Sync, +// { +// let data = stringify!($name).to_owned(); +// $( $( +// let data = format!("{data}/{}", $param); +// )* )? +// Ok(data) +// } +// )* +// }; +// } +// +// Generate handler functions for the router below +// handlers!( +// a, +// b0i, +// b0ii, +// b1, +// b2i(balance: token::Amount), +// b3(a1: token::Amount, a2: token::Amount, a3: token::Amount), +// b3i(a1: token::Amount, a2: token::Amount, a3: token::Amount), +// b3ii(a1: token::Amount, a2: token::Amount, a3: token::Amount), +// x, +// y(untyped_arg: &str), +// z(untyped_arg: &str), +// ); +// +// This handler is hand-written, because the test helper macro doesn't +// support optional args. +// pub fn b3iii( +// _ctx: RequestCtx<'_, D, H>, +// a1: token::Amount, +// a2: token::Amount, +// a3: Option, +// ) -> storage_api::Result +// where +// D: 'static + DB + for<'iter> DBIter<'iter> + Sync, +// H: 'static + StorageHasher + Sync, +// { +// let data = "b3iii".to_owned(); +// let data = format!("{data}/{}", a1); +// let data = format!("{data}/{}", a2); +// let data = a3.map(|a3| format!("{data}/{}", a3)).unwrap_or(data); +// Ok(data) +// } +// +// This handler is hand-written, because the test helper macro doesn't +// support optional args. +// pub fn b3iiii( +// _ctx: RequestCtx<'_, D, H>, +// a1: token::Amount, +// a2: token::Amount, +// a3: Option, +// a4: Option, +// ) -> storage_api::Result +// where +// D: 'static + DB + for<'iter> DBIter<'iter> + Sync, +// H: 'static + StorageHasher + Sync, +// { +// let data = "b3iiii".to_owned(); +// let data = format!("{data}/{}", a1); +// let data = format!("{data}/{}", a2); +// let data = a3.map(|a3| format!("{data}/{}", a3)).unwrap_or(data); +// let data = a4.map(|a4| format!("{data}/{}", a4)).unwrap_or(data); +// Ok(data) +// } +// +// This handler is hand-written, because the test helper macro doesn't +// support handlers with `with_options`. +// pub fn c( +// _ctx: RequestCtx<'_, D, H>, +// _request: &RequestQuery, +// ) -> storage_api::Result +// where +// D: 'static + DB + for<'iter> DBIter<'iter> + Sync, +// H: 'static + StorageHasher + Sync, +// { +// let data = "c".to_owned().try_to_vec().into_storage_result()?; +// Ok(ResponseQuery { +// data, +// ..ResponseQuery::default() +// }) +// } +// } +// +// You can expand the `router!` macro invocation with e.g.: +// ```shell +// cargo expand ledger::queries::router::test_rpc --features "ferveo-tpke, ibc-mocks, testing, wasm-runtime, tendermint-rpc" --tests --lib +// ``` +// #[cfg(test)] +// mod test_rpc { +// use super::test_rpc_handlers::*; +// use crate::types::storage::Epoch; +// use crate::types::token; +// +// Setup an RPC router for testing +// router! {TEST_RPC, +// ( "sub" ) = (sub TEST_SUB_RPC), +// ( "a" ) -> String = a, +// ( "b" ) = { +// ( "0" ) = { +// ( "i" ) -> String = b0i, +// ( "ii" ) -> String = b0ii, +// }, +// ( "1" ) -> String = b1, +// ( "2" ) = { +// ( "i" / [balance: token::Amount] ) -> String = b2i, +// }, +// ( "3" / [a1: token::Amount] / [a2: token::Amount] ) = { +// ( "i" / [a3: token:: Amount] ) -> String = b3i, +// ( [a3: token:: Amount] ) -> String = b3, +// ( [a3: token:: Amount] / "ii" ) -> String = b3ii, +// ( [a3: opt token::Amount] / "iii" ) -> String = b3iii, +// ( "iiii" / [a3: opt token::Amount] / "xyz" / [a4: opt Epoch] ) -> String = +// b3iiii, }, +// }, +// ( "c" ) -> String = (with_options c), +// } +// +// router! {TEST_SUB_RPC, +// ( "x" ) -> String = x, +// ( "y" / [untyped_arg] ) -> String = y, +// ( "z" / [untyped_arg] ) -> String = z, +// } +// } +// +// #[cfg(test)] +// mod test { +// use super::test_rpc::TEST_RPC; +// use crate::ledger::queries::testing::TestClient; +// use crate::ledger::queries::{RequestCtx, RequestQuery, Router}; +// use crate::ledger::storage_api; +// use crate::types::storage::Epoch; +// use crate::types::token; +// +// Test all the possible paths in `TEST_RPC` router. +// #[tokio::test] +// async fn test_router_macro() -> storage_api::Result<()> { +// let client = TestClient::new(TEST_RPC); +// +// Test request with an invalid path +// let request = RequestQuery { +// path: "/invalid".to_owned(), +// ..RequestQuery::default() +// }; +// let ctx = RequestCtx { +// event_log: &client.event_log, +// wl_storage: &client.wl_storage, +// vp_wasm_cache: client.vp_wasm_cache.clone(), +// tx_wasm_cache: client.tx_wasm_cache.clone(), +// storage_read_past_height_limit: None, +// }; +// let result = TEST_RPC.handle(ctx, &request); +// assert!(result.is_err()); +// +// Test requests to valid paths using the router's methods +// +// let result = TEST_RPC.a(&client).await.unwrap(); +// assert_eq!(result, "a"); +// +// let result = TEST_RPC.b0i(&client).await.unwrap(); +// assert_eq!(result, "b0i"); +// +// let result = TEST_RPC.b0ii(&client).await.unwrap(); +// assert_eq!(result, "b0ii"); +// +// let result = TEST_RPC.b1(&client).await.unwrap(); +// assert_eq!(result, "b1"); +// +// let balance = token::Amount::native_whole(123_000_000); +// let result = TEST_RPC.b2i(&client, &balance).await.unwrap(); +// assert_eq!(result, format!("b2i/{balance}")); +// +// let a1 = token::Amount::native_whole(345); +// let a2 = token::Amount::native_whole(123_000); +// let a3 = token::Amount::native_whole(1_000_999); +// let result = TEST_RPC.b3(&client, &a1, &a2, &a3).await.unwrap(); +// assert_eq!(result, format!("b3/{a1}/{a2}/{a3}")); +// +// let result = TEST_RPC.b3i(&client, &a1, &a2, &a3).await.unwrap(); +// assert_eq!(result, format!("b3i/{a1}/{a2}/{a3}")); +// +// let result = TEST_RPC.b3ii(&client, &a1, &a2, &a3).await.unwrap(); +// assert_eq!(result, format!("b3ii/{a1}/{a2}/{a3}")); +// +// let result = +// TEST_RPC.b3iii(&client, &a1, &a2, &Some(a3)).await.unwrap(); +// assert_eq!(result, format!("b3iii/{a1}/{a2}/{a3}")); +// +// let result = TEST_RPC.b3iii(&client, &a1, &a2, &None).await.unwrap(); +// assert_eq!(result, format!("b3iii/{a1}/{a2}")); +// +// let result = TEST_RPC +// .b3iiii(&client, &a1, &a2, &Some(a3), &None) +// .await +// .unwrap(); +// assert_eq!(result, format!("b3iiii/{a1}/{a2}/{a3}")); +// +// let a4 = Epoch::from(10); +// let result = TEST_RPC +// .b3iiii(&client, &a1, &a2, &Some(a3), &Some(a4)) +// .await +// .unwrap(); +// assert_eq!(result, format!("b3iiii/{a1}/{a2}/{a3}/{a4}")); +// +// let result = TEST_RPC +// .b3iiii(&client, &a1, &a2, &None, &None) +// .await +// .unwrap(); +// assert_eq!(result, format!("b3iiii/{a1}/{a2}")); +// +// let result = TEST_RPC.c(&client, None, None, false).await.unwrap(); +// assert_eq!(result.data, format!("c")); +// +// let result = TEST_RPC.test_sub_rpc().x(&client).await.unwrap(); +// assert_eq!(result, format!("x")); +// +// let arg = "test123"; +// let result = TEST_RPC.test_sub_rpc().y(&client, arg).await.unwrap(); +// assert_eq!(result, format!("y/{arg}")); +// +// let arg = "test321"; +// let result = TEST_RPC.test_sub_rpc().z(&client, arg).await.unwrap(); +// assert_eq!(result, format!("z/{arg}")); +// +// Ok(()) +// } +// } diff --git a/shared/src/ledger/queries/shell.rs b/shared/src/ledger/queries/shell.rs index 65ed48c3d1..f81f8433b0 100644 --- a/shared/src/ledger/queries/shell.rs +++ b/shared/src/ledger/queries/shell.rs @@ -4,7 +4,8 @@ use masp_primitives::merkle_tree::MerklePath; use masp_primitives::sapling::Node; use namada_core::types::address::Address; use namada_core::types::hash::Hash; -use namada_core::types::storage::BlockResults; +use namada_core::types::storage::{BlockHeight, BlockResults}; +use namada_core::types::token::MaspDenom; use crate::ledger::events::log::dumb_queries; use crate::ledger::events::Event; @@ -20,6 +21,7 @@ use crate::types::transaction::TxResult; type Conversion = ( Address, + MaspDenom, Epoch, masp_primitives::transaction::components::Amount, MerklePath, @@ -29,6 +31,9 @@ router! {SHELL, // Epoch of the last committed block ( "epoch" ) -> Epoch = epoch, + // Epoch of the input block height + ( "epoch_at_height" / [height: BlockHeight]) -> Option = epoch_at_height, + // Raw storage access - read value ( "value" / [storage_key: storage::Key] ) -> Vec = (with_options storage_value), @@ -137,7 +142,7 @@ where H: 'static + StorageHasher + Sync, { // Conversion values are constructed on request - if let Some((addr, epoch, conv, pos)) = ctx + if let Some(((addr, denom), epoch, conv, pos)) = ctx .wl_storage .storage .conversion_state @@ -146,6 +151,7 @@ where { Ok(( addr.clone(), + *denom, *epoch, Into::::into( conv.clone(), @@ -181,6 +187,17 @@ where Ok(data) } +fn epoch_at_height( + ctx: RequestCtx<'_, D, H>, + height: BlockHeight, +) -> storage_api::Result> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + Ok(ctx.wl_storage.storage.block.pred_epochs.get_epoch(height)) +} + /// Returns data with `vec![]` when the storage key is not found. For all /// borsh-encoded types, it is safe to check `data.is_empty()` to see if the /// value was found, except for unit - see `fn query_storage_value` in diff --git a/shared/src/ledger/queries/vp/mod.rs b/shared/src/ledger/queries/vp/mod.rs index ed83a492ec..74deb0838b 100644 --- a/shared/src/ledger/queries/vp/mod.rs +++ b/shared/src/ledger/queries/vp/mod.rs @@ -1,8 +1,7 @@ // Re-export to show in rustdoc! pub use pos::Pos; -pub use token::Token; - use pos::POS; +pub use token::Token; use token::TOKEN; mod pos; diff --git a/shared/src/ledger/queries/vp/token.rs b/shared/src/ledger/queries/vp/token.rs index 7f5e0a0b84..930a9412c5 100644 --- a/shared/src/ledger/queries/vp/token.rs +++ b/shared/src/ledger/queries/vp/token.rs @@ -1,9 +1,10 @@ -use namada_core::ledger::storage::{DB, DBIter, StorageHasher}; +use namada_core::ledger::storage::{DBIter, StorageHasher, DB}; use namada_core::ledger::storage_api; use namada_core::ledger::storage_api::token::read_denom; use namada_core::types::address::Address; use namada_core::types::token; use namada_core::types::token::Denomination; + use crate::ledger::queries::RequestCtx; router! {TOKEN, @@ -16,9 +17,9 @@ fn denomination( ctx: RequestCtx<'_, D, H>, addr: Address, ) -> storage_api::Result> - where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, { read_denom(ctx.wl_storage, &addr) -} \ No newline at end of file +} diff --git a/tx_prelude/src/token.rs b/tx_prelude/src/token.rs index 753f666a30..44d775843f 100644 --- a/tx_prelude/src/token.rs +++ b/tx_prelude/src/token.rs @@ -36,7 +36,9 @@ pub fn transfer( None => token::balance_key(token, dest), }; let src_bal: Option = match src { - Address::Internal(InternalAddress::IbcMint) => Some(Amount::max_signed()), + Address::Internal(InternalAddress::IbcMint) => { + Some(Amount::max_signed()) + } Address::Internal(InternalAddress::IbcBurn) => { log_string("invalid transfer from the burn address"); unreachable!() diff --git a/vp_prelude/src/token.rs b/vp_prelude/src/token.rs index dd54ab864f..1e302204a7 100644 --- a/vp_prelude/src/token.rs +++ b/vp_prelude/src/token.rs @@ -56,7 +56,9 @@ pub fn vp( let this_change = post.change() - pre.change(); change += this_change; // make sure that the spender approved the transaction - if !(this_change.non_negative() || verifiers.contains(owner) || *owner == address::masp()) + if !(this_change.non_negative() + || verifiers.contains(owner) + || *owner == address::masp()) { return reject(); } From 92976fd8b081713185f8c327df0c37b96679b3f3 Mon Sep 17 00:00:00 2001 From: satan Date: Tue, 11 Apr 2023 17:30:25 +0200 Subject: [PATCH 012/151] [feat]: First compiling refactor. Now for testing --- apps/src/lib/cli.rs | 130 ++------ apps/src/lib/client/rpc.rs | 49 ++- apps/src/lib/client/tx.rs | 70 +++-- apps/src/lib/client/types.rs | 7 +- apps/src/lib/config/genesis.rs | 4 +- .../lib/node/ledger/shell/finalize_block.rs | 47 ++- apps/src/lib/node/ledger/shell/init_chain.rs | 2 +- apps/src/lib/node/ledger/shell/mod.rs | 6 +- .../lib/node/ledger/shell/prepare_proposal.rs | 8 +- .../lib/node/ledger/shell/process_proposal.rs | 40 +-- core/src/types/token.rs | 12 +- core/src/types/transaction/mod.rs | 11 +- core/src/types/transaction/wrapper.rs | 59 ++-- core/src/types/uint.rs | 23 ++ tests/src/e2e/helpers.rs | 14 +- tests/src/e2e/ibc_tests.rs | 12 +- tests/src/e2e/ledger_tests.rs | 81 +++-- tests/src/e2e/multitoken_tests.rs | 23 +- tests/src/e2e/multitoken_tests/helpers.rs | 6 +- tests/src/native_vp/pos.rs | 62 ++-- tests/src/vm_host_env/mod.rs | 15 +- wasm/Cargo.lock | 283 +++++++++++++++++- wasm/wasm_source/src/tx_bond.rs | 6 +- .../src/tx_change_validator_commission.rs | 6 +- wasm/wasm_source/src/tx_unbond.rs | 7 +- wasm/wasm_source/src/tx_withdraw.rs | 4 +- wasm/wasm_source/src/vp_implicit.rs | 102 ++++++- wasm/wasm_source/src/vp_masp.rs | 37 ++- wasm/wasm_source/src/vp_testnet_faucet.rs | 32 +- wasm/wasm_source/src/vp_user.rs | 98 +++++- wasm/wasm_source/src/vp_validator.rs | 96 +++++- wasm_for_tests/wasm_source/Cargo.lock | 283 +++++++++++++++++- wasm_for_tests/wasm_source/src/lib.rs | 2 +- 33 files changed, 1243 insertions(+), 394 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index fa99319ed1..c2c89d9285 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -1567,7 +1567,6 @@ pub mod args { use std::str::FromStr; use namada::ibc::core::ics24_host::identifier::{ChannelId, PortId}; - use namada::ledger::queries::RPC; use namada::types::address::Address; use namada::types::chain::{ChainId, ChainIdPrefix}; use namada::types::governance::ProposalVote; @@ -1579,7 +1578,6 @@ pub mod args { use namada::types::token::NATIVE_MAX_DECIMAL_PLACES; use namada::types::transaction::GasLimit; use rust_decimal::Decimal; - use tendermint_rpc::HttpClient; use super::context::*; use super::utils::*; @@ -1886,64 +1884,32 @@ pub mod args { /// Transferred token address pub sub_prefix: Option, /// Transferred token amount - pub amount: token::DenominatedAmount, + pub amount: InputAmount, + } + + /// An amount read in by the cli + #[derive(Copy, Clone, Debug)] + pub enum InputAmount { + /// An amount whose representation has been validated + /// against the allowed representation in storage + Validated(token::DenominatedAmount), + /// The parsed amount read in from the cli. It has + /// not yet been validated against the allowed + /// representation in storage. + Unvalidated(token::DenominatedAmount), } impl TxTransfer { - pub async fn parse_from_context( - &mut self, + pub fn parse_from_context( + &self, ctx: &mut Context, ) -> ParsedTxTransferArgs { - let token = ctx.get(&self.token); ParsedTxTransferArgs { - tx: self.tx.parse_from_context(ctx).await, + tx: self.tx.parse_from_context(ctx), source: ctx.get_cached(&self.source), target: ctx.get(&self.target), - amount: self.validate_amount(&token).await, - token, - } - } - - /// Get the correct representation of the amount given the token type. - async fn validate_amount(&mut self, token: &Address) -> token::Amount { - let client = - HttpClient::new(self.tx.ledger_address.clone()).unwrap(); - let denom = RPC - .vp() - .token() - .denomination(&client, token) - .await - .unwrap_or_else(|err| { - eprintln!("Error in the query {}", err); - safe_exit(1) - }) - .unwrap_or_else(|| { - println!( - "No denomination found for token: {token}, the input \ - arguments could - not be parsed." - ); - safe_exit(1); - }); - let input_amount = self.amount.canonical(); - if denom < input_amount.denom { - println!( - "The input amount contained a higher precision than \ - allowed by {token}." - ); - safe_exit(1); - } else { - let validated = input_amount - .increase_precision(denom) - .unwrap_or_else(|| { - println!( - "The amount provided requires more the 256 bits \ - to represent." - ); - safe_exit(1); - }); - self.amount = validated; - self.amount.amount + amount: self.amount, + token: ctx.get(&self.token), } } } @@ -1955,7 +1921,7 @@ pub mod args { let target = TRANSFER_TARGET.parse(matches); let token = TOKEN.parse(matches); let sub_prefix = SUB_PREFIX.parse(matches); - let amount = AMOUNT.parse(matches); + let amount = InputAmount::Unvalidated(AMOUNT.parse(matches)); Self { tx, source, @@ -2931,7 +2897,7 @@ pub mod args { /// save it in the wallet. pub initialized_account_alias: Option, /// The amount being payed to include the transaction - pub fee_amount: token::DenominatedAmount, + pub fee_amount: InputAmount, /// The token in which the fee is being paid pub fee_token: WalletAddress, /// The max amount of gas used to process tx @@ -2943,11 +2909,7 @@ pub mod args { } impl Tx { - pub async fn parse_from_context( - &mut self, - ctx: &mut Context, - ) -> ParsedTxArgs { - let fee_token = ctx.get(&self.fee_token); + pub fn parse_from_context(&self, ctx: &mut Context) -> ParsedTxArgs { ParsedTxArgs { dry_run: self.dry_run, dump_tx: self.dump_tx, @@ -2957,8 +2919,8 @@ pub mod args { initialized_account_alias: self .initialized_account_alias .clone(), - fee_amount: self.validate_amount(&fee_token).await, - fee_token, + fee_amount: self.fee_amount, + fee_token: ctx.get(&self.fee_token), gas_limit: self.gas_limit.clone(), signing_key: self .signing_key @@ -2967,49 +2929,6 @@ pub mod args { signer: self.signer.as_ref().map(|signer| ctx.get(signer)), } } - - /// Get the correct representation of the fee amount given the token - /// type. - async fn validate_amount(&mut self, token: &Address) -> token::Amount { - let client = HttpClient::new(self.ledger_address.clone()).unwrap(); - let denom = RPC - .vp() - .token() - .denomination(&client, token) - .await - .unwrap_or_else(|err| { - eprintln!("Error in the query {}", err); - safe_exit(1) - }) - .unwrap_or_else(|| { - println!( - "No denomination found for token: {token}, the input \ - arguments could - not be parsed." - ); - safe_exit(1); - }); - let input_amount = self.fee_amount.canonical(); - if denom < input_amount.denom { - println!( - "The input amount contained a higher precision than \ - allowed by {token}." - ); - safe_exit(1); - } else { - let validated = input_amount - .increase_precision(denom) - .unwrap_or_else(|| { - println!( - "The amount provided requires more the 256 bits \ - to represent." - ); - safe_exit(1); - }); - self.fee_amount = validated; - self.fee_amount.amount - } - } } impl Args for Tx { @@ -3071,7 +2990,8 @@ pub mod args { let broadcast_only = BROADCAST_ONLY.parse(matches); let ledger_address = LEDGER_ADDRESS_DEFAULT.parse(matches); let initialized_account_alias = ALIAS_OPT.parse(matches); - let fee_amount = GAS_AMOUNT.parse(matches); + let fee_amount = + InputAmount::Unvalidated(GAS_AMOUNT.parse(matches)); let fee_token = GAS_TOKEN.parse(matches); let gas_limit = GAS_LIMIT.parse(matches).amount.into(); diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index c156e4ed8b..0e6d7d9ce9 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -55,6 +55,7 @@ use namada::types::transaction::{ use namada::types::{address, storage, token}; use tokio::time::{Duration, Instant}; +use crate::cli::args::InputAmount; use crate::cli::{self, args, Context}; use crate::client::tendermint_rpc_types::TxResponse; use crate::client::tx::{ @@ -224,11 +225,6 @@ pub async fn query_tx_deltas( .txs; for response_tx in txs { let height = BlockHeight(response_tx.height.value()); - let epoch = - query_epoch_at_height(&client, height).await.expect( - "This block height should not exceed that latest \ - committed block height", - ); let idx = TxIndex(response_tx.index); // Only process yet unprocessed transactions which have been // accepted by node VPs @@ -262,7 +258,7 @@ pub async fn query_tx_deltas( denom.denominate(&transfer.amount); if denominated != 0 { let tfer_delta = Amount::from_nonnegative( - (transfer.token.clone(), denom.into()), + (transfer.token.clone(), denom), denominated, ) .expect("invalid value for amount"); @@ -778,7 +774,7 @@ async fn print_balances( format!( "with {}: {}, owned by {}", sub_prefix, - format_denominated_amount(&client, &token, balance).await, + format_denominated_amount(client, token, balance).await, lookup_alias(ctx, owner) ), ), @@ -788,7 +784,7 @@ async fn print_balances( owner.clone(), format!( ": {}, owned by {}", - format_denominated_amount(&client, &token, balance) + format_denominated_amount(client, token, balance) .await, lookup_alias(ctx, owner) ), @@ -2813,3 +2809,40 @@ pub fn make_asset_type( // Generate the unique asset identifier from the unique token address AssetType::new(token_bytes.as_ref()).expect("unable to create asset type") } + +/// Get the correct representation of the amount given the token type. +pub async fn validate_amount( + client: &HttpClient, + amount: InputAmount, + token: &Address, +) -> token::DenominatedAmount { + let input_amount = match amount { + InputAmount::Unvalidated(amt) => amt.canonical(), + InputAmount::Validated(amt) => return amt, + }; + let denom = unwrap_client_response( + RPC.vp().token().denomination(client, token).await, + ) + .unwrap_or_else(|| { + println!( + "No denomination found for token: {token}, the input arguments \ + could + not be parsed." + ); + cli::safe_exit(1); + }); + if denom < input_amount.denom { + println!( + "The input amount contained a higher precision than allowed by \ + {token}." + ); + cli::safe_exit(1); + } else { + input_amount.increase_precision(denom).unwrap_or_else(|| { + println!( + "The amount provided requires more the 256 bits to represent." + ); + cli::safe_exit(1); + }) + } +} diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 6f2575bb0d..6e64df6114 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -40,7 +40,6 @@ use namada::ibc_proto::cosmos::base::v1beta1::Coin; use namada::ledger::governance::storage as gov_storage; use namada::ledger::masp; use namada::ledger::pos::{CommissionPair, PosParams}; -use namada::ledger::queries::RPC; use namada::proto::Tx; use namada::types::address::{masp, masp_tx_key, Address}; use namada::types::governance::{ @@ -69,11 +68,12 @@ use tokio::time::{Duration, Instant}; use super::rpc; use super::types::ShieldedTransferContext; +use crate::cli::args::InputAmount; use crate::cli::context::WalletAddress; use crate::cli::{args, safe_exit, Context}; use crate::client::rpc::{ format_denominated_amount, make_asset_type, query_conversion, - query_storage_value, unwrap_client_response, + query_storage_value, validate_amount, }; use crate::client::signing::{find_keypair, sign_tx, tx_signer, TxSigningKey}; use crate::client::tendermint_rpc_types::{TxBroadcastData, TxResponse}; @@ -848,9 +848,11 @@ impl ShieldedContext { // Record the changes to the transparent accounts let mut transfer_delta = TransferDelta::new(); for denom in MaspDenom::iter() { - let transparent_delta = - Amount::from_nonnegative((tx.token.clone(), denom), denom.denominate(&tx.amount.amount)) - .expect("invalid value for amount"); + let transparent_delta = Amount::from_nonnegative( + (tx.token.clone(), denom), + denom.denominate(&tx.amount.amount), + ) + .expect("invalid value for amount"); if transparent_delta == Amount::zero() { continue; } @@ -1367,12 +1369,15 @@ where .expect("unable to load MASP Parameters") }; let fee = if spending_key.is_some() { + let InputAmount::Validated(fee) = args.tx.fee_amount else { + unreachable!("The function `gen_shielded_transfer` is only called by `submit_tx` which validates amounts.") + }; // Transaction fees need to match the amount in the wrapper Transfer // when MASP source is used. This amount should be <= `u64::MAX`. let (_, fee) = convert_amount( epoch, &args.tx.fee_token, - MaspDenom::Zero.denominate(&args.tx.fee_amount), + MaspDenom::Zero.denominate(&fee.amount), MaspDenom::Zero, ); builder.set_fee(fee.clone())?; @@ -1386,8 +1391,11 @@ where let mut epoch_transitions = vec![]; // break up a transfer into a number of transfers with suitable // denominations + let InputAmount::Validated(amt) = args.amount else { + unreachable!("The function `gen_shielded_transfer` is only called by `submit_tx` which validates amounts.") + }; for denom in MaspDenom::iter() { - let denom_amount = denom.denominate(&args.amount); + let denom_amount = denom.denominate(&amt.amount); if denom_amount == 0 { continue; } @@ -1562,8 +1570,10 @@ where tx.map(Some) } -pub async fn submit_transfer(mut ctx: Context, mut args: args::TxTransfer) { - let parsed_args = args.parse_from_context(&mut ctx).await; +pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { + let mut parsed_args: ParsedTxTransferArgs = + args.parse_from_context(&mut ctx); + let client = HttpClient::new(args.tx.ledger_address.clone()).unwrap(); let source = parsed_args.source.effective_address(); let target = parsed_args.target.effective_address(); // Check that the source address exists on chain @@ -1597,6 +1607,17 @@ pub async fn submit_transfer(mut ctx: Context, mut args: args::TxTransfer) { safe_exit(1) } } + // validate the amount given + let validated_amount = + validate_amount(&client, parsed_args.amount, &parsed_args.token).await; + let validate_fee = validate_amount( + &client, + parsed_args.tx.fee_amount, + &parsed_args.tx.fee_token, + ) + .await; + parsed_args.amount = InputAmount::Validated(validated_amount); + parsed_args.tx.fee_amount = InputAmount::Validated(validate_fee); // Check source balance let (sub_prefix, balance_key) = match args.sub_prefix { Some(sub_prefix) => { @@ -1612,11 +1633,10 @@ pub async fn submit_transfer(mut ctx: Context, mut args: args::TxTransfer) { } None => (None, token::balance_key(&parsed_args.token, &source)), }; - let client = HttpClient::new(args.tx.ledger_address.clone()).unwrap(); match rpc::query_storage_value::(&client, &balance_key).await { Some(balance) => { - if balance < parsed_args.amount { + if balance < validated_amount.amount { let balance_amount = format_denominated_amount( &client, &parsed_args.token, @@ -1627,7 +1647,7 @@ pub async fn submit_transfer(mut ctx: Context, mut args: args::TxTransfer) { "The balance of the source {} of token {} is lower than \ the amount to be transferred. Amount to transfer is {} \ and the balance is {}.", - source, parsed_args.token, args.amount, balance_amount + source, parsed_args.token, validated_amount, balance_amount ); if !args.tx.force { safe_exit(1) @@ -1661,13 +1681,13 @@ pub async fn submit_transfer(mut ctx: Context, mut args: args::TxTransfer) { } else if source == masp_addr { ( TxSigningKey::SecretKey(masp_tx_key()), - parsed_args.amount, + validated_amount.amount, parsed_args.token.clone(), ) } else { ( TxSigningKey::WalletAddress(args.source.to_address()), - parsed_args.amount, + validated_amount.amount, parsed_args.token.clone(), ) }; @@ -1686,25 +1706,15 @@ pub async fn submit_transfer(mut ctx: Context, mut args: args::TxTransfer) { #[cfg(not(feature = "mainnet"))] let is_source_faucet = rpc::is_faucet_account(&source, args.tx.ledger_address.clone()).await; - let denom = unwrap_client_response( - RPC.vp() - .token() - .denomination(&client, &parsed_args.token) - .await, - ) - .unwrap_or_else(|| { - println!( - "No denomination found for token: {token}, defaulting to zero \ - decimal places" - ); - 0.into() - }); let transfer = token::Transfer { source, target, token, sub_prefix, - amount: DenominatedAmount { amount, denom }, + amount: DenominatedAmount { + amount, + denom: validated_amount.denom, + }, key, shielded: { let spending_key = parsed_args.source.spending_key(); @@ -1738,9 +1748,9 @@ pub async fn submit_transfer(mut ctx: Context, mut args: args::TxTransfer) { amount to be transferred and fees. Amount to \ transfer is {} {} and fees are {} {}.", parsed_args.source, - args.amount, + validated_amount, parsed_args.token, - args.tx.fee_amount, + validate_fee, parsed_args.tx.fee_token, ); safe_exit(1) diff --git a/apps/src/lib/client/types.rs b/apps/src/lib/client/types.rs index d75d5a596c..c5acbc2ba5 100644 --- a/apps/src/lib/client/types.rs +++ b/apps/src/lib/client/types.rs @@ -4,12 +4,13 @@ use masp_primitives::primitives::{Diversifier, Note, ViewingKey}; use masp_primitives::sapling::Node; use masp_primitives::transaction::components::Amount; use namada::types::address::Address; +use namada::types::key; use namada::types::masp::{TransferSource, TransferTarget}; use namada::types::storage::Epoch; use namada::types::transaction::GasLimit; -use namada::types::{key, token}; use super::rpc; +use crate::cli::args::InputAmount; use crate::cli::{args, Context}; use crate::client::tx::Conversions; use crate::facade::tendermint_config::net::Address as TendermintAddress; @@ -30,7 +31,7 @@ pub struct ParsedTxArgs { /// save it in the wallet. pub initialized_account_alias: Option, /// The amount being payed to include the transaction - pub fee_amount: token::Amount, + pub fee_amount: InputAmount, /// The token in which the fee is being paid pub fee_token: Address, /// The max amount of gas used to process tx @@ -52,7 +53,7 @@ pub struct ParsedTxTransferArgs { /// Transferred token address pub token: Address, /// Transferred token amount - pub amount: token::Amount, + pub amount: InputAmount, } #[async_trait(?Send)] diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index d072395e45..7dde5fbe5b 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -324,7 +324,9 @@ pub mod genesis_config { pos_data: GenesisValidator { address: Address::decode(config.address.as_ref().unwrap()) .unwrap(), - tokens: token::Amount::native_whole(config.tokens.unwrap_or_default()), + tokens: token::Amount::native_whole( + config.tokens.unwrap_or_default(), + ), consensus_key: config .consensus_public_key .as_ref() diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index 336c4d9415..ea8fd012c2 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -197,7 +197,9 @@ where .storage .write( &balance_key, - Amount::from(0).try_to_vec().unwrap(), + Amount::native_whole(0) + .try_to_vec() + .unwrap(), ) .unwrap(); tx_event["info"] = @@ -468,6 +470,7 @@ mod test_finalize_block { use namada::types::governance::ProposalVote; use namada::types::storage::Epoch; use namada::types::time::DurationSecs; + use namada::types::token::NATIVE_MAX_DECIMAL_PLACES; use namada::types::transaction::governance::{ InitProposalData, VoteProposalData, }; @@ -497,7 +500,10 @@ mod test_finalize_block { shell .wl_storage .storage - .write(&balance_key, Amount::whole(1000).try_to_vec().unwrap()) + .write( + &balance_key, + Amount::native_whole(1000).try_to_vec().unwrap(), + ) .unwrap(); // create some wrapper txs @@ -508,12 +514,16 @@ mod test_finalize_block { ); let wrapper = WrapperTx::new( Fee { - amount: MIN_FEE.into(), + amount: Amount::from_uint( + MIN_FEE, + NATIVE_MAX_DECIMAL_PLACES, + ) + .expect("Test failed"), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), raw_tx.clone(), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -581,12 +591,12 @@ mod test_finalize_block { ); let wrapper = WrapperTx::new( Fee { - amount: 0.into(), + amount: Default::default(), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), raw_tx.clone(), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -639,12 +649,12 @@ mod test_finalize_block { ); let wrapper = WrapperTx { fee: Fee { - amount: 0.into(), + amount: Default::default(), token: shell.wl_storage.storage.native_token.clone(), }, pk: keypair.ref_to(), epoch: Epoch(0), - gas_limit: 0.into(), + gas_limit: Default::default(), inner_tx, tx_hash: hash_tx(&tx), #[cfg(not(feature = "mainnet"))] @@ -699,7 +709,10 @@ mod test_finalize_block { shell .wl_storage .storage - .write(&balance_key, Amount::whole(1000).try_to_vec().unwrap()) + .write( + &balance_key, + Amount::native_whole(1000).try_to_vec().unwrap(), + ) .unwrap(); // create two decrypted txs @@ -718,12 +731,16 @@ mod test_finalize_block { ); let wrapper_tx = WrapperTx::new( Fee { - amount: MIN_FEE.into(), + amount: Amount::from_uint( + MIN_FEE, + NATIVE_MAX_DECIMAL_PLACES, + ) + .expect("Test failed"), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), raw_tx.clone(), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -755,12 +772,16 @@ mod test_finalize_block { ); let wrapper_tx = WrapperTx::new( Fee { - amount: MIN_FEE.into(), + amount: Amount::from_uint( + MIN_FEE, + NATIVE_MAX_DECIMAL_PLACES, + ) + .expect("Test failed"), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), raw_tx.clone(), Default::default(), #[cfg(not(feature = "mainnet"))] diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index 1198007bee..0bdaff27e2 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -218,7 +218,7 @@ where // withdrawal limit defaults to 1000 NAM when not set let withdrawal_limit = genesis .faucet_withdrawal_limit - .unwrap_or_else(|| token::Amount::whole(1_000)); + .unwrap_or_else(|| token::Amount::native_whole(1_000)); testnet_pow::init_faucet_storage( &mut self.wl_storage, &address, diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index c79d65a384..dbaf8610c4 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -731,7 +731,7 @@ where &self.wl_storage, ) .expect("Must be able to read wrapper tx fees parameter"); - fees.unwrap_or(token::Amount::native_whole(MIN_FEE)) + fees.unwrap_or_else(|| token::Amount::native_whole(MIN_FEE)) } #[cfg(not(feature = "mainnet"))] @@ -1001,12 +1001,12 @@ mod test_utils { ); let wrapper = WrapperTx::new( Fee { - amount: 0.into(), + amount: Default::default(), token: native_token, }, &keypair, Epoch(0), - 0.into(), + Default::default(), tx, Default::default(), #[cfg(not(feature = "mainnet"))] diff --git a/apps/src/lib/node/ledger/shell/prepare_proposal.rs b/apps/src/lib/node/ledger/shell/prepare_proposal.rs index 1783a127fd..933b8c15ba 100644 --- a/apps/src/lib/node/ledger/shell/prepare_proposal.rs +++ b/apps/src/lib/node/ledger/shell/prepare_proposal.rs @@ -224,12 +224,12 @@ mod test_prepare_proposal { Some( WrapperTx::new( Fee { - amount: 0.into(), + amount: Default::default(), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), tx, Default::default(), #[cfg(not(feature = "mainnet"))] @@ -284,12 +284,12 @@ mod test_prepare_proposal { })); let wrapper_tx = WrapperTx::new( Fee { - amount: 0.into(), + amount: Default::default(), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), tx, Default::default(), #[cfg(not(feature = "mainnet"))] diff --git a/apps/src/lib/node/ledger/shell/process_proposal.rs b/apps/src/lib/node/ledger/shell/process_proposal.rs index 11deec9e13..0b8e29a151 100644 --- a/apps/src/lib/node/ledger/shell/process_proposal.rs +++ b/apps/src/lib/node/ledger/shell/process_proposal.rs @@ -215,7 +215,7 @@ mod test_process_proposal { use namada::types::hash::Hash; use namada::types::key::*; use namada::types::storage::Epoch; - use namada::types::token::Amount; + use namada::types::token::{Amount, NATIVE_MAX_DECIMAL_PLACES}; use namada::types::transaction::encrypted::EncryptedTx; use namada::types::transaction::{EncryptionKey, Fee, WrapperTx}; @@ -238,12 +238,12 @@ mod test_process_proposal { ); let wrapper = WrapperTx::new( Fee { - amount: 0.into(), + amount: Default::default(), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), tx, Default::default(), #[cfg(not(feature = "mainnet"))] @@ -287,12 +287,13 @@ mod test_process_proposal { let timestamp = tx.timestamp; let mut wrapper = WrapperTx::new( Fee { - amount: 100.into(), + amount: Amount::from_uint(100, NATIVE_MAX_DECIMAL_PLACES) + .expect("Test failed"), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), tx, Default::default(), #[cfg(not(feature = "mainnet"))] @@ -318,7 +319,7 @@ mod test_process_proposal { }; // we mount a malleability attack to try and remove the fee - new_wrapper.fee.amount = 0.into(); + new_wrapper.fee.amount = Default::default(); let new_data = TxType::Wrapper(new_wrapper) .try_to_vec() .expect("Test failed"); @@ -371,12 +372,13 @@ mod test_process_proposal { ); let wrapper = WrapperTx::new( Fee { - amount: 1.into(), + amount: Amount::from_uint(1, NATIVE_MAX_DECIMAL_PLACES) + .expect("Test failed"), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), tx, Default::default(), #[cfg(not(feature = "mainnet"))] @@ -419,7 +421,7 @@ mod test_process_proposal { shell .wl_storage .storage - .write(&balance_key, Amount::whole(99).try_to_vec().unwrap()) + .write(&balance_key, Amount::native_whole(99).try_to_vec().unwrap()) .unwrap(); let tx = Tx::new( @@ -428,12 +430,12 @@ mod test_process_proposal { ); let wrapper = WrapperTx::new( Fee { - amount: Amount::whole(1_000_100), + amount: Amount::native_whole(1_000_100), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), tx, Default::default(), #[cfg(not(feature = "mainnet"))] @@ -478,12 +480,12 @@ mod test_process_proposal { ); let wrapper = WrapperTx::new( Fee { - amount: i.into(), + amount: Amount::native_whole(i as u64), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), tx.clone(), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -548,12 +550,12 @@ mod test_process_proposal { ); let wrapper = WrapperTx::new( Fee { - amount: 0.into(), + amount: Default::default(), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), tx, Default::default(), #[cfg(not(feature = "mainnet"))] @@ -609,12 +611,12 @@ mod test_process_proposal { ); let mut wrapper = WrapperTx::new( Fee { - amount: 0.into(), + amount: Default::default(), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), tx, Default::default(), #[cfg(not(feature = "mainnet"))] @@ -664,12 +666,12 @@ mod test_process_proposal { let inner_tx = EncryptedTx::encrypt(&tx, pubkey); let wrapper = WrapperTx { fee: Fee { - amount: 0.into(), + amount: Default::default(), token: shell.wl_storage.storage.native_token.clone(), }, pk: keypair.ref_to(), epoch: Epoch(0), - gas_limit: 0.into(), + gas_limit: Default::default(), inner_tx, tx_hash: hash_tx(&tx), #[cfg(not(feature = "mainnet"))] diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 5a426f3013..cd49a4c0ef 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -924,9 +924,9 @@ mod tests { #[test] fn test_amount_checked_sub() { - let max = Amount::from(u64::MAX); - let one = Amount::from(1); - let zero = Amount::from(0); + let max = Amount::native_whole(u64::MAX); + let one = Amount::native_whole(1); + let zero = Amount::native_whole(0); assert_eq!(zero.checked_sub(zero), Some(zero)); assert_eq!(zero.checked_sub(one), None); @@ -939,9 +939,9 @@ mod tests { #[test] fn test_amount_checked_add() { - let max = Amount::from(u64::MAX); - let one = Amount::from(1); - let zero = Amount::from(0); + let max = Amount::native_whole(u64::MAX); + let one = Amount::native_whole(1); + let zero = Amount::native_whole(0); assert_eq!(zero.checked_add(zero), Some(zero)); assert_eq!(zero.checked_add(one), Some(one)); diff --git a/core/src/types/transaction/mod.rs b/core/src/types/transaction/mod.rs index 0e0a5e980e..8d59fd5a9f 100644 --- a/core/src/types/transaction/mod.rs +++ b/core/src/types/transaction/mod.rs @@ -342,6 +342,7 @@ pub mod tx_types { use super::*; use crate::types::address::nam; use crate::types::storage::Epoch; + use crate::types::token::{Amount, NATIVE_MAX_DECIMAL_PLACES}; fn gen_keypair() -> common::SecretKey { use rand::prelude::ThreadRng; @@ -423,12 +424,13 @@ pub mod tx_types { // the signed tx let wrapper = WrapperTx::new( Fee { - amount: 10.into(), + amount: Amount::from_uint(10, NATIVE_MAX_DECIMAL_PLACES) + .expect("Test failed"), token: nam(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), tx.clone(), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -460,12 +462,13 @@ pub mod tx_types { // the signed tx let wrapper = WrapperTx::new( Fee { - amount: 10.into(), + amount: Amount::from_uint(10, NATIVE_MAX_DECIMAL_PLACES) + .expect("Test failed"), token: nam(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), tx, Default::default(), #[cfg(not(feature = "mainnet"))] diff --git a/core/src/types/transaction/wrapper.rs b/core/src/types/transaction/wrapper.rs index 604dc1dd78..472a05ce86 100644 --- a/core/src/types/transaction/wrapper.rs +++ b/core/src/types/transaction/wrapper.rs @@ -73,6 +73,7 @@ pub mod wrapper_tx { /// This struct only stores the multiple of GAS_LIMIT_RESOLUTION, /// not the raw amount #[derive( + Default, Debug, Clone, PartialEq, @@ -295,7 +296,9 @@ pub mod wrapper_tx { /// Test that serializing converts GasLimit to u64 correctly #[test] fn test_gas_limit_roundtrip() { - let limit = GasLimit { multiplier: 1 }; + let limit = GasLimit { + multiplier: 1.into(), + }; // Test serde roundtrip let js = serde_json::to_string(&limit).expect("Test failed"); assert_eq!(js, format!("{}", GAS_LIMIT_RESOLUTION)); @@ -321,31 +324,49 @@ pub mod wrapper_tx { .expect("Test failed"); let limit: GasLimit = serde_json::from_str(&js).expect("Test failed"); - assert_eq!(limit, GasLimit { multiplier: 2 }); + assert_eq!( + limit, + GasLimit { + multiplier: 2.into() + } + ); } /// Test that refund is calculated correctly #[test] fn test_gas_limit_refund() { - let limit = GasLimit { multiplier: 1 }; - let refund = limit.refund_amount(GAS_LIMIT_RESOLUTION - 1); - assert_eq!(refund, Amount::from(1u64)); + let limit = GasLimit { + multiplier: 1.into(), + }; + let refund = + limit.refund_amount(Uint::from(GAS_LIMIT_RESOLUTION - 1)); + assert_eq!(refund, Amount::from_uint(1, 0).expect("Test failed")); } /// Test that we don't refund more than GAS_LIMIT_RESOLUTION #[test] fn test_gas_limit_too_high_no_refund() { - let limit = GasLimit { multiplier: 2 }; - let refund = limit.refund_amount(GAS_LIMIT_RESOLUTION - 1); - assert_eq!(refund, Amount::from(GAS_LIMIT_RESOLUTION)); + let limit = GasLimit { + multiplier: 2.into(), + }; + let refund = + limit.refund_amount(Uint::from(GAS_LIMIT_RESOLUTION - 1)); + assert_eq!( + refund, + Amount::from_uint(GAS_LIMIT_RESOLUTION, 0) + .expect("Test failed") + ); } /// Test that if gas usage was underestimated, we issue no refund #[test] fn test_gas_limit_too_low_no_refund() { - let limit = GasLimit { multiplier: 1 }; - let refund = limit.refund_amount(GAS_LIMIT_RESOLUTION + 1); - assert_eq!(refund, Amount::from(0u64)); + let limit = GasLimit { + multiplier: 1.into(), + }; + let refund = + limit.refund_amount(Uint::from(GAS_LIMIT_RESOLUTION + 1)); + assert_eq!(refund, Amount::default()); } } @@ -354,6 +375,7 @@ pub mod wrapper_tx { use super::*; use crate::proto::SignedTxData; use crate::types::address::nam; + use crate::types::token::NATIVE_MAX_DECIMAL_PLACES; fn gen_keypair() -> common::SecretKey { use rand::prelude::ThreadRng; @@ -375,12 +397,13 @@ pub mod wrapper_tx { let wrapper = WrapperTx::new( Fee { - amount: 10.into(), + amount: Amount::from_uint(10, NATIVE_MAX_DECIMAL_PLACES) + .expect("Test failed"), token: nam(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), tx.clone(), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -403,12 +426,13 @@ pub mod wrapper_tx { let mut wrapper = WrapperTx::new( Fee { - amount: 10.into(), + amount: Amount::from_uint(10, NATIVE_MAX_DECIMAL_PLACES) + .expect("Test failed"), token: nam(), }, &gen_keypair(), Epoch(0), - 0.into(), + Default::default(), tx, Default::default(), #[cfg(not(feature = "mainnet"))] @@ -437,12 +461,13 @@ pub mod wrapper_tx { // the signed tx let mut tx = WrapperTx::new( Fee { - amount: 10.into(), + amount: Amount::from_uint(10, NATIVE_MAX_DECIMAL_PLACES) + .expect("Test failed"), token: nam(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), tx, Default::default(), #[cfg(not(feature = "mainnet"))] diff --git a/core/src/types/uint.rs b/core/src/types/uint.rs index 4d0d19484b..443a69caea 100644 --- a/core/src/types/uint.rs +++ b/core/src/types/uint.rs @@ -166,3 +166,26 @@ impl Sub for SignedUint { self + (-rhs) } } + +impl From for SignedUint { + fn from(val: i128) -> Self { + if val < 0 { + let abs = Self((-val).into()); + -abs + } else { + Self(val.into()) + } + } +} + +impl From for SignedUint { + fn from(val: i64) -> Self { + Self::from(val as i128) + } +} + +impl From for SignedUint { + fn from(val: i32) -> Self { + Self::from(val as i128) + } +} diff --git a/tests/src/e2e/helpers.rs b/tests/src/e2e/helpers.rs index 943962f620..a7b2714548 100644 --- a/tests/src/e2e/helpers.rs +++ b/tests/src/e2e/helpers.rs @@ -16,6 +16,7 @@ use namada::types::storage::Epoch; use namada::types::token; use namada_apps::config::genesis::genesis_config; use namada_apps::config::{Config, TendermintMode}; +use namada_core::types::token::NATIVE_MAX_DECIMAL_PLACES; use super::setup::{ self, sleep, NamadaBgCmd, Test, ENV_VAR_DEBUG, @@ -184,12 +185,13 @@ pub fn find_bonded_stake( .rsplit_once(' ') .unwrap() .1; - token::Amount::from_str(bonded_stake_str).map_err(|e| { - eyre!(format!( - "Bonded stake: {} parsed from {}, Error: {}\n\nOutput: {}", - bonded_stake_str, matched, e, unread - )) - }) + token::Amount::from_str(bonded_stake_str, NATIVE_MAX_DECIMAL_PLACES) + .map_err(|e| { + eyre!(format!( + "Bonded stake: {} parsed from {}, Error: {}\n\nOutput: {}", + bonded_stake_str, matched, e, unread + )) + }) } /// Get the last committed epoch. diff --git a/tests/src/e2e/ibc_tests.rs b/tests/src/e2e/ibc_tests.rs index 6c2c35d89a..9f6196b97d 100644 --- a/tests/src/e2e/ibc_tests.rs +++ b/tests/src/e2e/ibc_tests.rs @@ -701,7 +701,7 @@ fn transfer_token( ALBERT, &receiver, NAM, - &Amount::whole(100000), + &Amount::native_whole(100000), port_channel_id_a, None, None, @@ -820,7 +820,7 @@ fn transfer_back( BERTHA, &receiver, NAM, - &Amount::whole(50000), + &Amount::native_whole(50000), port_channel_id_b, Some(sub_prefix), None, @@ -879,7 +879,7 @@ fn transfer_timeout( ALBERT, &receiver, NAM, - &Amount::whole(100000), + &Amount::native_whole(100000), port_channel_id_a, None, Some(Duration::new(5, 0)), @@ -924,7 +924,7 @@ fn transfer_timeout_on_close( BERTHA, &receiver, NAM, - &Amount::whole(100000), + &Amount::native_whole(100000), port_channel_id_b, None, None, @@ -973,7 +973,7 @@ fn try_transfer_on_close( ALBERT, &receiver, NAM, - &Amount::whole(100000), + &Amount::native_whole(100000), port_channel_id_a, None, None, @@ -1095,7 +1095,7 @@ fn transfer( let rpc = get_actor_rpc(test, &Who::Validator(0)); let receiver = receiver.to_string(); - let amount = amount.to_string(); + let amount = amount.to_string_native(); let port_id = port_channel_id.port_id.to_string(); let channel_id = port_channel_id.channel_id.to_string(); let mut tx_args = vec![ diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 7649b379f2..b532cc7102 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -26,6 +26,7 @@ use namada_apps::client::tx::ShieldedContext; use namada_apps::config::genesis::genesis_config::{ GenesisConfig, ParametersConfig, PosParamsConfig, }; +use namada_core::types::token::{MaspDenom, NATIVE_MAX_DECIMAL_PLACES}; use serde_json::json; use setup::constants::*; @@ -459,7 +460,7 @@ fn ledger_txs_and_queries() -> Result<()> { } let christel = find_address(&test, CHRISTEL)?; // as setup in `genesis/e2e-tests-single-node.toml` - let christel_balance = token::Amount::whole(1000000); + let christel_balance = token::Amount::native_whole(1000000); let nam = find_address(&test, NAM)?; let storage_key = token::balance_key(&nam, &christel).to_string(); let query_args_and_expected_response = vec![ @@ -1051,8 +1052,10 @@ fn masp_incentives() -> Result<()> { client.exp_string("BTC: 20")?; client.assert_success(); - let amt20 = token::Amount::from_str("20").unwrap(); - let amt30 = token::Amount::from_str("30").unwrap(); + let amt20 = + token::Amount::from_uint(20, NATIVE_MAX_DECIMAL_PLACES).unwrap(); + let amt30 = + token::Amount::from_uint(30, NATIVE_MAX_DECIMAL_PLACES).unwrap(); // Assert NAM balance at VK(A) is 20*BTC_reward*(epoch_1-epoch_0) let mut client = run!( @@ -1071,7 +1074,8 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "NAM: {}", - (amt20 * masp_rewards[&btc()]).0 * (ep1.0 - ep0.0) + ((amt20 * masp_rewards[&(btc(), MaspDenom::Zero)]).0 * (ep1.0 - ep0.0)) + .to_string_native() ))?; client.assert_success(); @@ -1092,7 +1096,8 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "NAM: {}", - (amt20 * masp_rewards[&btc()]).0 * (ep1.0 - ep0.0) + ((amt20 * masp_rewards[&(btc(), MaspDenom::Zero)]).0 * (ep1.0 - ep0.0)) + .to_string_native() ))?; client.assert_success(); @@ -1134,7 +1139,8 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "NAM: {}", - (amt20 * masp_rewards[&btc()]).0 * (ep2.0 - ep0.0) + ((amt20 * masp_rewards[&(btc(), MaspDenom::Zero)]).0 * (ep2.0 - ep0.0)) + .to_string_native() ))?; client.assert_success(); @@ -1155,7 +1161,8 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "NAM: {}", - (amt20 * masp_rewards[&btc()]).0 * (ep2.0 - ep0.0) + ((amt20 * masp_rewards[&(btc(), MaspDenom::Zero)]).0 * (ep2.0 - ep0.0)) + .to_string_native(), ))?; client.assert_success(); @@ -1258,7 +1265,8 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "NAM: {}", - (amt30 * masp_rewards[ð()]).0 * (ep4.0 - ep3.0) + ((amt30 * masp_rewards[&(eth(), MaspDenom::Zero)]).0 * (ep4.0 - ep3.0)) + .to_string_native(), ))?; client.assert_success(); @@ -1280,8 +1288,11 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "NAM: {}", - ((amt20 * masp_rewards[&btc()]).0 * (ep4.0 - ep0.0)) - + ((amt30 * masp_rewards[ð()]).0 * (ep4.0 - ep3.0)) + (((amt20 * masp_rewards[&(btc(), MaspDenom::Zero)]).0 + * (ep4.0 - ep0.0)) + + ((amt30 * masp_rewards[&(eth(), MaspDenom::Zero)]).0 + * (ep4.0 - ep3.0))) + .to_string_native() ))?; client.assert_success(); @@ -1349,7 +1360,8 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "NAM: {}", - (amt30 * masp_rewards[ð()]).0 * (ep.0 - ep3.0) + ((amt30 * masp_rewards[&(eth(), MaspDenom::Zero)]).0 * (ep.0 - ep3.0)) + .to_string_native() ))?; client.assert_success(); @@ -1372,8 +1384,11 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "NAM: {}", - ((amt20 * masp_rewards[&btc()]).0 * (ep.0 - ep0.0)) - + ((amt30 * masp_rewards[ð()]).0 * (ep.0 - ep3.0)) + (((amt20 * masp_rewards[&(btc(), MaspDenom::Zero)]).0 + * (ep.0 - ep0.0)) + + ((amt30 * masp_rewards[&(eth(), MaspDenom::Zero)]).0 + * (ep.0 - ep3.0))) + .to_string_native() ))?; client.assert_success(); @@ -1439,7 +1454,8 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "NAM: {}", - (amt20 * masp_rewards[&btc()]).0 * (ep6.0 - ep0.0) + ((amt20 * masp_rewards[&(btc(), MaspDenom::Zero)]).0 * (ep6.0 - ep0.0)) + .to_string_native() ))?; client.assert_success(); @@ -1461,8 +1477,11 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "NAM: {}", - ((amt20 * masp_rewards[&btc()]).0 * (ep6.0 - ep0.0)) - + ((amt30 * masp_rewards[ð()]).0 * (ep5.0 - ep3.0)) + (((amt20 * masp_rewards[&(btc(), MaspDenom::Zero)]).0 + * (ep6.0 - ep0.0)) + + ((amt30 * masp_rewards[&(eth(), MaspDenom::Zero)]).0 + * (ep5.0 - ep3.0))) + .to_string_native() ))?; client.assert_success(); @@ -1486,7 +1505,8 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "NAM: {}", - (amt20 * masp_rewards[&btc()]).0 * (ep6.0 - ep0.0) + ((amt20 * masp_rewards[&(btc(), MaspDenom::Zero)]).0 * (ep6.0 - ep0.0)) + .to_string_native() ))?; client.assert_success(); @@ -1507,7 +1527,8 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "NAM: {}", - (amt30 * masp_rewards[ð()]).0 * (ep5.0 - ep3.0) + ((amt30 * masp_rewards[&(eth(), MaspDenom::Zero)]).0 * (ep5.0 - ep3.0)) + .to_string_native() ))?; client.assert_success(); @@ -1529,8 +1550,11 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "NAM: {}", - ((amt20 * masp_rewards[&btc()]).0 * (ep6.0 - ep0.0)) - + ((amt30 * masp_rewards[ð()]).0 * (ep5.0 - ep3.0)) + (((amt20 * masp_rewards[&(btc(), MaspDenom::Zero)]).0 + * (ep6.0 - ep0.0)) + + ((amt30 * masp_rewards[&(eth(), MaspDenom::Zero)]).0 + * (ep5.0 - ep3.0))) + .to_string_native() ))?; client.assert_success(); @@ -1551,7 +1575,9 @@ fn masp_incentives() -> Result<()> { "--token", NAM, "--amount", - &((amt30 * masp_rewards[ð()]).0 * (ep5.0 - ep3.0)).to_string(), + &((amt30 * masp_rewards[&(eth(), MaspDenom::Zero)]).0 + * (ep5.0 - ep3.0)) + .to_string_native(), "--signer", BERTHA, "--ledger-address", @@ -1578,7 +1604,9 @@ fn masp_incentives() -> Result<()> { "--token", NAM, "--amount", - &((amt20 * masp_rewards[&btc()]).0 * (ep6.0 - ep0.0)).to_string(), + &((amt20 * masp_rewards[&(btc(), MaspDenom::Zero)]).0 + * (ep6.0 - ep0.0)) + .to_string_native(), "--signer", ALBERT, "--ledger-address", @@ -1673,7 +1701,10 @@ fn invalid_transactions() -> Result<()> { target: find_address(&test, ALBERT)?, token: find_address(&test, NAM)?, sub_prefix: None, - amount: token::Amount::whole(1), + amount: token::DenominatedAmount { + amount: token::Amount::native_whole(1), + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }, key: None, shielded: None, }; @@ -2167,7 +2198,9 @@ fn pos_init_validator() -> Result<()> { // 7. Check the new validator's bonded stake let bonded_stake = find_bonded_stake(&test, new_validator, &validator_one_rpc)?; - assert_eq!(bonded_stake, token::Amount::from_str("11_000.5").unwrap()); + let amount = + token::Amount::from_str("11_000.5", NATIVE_MAX_DECIMAL_PLACES).unwrap(); + assert_eq!(bonded_stake, amount); Ok(()) } diff --git a/tests/src/e2e/multitoken_tests.rs b/tests/src/e2e/multitoken_tests.rs index 460395cc59..0f2b15d877 100644 --- a/tests/src/e2e/multitoken_tests.rs +++ b/tests/src/e2e/multitoken_tests.rs @@ -26,7 +26,7 @@ fn test_multitoken_transfer_implicit_to_implicit() -> Result<()> { println!("Fake multitoken VP established at {}", multitoken_vp_addr); let albert_addr = e2e::helpers::find_address(&test, ALBERT)?; - let albert_starting_red_balance = token::Amount::from(100_000_000); + let albert_starting_red_balance = token::Amount::native_whole(100_000_000); helpers::mint_red_tokens( &test, &rpc_addr, @@ -35,7 +35,7 @@ fn test_multitoken_transfer_implicit_to_implicit() -> Result<()> { &albert_starting_red_balance, )?; - let transfer_amount = token::Amount::from(10_000_000); + let transfer_amount = token::Amount::native_whole(10_000_000); // make a transfer from Albert to Bertha, signed by Christel - this should // be rejected @@ -70,7 +70,7 @@ fn test_multitoken_transfer_implicit_to_implicit() -> Result<()> { ALBERT, BERTHA, ALBERT, - &token::Amount::from(10_000_000), + &token::Amount::native_whole(10_000_000), )?; authorized_transfer.exp_string("Transaction applied with result")?; authorized_transfer.exp_string("Transaction is valid")?; @@ -110,7 +110,8 @@ fn test_multitoken_transfer_established_to_implicit() -> Result<()> { established_alias, )?; - let established_starting_red_balance = token::Amount::from(100_000_000); + let established_starting_red_balance = + token::Amount::native_whole(100_000_000); // mint some red tokens for the established account let established_addr = e2e::helpers::find_address(&test, established_alias)?; @@ -122,7 +123,7 @@ fn test_multitoken_transfer_established_to_implicit() -> Result<()> { &established_starting_red_balance, )?; - let transfer_amount = token::Amount::from(10_000_000); + let transfer_amount = token::Amount::native_whole(10_000_000); // attempt an unauthorized transfer to Albert from the established account let mut unauthorized_transfer = helpers::attempt_red_tokens_transfer( &test, @@ -197,7 +198,7 @@ fn test_multitoken_transfer_implicit_to_established() -> Result<()> { )?; let albert_addr = e2e::helpers::find_address(&test, ALBERT)?; - let albert_starting_red_balance = token::Amount::from(100_000_000); + let albert_starting_red_balance = token::Amount::native_whole(100_000_000); helpers::mint_red_tokens( &test, &rpc_addr, @@ -206,7 +207,7 @@ fn test_multitoken_transfer_implicit_to_established() -> Result<()> { &albert_starting_red_balance, )?; - let transfer_amount = token::Amount::from(10_000_000); + let transfer_amount = token::Amount::native_whole(10_000_000); // attempt an unauthorized transfer from Albert to the established account let mut unauthorized_transfer = helpers::attempt_red_tokens_transfer( @@ -280,7 +281,8 @@ fn test_multitoken_transfer_established_to_established() -> Result<()> { established_alias, )?; - let established_starting_red_balance = token::Amount::from(100_000_000); + let established_starting_red_balance = + token::Amount::native_whole(100_000_000); // mint some red tokens for the established account let established_addr = e2e::helpers::find_address(&test, established_alias)?; @@ -302,7 +304,8 @@ fn test_multitoken_transfer_established_to_established() -> Result<()> { receiver_alias, )?; - let established_starting_red_balance = token::Amount::from(100_000_000); + let established_starting_red_balance = + token::Amount::native_whole(100_000_000); // mint some red tokens for the established account let established_addr = e2e::helpers::find_address(&test, established_alias)?; @@ -314,7 +317,7 @@ fn test_multitoken_transfer_established_to_established() -> Result<()> { &established_starting_red_balance, )?; - let transfer_amount = token::Amount::from(10_000_000); + let transfer_amount = token::Amount::native_whole(10_000_000); // attempt an unauthorized transfer let mut unauthorized_transfer = helpers::attempt_red_tokens_transfer( diff --git a/tests/src/e2e/multitoken_tests/helpers.rs b/tests/src/e2e/multitoken_tests/helpers.rs index fb1138ca82..73e230862b 100644 --- a/tests/src/e2e/multitoken_tests/helpers.rs +++ b/tests/src/e2e/multitoken_tests/helpers.rs @@ -1,11 +1,11 @@ //! Helpers for use in multitoken tests. use std::path::PathBuf; -use std::str::FromStr; use borsh::BorshSerialize; use color_eyre::eyre::Result; use eyre::Context; use namada_core::types::address::Address; +use namada_core::types::token::NATIVE_MAX_DECIMAL_PLACES; use namada_core::types::{storage, token}; use namada_test_utils::tx_data::TxWriteData; use namada_tx_prelude::storage::KeySeg; @@ -138,7 +138,7 @@ pub fn attempt_red_tokens_transfer( signer: &str, amount: &token::Amount, ) -> Result { - let amount = amount.to_string(); + let amount = amount.to_string_native(); let transfer_args = vec![ "transfer", "--token", @@ -184,6 +184,6 @@ pub fn fetch_red_token_balance( println!("Got balance for {}: {}", owner_alias, matched); let decimal = decimal_regex.find(&matched).unwrap().as_str(); client_balance.assert_success(); - token::Amount::from_str(decimal) + token::Amount::from_str(decimal, NATIVE_MAX_DECIMAL_PLACES) .wrap_err(format!("Failed to parse {}", matched)) } diff --git a/tests/src/native_vp/pos.rs b/tests/src/native_vp/pos.rs index d3e3773f47..b2e6166703 100644 --- a/tests/src/native_vp/pos.rs +++ b/tests/src/native_vp/pos.rs @@ -517,7 +517,7 @@ mod tests { validator: &Address, amount: token::Amount, ) -> bool { - let raw_amount: u64 = amount.into(); + let raw_amount: u128 = amount.try_into().unwrap(); let mut total_bonds: u64 = 0; for action in self.all_valid_actions().into_iter() { match action { @@ -528,8 +528,8 @@ mod tests { } => { if owner == &bond_owner && validator == &bond_validator { - let raw_amount: u64 = amount.into(); - total_bonds += raw_amount; + let raw_amount: u128 = amount.try_into().unwrap(); + total_bonds += raw_amount as u64; } } ValidPosAction::Unbond { @@ -539,15 +539,15 @@ mod tests { } => { if owner == &bond_owner && validator == &bond_validator { - let raw_amount: u64 = amount.into(); - total_bonds -= raw_amount; + let raw_amount: u128 = amount.try_into().unwrap(); + total_bonds -= raw_amount as u64; } } _ => {} } } - total_bonds >= raw_amount + total_bonds as u128 >= raw_amount } /// Find if the given owner and validator has unbonds that are ready to @@ -591,6 +591,9 @@ pub mod testing { use namada::types::key::RefTo; use namada::types::storage::Epoch; use namada::types::{address, key, token}; + use namada_core::types::token::{ + Amount, Change, NATIVE_MAX_DECIMAL_PLACES, + }; use namada_tx_prelude::{Address, StorageRead, StorageWrite}; use proptest::prelude::*; use rust_decimal::Decimal; @@ -655,14 +658,14 @@ pub mod testing { Bond { owner: Address, validator: Address, - delta: i128, + delta: Change, offset: DynEpochOffset, }, /// Add tokens unbonded from a bond at unbonding offset Unbond { owner: Address, validator: Address, - delta: i128, + delta: Change, }, /// Withdraw tokens from an unbond at the current epoch WithdrawUnbond { @@ -670,12 +673,12 @@ pub mod testing { validator: Address, }, TotalDeltas { - delta: i128, + delta: Change, offset: Either, }, ValidatorSet { validator: Address, - token_delta: i128, + token_delta: Change, offset: DynEpochOffset, }, ValidatorConsensusKey { @@ -685,7 +688,7 @@ pub mod testing { }, ValidatorDeltas { validator: Address, - delta: i128, + delta: Change, offset: DynEpochOffset, }, ValidatorState { @@ -693,7 +696,7 @@ pub mod testing { state: ValidatorState, }, StakingTokenPosBalance { - delta: i128, + delta: Change, }, ValidatorAddressRawHash { address: Address, @@ -764,7 +767,11 @@ pub mod testing { arb_validator, ) .prop_map(|(amount, owner, validator)| ValidPosAction::Bond { - amount: amount.into(), + amount: Amount::from_uint( + amount, + NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(), owner, validator, }); @@ -794,12 +801,19 @@ pub mod testing { // them let arb_unbond = arb_current_bond.prop_flat_map( |(bond_id, current_bond_amount)| { - let current_bond_amount: u64 = - current_bond_amount.into(); + let current_bond_amount = + >::try_into( + current_bond_amount, + ) + .unwrap() as u64; // Unbond an arbitrary amount up to what's available (0..current_bond_amount).prop_map(move |amount| { ValidPosAction::Unbond { - amount: amount.into(), + amount: Amount::from_uint( + amount, + NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(), owner: bond_id.source.clone(), validator: bond_id.validator.clone(), } @@ -898,7 +912,7 @@ pub mod testing { }, PosStorageChange::ValidatorSet { validator: address.clone(), - token_delta: 0, + token_delta: 0.into(), offset, }, PosStorageChange::ValidatorConsensusKey { @@ -911,7 +925,7 @@ pub mod testing { }, PosStorageChange::ValidatorDeltas { validator: address.clone(), - delta: 0, + delta: 0.into(), offset, }, PosStorageChange::ValidatorCommissionRate { @@ -1317,13 +1331,11 @@ pub mod testing { ); let mut balance: token::Amount = tx::ctx().read(&balance_key).unwrap().unwrap_or_default(); - if delta < 0 { - let to_spend: u64 = (-delta).try_into().unwrap(); - let to_spend: token::Amount = to_spend.into(); + if !delta.non_negative() { + let to_spend = token::Amount::from_change(delta); balance.spend(&to_spend); } else { - let to_recv: u64 = delta.try_into().unwrap(); - let to_recv: token::Amount = to_recv.into(); + let to_recv = token::Amount::from_change(delta); balance.receive(&to_recv); } tx::ctx().write(&balance_key, balance).unwrap(); @@ -1378,7 +1390,7 @@ pub mod testing { pub fn apply_validator_set_change( _validator: Address, - _token_delta: i128, + _token_delta: Change, _offset: DynEpochOffset, _current_epoch: Epoch, _params: &PosParams, @@ -1557,7 +1569,7 @@ pub mod testing { PosStorageChange::Bond { owner, validator, - delta, + delta: delta.into(), offset, }, ] diff --git a/tests/src/vm_host_env/mod.rs b/tests/src/vm_host_env/mod.rs index 04a545e8b1..8754928b3e 100644 --- a/tests/src/vm_host_env/mod.rs +++ b/tests/src/vm_host_env/mod.rs @@ -35,6 +35,7 @@ mod tests { use namada::types::time::DateTimeUtc; use namada::types::token::{self, Amount}; use namada::types::{address, key}; + use namada_core::types::token::NATIVE_MAX_DECIMAL_PLACES; use namada_tx_prelude::{ BorshDeserialize, BorshSerialize, StorageRead, StorageWrite, }; @@ -1312,7 +1313,9 @@ mod tests { let denom = format!("{}/{}/{}", port_id, channel_id, token); let key_prefix = ibc_storage::ibc_token_prefix(denom).unwrap(); let key = token::multitoken_balance_key(&key_prefix, &sender); - let init_bal = Amount::from(1_000_000_000u64); + let init_bal = + Amount::from_uint(1_000_000_000u64, NATIVE_MAX_DECIMAL_PLACES) + .unwrap(); writes.insert(key, init_bal.try_to_vec().unwrap()); writes.into_iter().for_each(|(key, val)| { tx_host_env::with(|env| { @@ -1373,7 +1376,9 @@ mod tests { let denom = format!("{}/{}/{}", port_id, channel_id, token); let key_prefix = ibc_storage::ibc_token_prefix(denom).unwrap(); let key = token::multitoken_balance_key(&key_prefix, &receiver); - let init_bal = Amount::from(1_000_000_000u64); + let init_bal = + Amount::from_uint(1_000_000_000u64, NATIVE_MAX_DECIMAL_PLACES) + .unwrap(); writes.insert(key, init_bal.try_to_vec().unwrap()); writes.into_iter().for_each(|(key, val)| { @@ -1452,7 +1457,11 @@ mod tests { &key_prefix, &address::Address::Internal(address::InternalAddress::IbcEscrow), ); - let val = Amount::from(1_000_000_000u64).try_to_vec().unwrap(); + let val = + Amount::from_uint(1_000_000_000u64, NATIVE_MAX_DECIMAL_PLACES) + .unwrap() + .try_to_vec() + .unwrap(); tx_host_env::with(|env| { env.wl_storage .storage diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index f0ec20a6a7..30dd14eb1a 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -287,7 +287,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43473b34abc4b0b405efa0a250bac87eea888182b21687ee5c8115d279b0fda5" dependencies = [ - "bitvec", + "bitvec 0.22.3", "blake2s_simd 0.5.11", "byteorder", "crossbeam-channel 0.5.6", @@ -374,10 +374,22 @@ version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5237f00a8c86130a0cc317830e558b966dd7850d48a953d998c813f01a41b527" dependencies = [ - "funty", - "radium", + "funty 1.2.0", + "radium 0.6.2", "tap", - "wyz", + "wyz 0.4.0", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty 2.0.0", + "radium 0.7.0", + "tap", + "wyz 0.5.1", ] [[package]] @@ -502,7 +514,7 @@ source = "git+https://github.com/heliaxdev/borsh-rs.git?rev=cd5223e5103c4f139e0c dependencies = [ "borsh-derive-internal", "borsh-schema-derive-internal", - "proc-macro-crate", + "proc-macro-crate 0.1.5", "proc-macro2", "syn", ] @@ -533,6 +545,12 @@ version = "3.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" +[[package]] +name = "byte-slice-cast" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c" + [[package]] name = "bytecheck" version = "0.6.9" @@ -1334,6 +1352,50 @@ dependencies = [ "version_check", ] +[[package]] +name = "ethabi" +version = "18.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7413c5f74cc903ea37386a8965a936cbeb334bd270862fdece542c1b2dcbc898" +dependencies = [ + "ethereum-types", + "hex", + "once_cell", + "regex", + "serde", + "serde_json", + "sha3 0.10.6", + "thiserror", + "uint", +] + +[[package]] +name = "ethbloom" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c22d4b5885b6aa2fe5e8b9329fb8d232bf739e434e6b87347c63bdd00c120f60" +dependencies = [ + "crunchy 0.2.2", + "fixed-hash", + "impl-rlp", + "impl-serde", + "tiny-keccak", +] + +[[package]] +name = "ethereum-types" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d215cbf040552efcbe99a38372fe80ab9d00268e20012b79fcd0f073edd8ee" +dependencies = [ + "ethbloom", + "fixed-hash", + "impl-rlp", + "impl-serde", + "primitive-types", + "uint", +] + [[package]] name = "eyre" version = "0.6.8" @@ -1378,11 +1440,23 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "131655483be284720a17d74ff97592b8e76576dc25563148601df2d7c9080924" dependencies = [ - "bitvec", + "bitvec 0.22.3", "rand_core 0.6.4", "subtle", ] +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rand 0.8.5", + "rustc-hex", + "static_assertions", +] + [[package]] name = "fixedbitset" version = "0.4.2" @@ -1434,6 +1508,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1847abb9cb65d566acd5942e94aea9c8f547ad02c98e1649326fc0e8910b8b1e" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.25" @@ -2012,7 +2092,7 @@ dependencies = [ "prost", "ripemd160", "sha2 0.9.9", - "sha3", + "sha3 0.9.1", "sp-std", ] @@ -2032,6 +2112,55 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-num-traits" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951641f13f873bff03d4bf19ae8bec531935ac0ac2cc775f84d7edfdcfed3f17" +dependencies = [ + "integer-sqrt", + "num-traits", + "uint", +] + +[[package]] +name = "impl-rlp" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28220f89297a075ddc7245cd538076ee98b01f2a9c23a53a4f1105d5a322808" +dependencies = [ + "rlp", +] + +[[package]] +name = "impl-serde" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc88fc67028ae3db0c853baa36269d398d5f45b6982f95549ff5def78c935cd" +dependencies = [ + "serde", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d7a9f6330b71fea57921c9b61c47ee6e84f72d394754eff6163ae67e7395eb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "incrementalmerkletree" version = "0.2.0" @@ -2085,6 +2214,15 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "integer-sqrt" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770" +dependencies = [ + "num-traits", +] + [[package]] name = "itertools" version = "0.10.5" @@ -2115,7 +2253,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7baec19d4e83f9145d4891178101a604565edff9645770fc979804138b04c" dependencies = [ - "bitvec", + "bitvec 0.22.3", "bls12_381", "ff", "group", @@ -2285,7 +2423,7 @@ source = "git+https://github.com/anoma/masp?rev=bee40fc465f6afbd10558d12fe96eb17 dependencies = [ "aes", "bip0039", - "bitvec", + "bitvec 0.22.3", "blake2b_simd 1.0.0", "blake2s_simd 1.0.0", "bls12_381", @@ -2511,10 +2649,12 @@ dependencies = [ "data-encoding", "derivative", "ed25519-consensus", + "ethabi", "ferveo-common", "ibc", "ibc-proto", "ics23", + "impl-num-traits", "index-set", "itertools", "libsecp256k1", @@ -2537,6 +2677,7 @@ dependencies = [ "thiserror", "tonic-build", "tracing", + "uint", "zeroize", ] @@ -2791,7 +2932,7 @@ dependencies = [ "aes", "arrayvec 0.7.2", "bigint", - "bitvec", + "bitvec 0.22.3", "blake2b_simd 1.0.0", "ff", "fpe", @@ -2818,6 +2959,32 @@ dependencies = [ "group", ] +[[package]] +name = "parity-scale-codec" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "637935964ff85a605d114591d4d2c13c5d1ba2806dae97cea6bf180238a749ac" +dependencies = [ + "arrayvec 0.7.2", + "bitvec 1.0.1", + "byte-slice-cast", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b26a931f824dd4eca30b3e43bb4f31cd5f0d3a403c5f5ff27106b805bfde7b" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "parity-wasm" version = "0.45.0" @@ -3011,6 +3178,19 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "primitive-types" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f3486ccba82358b11a77516035647c34ba167dfa53312630de83b12bd4f3d66" +dependencies = [ + "fixed-hash", + "impl-codec", + "impl-rlp", + "impl-serde", + "uint", +] + [[package]] name = "proc-macro-crate" version = "0.1.5" @@ -3020,6 +3200,16 @@ dependencies = [ "toml", ] +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -3209,6 +3399,12 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.7.3" @@ -3495,6 +3691,16 @@ dependencies = [ "syn", ] +[[package]] +name = "rlp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" +dependencies = [ + "bytes", + "rustc-hex", +] + [[package]] name = "rust_decimal" version = "1.26.1" @@ -3529,6 +3735,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + [[package]] name = "rustc_version" version = "0.3.3" @@ -3893,6 +4105,16 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "sha3" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdf0c33fae925bdc080598b84bc15c55e7b9a4a43b3c704da051f977469691c9" +dependencies = [ + "digest 0.10.5", + "keccak", +] + [[package]] name = "sharded-slab" version = "0.1.4" @@ -4447,6 +4669,23 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" + +[[package]] +name = "toml_edit" +version = "0.19.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "tonic" version = "0.6.2" @@ -4638,9 +4877,9 @@ checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" [[package]] name = "uint" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a45526d29728d135c2900b0d30573fe3ee79fceb12ef534c7bb30e810a91b601" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" dependencies = [ "byteorder", "crunchy 0.2.2", @@ -5312,6 +5551,15 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +[[package]] +name = "winnow" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" +dependencies = [ + "memchr", +] + [[package]] name = "wyz" version = "0.4.0" @@ -5321,6 +5569,15 @@ dependencies = [ "tap", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "zcash_encoding" version = "0.0.0" @@ -5360,7 +5617,7 @@ source = "git+https://github.com/zcash/librustzcash/?rev=2425a08#2425a0869098e3b dependencies = [ "aes", "bip0039", - "bitvec", + "bitvec 0.22.3", "blake2b_simd 1.0.0", "blake2s_simd 1.0.0", "bls12_381", diff --git a/wasm/wasm_source/src/tx_bond.rs b/wasm/wasm_source/src/tx_bond.rs index b949731a70..b4cfd51842 100644 --- a/wasm/wasm_source/src/tx_bond.rs +++ b/wasm/wasm_source/src/tx_bond.rs @@ -186,7 +186,7 @@ mod tests { // Check that the validator set and deltas are unchanged before pipeline // length and that they are updated between the pipeline and // unbonding lengths - if bond.amount == token::Amount::from(0) { + if bond.amount.is_zero() { // None of the optional storage fields should have been updated assert_eq!(epoched_validator_set_pre, epoched_validator_set_post); assert_eq!( @@ -217,7 +217,7 @@ mod tests { ..=pos_params.unbonding_len as usize { let expected_stake = - i128::from(initial_stake) + i128::from(bond.amount); + initial_stake.change() + bond.amount.change(); assert_eq!( epoched_validator_stake_post[epoch], token::Amount::from_change(expected_stake), @@ -341,7 +341,7 @@ mod tests { // Generate initial stake (initial_stake in token::testing::arb_amount_ceiled((i64::MAX/8) as u64)) // Use the initial stake to limit the bond amount - (bond in arb_bond(((i64::MAX/8) as u64) - u64::from(initial_stake)), + (bond in arb_bond(((i64::MAX/8) as u64) - u128::try_from(initial_stake).unwrap() as u64), // Use the generated initial stake too initial_stake in Just(initial_stake), ) -> (token::Amount, transaction::pos::Bond) { diff --git a/wasm/wasm_source/src/tx_change_validator_commission.rs b/wasm/wasm_source/src/tx_change_validator_commission.rs index 3b77c9197c..5e68993097 100644 --- a/wasm/wasm_source/src/tx_change_validator_commission.rs +++ b/wasm/wasm_source/src/tx_change_validator_commission.rs @@ -68,7 +68,11 @@ mod tests { let consensus_key = key::testing::keypair_1().ref_to(); let genesis_validators = [GenesisValidator { address: commission_change.validator.clone(), - tokens: token::Amount::from(1_000_000), + tokens: token::Amount::from_uint( + 1_000_000, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(), consensus_key, commission_rate: initial_rate, max_commission_rate_change: max_change, diff --git a/wasm/wasm_source/src/tx_unbond.rs b/wasm/wasm_source/src/tx_unbond.rs index 42fa0666bc..33d8653a12 100644 --- a/wasm/wasm_source/src/tx_unbond.rs +++ b/wasm/wasm_source/src/tx_unbond.rs @@ -202,7 +202,7 @@ mod tests { let expected_amount_before_pipeline = if is_delegation { // When this is a delegation, there will be no bond until pipeline - 0.into() + token::Amount::default() } else { // Before pipeline offset, there can only be self-bond initial_stake @@ -276,7 +276,7 @@ mod tests { { let epoch = pos_params.unbonding_len + 1; let expected_stake = - i128::from(initial_stake) - i128::from(unbond.amount); + initial_stake.change() - unbond.amount.change(); assert_eq!( read_validator_stake( ctx(), @@ -405,7 +405,8 @@ mod tests { token::testing::arb_amount_ceiled((i64::MAX / 8) as u64).prop_flat_map( |initial_stake| { // Use the initial stake to limit the bond amount - let unbond = arb_unbond(u64::from(initial_stake)); + let unbond = + arb_unbond(u128::try_from(initial_stake).unwrap() as u64); // Use the generated initial stake too too (Just(initial_stake), unbond) }, diff --git a/wasm/wasm_source/src/tx_withdraw.rs b/wasm/wasm_source/src/tx_withdraw.rs index 80c0f00265..a16dc81572 100644 --- a/wasm/wasm_source/src/tx_withdraw.rs +++ b/wasm/wasm_source/src/tx_withdraw.rs @@ -14,7 +14,7 @@ fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { let slashed = ctx.withdraw_tokens(withdraw.source.as_ref(), &withdraw.validator)?; if slashed != token::Amount::default() { - debug_log!("New withdrawal slashed for {}", slashed); + debug_log!("New withdrawal slashed for {}", slashed.to_string_native()); } Ok(()) } @@ -216,7 +216,7 @@ mod tests { // stake let unbonded_amount = token::testing::arb_amount_non_zero_ceiled( - initial_stake.into(), + u128::try_from(initial_stake).unwrap() as u64, ); // Use the generated initial stake too too (Just(initial_stake), unbonded_amount) diff --git a/wasm/wasm_source/src/vp_implicit.rs b/wasm/wasm_source/src/vp_implicit.rs index 31a920a540..97b50c2511 100644 --- a/wasm/wasm_source/src/vp_implicit.rs +++ b/wasm/wasm_source/src/vp_implicit.rs @@ -123,12 +123,17 @@ fn validate_tx( ctx.read_post(key)?.unwrap_or_default(); let change = post.change() - pre.change(); // debit has to signed, credit doesn't - let valid = change >= 0 || *valid_sig; + let valid = change.non_negative() || *valid_sig; + let sign = if change.non_negative() { "" } else { "-" }; + let denom_amount = token::Amount::from_change(change) + .denominated(owner, &ctx.pre()) + .unwrap(); debug_log!( - "token key: {}, change: {}, valid_sig: {}, valid \ + "token key: {}, change: {}{}, valid_sig: {}, valid \ modification: {}", key, - change, + sign, + denom_amount, *valid_sig, valid ); @@ -338,8 +343,11 @@ mod tests { let vp_owner: Address = (&public_key).into(); let source = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); - + let amount = token::Amount::from_uint( + 10_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &source, &token]); @@ -347,6 +355,10 @@ mod tests { // able to transfer from it tx_env.credit_tokens(&source, &token, None, amount); + let amount = token::DenominatedAmount { + amount, + denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), + }; // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { // Apply transfer in a transaction @@ -382,7 +394,11 @@ mod tests { // Init PoS genesis let pos_params = PosParams::default(); let validator = address::testing::established_address_3(); - let initial_stake = token::Amount::from(10_098_123); + let initial_stake = token::Amount::from_uint( + 10_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); let consensus_key = key::testing::keypair_2().ref_to(); let commission_rate = rust_decimal::Decimal::new(5, 2); let max_commission_rate_change = rust_decimal::Decimal::new(1, 2); @@ -405,9 +421,21 @@ mod tests { let vp_owner: Address = (&public_key).into(); let target = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); - let bond_amount = token::Amount::from(5_098_123); - let unbond_amount = token::Amount::from(3_098_123); + let amount = token::Amount::from_uint( + 10_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); + let bond_amount = token::Amount::from_uint( + 5_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); + let unbond_amount = token::Amount::from_uint( + 3_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&target, &token]); @@ -446,7 +474,11 @@ mod tests { // Init PoS genesis let pos_params = PosParams::default(); let validator = address::testing::established_address_3(); - let initial_stake = token::Amount::from(10_098_123); + let initial_stake = token::Amount::from_uint( + 10_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); let consensus_key = key::testing::keypair_2().ref_to(); let commission_rate = rust_decimal::Decimal::new(5, 2); let max_commission_rate_change = rust_decimal::Decimal::new(1, 2); @@ -469,9 +501,21 @@ mod tests { let vp_owner: Address = (&public_key).into(); let target = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); - let bond_amount = token::Amount::from(5_098_123); - let unbond_amount = token::Amount::from(3_098_123); + let amount = token::Amount::from_uint( + 10_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); + let bond_amount = token::Amount::from_uint( + 5_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); + let unbond_amount = token::Amount::from_uint( + 3_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&target, &token]); @@ -519,7 +563,11 @@ mod tests { let vp_owner: Address = (&public_key).into(); let target = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); + let amount = token::Amount::from_uint( + 10_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &target, &token]); @@ -528,6 +576,11 @@ mod tests { // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); + let amount = token::DenominatedAmount { + amount, + denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), + }; + // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { // Apply transfer in a transaction @@ -567,7 +620,11 @@ mod tests { let vp_owner: Address = (&public_key).into(); let target = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); + let amount = token::Amount::from_uint( + 10_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &target, &token]); @@ -578,6 +635,10 @@ mod tests { tx_env.write_public_key(&vp_owner, &public_key); + let amount = token::DenominatedAmount { + amount, + denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), + }; // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { // Apply transfer in a transaction @@ -621,7 +682,11 @@ mod tests { let source = address::testing::established_address_2(); let target = address::testing::established_address_3(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); + let amount = token::Amount::from_uint( + 10_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &source, &target, &token]); @@ -630,6 +695,11 @@ mod tests { // be able to transfer from it tx_env.credit_tokens(&source, &token, None, amount); + let amount = token::DenominatedAmount { + amount, + denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), + }; + // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { tx::ctx().insert_verifier(address).unwrap(); diff --git a/wasm/wasm_source/src/vp_masp.rs b/wasm/wasm_source/src/vp_masp.rs index a9b7f53230..9dd284e3af 100644 --- a/wasm/wasm_source/src/vp_masp.rs +++ b/wasm/wasm_source/src/vp_masp.rs @@ -12,16 +12,17 @@ fn convert_amount( epoch: Epoch, token: &Address, val: token::Amount, + denom: token::MaspDenom, ) -> (AssetType, Amount) { // Timestamp the chosen token with the current epoch - let token_bytes = (token, epoch.0) + let token_bytes = (token, denom, epoch.0) .try_to_vec() .expect("token should serialize"); // Generate the unique asset identifier from the unique token address let asset_type = AssetType::new(token_bytes.as_ref()) .expect("unable to create asset type"); // Combine the value and unit into one amount - let amount = Amount::from_nonnegative(asset_type, u64::from(val)) + let amount = Amount::from_nonnegative(asset_type, denom.denominate(&val)) .expect("invalid value or asset type for amount"); (asset_type, amount) } @@ -59,14 +60,17 @@ fn validate_tx( // Note that the asset type is timestamped so shields // where the shielded value has an incorrect timestamp // are automatically rejected - let (_transp_asset, transp_amt) = convert_amount( - ctx.get_block_epoch().unwrap(), - &transfer.token, - transfer.amount, - ); + for denom in token::MaspDenom::iter() { + let (_transp_asset, transp_amt) = convert_amount( + ctx.get_block_epoch().unwrap(), + &transfer.token, + transfer.amount.into(), + denom, + ); - // Non-masp sources add to transparent tx pool - transparent_tx_pool += transp_amt; + // Non-masp sources add to transparent tx pool + transparent_tx_pool += transp_amt; + } } // Handle unshielding/transparent output @@ -74,13 +78,16 @@ fn validate_tx( // Timestamp is derived to allow unshields for older tokens let atype = shielded_tx.value_balance.components().next().unwrap().0; + for denom in token::MaspDenom::iter() { + let transp_amt = Amount::from_nonnegative( + *atype, + denom.denominate(&transfer.amount), + ) + .expect("invalid value or asset type for amount"); - let transp_amt = - Amount::from_nonnegative(*atype, u64::from(transfer.amount)) - .expect("invalid value or asset type for amount"); - - // Non-masp destinations subtract from transparent tx pool - transparent_tx_pool -= transp_amt; + // Non-masp destinations subtract from transparent tx pool + transparent_tx_pool -= transp_amt; + } } match transparent_tx_pool.partial_cmp(&Amount::zero()) { diff --git a/wasm/wasm_source/src/vp_testnet_faucet.rs b/wasm/wasm_source/src/vp_testnet_faucet.rs index 1b8802df6e..ab94e88c9f 100644 --- a/wasm/wasm_source/src/vp_testnet_faucet.rs +++ b/wasm/wasm_source/src/vp_testnet_faucet.rs @@ -57,7 +57,7 @@ fn validate_tx( ctx.read_post(key)?.unwrap_or_default(); let change = post.change() - pre.change(); - if change < 0 { + if !change.non_negative() { // Allow to withdraw without a sig if there's a valid PoW if ctx.has_valid_pow() { let max_free_debit = @@ -152,7 +152,11 @@ mod tests { let vp_owner = address::testing::established_address_1(); let source = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); + let amount = token::Amount::from_uint( + 10_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &source, &token]); @@ -161,6 +165,11 @@ mod tests { // able to transfer from it tx_env.credit_tokens(&source, &token, None, amount); + let amount = token::DenominatedAmount { + amount, + denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), + }; + // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { // Apply transfer in a transaction @@ -290,12 +299,12 @@ mod tests { // Init the VP let vp_owner = address::testing::established_address_1(); let difficulty = testnet_pow::Difficulty::try_new(0).unwrap(); - let withdrawal_limit = token::Amount::from(MAX_FREE_DEBIT as u64); + let withdrawal_limit = token::Amount::from_uint(MAX_FREE_DEBIT as u64, token::NATIVE_MAX_DECIMAL_PLACES).unwrap(); testnet_pow::init_faucet_storage(&mut tx_env.wl_storage, &vp_owner, difficulty, withdrawal_limit).unwrap(); let target = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from(amount); + let amount = token::Amount::from_uint(amount, token::NATIVE_MAX_DECIMAL_PLACES).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &target, &token]); @@ -304,6 +313,10 @@ mod tests { // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); tx_env.commit_genesis(); + let amount = token::DenominatedAmount { + amount, + denom: token::NATIVE_MAX_DECIMAL_PLACES.into() + }; // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -330,13 +343,13 @@ mod tests { // Init the VP let vp_owner = address::testing::established_address_1(); let difficulty = testnet_pow::Difficulty::try_new(0).unwrap(); - let withdrawal_limit = token::Amount::from(MAX_FREE_DEBIT as u64); + let withdrawal_limit = token::Amount::from_uint(MAX_FREE_DEBIT as u64, token::NATIVE_MAX_DECIMAL_PLACES).unwrap(); testnet_pow::init_faucet_storage(&mut tx_env.wl_storage, &vp_owner, difficulty, withdrawal_limit).unwrap(); let target = address::testing::established_address_2(); let target_key = key::testing::keypair_1(); let token = address::nam(); - let amount = token::Amount::from(amount); + let amount = token::Amount::from_uint(amount, token::NATIVE_MAX_DECIMAL_PLACES).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &target, &token]); @@ -358,6 +371,11 @@ mod tests { sig, }; + let amount = token::DenominatedAmount { + amount, + denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), + }; + // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { // Don't call `Solution::invalidate_if_valid` - this is done by the @@ -392,7 +410,7 @@ mod tests { // Init the VP let difficulty = testnet_pow::Difficulty::try_new(0).unwrap(); - let withdrawal_limit = token::Amount::from(MAX_FREE_DEBIT as u64); + let withdrawal_limit = token::Amount::from_uint(MAX_FREE_DEBIT as u64, token::NATIVE_MAX_DECIMAL_PLACES).unwrap(); testnet_pow::init_faucet_storage(&mut tx_env.wl_storage, &vp_owner, difficulty, withdrawal_limit).unwrap(); let keypair = key::testing::keypair_1(); diff --git a/wasm/wasm_source/src/vp_user.rs b/wasm/wasm_source/src/vp_user.rs index b8cbc20982..0e9c0556d9 100644 --- a/wasm/wasm_source/src/vp_user.rs +++ b/wasm/wasm_source/src/vp_user.rs @@ -98,12 +98,16 @@ fn validate_tx( ctx.read_post(key)?.unwrap_or_default(); let change = post.change() - pre.change(); // debit has to signed, credit doesn't - let valid = change >= 0 || addr == masp() || *valid_sig; + let valid = + change.non_negative() || addr == masp() || *valid_sig; + let denom_amount = token::Amount::from(change) + .denominated(owner, &ctx.pre()) + .unwrap(); debug_log!( "token key: {}, change: {}, valid_sig: {}, valid \ modification: {}", key, - change, + denom_amount, *valid_sig, valid ); @@ -234,7 +238,11 @@ mod tests { let vp_owner = address::testing::established_address_1(); let source = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); + let amount = token::Amount::from_uint( + 10_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &source, &token]); @@ -243,6 +251,10 @@ mod tests { // able to transfer from it tx_env.credit_tokens(&source, &token, None, amount); + let amount = token::DenominatedAmount { + amount, + denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), + }; // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { // Apply transfer in a transaction @@ -280,7 +292,11 @@ mod tests { let vp_owner = address::testing::established_address_1(); let target = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); + let amount = token::Amount::from_uint( + 10_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &target, &token]); @@ -289,6 +305,10 @@ mod tests { // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); + let amount = token::DenominatedAmount { + amount, + denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), + }; // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { // Apply transfer in a transaction @@ -328,7 +348,11 @@ mod tests { let public_key = keypair.ref_to(); let target = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); + let amount = token::Amount::from_uint( + 10_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &target, &token]); @@ -339,6 +363,11 @@ mod tests { tx_env.write_public_key(&vp_owner, &public_key); + let amount = token::DenominatedAmount { + amount, + denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), + }; + // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { // Apply transfer in a transaction @@ -377,7 +406,11 @@ mod tests { // Init PoS genesis let pos_params = PosParams::default(); let validator = address::testing::established_address_3(); - let initial_stake = token::Amount::from(10_098_123); + let initial_stake = token::Amount::from_uint( + 10_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); let consensus_key = key::testing::keypair_2().ref_to(); let commission_rate = rust_decimal::Decimal::new(5, 2); let max_commission_rate_change = rust_decimal::Decimal::new(1, 2); @@ -400,9 +433,21 @@ mod tests { let vp_owner: Address = address::testing::established_address_2(); let target = address::testing::established_address_3(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); - let bond_amount = token::Amount::from(5_098_123); - let unbond_amount = token::Amount::from(3_098_123); + let amount = token::Amount::from_uint( + 10_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); + let bond_amount = token::Amount::from_uint( + 5_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); + let unbond_amount = token::Amount::from_uint( + 3_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&target, &token]); @@ -441,7 +486,11 @@ mod tests { // Init PoS genesis let pos_params = PosParams::default(); let validator = address::testing::established_address_3(); - let initial_stake = token::Amount::from(10_098_123); + let initial_stake = token::Amount::from_uint( + 10_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); let consensus_key = key::testing::keypair_2().ref_to(); let commission_rate = rust_decimal::Decimal::new(5, 2); let max_commission_rate_change = rust_decimal::Decimal::new(1, 2); @@ -464,9 +513,21 @@ mod tests { let vp_owner: Address = address::testing::established_address_2(); let target = address::testing::established_address_3(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); - let bond_amount = token::Amount::from(5_098_123); - let unbond_amount = token::Amount::from(3_098_123); + let amount = token::Amount::from_uint( + 10_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); + let bond_amount = token::Amount::from_uint( + 5_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); + let unbond_amount = token::Amount::from_uint( + 3_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&target, &token]); @@ -513,7 +574,11 @@ mod tests { let source = address::testing::established_address_2(); let target = address::testing::established_address_3(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); + let amount = token::Amount::from_uint( + 10_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &source, &target, &token]); @@ -522,6 +587,11 @@ mod tests { // be able to transfer from it tx_env.credit_tokens(&source, &token, None, amount); + let amount = token::DenominatedAmount { + amount, + denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), + }; + // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { tx::ctx().insert_verifier(address).unwrap(); diff --git a/wasm/wasm_source/src/vp_validator.rs b/wasm/wasm_source/src/vp_validator.rs index c9e4700d8e..f616e9dd31 100644 --- a/wasm/wasm_source/src/vp_validator.rs +++ b/wasm/wasm_source/src/vp_validator.rs @@ -98,12 +98,15 @@ fn validate_tx( ctx.read_post(key)?.unwrap_or_default(); let change = post.change() - pre.change(); // debit has to signed, credit doesn't - let valid = change >= 0 || *valid_sig; + let valid = change.non_negative() || *valid_sig; + let amount = token::Amount::from(change) + .denominated(owner, &ctx.pre()) + .unwrap(); debug_log!( "token key: {}, change: {}, valid_sig: {}, valid \ modification: {}", key, - change, + amount, *valid_sig, valid ); @@ -243,7 +246,11 @@ mod tests { let vp_owner = address::testing::established_address_1(); let source = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); + let amount = token::Amount::from_uint( + 10_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &source, &token]); @@ -251,7 +258,10 @@ mod tests { // Credit the tokens to the source before running the transaction to be // able to transfer from it tx_env.credit_tokens(&source, &token, None, amount); - + let amount = token::DenominatedAmount { + amount, + denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), + }; // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { // Apply transfer in a transaction @@ -289,7 +299,11 @@ mod tests { let vp_owner = address::testing::established_address_1(); let target = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); + let amount = token::Amount::from_uint( + 10_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &target, &token]); @@ -297,6 +311,10 @@ mod tests { // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); + let amount = token::DenominatedAmount { + amount, + denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), + }; // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -337,7 +355,11 @@ mod tests { let public_key = keypair.ref_to(); let target = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); + let amount = token::Amount::from_uint( + 10_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &target, &token]); @@ -347,6 +369,10 @@ mod tests { tx_env.credit_tokens(&vp_owner, &token, None, amount); tx_env.write_public_key(&vp_owner, &public_key); + let amount = token::DenominatedAmount { + amount, + denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), + }; // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -386,7 +412,11 @@ mod tests { // Init PoS genesis let pos_params = PosParams::default(); let validator = address::testing::established_address_3(); - let initial_stake = token::Amount::from(10_098_123); + let initial_stake = token::Amount::from_uint( + 10_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); let consensus_key = key::testing::keypair_2().ref_to(); let commission_rate = Decimal::new(5, 2); let max_commission_rate_change = Decimal::new(1, 2); @@ -409,9 +439,21 @@ mod tests { let vp_owner: Address = address::testing::established_address_2(); let target = address::testing::established_address_3(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); - let bond_amount = token::Amount::from(5_098_123); - let unbond_amount = token::Amount::from(3_098_123); + let amount = token::Amount::from_uint( + 10_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); + let bond_amount = token::Amount::from_uint( + 5_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); + let unbond_amount = token::Amount::from_uint( + 3_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&target, &token]); @@ -456,7 +498,11 @@ mod tests { // Init PoS genesis let pos_params = PosParams::default(); let validator = address::testing::established_address_3(); - let initial_stake = token::Amount::from(10_098_123); + let initial_stake = token::Amount::from_uint( + 10_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); let consensus_key = key::testing::keypair_2().ref_to(); let commission_rate = Decimal::new(5, 2); let max_commission_rate_change = Decimal::new(1, 2); @@ -479,9 +525,21 @@ mod tests { let vp_owner: Address = address::testing::established_address_2(); let target = address::testing::established_address_3(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); - let bond_amount = token::Amount::from(5_098_123); - let unbond_amount = token::Amount::from(3_098_123); + let amount = token::Amount::from_uint( + 10_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); + let bond_amount = token::Amount::from_uint( + 5_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); + let unbond_amount = token::Amount::from_uint( + 3_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&target, &token]); @@ -534,7 +592,11 @@ mod tests { let source = address::testing::established_address_2(); let target = address::testing::established_address_3(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); + let amount = token::Amount::from_uint( + 10_098_123, + token::NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &source, &target, &token]); @@ -542,6 +604,10 @@ mod tests { // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it tx_env.credit_tokens(&source, &token, None, amount); + let amount = token::DenominatedAmount { + amount, + denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), + }; // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index fd5ad1519e..afb2c6f3e5 100644 --- a/wasm_for_tests/wasm_source/Cargo.lock +++ b/wasm_for_tests/wasm_source/Cargo.lock @@ -287,7 +287,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43473b34abc4b0b405efa0a250bac87eea888182b21687ee5c8115d279b0fda5" dependencies = [ - "bitvec", + "bitvec 0.22.3", "blake2s_simd 0.5.11", "byteorder", "crossbeam-channel 0.5.6", @@ -374,10 +374,22 @@ version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5237f00a8c86130a0cc317830e558b966dd7850d48a953d998c813f01a41b527" dependencies = [ - "funty", - "radium", + "funty 1.2.0", + "radium 0.6.2", "tap", - "wyz", + "wyz 0.4.0", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty 2.0.0", + "radium 0.7.0", + "tap", + "wyz 0.5.1", ] [[package]] @@ -502,7 +514,7 @@ source = "git+https://github.com/heliaxdev/borsh-rs.git?rev=cd5223e5103c4f139e0c dependencies = [ "borsh-derive-internal", "borsh-schema-derive-internal", - "proc-macro-crate", + "proc-macro-crate 0.1.5", "proc-macro2", "syn", ] @@ -533,6 +545,12 @@ version = "3.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" +[[package]] +name = "byte-slice-cast" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c" + [[package]] name = "bytecheck" version = "0.6.9" @@ -1334,6 +1352,50 @@ dependencies = [ "version_check", ] +[[package]] +name = "ethabi" +version = "18.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7413c5f74cc903ea37386a8965a936cbeb334bd270862fdece542c1b2dcbc898" +dependencies = [ + "ethereum-types", + "hex", + "once_cell", + "regex", + "serde", + "serde_json", + "sha3 0.10.6", + "thiserror", + "uint", +] + +[[package]] +name = "ethbloom" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c22d4b5885b6aa2fe5e8b9329fb8d232bf739e434e6b87347c63bdd00c120f60" +dependencies = [ + "crunchy 0.2.2", + "fixed-hash", + "impl-rlp", + "impl-serde", + "tiny-keccak", +] + +[[package]] +name = "ethereum-types" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d215cbf040552efcbe99a38372fe80ab9d00268e20012b79fcd0f073edd8ee" +dependencies = [ + "ethbloom", + "fixed-hash", + "impl-rlp", + "impl-serde", + "primitive-types", + "uint", +] + [[package]] name = "eyre" version = "0.6.8" @@ -1378,11 +1440,23 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "131655483be284720a17d74ff97592b8e76576dc25563148601df2d7c9080924" dependencies = [ - "bitvec", + "bitvec 0.22.3", "rand_core 0.6.4", "subtle", ] +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rand 0.8.5", + "rustc-hex", + "static_assertions", +] + [[package]] name = "fixedbitset" version = "0.4.2" @@ -1434,6 +1508,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1847abb9cb65d566acd5942e94aea9c8f547ad02c98e1649326fc0e8910b8b1e" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.25" @@ -2012,7 +2092,7 @@ dependencies = [ "prost", "ripemd160", "sha2 0.9.9", - "sha3", + "sha3 0.9.1", "sp-std", ] @@ -2032,6 +2112,55 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-num-traits" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951641f13f873bff03d4bf19ae8bec531935ac0ac2cc775f84d7edfdcfed3f17" +dependencies = [ + "integer-sqrt", + "num-traits", + "uint", +] + +[[package]] +name = "impl-rlp" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28220f89297a075ddc7245cd538076ee98b01f2a9c23a53a4f1105d5a322808" +dependencies = [ + "rlp", +] + +[[package]] +name = "impl-serde" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc88fc67028ae3db0c853baa36269d398d5f45b6982f95549ff5def78c935cd" +dependencies = [ + "serde", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d7a9f6330b71fea57921c9b61c47ee6e84f72d394754eff6163ae67e7395eb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "incrementalmerkletree" version = "0.2.0" @@ -2085,6 +2214,15 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "integer-sqrt" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770" +dependencies = [ + "num-traits", +] + [[package]] name = "itertools" version = "0.10.5" @@ -2115,7 +2253,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7baec19d4e83f9145d4891178101a604565edff9645770fc979804138b04c" dependencies = [ - "bitvec", + "bitvec 0.22.3", "bls12_381", "ff", "group", @@ -2285,7 +2423,7 @@ source = "git+https://github.com/anoma/masp?rev=bee40fc465f6afbd10558d12fe96eb17 dependencies = [ "aes", "bip0039", - "bitvec", + "bitvec 0.22.3", "blake2b_simd 1.0.0", "blake2s_simd 1.0.0", "bls12_381", @@ -2511,10 +2649,12 @@ dependencies = [ "data-encoding", "derivative", "ed25519-consensus", + "ethabi", "ferveo-common", "ibc", "ibc-proto", "ics23", + "impl-num-traits", "index-set", "itertools", "libsecp256k1", @@ -2537,6 +2677,7 @@ dependencies = [ "thiserror", "tonic-build", "tracing", + "uint", "zeroize", ] @@ -2784,7 +2925,7 @@ dependencies = [ "aes", "arrayvec 0.7.2", "bigint", - "bitvec", + "bitvec 0.22.3", "blake2b_simd 1.0.0", "ff", "fpe", @@ -2811,6 +2952,32 @@ dependencies = [ "group", ] +[[package]] +name = "parity-scale-codec" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "637935964ff85a605d114591d4d2c13c5d1ba2806dae97cea6bf180238a749ac" +dependencies = [ + "arrayvec 0.7.2", + "bitvec 1.0.1", + "byte-slice-cast", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b26a931f824dd4eca30b3e43bb4f31cd5f0d3a403c5f5ff27106b805bfde7b" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "parity-wasm" version = "0.45.0" @@ -3004,6 +3171,19 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "primitive-types" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f3486ccba82358b11a77516035647c34ba167dfa53312630de83b12bd4f3d66" +dependencies = [ + "fixed-hash", + "impl-codec", + "impl-rlp", + "impl-serde", + "uint", +] + [[package]] name = "proc-macro-crate" version = "0.1.5" @@ -3013,6 +3193,16 @@ dependencies = [ "toml", ] +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -3202,6 +3392,12 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.7.3" @@ -3488,6 +3684,16 @@ dependencies = [ "syn", ] +[[package]] +name = "rlp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" +dependencies = [ + "bytes", + "rustc-hex", +] + [[package]] name = "rust_decimal" version = "1.26.1" @@ -3522,6 +3728,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + [[package]] name = "rustc_version" version = "0.3.3" @@ -3886,6 +4098,16 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "sha3" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdf0c33fae925bdc080598b84bc15c55e7b9a4a43b3c704da051f977469691c9" +dependencies = [ + "digest 0.10.5", + "keccak", +] + [[package]] name = "sharded-slab" version = "0.1.4" @@ -4440,6 +4662,23 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" + +[[package]] +name = "toml_edit" +version = "0.19.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "tonic" version = "0.6.2" @@ -4620,9 +4859,9 @@ checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" [[package]] name = "uint" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a45526d29728d135c2900b0d30573fe3ee79fceb12ef534c7bb30e810a91b601" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" dependencies = [ "byteorder", "crunchy 0.2.2", @@ -5283,6 +5522,15 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +[[package]] +name = "winnow" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" +dependencies = [ + "memchr", +] + [[package]] name = "wyz" version = "0.4.0" @@ -5292,6 +5540,15 @@ dependencies = [ "tap", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "zcash_encoding" version = "0.0.0" @@ -5331,7 +5588,7 @@ source = "git+https://github.com/zcash/librustzcash/?rev=2425a08#2425a0869098e3b dependencies = [ "aes", "bip0039", - "bitvec", + "bitvec 0.22.3", "blake2b_simd 1.0.0", "blake2s_simd 1.0.0", "bls12_381", diff --git a/wasm_for_tests/wasm_source/src/lib.rs b/wasm_for_tests/wasm_source/src/lib.rs index 7f3584ad44..425252eb83 100644 --- a/wasm_for_tests/wasm_source/src/lib.rs +++ b/wasm_for_tests/wasm_source/src/lib.rs @@ -152,7 +152,7 @@ pub mod main { let target_key = token::balance_key(&token, &target); let mut target_bal: token::Amount = ctx.read(&target_key)?.unwrap_or_default(); - target_bal.receive(&amount); + target_bal.receive(&amount.amount); ctx.write(&target_key, target_bal)?; Ok(()) } From b6517878a8e9d10a35e16652c35a0229b5b6b995 Mon Sep 17 00:00:00 2001 From: satan Date: Wed, 12 Apr 2023 12:16:41 +0200 Subject: [PATCH 013/151] [feat]: Fixed unit tests --- core/src/types/token.rs | 67 +++++++++++++++++++++++---- core/src/types/transaction/wrapper.rs | 45 ++++++++++++++---- core/src/types/uint.rs | 8 ++-- shared/src/ledger/ibc/vp/token.rs | 2 +- tests/Cargo.toml | 1 + 5 files changed, 100 insertions(+), 23 deletions(-) diff --git a/core/src/types/token.rs b/core/src/types/token.rs index cd49a4c0ef..7d1ba8a043 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -101,10 +101,23 @@ impl Amount { } /// Checked addition. Returns `None` on overflow or if - /// the amount exceed [`uint::MAX_SIGNED_VALUE`] + /// the amount exceed [`uint::MAX_VALUE`] pub fn checked_add(&self, amount: Amount) -> Option { self.raw.checked_add(amount.raw).and_then(|result| { - if result < uint::MAX_SIGNED_VALUE { + if result <= uint::MAX_VALUE { + Some(Self { raw: result }) + } else { + None + } + }) + } + + /// Checked addition. Returns `None` on overflow or if + /// the amount exceed [`uint::MAX_SIGNED_VALUE`] + pub fn checked_signed_add(&self, amount: Amount) -> Option { + self.raw.checked_add(amount.raw).and_then(|result| { + // TODO: Should this be `MAX_SIGNED_VALUE` or `MAX_VALUE`? + if result <= uint::MAX_SIGNED_VALUE { Some(Self { raw: result }) } else { None @@ -334,7 +347,11 @@ impl Display for DenominatedAmount { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let string = self.to_string_precise(); let string = string.trim_end_matches(&['0', '.']); - f.write_str(string) + if string.is_empty() { + f.write_str("0") + } else { + f.write_str(string) + } } } @@ -902,24 +919,45 @@ mod tests { #[test] fn test_token_display() { - let max = Amount::from_uint(u64::MAX, NATIVE_MAX_DECIMAL_PLACES) + let max = Amount::from_uint(u64::MAX, 0) .expect("Test failed"); assert_eq!("18446744073709.551615", max.to_string_native()); + let max = DenominatedAmount { + amount: max, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + assert_eq!("18446744073709.551615", max.to_string()); let whole = Amount::from_uint( u64::MAX / NATIVE_SCALE * NATIVE_SCALE, - NATIVE_MAX_DECIMAL_PLACES, + 0, ) .expect("Test failed"); - assert_eq!("18446744073709", whole.to_string_native()); + assert_eq!("18446744073709.000000", whole.to_string_native()); + let whole = DenominatedAmount { + amount: whole, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + assert_eq!("18446744073709", whole.to_string()); let trailing_zeroes = - Amount::from_uint(123000, NATIVE_MAX_DECIMAL_PLACES) + Amount::from_uint(123000, 0) .expect("Test failed"); - assert_eq!("0.123", trailing_zeroes.to_string_native()); + assert_eq!("0.123000", trailing_zeroes.to_string_native()); + let trailing_zeroes = DenominatedAmount { + amount: trailing_zeroes, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + assert_eq!("0.123", trailing_zeroes.to_string()); + let zero = Amount::default(); - assert_eq!("0", zero.to_string_native()); + assert_eq!("0.000000", zero.to_string_native()); + let zero = DenominatedAmount { + amount: zero, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + assert_eq!("0", zero.to_string()); } #[test] @@ -939,18 +977,27 @@ mod tests { #[test] fn test_amount_checked_add() { - let max = Amount::native_whole(u64::MAX); + let max = Amount::max(); + let max_signed = Amount::max_signed(); let one = Amount::native_whole(1); let zero = Amount::native_whole(0); assert_eq!(zero.checked_add(zero), Some(zero)); + assert_eq!(zero.checked_signed_add(zero), Some(zero)); assert_eq!(zero.checked_add(one), Some(one)); assert_eq!(zero.checked_add(max - one), Some(max - one)); + assert_eq!(zero.checked_signed_add(max_signed - one), Some(max_signed - one)); assert_eq!(zero.checked_add(max), Some(max)); + assert_eq!(zero.checked_signed_add(max_signed), Some(max_signed)); assert_eq!(max.checked_add(zero), Some(max)); + assert_eq!(max.checked_signed_add(zero), None); assert_eq!(max.checked_add(one), None); assert_eq!(max.checked_add(max), None); + + assert_eq!(max_signed.checked_add(zero), Some(max_signed)); + assert_eq!(max_signed.checked_add(one), Some(max_signed + one)); + assert_eq!(max_signed.checked_signed_add(max_signed), None); } } diff --git a/core/src/types/transaction/wrapper.rs b/core/src/types/transaction/wrapper.rs index 472a05ce86..83f6e77935 100644 --- a/core/src/types/transaction/wrapper.rs +++ b/core/src/types/transaction/wrapper.rs @@ -4,11 +4,13 @@ #[cfg(feature = "ferveo-tpke")] pub mod wrapper_tx { use std::convert::TryFrom; + use std::fmt::Formatter; pub use ark_bls12_381::Bls12_381 as EllipticCurve; pub use ark_ec::{AffineCurve, PairingEngine}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; - use serde::{Deserialize, Serialize}; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use serde::de::Error; use thiserror::Error; use crate::proto::Tx; @@ -77,18 +79,46 @@ pub mod wrapper_tx { Debug, Clone, PartialEq, - Serialize, - Deserialize, BorshSerialize, BorshDeserialize, BorshSchema, )] - #[serde(from = "Uint")] - #[serde(into = "Uint")] pub struct GasLimit { multiplier: Uint, } + impl Serialize for GasLimit { + fn serialize(&self, serializer: S) -> Result where S: Serializer { + let limit = Uint::from(self).to_string(); + Serialize::serialize(&limit, serializer) + } + } + + impl<'de> Deserialize<'de> for GasLimit { + fn deserialize(deserializer: D) -> Result + where D: Deserializer<'de> + { + struct GasLimitVisitor; + + impl<'a> serde::de::Visitor<'a> for GasLimitVisitor { + type Value = GasLimit; + + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + formatter.write_str("A string representing 256-bit unsigned integer") + } + + fn visit_str(self, v: &str) -> Result + where E: Error + { + let uint = Uint::from_dec_str(v) + .map_err(|e| E::custom(e.to_string()))?; + Ok(GasLimit::from(uint)) + } + } + deserializer.deserialize_any(GasLimitVisitor) + } + } + impl GasLimit { /// We refund unused gas up to GAS_LIMIT_RESOLUTION pub fn refund_amount(&self, used_gas: Uint) -> Amount { @@ -301,7 +331,7 @@ pub mod wrapper_tx { }; // Test serde roundtrip let js = serde_json::to_string(&limit).expect("Test failed"); - assert_eq!(js, format!("{}", GAS_LIMIT_RESOLUTION)); + assert_eq!(js, format!(r#""{}""#, GAS_LIMIT_RESOLUTION)); let new_limit: GasLimit = serde_json::from_str(&js).expect("Test failed"); assert_eq!(new_limit, limit); @@ -320,8 +350,7 @@ pub mod wrapper_tx { /// multiple #[test] fn test_deserialize_not_multiple_of_resolution() { - let js = serde_json::to_string(&(GAS_LIMIT_RESOLUTION + 1)) - .expect("Test failed"); + let js = format!(r#""{}""#, &(GAS_LIMIT_RESOLUTION + 1)); let limit: GasLimit = serde_json::from_str(&js).expect("Test failed"); assert_eq!( diff --git a/core/src/types/uint.rs b/core/src/types/uint.rs index 443a69caea..0f8f0daa53 100644 --- a/core/src/types/uint.rs +++ b/core/src/types/uint.rs @@ -32,7 +32,7 @@ pub const MAX_VALUE: Uint = Uint([u64::MAX; 4]); impl Uint { /// Compute the two's complement of a number. - fn negate(&self) -> Option { + fn negate(&self) -> Self { Self( self.0 .into_iter() @@ -41,7 +41,7 @@ impl Uint { .try_into() .expect("This cannot fail"), ) - .checked_add(Uint::from(1u64)) + .overflowing_add(Uint::from(1u64)).0 } } @@ -69,7 +69,7 @@ impl SignedUint { if self.non_negative() { self.0 } else { - self.0.negate().unwrap() + self.0.negate() } } @@ -116,7 +116,7 @@ impl Neg for SignedUint { type Output = Self; fn neg(self) -> Self::Output { - Self(self.0.negate().expect("This should not fail")) + Self(self.0.negate()) } } diff --git a/shared/src/ledger/ibc/vp/token.rs b/shared/src/ledger/ibc/vp/token.rs index 0f34024da4..b28019849f 100644 --- a/shared/src/ledger/ibc/vp/token.rs +++ b/shared/src/ledger/ibc/vp/token.rs @@ -84,7 +84,7 @@ where SignedTxData::try_from_slice(tx_data).map_err(Error::Decoding)?; let tx_data = &signed.data.ok_or(Error::NoTxData)?; - // Check the non-onwer balance updates + // Check the non-owner balance updates let ibc_keys_changed: HashSet = keys_changed .iter() .filter(|k| { diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 813793cc00..602550367d 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -15,6 +15,7 @@ mainnet = [ abciplus = [ "namada/abciplus", "namada/ibc-mocks", + "namada_apps/abciplus", "namada_vp_prelude/abciplus", "namada_tx_prelude/abciplus", ] From 2356c5de866c7012f8b7a737bb6fb070fddcb8e6 Mon Sep 17 00:00:00 2001 From: satan Date: Wed, 12 Apr 2023 12:18:28 +0200 Subject: [PATCH 014/151] [chore]: Formatting --- core/src/types/token.rs | 20 +++++++++----------- core/src/types/transaction/wrapper.rs | 22 ++++++++++++++++------ core/src/types/uint.rs | 3 ++- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 7d1ba8a043..a9541993d1 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -919,8 +919,7 @@ mod tests { #[test] fn test_token_display() { - let max = Amount::from_uint(u64::MAX, 0) - .expect("Test failed"); + let max = Amount::from_uint(u64::MAX, 0).expect("Test failed"); assert_eq!("18446744073709.551615", max.to_string_native()); let max = DenominatedAmount { amount: max, @@ -928,11 +927,9 @@ mod tests { }; assert_eq!("18446744073709.551615", max.to_string()); - let whole = Amount::from_uint( - u64::MAX / NATIVE_SCALE * NATIVE_SCALE, - 0, - ) - .expect("Test failed"); + let whole = + Amount::from_uint(u64::MAX / NATIVE_SCALE * NATIVE_SCALE, 0) + .expect("Test failed"); assert_eq!("18446744073709.000000", whole.to_string_native()); let whole = DenominatedAmount { amount: whole, @@ -941,8 +938,7 @@ mod tests { assert_eq!("18446744073709", whole.to_string()); let trailing_zeroes = - Amount::from_uint(123000, 0) - .expect("Test failed"); + Amount::from_uint(123000, 0).expect("Test failed"); assert_eq!("0.123000", trailing_zeroes.to_string_native()); let trailing_zeroes = DenominatedAmount { amount: trailing_zeroes, @@ -950,7 +946,6 @@ mod tests { }; assert_eq!("0.123", trailing_zeroes.to_string()); - let zero = Amount::default(); assert_eq!("0.000000", zero.to_string_native()); let zero = DenominatedAmount { @@ -986,7 +981,10 @@ mod tests { assert_eq!(zero.checked_signed_add(zero), Some(zero)); assert_eq!(zero.checked_add(one), Some(one)); assert_eq!(zero.checked_add(max - one), Some(max - one)); - assert_eq!(zero.checked_signed_add(max_signed - one), Some(max_signed - one)); + assert_eq!( + zero.checked_signed_add(max_signed - one), + Some(max_signed - one) + ); assert_eq!(zero.checked_add(max), Some(max)); assert_eq!(zero.checked_signed_add(max_signed), Some(max_signed)); diff --git a/core/src/types/transaction/wrapper.rs b/core/src/types/transaction/wrapper.rs index 83f6e77935..448de45540 100644 --- a/core/src/types/transaction/wrapper.rs +++ b/core/src/types/transaction/wrapper.rs @@ -9,8 +9,8 @@ pub mod wrapper_tx { pub use ark_bls12_381::Bls12_381 as EllipticCurve; pub use ark_ec::{AffineCurve, PairingEngine}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; - use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::de::Error; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; use thiserror::Error; use crate::proto::Tx; @@ -88,7 +88,10 @@ pub mod wrapper_tx { } impl Serialize for GasLimit { - fn serialize(&self, serializer: S) -> Result where S: Serializer { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { let limit = Uint::from(self).to_string(); Serialize::serialize(&limit, serializer) } @@ -96,19 +99,26 @@ pub mod wrapper_tx { impl<'de> Deserialize<'de> for GasLimit { fn deserialize(deserializer: D) -> Result - where D: Deserializer<'de> + where + D: Deserializer<'de>, { struct GasLimitVisitor; impl<'a> serde::de::Visitor<'a> for GasLimitVisitor { type Value = GasLimit; - fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { - formatter.write_str("A string representing 256-bit unsigned integer") + fn expecting( + &self, + formatter: &mut Formatter, + ) -> std::fmt::Result { + formatter.write_str( + "A string representing 256-bit unsigned integer", + ) } fn visit_str(self, v: &str) -> Result - where E: Error + where + E: Error, { let uint = Uint::from_dec_str(v) .map_err(|e| E::custom(e.to_string()))?; diff --git a/core/src/types/uint.rs b/core/src/types/uint.rs index 0f8f0daa53..fde3571c19 100644 --- a/core/src/types/uint.rs +++ b/core/src/types/uint.rs @@ -41,7 +41,8 @@ impl Uint { .try_into() .expect("This cannot fail"), ) - .overflowing_add(Uint::from(1u64)).0 + .overflowing_add(Uint::from(1u64)) + .0 } } From b244e1b1b4149b8402bf285345a0a66f048411a8 Mon Sep 17 00:00:00 2001 From: satan Date: Thu, 13 Apr 2023 11:02:24 +0200 Subject: [PATCH 015/151] [chore]: Added test coverage for new token types and fixed the bugs they found --- apps/src/lib/cli.rs | 8 +- apps/src/lib/client/rpc.rs | 2 +- core/src/types/token.rs | 94 ++++++++++++++++++--- core/src/types/uint.rs | 162 +++++++++++++++++++++++++++++++++++-- 4 files changed, 244 insertions(+), 22 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index c2c89d9285..c735e70eb5 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -2211,8 +2211,8 @@ pub mod args { let amount = amount .canonical() .increase_precision(NATIVE_MAX_DECIMAL_PLACES.into()) - .unwrap_or_else(|| { - println!("Could not parse bond amount"); + .unwrap_or_else(|e| { + println!("Could not parse bond amount: {:?}", e); safe_exit(1); }) .amount; @@ -2258,8 +2258,8 @@ pub mod args { let amount = amount .canonical() .increase_precision(NATIVE_MAX_DECIMAL_PLACES.into()) - .unwrap_or_else(|| { - println!("Could not parse bond amount"); + .unwrap_or_else(|e| { + println!("Could not parse bond amount: {:?}", e); safe_exit(1); }) .amount; diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 0e6d7d9ce9..443885857a 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -2838,7 +2838,7 @@ pub async fn validate_amount( ); cli::safe_exit(1); } else { - input_amount.increase_precision(denom).unwrap_or_else(|| { + input_amount.increase_precision(denom).unwrap_or_else(|_| { println!( "The amount provided requires more the 256 bits to represent." ); diff --git a/core/src/types/token.rs b/core/src/types/token.rs index a9541993d1..71d8a56b8a 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -116,7 +116,6 @@ impl Amount { /// the amount exceed [`uint::MAX_SIGNED_VALUE`] pub fn checked_signed_add(&self, amount: Amount) -> Option { self.raw.checked_add(amount.raw).and_then(|result| { - // TODO: Should this be `MAX_SIGNED_VALUE` or `MAX_VALUE`? if result <= uint::MAX_SIGNED_VALUE { Some(Self { raw: result }) } else { @@ -163,10 +162,9 @@ impl Amount { string: impl AsRef, denom: impl Into, ) -> Result { - match Decimal::from_str(string.as_ref()) { - Ok(dec) => Ok(Self::from_decimal(dec, denom)?), - Err(err) => Err(AmountParseError::InvalidDecimal(err)), - } + DenominatedAmount::from_str(string.as_ref())? + .increase_precision(denom.into().into()) + .map(Into::into) } /// Attempt to convert a float to an `Amount` with the specified @@ -200,10 +198,9 @@ impl Amount { /// Given a u64 and [`MaspDenom`], construct the corresponding /// amount. pub fn from_masp_denominated(val: u64, denom: MaspDenom) -> Self { - let val = Uint::from(val); - let denom = Uint::from(denom as u64); - let scaling = Uint::from(2).pow(denom); - Self { raw: val * scaling } + let mut raw = [0u64; 4]; + raw[denom as usize] = val; + Self { raw: Uint(raw)} } /// Get a string representation of a native token amount. @@ -329,9 +326,9 @@ impl DenominatedAmount { /// Attempt to increase the precision of an amount. Can fail /// if the resulting amount does not fit into 256 bits. - pub fn increase_precision(self, denom: Denomination) -> Option { + pub fn increase_precision(self, denom: Denomination) -> Result { if denom.0 < self.denom.0 { - return None; + return Err(AmountParseError::PrecisionDecrease); } Uint::from(10) .checked_pow(Uint::from(denom.0 - self.denom.0)) @@ -340,6 +337,7 @@ impl DenominatedAmount { amount: Amount { raw: amount }, denom, }) + .ok_or(AmountParseError::PrecisionOverflow) } } @@ -359,7 +357,7 @@ impl FromStr for DenominatedAmount { type Err = AmountParseError; fn from_str(s: &str) -> Result { - let precision = s.find('.').map(|pos| s.len() - pos); + let precision = s.find('.').map(|pos| s.len() - pos - 1); let digits = s .chars() .filter_map(|c| { @@ -613,6 +611,10 @@ pub enum AmountParseError { FromString, #[error("Could not parse string as a correctly formatted number.")] NotNumeric, + #[error("This amount cannot handle the requested precision in 256 bits.")] + PrecisionOverflow, + #[error("More precision given in the amount than requested.")] + PrecisionDecrease, } impl From for Change { @@ -901,6 +903,7 @@ impl TryFrom for Transfer { #[cfg(test)] mod tests { use proptest::prelude::*; + use rust_decimal_macros::dec; use super::*; @@ -953,6 +956,20 @@ mod tests { denom: NATIVE_MAX_DECIMAL_PLACES.into(), }; assert_eq!("0", zero.to_string()); + + let amount = DenominatedAmount { + amount: Amount::from_uint(1120, 0).expect("Test failed"), + denom: 3u8.into(), + }; + assert_eq!("1.12", amount.to_string()); + assert_eq!("1.120", amount.to_string_precise()); + + let amount = DenominatedAmount { + amount: Amount::from_uint(1120, 0).expect("Test failed"), + denom: 5u8.into(), + }; + assert_eq!("0.0112", amount.to_string()); + assert_eq!("0.01120", amount.to_string_precise()); } #[test] @@ -997,6 +1014,59 @@ mod tests { assert_eq!(max_signed.checked_add(one), Some(max_signed + one)); assert_eq!(max_signed.checked_signed_add(max_signed), None); } + + #[test] + fn test_amount_from_decimal() { + assert!(Amount::from_decimal(dec!(1.12), 1).is_err()); + assert!(Amount::from_decimal(dec!(1.12), 80).is_err()); + let amount = Amount::from_decimal(dec!(1.12), 3).expect("Test failed"); + assert_eq!(amount, Amount::from_uint(1120, 0).expect("Test failed")); + + } + + #[test] + fn test_amount_from_string() { + assert!(Amount::from_str("1.12", 1).is_err()); + assert!(Amount::from_str("0.0", 0).is_err()); + assert!(Amount::from_str("1.12", 80).is_err()); + assert!(Amount::from_str("1.12.1", 3).is_err()); + assert!(Amount::from_str("1.1a", 3).is_err()); + assert_eq!(Amount::zero(), Amount::from_str("0.0", 1).expect("Test failed")); + assert_eq!(Amount::zero(), Amount::from_str(".0", 1).expect("Test failed")); + + let amount = Amount::from_str("1.12", 3).expect("Test failed"); + assert_eq!(amount, Amount::from_uint(1120, 0).expect("Test failed")); + let amount = Amount::from_str(".34", 3).expect("Test failed"); + assert_eq!(amount, Amount::from_uint(340, 0).expect("Test failed")); + let amount = Amount::from_str("0.34", 3).expect("Test failed"); + assert_eq!(amount, Amount::from_uint(340, 0).expect("Test failed")); + let amount = Amount::from_str("34", 1).expect("Test failed"); + assert_eq!(amount, Amount::from_uint(340, 0).expect("Test failed")); + } + + #[test] + fn test_from_masp_denominated() { + let uint = Uint([15u64, 16, 17, 18]); + let original = Amount::from_uint(uint, 0).expect("Test failed"); + for denom in MaspDenom::iter() { + let word = denom.denominate(&original); + assert_eq!(word, denom as u64 + 15u64); + let amount = Amount::from_masp_denominated(word, denom); + let raw = Uint::from(amount).0; + let mut expected = [0u64; 4]; + expected[denom as usize] = word; + assert_eq!(raw, expected); + } + } + + #[test] + fn test_key_seg() { + let original = Amount::from_uint(1234560000, 0).expect("Test failed"); + let key = original.raw(); + let amount = Amount::parse(key).expect("Test failed"); + assert_eq!(amount, original); + } + } /// Helpers for testing with addresses. diff --git a/core/src/types/uint.rs b/core/src/types/uint.rs index fde3571c19..16c4045fd2 100644 --- a/core/src/types/uint.rs +++ b/core/src/types/uint.rs @@ -43,6 +43,17 @@ impl Uint { ) .overflowing_add(Uint::from(1u64)) .0 + .canonical() + } + + /// There are two valid representations of zero: plus and + /// minus. We only allow the positive representation. + fn canonical(self) -> Self { + if self == MINUS_ZERO { + Self::zero() + } else { + self + } } } @@ -52,6 +63,8 @@ impl Uint { pub const MAX_SIGNED_VALUE: Uint = Uint([u64::MAX, u64::MAX, u64::MAX, 9223372036854775807]); +const MINUS_ZERO: Uint = Uint([0u64, 0u64, 0u64, 9223372036854775808]); + /// A signed 256 big integer. #[derive( Copy, Clone, Debug, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize, @@ -82,7 +95,7 @@ impl SignedUint { /// Get a string representation of `self` as a /// native token amount. pub fn to_string_native(&self) -> String { - let mut sign = if self.non_negative() { + let mut sign = if !self.non_negative() { String::from("-") } else { String::new() @@ -90,6 +103,34 @@ impl SignedUint { sign.push_str(&token::Amount::from(*self).to_string_native()); sign } + + /// Adds two [`SignedUint`]'s if the absolute value does + /// not exceed [`MAX_SIGNED_AMOUNT`], else returns `None`. + pub fn checked_add(&self, other: &Self) -> Option { + if self.non_negative() == other.non_negative() { + self.abs().checked_add(other.abs()) + .and_then(|val| Self::try_from(val) + .ok() + .map(|val| if !self.non_negative() { + -val + } else { + val + })) + } else { + Some(*self + *other) + } + } + + /// Subtracts two [`SignedUint`]'s if the absolute value does + /// not exceed [`MAX_SIGNED_AMOUNT`], else returns `None`. + pub fn checked_sub(&self, other: &Self) -> Option { + self.checked_add(&other.neg()) + } + + /// Changed the inner Uint into a canonical representation. + fn canonical(self) -> Self { + Self(self.0.canonical()) + } } impl From for SignedUint { @@ -103,7 +144,7 @@ impl TryFrom for SignedUint { type Error = Box; fn try_from(value: Uint) -> Result { - if value.0 <= MAX_SIGNED_VALUE.0 { + if value <= MAX_SIGNED_VALUE { Ok(Self(value)) } else { Err("The given integer is too large to be represented asa \ @@ -148,9 +189,17 @@ impl Add for SignedUint { match (self.non_negative(), rhs.non_negative()) { (true, true) => Self(self.0 + rhs.0), (false, false) => -Self(self.abs() + rhs.abs()), - (true, false) => Self(self.0 - rhs.abs()), - (false, true) => Self(rhs.0 - self.abs()), - } + (true, false) => if self.0 >= rhs.abs() { + Self(self.0 - rhs.abs()) + } else { + -Self(rhs.abs() - self.0) + } + (false, true) => if rhs.0 >= self.abs() { + Self(rhs.0 - self.abs()) + } else { + -Self(self.abs() - rhs.0) + }, + }.canonical() } } @@ -190,3 +239,106 @@ impl From for SignedUint { Self::from(val as i128) } } + + +#[cfg(test)] +mod test_uint { + use super::*; + + /// Test that adding one to the max signed + /// value gives zero. + #[test] + fn test_max_signed_value() { + let signed = SignedUint::try_from(MAX_SIGNED_VALUE).expect("Test failed"); + let one = SignedUint::try_from(Uint::from(1u64)).expect("Test failed"); + let overflow = signed + one; + assert_eq!(overflow, SignedUint::try_from(Uint::zero()).expect("Test failed")); + assert!(signed.checked_add(&one).is_none()); + assert!((-signed).checked_sub(&one).is_none()); + } + + /// Sanity on our constants and that the minus zero representation + /// is not allowed. + #[test] + fn test_minus_zero_not_allowed() { + let larger = Uint([0, 0, 0, 2u64.pow(63)]); + let smaller = Uint([u64::MAX, u64::MAX, u64::MAX, 2u64.pow(63) -1]); + assert!(larger > smaller); + assert_eq!(smaller, MAX_SIGNED_VALUE); + assert_eq!(larger, MINUS_ZERO); + assert!(SignedUint::try_from(MINUS_ZERO).is_err()); + let zero = Uint::zero(); + assert_eq!(zero, zero.negate()); + } + + /// Test that we correctly reserve the right bit for indicating the + /// sign. + #[test] + fn test_non_negative() { + let zero = SignedUint::try_from(Uint::zero()).expect("Test failed"); + assert!(zero.non_negative()); + assert!((-zero).non_negative()); + let negative = SignedUint(Uint([1u64, 0, 0, 2u64.pow(63)])); + assert!(!negative.non_negative()); + assert!((-negative).non_negative()); + let positive = SignedUint(MAX_SIGNED_VALUE); + assert!(positive.non_negative()); + assert!(!(-positive).non_negative()); + } + + /// Test that the absolute vale is computed correctly + #[test] + fn test_abs() { + let zero = SignedUint::try_from(Uint::zero()).expect("Test failed"); + let neg_one = SignedUint(Uint::max_value()); + let neg_eight = SignedUint(Uint::max_value() - Uint::from(7)); + let two = SignedUint(Uint::from(2)); + let ten = SignedUint(Uint::from(10)); + + assert_eq!(zero.abs(), Uint::zero()); + assert_eq!(neg_one.abs(), Uint::from(1)); + assert_eq!(neg_eight.abs(), Uint::from(8)); + assert_eq!(two.abs(), Uint::from(2)); + assert_eq!(ten.abs(), Uint::from(10)); + } + + /// Test that the absolute vale is computed correctly + #[test] + fn test_to_string_native() { + let native_scaling = Uint::exp10(6); + let zero = SignedUint::try_from(Uint::zero()).expect("Test failed"); + let neg_one = -SignedUint(native_scaling); + let neg_eight = -SignedUint(Uint::from(8) * native_scaling); + let two = SignedUint(Uint::from(2) * native_scaling); + let ten = SignedUint(Uint::from(10) * native_scaling); + + assert_eq!(zero.to_string_native(), "0.000000"); + assert_eq!(neg_one.to_string_native(), "-1.000000"); + assert_eq!(neg_eight.to_string_native(), "-8.000000"); + assert_eq!(two.to_string_native(), "2.000000"); + assert_eq!(ten.to_string_native(), "10.000000"); + } + + /// Test that we correctly handle arithmetic with two's complement + #[test] + fn test_arithmetic() { + let zero = SignedUint::try_from(Uint::zero()).expect("Test failed"); + let neg_one = SignedUint(Uint::max_value()); + let neg_eight = SignedUint(Uint::max_value() - Uint::from(7)); + let two = SignedUint(Uint::from(2)); + let ten = SignedUint(Uint::from(10)); + + assert_eq!(zero + neg_one, neg_one); + assert_eq!(neg_one - zero, neg_one); + assert_eq!(zero - neg_one, SignedUint(Uint::one())); + assert_eq!(two - neg_eight, ten); + assert_eq!(two + ten, SignedUint(Uint::from(12))); + assert_eq!(ten - two, -neg_eight); + assert_eq!(two - ten, neg_eight); + assert_eq!(neg_eight + neg_one, -SignedUint(Uint::from(9))); + assert_eq!(neg_one - neg_eight, SignedUint(Uint::from(7))); + assert_eq!(neg_eight - neg_one, -SignedUint(Uint::from(7))); + assert_eq!(neg_eight - two, -ten); + assert!((two - two).is_zero()); + } +} \ No newline at end of file From b58d49bb039cd10573954e57f1452a19956ba337 Mon Sep 17 00:00:00 2001 From: satan Date: Thu, 13 Apr 2023 13:25:09 +0200 Subject: [PATCH 016/151] [fix]: Fixed some conversions to amounts in tests. Fixed PoS state machine tests --- .../lib/node/ledger/shell/finalize_block.rs | 19 +--- .../lib/node/ledger/shell/process_proposal.rs | 8 +- core/src/types/token.rs | 19 ++-- core/src/types/transaction/mod.rs | 6 +- core/src/types/transaction/wrapper.rs | 7 +- core/src/types/uint.rs | 60 ++++++++----- proof_of_stake/src/lib.rs | 10 +-- proof_of_stake/src/tests.rs | 86 +++++++++---------- proof_of_stake/src/tests/state_machine.rs | 17 ++-- rust-toolchain.toml | 2 +- 10 files changed, 123 insertions(+), 111 deletions(-) diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index ea8fd012c2..054405b067 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -470,7 +470,6 @@ mod test_finalize_block { use namada::types::governance::ProposalVote; use namada::types::storage::Epoch; use namada::types::time::DurationSecs; - use namada::types::token::NATIVE_MAX_DECIMAL_PLACES; use namada::types::transaction::governance::{ InitProposalData, VoteProposalData, }; @@ -514,11 +513,7 @@ mod test_finalize_block { ); let wrapper = WrapperTx::new( Fee { - amount: Amount::from_uint( - MIN_FEE, - NATIVE_MAX_DECIMAL_PLACES, - ) - .expect("Test failed"), + amount: Amount::from_uint(MIN_FEE, 0).expect("Test failed"), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, @@ -731,11 +726,7 @@ mod test_finalize_block { ); let wrapper_tx = WrapperTx::new( Fee { - amount: Amount::from_uint( - MIN_FEE, - NATIVE_MAX_DECIMAL_PLACES, - ) - .expect("Test failed"), + amount: Amount::from_uint(MIN_FEE, 0).expect("Test failed"), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, @@ -772,11 +763,7 @@ mod test_finalize_block { ); let wrapper_tx = WrapperTx::new( Fee { - amount: Amount::from_uint( - MIN_FEE, - NATIVE_MAX_DECIMAL_PLACES, - ) - .expect("Test failed"), + amount: Amount::from_uint(MIN_FEE, 0).expect("Test failed"), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, diff --git a/apps/src/lib/node/ledger/shell/process_proposal.rs b/apps/src/lib/node/ledger/shell/process_proposal.rs index 0b8e29a151..98ab3654f5 100644 --- a/apps/src/lib/node/ledger/shell/process_proposal.rs +++ b/apps/src/lib/node/ledger/shell/process_proposal.rs @@ -215,7 +215,7 @@ mod test_process_proposal { use namada::types::hash::Hash; use namada::types::key::*; use namada::types::storage::Epoch; - use namada::types::token::{Amount, NATIVE_MAX_DECIMAL_PLACES}; + use namada::types::token::Amount; use namada::types::transaction::encrypted::EncryptedTx; use namada::types::transaction::{EncryptionKey, Fee, WrapperTx}; @@ -287,8 +287,7 @@ mod test_process_proposal { let timestamp = tx.timestamp; let mut wrapper = WrapperTx::new( Fee { - amount: Amount::from_uint(100, NATIVE_MAX_DECIMAL_PLACES) - .expect("Test failed"), + amount: Amount::from_uint(100, 0).expect("Test failed"), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, @@ -372,8 +371,7 @@ mod test_process_proposal { ); let wrapper = WrapperTx::new( Fee { - amount: Amount::from_uint(1, NATIVE_MAX_DECIMAL_PLACES) - .expect("Test failed"), + amount: Amount::from_uint(1, 0).expect("Test failed"), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 71d8a56b8a..0851d8463f 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -200,7 +200,7 @@ impl Amount { pub fn from_masp_denominated(val: u64, denom: MaspDenom) -> Self { let mut raw = [0u64; 4]; raw[denom as usize] = val; - Self { raw: Uint(raw)} + Self { raw: Uint(raw) } } /// Get a string representation of a native token amount. @@ -326,7 +326,10 @@ impl DenominatedAmount { /// Attempt to increase the precision of an amount. Can fail /// if the resulting amount does not fit into 256 bits. - pub fn increase_precision(self, denom: Denomination) -> Result { + pub fn increase_precision( + self, + denom: Denomination, + ) -> Result { if denom.0 < self.denom.0 { return Err(AmountParseError::PrecisionDecrease); } @@ -1021,7 +1024,6 @@ mod tests { assert!(Amount::from_decimal(dec!(1.12), 80).is_err()); let amount = Amount::from_decimal(dec!(1.12), 3).expect("Test failed"); assert_eq!(amount, Amount::from_uint(1120, 0).expect("Test failed")); - } #[test] @@ -1031,8 +1033,14 @@ mod tests { assert!(Amount::from_str("1.12", 80).is_err()); assert!(Amount::from_str("1.12.1", 3).is_err()); assert!(Amount::from_str("1.1a", 3).is_err()); - assert_eq!(Amount::zero(), Amount::from_str("0.0", 1).expect("Test failed")); - assert_eq!(Amount::zero(), Amount::from_str(".0", 1).expect("Test failed")); + assert_eq!( + Amount::zero(), + Amount::from_str("0.0", 1).expect("Test failed") + ); + assert_eq!( + Amount::zero(), + Amount::from_str(".0", 1).expect("Test failed") + ); let amount = Amount::from_str("1.12", 3).expect("Test failed"); assert_eq!(amount, Amount::from_uint(1120, 0).expect("Test failed")); @@ -1066,7 +1074,6 @@ mod tests { let amount = Amount::parse(key).expect("Test failed"); assert_eq!(amount, original); } - } /// Helpers for testing with addresses. diff --git a/core/src/types/transaction/mod.rs b/core/src/types/transaction/mod.rs index 8d59fd5a9f..5683c5a2fe 100644 --- a/core/src/types/transaction/mod.rs +++ b/core/src/types/transaction/mod.rs @@ -342,7 +342,7 @@ pub mod tx_types { use super::*; use crate::types::address::nam; use crate::types::storage::Epoch; - use crate::types::token::{Amount, NATIVE_MAX_DECIMAL_PLACES}; + use crate::types::token::Amount; fn gen_keypair() -> common::SecretKey { use rand::prelude::ThreadRng; @@ -424,7 +424,7 @@ pub mod tx_types { // the signed tx let wrapper = WrapperTx::new( Fee { - amount: Amount::from_uint(10, NATIVE_MAX_DECIMAL_PLACES) + amount: Amount::from_uint(10, 0) .expect("Test failed"), token: nam(), }, @@ -462,7 +462,7 @@ pub mod tx_types { // the signed tx let wrapper = WrapperTx::new( Fee { - amount: Amount::from_uint(10, NATIVE_MAX_DECIMAL_PLACES) + amount: Amount::from_uint(10, 0) .expect("Test failed"), token: nam(), }, diff --git a/core/src/types/transaction/wrapper.rs b/core/src/types/transaction/wrapper.rs index 448de45540..4dac6e7ce2 100644 --- a/core/src/types/transaction/wrapper.rs +++ b/core/src/types/transaction/wrapper.rs @@ -414,7 +414,6 @@ pub mod wrapper_tx { use super::*; use crate::proto::SignedTxData; use crate::types::address::nam; - use crate::types::token::NATIVE_MAX_DECIMAL_PLACES; fn gen_keypair() -> common::SecretKey { use rand::prelude::ThreadRng; @@ -436,7 +435,7 @@ pub mod wrapper_tx { let wrapper = WrapperTx::new( Fee { - amount: Amount::from_uint(10, NATIVE_MAX_DECIMAL_PLACES) + amount: Amount::from_uint(10, 0) .expect("Test failed"), token: nam(), }, @@ -465,7 +464,7 @@ pub mod wrapper_tx { let mut wrapper = WrapperTx::new( Fee { - amount: Amount::from_uint(10, NATIVE_MAX_DECIMAL_PLACES) + amount: Amount::from_uint(10, 0) .expect("Test failed"), token: nam(), }, @@ -500,7 +499,7 @@ pub mod wrapper_tx { // the signed tx let mut tx = WrapperTx::new( Fee { - amount: Amount::from_uint(10, NATIVE_MAX_DECIMAL_PLACES) + amount: Amount::from_uint(10, 0) .expect("Test failed"), token: nam(), }, diff --git a/core/src/types/uint.rs b/core/src/types/uint.rs index 16c4045fd2..223044387a 100644 --- a/core/src/types/uint.rs +++ b/core/src/types/uint.rs @@ -10,6 +10,7 @@ use serde::{Deserialize, Serialize}; use uint::construct_uint; use crate::types::token; +use crate::types::token::Amount; construct_uint! { /// Namada native type to replace for unsigned 256 bit @@ -108,14 +109,11 @@ impl SignedUint { /// not exceed [`MAX_SIGNED_AMOUNT`], else returns `None`. pub fn checked_add(&self, other: &Self) -> Option { if self.non_negative() == other.non_negative() { - self.abs().checked_add(other.abs()) - .and_then(|val| Self::try_from(val) + self.abs().checked_add(other.abs()).and_then(|val| { + Self::try_from(val) .ok() - .map(|val| if !self.non_negative() { - -val - } else { - val - })) + .map(|val| if !self.non_negative() { -val } else { val }) + }) } else { Some(*self + *other) } @@ -189,17 +187,22 @@ impl Add for SignedUint { match (self.non_negative(), rhs.non_negative()) { (true, true) => Self(self.0 + rhs.0), (false, false) => -Self(self.abs() + rhs.abs()), - (true, false) => if self.0 >= rhs.abs() { - Self(self.0 - rhs.abs()) - } else { - -Self(rhs.abs() - self.0) + (true, false) => { + if self.0 >= rhs.abs() { + Self(self.0 - rhs.abs()) + } else { + -Self(rhs.abs() - self.0) + } } - (false, true) => if rhs.0 >= self.abs() { - Self(rhs.0 - self.abs()) - } else { - -Self(self.abs() - rhs.0) - }, - }.canonical() + (false, true) => { + if rhs.0 >= self.abs() { + Self(rhs.0 - self.abs()) + } else { + -Self(self.abs() - rhs.0) + } + } + } + .canonical() } } @@ -240,6 +243,17 @@ impl From for SignedUint { } } +impl TryFrom for i128 { + type Error = std::io::Error; + + fn try_from(value: SignedUint) -> Result { + if !value.non_negative() { + Ok(-(u128::try_from(Amount::from_change(value))? as i128)) + } else { + Ok(u128::try_from(Amount::from_change(value))? as i128) + } + } +} #[cfg(test)] mod test_uint { @@ -249,10 +263,14 @@ mod test_uint { /// value gives zero. #[test] fn test_max_signed_value() { - let signed = SignedUint::try_from(MAX_SIGNED_VALUE).expect("Test failed"); + let signed = + SignedUint::try_from(MAX_SIGNED_VALUE).expect("Test failed"); let one = SignedUint::try_from(Uint::from(1u64)).expect("Test failed"); let overflow = signed + one; - assert_eq!(overflow, SignedUint::try_from(Uint::zero()).expect("Test failed")); + assert_eq!( + overflow, + SignedUint::try_from(Uint::zero()).expect("Test failed") + ); assert!(signed.checked_add(&one).is_none()); assert!((-signed).checked_sub(&one).is_none()); } @@ -262,7 +280,7 @@ mod test_uint { #[test] fn test_minus_zero_not_allowed() { let larger = Uint([0, 0, 0, 2u64.pow(63)]); - let smaller = Uint([u64::MAX, u64::MAX, u64::MAX, 2u64.pow(63) -1]); + let smaller = Uint([u64::MAX, u64::MAX, u64::MAX, 2u64.pow(63) - 1]); assert!(larger > smaller); assert_eq!(smaller, MAX_SIGNED_VALUE); assert_eq!(larger, MINUS_ZERO); @@ -341,4 +359,4 @@ mod test_uint { assert_eq!(neg_eight - two, -ten); assert!((two - two).is_zero()); } -} \ No newline at end of file +} diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index e3fdcde9f2..c9c468f79b 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -19,8 +19,8 @@ pub mod storage; pub mod types; // pub mod validation; -//#[cfg(test)] -// mod tests; +#[cfg(test)] +mod tests; use core::fmt::Debug; use std::collections::{BTreeMap, HashMap, HashSet}; @@ -42,7 +42,7 @@ use namada_core::types::key::{ }; pub use namada_core::types::storage::Epoch; use namada_core::types::token; -use namada_core::types::token::{Amount, NATIVE_MAX_DECIMAL_PLACES}; +use namada_core::types::token::Amount; use once_cell::unsync::Lazy; use parameters::PosParams; use rust_decimal::Decimal; @@ -1628,7 +1628,7 @@ where slash_rate, u128::try_from(amount).expect("Amount out of bounds"), ), - NATIVE_MAX_DECIMAL_PLACES, + 0, ) .expect("Amount out of bounds"); slashed += to_slash; @@ -1745,7 +1745,7 @@ where rate, u128::try_from(current_stake).expect("Amount out of bounds"), ), - NATIVE_MAX_DECIMAL_PLACES, + 0, ) .expect("Amount out of bounds"); let token_change = -token::Change::from(slashed_amount); diff --git a/proof_of_stake/src/tests.rs b/proof_of_stake/src/tests.rs index 22149a191e..9b7de24f7c 100644 --- a/proof_of_stake/src/tests.rs +++ b/proof_of_stake/src/tests.rs @@ -215,7 +215,7 @@ fn test_bonds_aux(params: PosParams, validators: Vec) { read_total_stake(&s, ¶ms, pipeline_epoch).unwrap(); // Self-bond - let amount_self_bond = token::Amount::from(100_500_000); + let amount_self_bond = token::Amount::from_uint(100_500_000, 0).unwrap(); credit_tokens( &mut s, &staking_token_address(), @@ -325,7 +325,7 @@ fn test_bonds_aux(params: PosParams, validators: Vec) { // Get a non-validating account with tokens let delegator = address::testing::gen_implicit_address(); - let amount_del = token::Amount::from(201_000_000); + let amount_del = token::Amount::from_uint(201_000_000, 0).unwrap(); credit_tokens(&mut s, &staking_token_address(), &delegator, amount_del) .unwrap(); let balance_key = token::balance_key(&staking_token_address(), &delegator); @@ -461,7 +461,7 @@ fn test_bonds_aux(params: PosParams, validators: Vec) { let pipeline_epoch = current_epoch + params.pipeline_len; // Unbond the self-bond - let amount_self_unbond = token::Amount::from(50_000); + let amount_self_unbond = token::Amount::from_uint(50_000, 0).unwrap(); unbond_tokens( &mut s, None, @@ -511,7 +511,7 @@ fn test_bonds_aux(params: PosParams, validators: Vec) { ); // Unbond delegation - let amount_undel = token::Amount::from(1_000_000); + let amount_undel = token::Amount::from_uint(1_000_000, 0).unwrap(); unbond_tokens( &mut s, Some(&delegator), @@ -694,7 +694,7 @@ fn test_become_validator_aux( current_epoch = advance_epoch(&mut s, ¶ms); // Self-bond to the new validator - let amount = token::Amount::from(100_500_000); + let amount = token::Amount::from_uint(100_500_000, 0).unwrap(); credit_tokens(&mut s, &staking_token_address(), &new_validator, amount) .unwrap(); bond_tokens(&mut s, None, &new_validator, amount, current_epoch).unwrap(); @@ -831,20 +831,20 @@ fn test_validator_sets() { // Start with two genesis validators with 1 NAM stake let epoch = Epoch::default(); - let ((val1, pk1), stake1) = (gen_validator(), token::Amount::whole(1)); - let ((val2, pk2), stake2) = (gen_validator(), token::Amount::whole(1)); - let ((val3, pk3), stake3) = (gen_validator(), token::Amount::whole(10)); - let ((val4, pk4), stake4) = (gen_validator(), token::Amount::whole(1)); - let ((val5, pk5), stake5) = (gen_validator(), token::Amount::whole(100)); - let ((val6, pk6), stake6) = (gen_validator(), token::Amount::whole(1)); - let ((val7, pk7), stake7) = (gen_validator(), token::Amount::whole(1)); - println!("val1: {val1}, {pk1}, {stake1}"); - println!("val2: {val2}, {pk2}, {stake2}"); - println!("val3: {val3}, {pk3}, {stake3}"); - println!("val4: {val4}, {pk4}, {stake4}"); - println!("val5: {val5}, {pk5}, {stake5}"); - println!("val6: {val6}, {pk6}, {stake6}"); - println!("val7: {val7}, {pk7}, {stake7}"); + let ((val1, pk1), stake1) = (gen_validator(), token::Amount::native_whole(1)); + let ((val2, pk2), stake2) = (gen_validator(), token::Amount::native_whole(1)); + let ((val3, pk3), stake3) = (gen_validator(), token::Amount::native_whole(10)); + let ((val4, pk4), stake4) = (gen_validator(), token::Amount::native_whole(1)); + let ((val5, pk5), stake5) = (gen_validator(), token::Amount::native_whole(100)); + let ((val6, pk6), stake6) = (gen_validator(), token::Amount::native_whole(1)); + let ((val7, pk7), stake7) = (gen_validator(), token::Amount::native_whole(1)); + println!("val1: {val1}, {pk1}, {}", stake1.to_string_native()); + println!("val2: {val2}, {pk2}, {}", stake2.to_string_native()); + println!("val3: {val3}, {pk3}, {}", stake3.to_string_native()); + println!("val4: {val4}, {pk4}, {}", stake4.to_string_native()); + println!("val5: {val5}, {pk5}, {}", stake5.to_string_native()); + println!("val6: {val6}, {pk6}, {}", stake6.to_string_native()); + println!("val7: {val7}, {pk7}, {}", stake7.to_string_native()); init_genesis( &mut s, @@ -877,14 +877,14 @@ fn test_validator_sets() { tm_updates[0], ValidatorSetUpdate::Consensus(ConsensusValidator { consensus_key: pk1.clone(), - bonded_stake: stake1.into(), + bonded_stake: u128::try_from(stake1).unwrap() as u64, }) ); assert_eq!( tm_updates[1], ValidatorSetUpdate::Consensus(ConsensusValidator { consensus_key: pk2.clone(), - bonded_stake: stake2.into(), + bonded_stake: u128::try_from(stake2).unwrap() as u64, }) ); @@ -1023,7 +1023,7 @@ fn test_validator_sets() { tm_updates[0], ValidatorSetUpdate::Consensus(ConsensusValidator { consensus_key: pk3, - bonded_stake: stake3.into(), + bonded_stake: u128::try_from(stake3).unwrap() as u64, }) ); @@ -1078,16 +1078,16 @@ fn test_validator_sets() { tm_updates[0], ValidatorSetUpdate::Consensus(ConsensusValidator { consensus_key: pk5, - bonded_stake: stake5.into(), + bonded_stake: u128::try_from(stake5).unwrap() as u64, }) ); assert_eq!(tm_updates[1], ValidatorSetUpdate::Deactivated(pk2)); // Unbond some stake from val1, it should be be swapped with the greatest // below-capacity validator val2 into the below-capacity set - let unbond = token::Amount::from(500_000); + let unbond = token::Amount::from_uint(500_000, 0).unwrap(); let stake1 = stake1 - unbond; - println!("val1 {val1} new stake {stake1}"); + println!("val1 {val1} new stake {}", stake1.to_string_native()); // Because `update_validator_set` and `update_validator_deltas` are // effective from pipeline offset, we use pipeline epoch for the rest of the // checks @@ -1274,16 +1274,16 @@ fn test_validator_sets() { tm_updates[0], ValidatorSetUpdate::Consensus(ConsensusValidator { consensus_key: pk4.clone(), - bonded_stake: stake4.into(), + bonded_stake: u128::try_from(stake4).unwrap() as u64, }) ); assert_eq!(tm_updates[1], ValidatorSetUpdate::Deactivated(pk1)); // Bond some stake to val6, it should be be swapped with the lowest // consensus validator val2 into the consensus set - let bond = token::Amount::from(500_000); + let bond = token::Amount::from_uint(500_000, 0).unwrap(); let stake6 = stake6 + bond; - println!("val6 {val6} new stake {stake6}"); + println!("val6 {val6} new stake {}", stake6.to_string_native()); update_validator_set(&mut s, ¶ms, &val6, bond.change(), epoch).unwrap(); update_validator_deltas(&mut s, ¶ms, &val6, bond.change(), epoch) .unwrap(); @@ -1390,7 +1390,7 @@ fn test_validator_sets() { tm_updates[0], ValidatorSetUpdate::Consensus(ConsensusValidator { consensus_key: pk6, - bonded_stake: stake6.into(), + bonded_stake: u128::try_from(stake6).unwrap() as u64, }) ); assert_eq!(tm_updates[1], ValidatorSetUpdate::Deactivated(pk4)); @@ -1454,14 +1454,14 @@ fn test_validator_sets_swap() { // Start with two genesis validators, one with 1 voting power and other 0 let epoch = Epoch::default(); // 1M voting power - let ((val1, pk1), stake1) = (gen_validator(), token::Amount::whole(10)); + let ((val1, pk1), stake1) = (gen_validator(), token::Amount::native_whole(10)); // 0 voting power - let ((val2, pk2), stake2) = (gen_validator(), token::Amount::from(5)); + let ((val2, pk2), stake2) = (gen_validator(), token::Amount::from_uint(5, 0).unwrap()); // 0 voting power - let ((val3, pk3), stake3) = (gen_validator(), token::Amount::from(5)); - println!("val1: {val1}, {pk1}, {stake1}"); - println!("val2: {val2}, {pk2}, {stake2}"); - println!("val3: {val3}, {pk3}, {stake3}"); + let ((val3, pk3), stake3) = (gen_validator(), token::Amount::from_uint(5, 0).unwrap()); + println!("val1: {val1}, {pk1}, {}", stake1.to_string_native()); + println!("val2: {val2}, {pk2}, {}", stake2.to_string_native()); + println!("val3: {val3}, {pk3}, {}", stake3.to_string_native()); init_genesis( &mut s, @@ -1498,14 +1498,14 @@ fn test_validator_sets_swap() { // Add 2 bonds, one for val2 and greater one for val3 let bonds_epoch_1 = pipeline_epoch; - let bond2 = token::Amount::from(1); + let bond2 = token::Amount::from_uint(1, 0).unwrap(); let stake2 = stake2 + bond2; - let bond3 = token::Amount::from(4); + let bond3 = token::Amount::from_uint(4, 0).unwrap(); let stake3 = stake3 + bond3; assert!(stake2 < stake3); - assert_eq!(into_tm_voting_power(params.tm_votes_per_token, stake2), 0); - assert_eq!(into_tm_voting_power(params.tm_votes_per_token, stake3), 0); + assert_eq!(into_tm_voting_power(params.tm_votes_per_token, u128::try_from(stake2).unwrap() as u64), 0); + assert_eq!(into_tm_voting_power(params.tm_votes_per_token, u128::try_from(stake3).unwrap() as u64), 0); update_validator_set(&mut s, ¶ms, &val2, bond2.change(), epoch) .unwrap(); @@ -1523,13 +1523,13 @@ fn test_validator_sets_swap() { // Add 2 more bonds, same amount for `val2` and val3` let bonds_epoch_2 = pipeline_epoch; - let bonds = token::Amount::whole(1); + let bonds = token::Amount::native_whole(1); let stake2 = stake2 + bonds; let stake3 = stake3 + bonds; assert!(stake2 < stake3); assert_eq!( - into_tm_voting_power(params.tm_votes_per_token, stake2), - into_tm_voting_power(params.tm_votes_per_token, stake3) + into_tm_voting_power(params.tm_votes_per_token, u128::try_from(stake2).unwrap() as u64), + into_tm_voting_power(params.tm_votes_per_token, u128::try_from(stake3).unwrap() as u64) ); update_validator_set(&mut s, ¶ms, &val2, bonds.change(), epoch) @@ -1567,7 +1567,7 @@ fn test_validator_sets_swap() { tm_updates[0], ValidatorSetUpdate::Consensus(ConsensusValidator { consensus_key: pk3, - bonded_stake: stake3.into(), + bonded_stake: u128::try_from(stake3).unwrap() as u64, }) ); } diff --git a/proof_of_stake/src/tests/state_machine.rs b/proof_of_stake/src/tests/state_machine.rs index 487e52e511..8443842165 100644 --- a/proof_of_stake/src/tests/state_machine.rs +++ b/proof_of_stake/src/tests/state_machine.rs @@ -17,6 +17,7 @@ use rust_decimal::Decimal; // Use `RUST_LOG=info` (or another tracing level) and `--nocapture` to see // `tracing` logs from tests use test_log::test; +use namada_core::types::token::Change; use super::arb_genesis_validators; use crate::parameters::testing::{arb_pos_params, arb_rate}; @@ -584,8 +585,9 @@ impl ConcretePosState { assert!( consensus_stake >= below_cap_stake, "Consensus validator {consensus_addr} with stake \ - {consensus_stake} and below-capacity {below_cap_addr} \ - with stake {below_cap_stake} should be swapped." + {} and below-capacity {below_cap_addr} \ + with stake {} should be swapped.", + consensus_stake.to_string_native(), below_cap_stake.to_string_native() ); } } @@ -759,11 +761,12 @@ impl AbstractStateMachine for AbstractPosState { let arb_unbondable = prop::sample::select(unbondable); let arb_unbond = arb_unbondable.prop_flat_map(|(id, deltas_sum)| { + let deltas_sum = i128::try_from(deltas_sum).unwrap(); // Generate an amount to unbond, up to the sum assert!(deltas_sum > 0); (0..deltas_sum).prop_map(move |to_unbond| { let id = id.clone(); - let amount = token::Amount::from_change(to_unbond); + let amount = token::Amount::from_change(Change::from(to_unbond)); Transition::Unbond { id, amount } }) }); @@ -952,7 +955,7 @@ impl AbstractPosState { let bond = bonds.entry(id.clone()).or_default(); *bond += change; // Remove fully unbonded entries - if *bond == 0 { + if bond.is_zero() { bonds.remove(id); } } @@ -1120,9 +1123,9 @@ impl AbstractPosState { |mut acc, (_epoch, bonds)| { for (id, delta) in bonds { let entry = acc.entry(id.clone()).or_default(); - *entry += delta; + *entry += *delta; // Remove entries that are fully unbonded - if *entry == 0 { + if entry.is_zero() { acc.remove(id); } } @@ -1198,5 +1201,5 @@ fn arb_delegation( // Bond up to 10 tokens (10M micro units) to avoid overflows pub fn arb_bond_amount() -> impl Strategy { - (1_u64..10).prop_map(token::Amount::from) + (1_u64..10).prop_map(|val| token::Amount::from_uint(val, 0).unwrap()) } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index bb2965ded6..173d7f6b62 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.65.0" +channel = "1.68.0" components = ["rustc", "cargo", "rust-std", "rust-docs", "rls", "rust-src", "rust-analysis"] targets = ['wasm32-unknown-unknown'] \ No newline at end of file From 8efcc323b74c772110a4df031f241d729d642c72 Mon Sep 17 00:00:00 2001 From: satan Date: Thu, 13 Apr 2023 13:46:46 +0200 Subject: [PATCH 017/151] [fix]: Fixed the router tests --- core/src/types/transaction/mod.rs | 6 +- core/src/types/transaction/wrapper.rs | 9 +- proof_of_stake/src/tests.rs | 56 ++- proof_of_stake/src/tests/state_machine.rs | 14 +- shared/src/ledger/queries/router.rs | 546 +++++++++++----------- 5 files changed, 341 insertions(+), 290 deletions(-) diff --git a/core/src/types/transaction/mod.rs b/core/src/types/transaction/mod.rs index 5683c5a2fe..b1148b5eeb 100644 --- a/core/src/types/transaction/mod.rs +++ b/core/src/types/transaction/mod.rs @@ -424,8 +424,7 @@ pub mod tx_types { // the signed tx let wrapper = WrapperTx::new( Fee { - amount: Amount::from_uint(10, 0) - .expect("Test failed"), + amount: Amount::from_uint(10, 0).expect("Test failed"), token: nam(), }, &keypair, @@ -462,8 +461,7 @@ pub mod tx_types { // the signed tx let wrapper = WrapperTx::new( Fee { - amount: Amount::from_uint(10, 0) - .expect("Test failed"), + amount: Amount::from_uint(10, 0).expect("Test failed"), token: nam(), }, &keypair, diff --git a/core/src/types/transaction/wrapper.rs b/core/src/types/transaction/wrapper.rs index 4dac6e7ce2..1bfd345400 100644 --- a/core/src/types/transaction/wrapper.rs +++ b/core/src/types/transaction/wrapper.rs @@ -435,8 +435,7 @@ pub mod wrapper_tx { let wrapper = WrapperTx::new( Fee { - amount: Amount::from_uint(10, 0) - .expect("Test failed"), + amount: Amount::from_uint(10, 0).expect("Test failed"), token: nam(), }, &keypair, @@ -464,8 +463,7 @@ pub mod wrapper_tx { let mut wrapper = WrapperTx::new( Fee { - amount: Amount::from_uint(10, 0) - .expect("Test failed"), + amount: Amount::from_uint(10, 0).expect("Test failed"), token: nam(), }, &gen_keypair(), @@ -499,8 +497,7 @@ pub mod wrapper_tx { // the signed tx let mut tx = WrapperTx::new( Fee { - amount: Amount::from_uint(10, 0) - .expect("Test failed"), + amount: Amount::from_uint(10, 0).expect("Test failed"), token: nam(), }, &keypair, diff --git a/proof_of_stake/src/tests.rs b/proof_of_stake/src/tests.rs index 9b7de24f7c..921dce9df9 100644 --- a/proof_of_stake/src/tests.rs +++ b/proof_of_stake/src/tests.rs @@ -831,13 +831,20 @@ fn test_validator_sets() { // Start with two genesis validators with 1 NAM stake let epoch = Epoch::default(); - let ((val1, pk1), stake1) = (gen_validator(), token::Amount::native_whole(1)); - let ((val2, pk2), stake2) = (gen_validator(), token::Amount::native_whole(1)); - let ((val3, pk3), stake3) = (gen_validator(), token::Amount::native_whole(10)); - let ((val4, pk4), stake4) = (gen_validator(), token::Amount::native_whole(1)); - let ((val5, pk5), stake5) = (gen_validator(), token::Amount::native_whole(100)); - let ((val6, pk6), stake6) = (gen_validator(), token::Amount::native_whole(1)); - let ((val7, pk7), stake7) = (gen_validator(), token::Amount::native_whole(1)); + let ((val1, pk1), stake1) = + (gen_validator(), token::Amount::native_whole(1)); + let ((val2, pk2), stake2) = + (gen_validator(), token::Amount::native_whole(1)); + let ((val3, pk3), stake3) = + (gen_validator(), token::Amount::native_whole(10)); + let ((val4, pk4), stake4) = + (gen_validator(), token::Amount::native_whole(1)); + let ((val5, pk5), stake5) = + (gen_validator(), token::Amount::native_whole(100)); + let ((val6, pk6), stake6) = + (gen_validator(), token::Amount::native_whole(1)); + let ((val7, pk7), stake7) = + (gen_validator(), token::Amount::native_whole(1)); println!("val1: {val1}, {pk1}, {}", stake1.to_string_native()); println!("val2: {val2}, {pk2}, {}", stake2.to_string_native()); println!("val3: {val3}, {pk3}, {}", stake3.to_string_native()); @@ -1454,11 +1461,14 @@ fn test_validator_sets_swap() { // Start with two genesis validators, one with 1 voting power and other 0 let epoch = Epoch::default(); // 1M voting power - let ((val1, pk1), stake1) = (gen_validator(), token::Amount::native_whole(10)); + let ((val1, pk1), stake1) = + (gen_validator(), token::Amount::native_whole(10)); // 0 voting power - let ((val2, pk2), stake2) = (gen_validator(), token::Amount::from_uint(5, 0).unwrap()); + let ((val2, pk2), stake2) = + (gen_validator(), token::Amount::from_uint(5, 0).unwrap()); // 0 voting power - let ((val3, pk3), stake3) = (gen_validator(), token::Amount::from_uint(5, 0).unwrap()); + let ((val3, pk3), stake3) = + (gen_validator(), token::Amount::from_uint(5, 0).unwrap()); println!("val1: {val1}, {pk1}, {}", stake1.to_string_native()); println!("val2: {val2}, {pk2}, {}", stake2.to_string_native()); println!("val3: {val3}, {pk3}, {}", stake3.to_string_native()); @@ -1504,8 +1514,20 @@ fn test_validator_sets_swap() { let stake3 = stake3 + bond3; assert!(stake2 < stake3); - assert_eq!(into_tm_voting_power(params.tm_votes_per_token, u128::try_from(stake2).unwrap() as u64), 0); - assert_eq!(into_tm_voting_power(params.tm_votes_per_token, u128::try_from(stake3).unwrap() as u64), 0); + assert_eq!( + into_tm_voting_power( + params.tm_votes_per_token, + u128::try_from(stake2).unwrap() as u64 + ), + 0 + ); + assert_eq!( + into_tm_voting_power( + params.tm_votes_per_token, + u128::try_from(stake3).unwrap() as u64 + ), + 0 + ); update_validator_set(&mut s, ¶ms, &val2, bond2.change(), epoch) .unwrap(); @@ -1528,8 +1550,14 @@ fn test_validator_sets_swap() { let stake3 = stake3 + bonds; assert!(stake2 < stake3); assert_eq!( - into_tm_voting_power(params.tm_votes_per_token, u128::try_from(stake2).unwrap() as u64), - into_tm_voting_power(params.tm_votes_per_token, u128::try_from(stake3).unwrap() as u64) + into_tm_voting_power( + params.tm_votes_per_token, + u128::try_from(stake2).unwrap() as u64 + ), + into_tm_voting_power( + params.tm_votes_per_token, + u128::try_from(stake3).unwrap() as u64 + ) ); update_validator_set(&mut s, ¶ms, &val2, bonds.change(), epoch) diff --git a/proof_of_stake/src/tests/state_machine.rs b/proof_of_stake/src/tests/state_machine.rs index 8443842165..3662f4e382 100644 --- a/proof_of_stake/src/tests/state_machine.rs +++ b/proof_of_stake/src/tests/state_machine.rs @@ -9,6 +9,7 @@ use namada_core::types::address::{self, Address}; use namada_core::types::key; use namada_core::types::key::common::PublicKey; use namada_core::types::storage::Epoch; +use namada_core::types::token::Change; use proptest::prelude::*; use proptest::prop_state_machine; use proptest::state_machine::{AbstractStateMachine, StateMachineTest}; @@ -17,7 +18,6 @@ use rust_decimal::Decimal; // Use `RUST_LOG=info` (or another tracing level) and `--nocapture` to see // `tracing` logs from tests use test_log::test; -use namada_core::types::token::Change; use super::arb_genesis_validators; use crate::parameters::testing::{arb_pos_params, arb_rate}; @@ -584,10 +584,11 @@ impl ConcretePosState { { assert!( consensus_stake >= below_cap_stake, - "Consensus validator {consensus_addr} with stake \ - {} and below-capacity {below_cap_addr} \ - with stake {} should be swapped.", - consensus_stake.to_string_native(), below_cap_stake.to_string_native() + "Consensus validator {consensus_addr} with stake {} and \ + below-capacity {below_cap_addr} with stake {} should be \ + swapped.", + consensus_stake.to_string_native(), + below_cap_stake.to_string_native() ); } } @@ -766,7 +767,8 @@ impl AbstractStateMachine for AbstractPosState { assert!(deltas_sum > 0); (0..deltas_sum).prop_map(move |to_unbond| { let id = id.clone(); - let amount = token::Amount::from_change(Change::from(to_unbond)); + let amount = + token::Amount::from_change(Change::from(to_unbond)); Transition::Unbond { id, amount } }) }); diff --git a/shared/src/ledger/queries/router.rs b/shared/src/ledger/queries/router.rs index f959a7a54c..799a34e5bd 100644 --- a/shared/src/ledger/queries/router.rs +++ b/shared/src/ledger/queries/router.rs @@ -828,263 +828,289 @@ macro_rules! router { ); } -// You can expand the `handlers!` macro invocation with e.g.: -// ```shell -// cargo expand ledger::queries::router::test_rpc_handlers --features "ferveo-tpke, ibc-mocks, testing, wasm-runtime, tendermint-rpc" --tests --lib -// ``` -// #[cfg(test)] -// mod test_rpc_handlers { -// use borsh::BorshSerialize; -// -// use crate::ledger::queries::{ -// EncodedResponseQuery, RequestCtx, RequestQuery, ResponseQuery, -// }; -// use crate::ledger::storage::{DBIter, StorageHasher, DB}; -// use crate::ledger::storage_api::{self, ResultExt}; -// use crate::types::storage::Epoch; -// use crate::types::token; -// -// A little macro to generate boilerplate for RPC handler functions. -// These are implemented to return their name as a String, joined by -// slashes with their argument values turned `to_string()`, if any. -// macro_rules! handlers { -// ( -// name and params, if any -// $( $name:ident $( ( $( $param:ident: $param_ty:ty ),* ) )? ),* -// optional trailing comma -// $(,)? ) => { -// $( -// pub fn $name( -// _ctx: RequestCtx<'_, D, H>, -// $( $( $param: $param_ty ),* )? -// ) -> storage_api::Result -// where -// D: 'static + DB + for<'iter> DBIter<'iter> + Sync, -// H: 'static + StorageHasher + Sync, -// { -// let data = stringify!($name).to_owned(); -// $( $( -// let data = format!("{data}/{}", $param); -// )* )? -// Ok(data) -// } -// )* -// }; -// } -// -// Generate handler functions for the router below -// handlers!( -// a, -// b0i, -// b0ii, -// b1, -// b2i(balance: token::Amount), -// b3(a1: token::Amount, a2: token::Amount, a3: token::Amount), -// b3i(a1: token::Amount, a2: token::Amount, a3: token::Amount), -// b3ii(a1: token::Amount, a2: token::Amount, a3: token::Amount), -// x, -// y(untyped_arg: &str), -// z(untyped_arg: &str), -// ); -// -// This handler is hand-written, because the test helper macro doesn't -// support optional args. -// pub fn b3iii( -// _ctx: RequestCtx<'_, D, H>, -// a1: token::Amount, -// a2: token::Amount, -// a3: Option, -// ) -> storage_api::Result -// where -// D: 'static + DB + for<'iter> DBIter<'iter> + Sync, -// H: 'static + StorageHasher + Sync, -// { -// let data = "b3iii".to_owned(); -// let data = format!("{data}/{}", a1); -// let data = format!("{data}/{}", a2); -// let data = a3.map(|a3| format!("{data}/{}", a3)).unwrap_or(data); -// Ok(data) -// } -// -// This handler is hand-written, because the test helper macro doesn't -// support optional args. -// pub fn b3iiii( -// _ctx: RequestCtx<'_, D, H>, -// a1: token::Amount, -// a2: token::Amount, -// a3: Option, -// a4: Option, -// ) -> storage_api::Result -// where -// D: 'static + DB + for<'iter> DBIter<'iter> + Sync, -// H: 'static + StorageHasher + Sync, -// { -// let data = "b3iiii".to_owned(); -// let data = format!("{data}/{}", a1); -// let data = format!("{data}/{}", a2); -// let data = a3.map(|a3| format!("{data}/{}", a3)).unwrap_or(data); -// let data = a4.map(|a4| format!("{data}/{}", a4)).unwrap_or(data); -// Ok(data) -// } -// -// This handler is hand-written, because the test helper macro doesn't -// support handlers with `with_options`. -// pub fn c( -// _ctx: RequestCtx<'_, D, H>, -// _request: &RequestQuery, -// ) -> storage_api::Result -// where -// D: 'static + DB + for<'iter> DBIter<'iter> + Sync, -// H: 'static + StorageHasher + Sync, -// { -// let data = "c".to_owned().try_to_vec().into_storage_result()?; -// Ok(ResponseQuery { -// data, -// ..ResponseQuery::default() -// }) -// } -// } -// -// You can expand the `router!` macro invocation with e.g.: -// ```shell -// cargo expand ledger::queries::router::test_rpc --features "ferveo-tpke, ibc-mocks, testing, wasm-runtime, tendermint-rpc" --tests --lib -// ``` -// #[cfg(test)] -// mod test_rpc { -// use super::test_rpc_handlers::*; -// use crate::types::storage::Epoch; -// use crate::types::token; -// -// Setup an RPC router for testing -// router! {TEST_RPC, -// ( "sub" ) = (sub TEST_SUB_RPC), -// ( "a" ) -> String = a, -// ( "b" ) = { -// ( "0" ) = { -// ( "i" ) -> String = b0i, -// ( "ii" ) -> String = b0ii, -// }, -// ( "1" ) -> String = b1, -// ( "2" ) = { -// ( "i" / [balance: token::Amount] ) -> String = b2i, -// }, -// ( "3" / [a1: token::Amount] / [a2: token::Amount] ) = { -// ( "i" / [a3: token:: Amount] ) -> String = b3i, -// ( [a3: token:: Amount] ) -> String = b3, -// ( [a3: token:: Amount] / "ii" ) -> String = b3ii, -// ( [a3: opt token::Amount] / "iii" ) -> String = b3iii, -// ( "iiii" / [a3: opt token::Amount] / "xyz" / [a4: opt Epoch] ) -> String = -// b3iiii, }, -// }, -// ( "c" ) -> String = (with_options c), -// } -// -// router! {TEST_SUB_RPC, -// ( "x" ) -> String = x, -// ( "y" / [untyped_arg] ) -> String = y, -// ( "z" / [untyped_arg] ) -> String = z, -// } -// } -// -// #[cfg(test)] -// mod test { -// use super::test_rpc::TEST_RPC; -// use crate::ledger::queries::testing::TestClient; -// use crate::ledger::queries::{RequestCtx, RequestQuery, Router}; -// use crate::ledger::storage_api; -// use crate::types::storage::Epoch; -// use crate::types::token; -// -// Test all the possible paths in `TEST_RPC` router. -// #[tokio::test] -// async fn test_router_macro() -> storage_api::Result<()> { -// let client = TestClient::new(TEST_RPC); -// -// Test request with an invalid path -// let request = RequestQuery { -// path: "/invalid".to_owned(), -// ..RequestQuery::default() -// }; -// let ctx = RequestCtx { -// event_log: &client.event_log, -// wl_storage: &client.wl_storage, -// vp_wasm_cache: client.vp_wasm_cache.clone(), -// tx_wasm_cache: client.tx_wasm_cache.clone(), -// storage_read_past_height_limit: None, -// }; -// let result = TEST_RPC.handle(ctx, &request); -// assert!(result.is_err()); -// -// Test requests to valid paths using the router's methods -// -// let result = TEST_RPC.a(&client).await.unwrap(); -// assert_eq!(result, "a"); -// -// let result = TEST_RPC.b0i(&client).await.unwrap(); -// assert_eq!(result, "b0i"); -// -// let result = TEST_RPC.b0ii(&client).await.unwrap(); -// assert_eq!(result, "b0ii"); -// -// let result = TEST_RPC.b1(&client).await.unwrap(); -// assert_eq!(result, "b1"); -// -// let balance = token::Amount::native_whole(123_000_000); -// let result = TEST_RPC.b2i(&client, &balance).await.unwrap(); -// assert_eq!(result, format!("b2i/{balance}")); -// -// let a1 = token::Amount::native_whole(345); -// let a2 = token::Amount::native_whole(123_000); -// let a3 = token::Amount::native_whole(1_000_999); -// let result = TEST_RPC.b3(&client, &a1, &a2, &a3).await.unwrap(); -// assert_eq!(result, format!("b3/{a1}/{a2}/{a3}")); -// -// let result = TEST_RPC.b3i(&client, &a1, &a2, &a3).await.unwrap(); -// assert_eq!(result, format!("b3i/{a1}/{a2}/{a3}")); -// -// let result = TEST_RPC.b3ii(&client, &a1, &a2, &a3).await.unwrap(); -// assert_eq!(result, format!("b3ii/{a1}/{a2}/{a3}")); -// -// let result = -// TEST_RPC.b3iii(&client, &a1, &a2, &Some(a3)).await.unwrap(); -// assert_eq!(result, format!("b3iii/{a1}/{a2}/{a3}")); -// -// let result = TEST_RPC.b3iii(&client, &a1, &a2, &None).await.unwrap(); -// assert_eq!(result, format!("b3iii/{a1}/{a2}")); -// -// let result = TEST_RPC -// .b3iiii(&client, &a1, &a2, &Some(a3), &None) -// .await -// .unwrap(); -// assert_eq!(result, format!("b3iiii/{a1}/{a2}/{a3}")); -// -// let a4 = Epoch::from(10); -// let result = TEST_RPC -// .b3iiii(&client, &a1, &a2, &Some(a3), &Some(a4)) -// .await -// .unwrap(); -// assert_eq!(result, format!("b3iiii/{a1}/{a2}/{a3}/{a4}")); -// -// let result = TEST_RPC -// .b3iiii(&client, &a1, &a2, &None, &None) -// .await -// .unwrap(); -// assert_eq!(result, format!("b3iiii/{a1}/{a2}")); -// -// let result = TEST_RPC.c(&client, None, None, false).await.unwrap(); -// assert_eq!(result.data, format!("c")); -// -// let result = TEST_RPC.test_sub_rpc().x(&client).await.unwrap(); -// assert_eq!(result, format!("x")); -// -// let arg = "test123"; -// let result = TEST_RPC.test_sub_rpc().y(&client, arg).await.unwrap(); -// assert_eq!(result, format!("y/{arg}")); -// -// let arg = "test321"; -// let result = TEST_RPC.test_sub_rpc().z(&client, arg).await.unwrap(); -// assert_eq!(result, format!("z/{arg}")); -// -// Ok(()) -// } -// } +/// You can expand the `handlers!` macro invocation with e.g.: +/// ```shell +/// cargo expand ledger::queries::router::test_rpc_handlers --features "ferveo-tpke, ibc-mocks, testing, wasm-runtime, tendermint-rpc" --tests --lib +/// ``` +#[cfg(test)] +mod test_rpc_handlers { + use borsh::BorshSerialize; + + use crate::ledger::queries::{ + EncodedResponseQuery, RequestCtx, RequestQuery, ResponseQuery, + }; + use crate::ledger::storage::{DBIter, StorageHasher, DB}; + use crate::ledger::storage_api::{self, ResultExt}; + use crate::types::storage::Epoch; + use crate::types::token; + + /// A little macro to generate boilerplate for RPC handler functions. + /// These are implemented to return their name as a String, joined by + /// slashes with their argument values turned `to_string()`, if any. + macro_rules! handlers { + ( + // name and params, if any + $( $name:ident $( ( $( $param:ident: $param_ty:ty ),* ) )? ),* + // optional trailing comma + $(,)? ) => { + $( + pub fn $name( + _ctx: RequestCtx<'_, D, H>, + $( $( $param: $param_ty ),* )? + ) -> storage_api::Result + where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, + { + let data = stringify!($name).to_owned(); + $( $( + let data = format!("{data}/{}", $param); + )* )? + Ok(data) + } + )* + }; + } + + // Generate handler functions for the router below + handlers!( + a, + b0i, + b0ii, + b1, + b2i(balance: token::DenominatedAmount), + b3( + a1: token::DenominatedAmount, + a2: token::DenominatedAmount, + a3: token::DenominatedAmount + ), + b3i( + a1: token::DenominatedAmount, + a2: token::DenominatedAmount, + a3: token::DenominatedAmount + ), + b3ii( + a1: token::DenominatedAmount, + a2: token::DenominatedAmount, + a3: token::DenominatedAmount + ), + x, + y(untyped_arg: &str), + z(untyped_arg: &str), + ); + + /// This handler is hand-written, because the test helper macro doesn't + /// support optional args. + pub fn b3iii( + _ctx: RequestCtx<'_, D, H>, + a1: token::DenominatedAmount, + a2: token::DenominatedAmount, + a3: Option, + ) -> storage_api::Result + where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, + { + let data = "b3iii".to_owned(); + let data = format!("{data}/{}", a1); + let data = format!("{data}/{}", a2); + let data = a3.map(|a3| format!("{data}/{}", a3)).unwrap_or(data); + Ok(data) + } + + /// This handler is hand-written, because the test helper macro doesn't + /// support optional args. + pub fn b3iiii( + _ctx: RequestCtx<'_, D, H>, + a1: token::DenominatedAmount, + a2: token::DenominatedAmount, + a3: Option, + a4: Option, + ) -> storage_api::Result + where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, + { + let data = "b3iiii".to_owned(); + let data = format!("{data}/{}", a1); + let data = format!("{data}/{}", a2); + let data = a3.map(|a3| format!("{data}/{}", a3)).unwrap_or(data); + let data = a4.map(|a4| format!("{data}/{}", a4)).unwrap_or(data); + Ok(data) + } + + /// This handler is hand-written, because the test helper macro doesn't + /// support handlers with `with_options`. + pub fn c( + _ctx: RequestCtx<'_, D, H>, + _request: &RequestQuery, + ) -> storage_api::Result + where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, + { + let data = "c".to_owned().try_to_vec().into_storage_result()?; + Ok(ResponseQuery { + data, + ..ResponseQuery::default() + }) + } +} + +/// You can expand the `router!` macro invocation with e.g.: +/// ```shell +/// cargo expand ledger::queries::router::test_rpc --features "ferveo-tpke, ibc-mocks, testing, wasm-runtime, tendermint-rpc" --tests --lib +/// ``` +#[cfg(test)] +mod test_rpc { + use super::test_rpc_handlers::*; + use crate::types::storage::Epoch; + use crate::types::token; + + // Setup an RPC router for testing + router! {TEST_RPC, + ( "sub" ) = (sub TEST_SUB_RPC), + ( "a" ) -> String = a, + ( "b" ) = { + ( "0" ) = { + ( "i" ) -> String = b0i, + ( "ii" ) -> String = b0ii, + }, + ( "1" ) -> String = b1, + ( "2" ) = { + ( "i" / [balance: token::DenominatedAmount] ) -> String = b2i, + }, + ( "3" / [a1: token::DenominatedAmount] / [a2: token::DenominatedAmount] ) = { + ( "i" / [a3: token::DenominatedAmount] ) -> String = b3i, + ( [a3: token::DenominatedAmount] ) -> String = b3, + ( [a3: token::DenominatedAmount] / "ii" ) -> String = b3ii, + ( [a3: opt token::DenominatedAmount] / "iii" ) -> String = b3iii, + ( "iiii" / [a3: opt token::DenominatedAmount] / "xyz" / [a4: opt Epoch] ) -> String = b3iiii, + }, + }, + ( "c" ) -> String = (with_options c), + } + + router! {TEST_SUB_RPC, + ( "x" ) -> String = x, + ( "y" / [untyped_arg] ) -> String = y, + ( "z" / [untyped_arg] ) -> String = z, + } +} + +#[cfg(test)] +mod test { + use namada_core::types::token::NATIVE_MAX_DECIMAL_PLACES; + + use super::test_rpc::TEST_RPC; + use crate::ledger::queries::testing::TestClient; + use crate::ledger::queries::{RequestCtx, RequestQuery, Router}; + use crate::ledger::storage_api; + use crate::types::storage::Epoch; + use crate::types::token; + + /// Test all the possible paths in `TEST_RPC` router. + #[tokio::test] + async fn test_router_macro() -> storage_api::Result<()> { + let client = TestClient::new(TEST_RPC); + + // Test request with an invalid path + let request = RequestQuery { + path: "/invalid".to_owned(), + ..RequestQuery::default() + }; + let ctx = RequestCtx { + event_log: &client.event_log, + wl_storage: &client.wl_storage, + vp_wasm_cache: client.vp_wasm_cache.clone(), + tx_wasm_cache: client.tx_wasm_cache.clone(), + storage_read_past_height_limit: None, + }; + let result = TEST_RPC.handle(ctx, &request); + assert!(result.is_err()); + + // Test requests to valid paths using the router's methods + + let result = TEST_RPC.a(&client).await.unwrap(); + assert_eq!(result, "a"); + + let result = TEST_RPC.b0i(&client).await.unwrap(); + assert_eq!(result, "b0i"); + + let result = TEST_RPC.b0ii(&client).await.unwrap(); + assert_eq!(result, "b0ii"); + + let result = TEST_RPC.b1(&client).await.unwrap(); + assert_eq!(result, "b1"); + + let balance = token::DenominatedAmount { + amount: token::Amount::native_whole(123_000_000), + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + let result = TEST_RPC.b2i(&client, &balance).await.unwrap(); + assert_eq!(result, format!("b2i/{balance}")); + + let a1 = token::DenominatedAmount { + amount: token::Amount::native_whole(345), + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + let a2 = token::DenominatedAmount { + amount: token::Amount::native_whole(123_000), + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + let a3 = token::DenominatedAmount { + amount: token::Amount::native_whole(1_000_999), + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + let result = TEST_RPC.b3(&client, &a1, &a2, &a3).await.unwrap(); + assert_eq!(result, format!("b3/{a1}/{a2}/{a3}")); + + let result = TEST_RPC.b3i(&client, &a1, &a2, &a3).await.unwrap(); + assert_eq!(result, format!("b3i/{a1}/{a2}/{a3}")); + + let result = TEST_RPC.b3ii(&client, &a1, &a2, &a3).await.unwrap(); + assert_eq!(result, format!("b3ii/{a1}/{a2}/{a3}")); + + let result = + TEST_RPC.b3iii(&client, &a1, &a2, &Some(a3)).await.unwrap(); + assert_eq!(result, format!("b3iii/{a1}/{a2}/{a3}")); + + let result = TEST_RPC.b3iii(&client, &a1, &a2, &None).await.unwrap(); + assert_eq!(result, format!("b3iii/{a1}/{a2}")); + + let result = TEST_RPC + .b3iiii(&client, &a1, &a2, &Some(a3), &None) + .await + .unwrap(); + assert_eq!(result, format!("b3iiii/{a1}/{a2}/{a3}")); + + let a4 = Epoch::from(10); + let result = TEST_RPC + .b3iiii(&client, &a1, &a2, &Some(a3), &Some(a4)) + .await + .unwrap(); + assert_eq!(result, format!("b3iiii/{a1}/{a2}/{a3}/{a4}")); + + let result = TEST_RPC + .b3iiii(&client, &a1, &a2, &None, &None) + .await + .unwrap(); + assert_eq!(result, format!("b3iiii/{a1}/{a2}")); + + let result = TEST_RPC.c(&client, None, None, false).await.unwrap(); + assert_eq!(result.data, format!("c")); + + let result = TEST_RPC.test_sub_rpc().x(&client).await.unwrap(); + assert_eq!(result, format!("x")); + + let arg = "test123"; + let result = TEST_RPC.test_sub_rpc().y(&client, arg).await.unwrap(); + assert_eq!(result, format!("y/{arg}")); + + let arg = "test321"; + let result = TEST_RPC.test_sub_rpc().z(&client, arg).await.unwrap(); + assert_eq!(result, format!("z/{arg}")); + + Ok(()) + } +} From 70702a108fb758f70150382a628bad892185d954 Mon Sep 17 00:00:00 2001 From: satan Date: Thu, 13 Apr 2023 14:08:53 +0200 Subject: [PATCH 018/151] [fix]: Cleanup up the wasms --- tests/src/native_vp/pos.rs | 16 +---- tests/src/vm_host_env/ibc.rs | 2 +- tests/src/vm_host_env/mod.rs | 18 ++--- wasm/checksums.json | 36 +++++----- .../src/tx_change_validator_commission.rs | 6 +- wasm/wasm_source/src/vp_testnet_faucet.rs | 16 ++--- wasm/wasm_source/src/vp_user.rs | 72 ++++--------------- wasm/wasm_source/src/vp_validator.rs | 72 ++++--------------- 8 files changed, 59 insertions(+), 179 deletions(-) diff --git a/tests/src/native_vp/pos.rs b/tests/src/native_vp/pos.rs index b2e6166703..6b606f33ca 100644 --- a/tests/src/native_vp/pos.rs +++ b/tests/src/native_vp/pos.rs @@ -591,9 +591,7 @@ pub mod testing { use namada::types::key::RefTo; use namada::types::storage::Epoch; use namada::types::{address, key, token}; - use namada_core::types::token::{ - Amount, Change, NATIVE_MAX_DECIMAL_PLACES, - }; + use namada_core::types::token::{Amount, Change}; use namada_tx_prelude::{Address, StorageRead, StorageWrite}; use proptest::prelude::*; use rust_decimal::Decimal; @@ -767,11 +765,7 @@ pub mod testing { arb_validator, ) .prop_map(|(amount, owner, validator)| ValidPosAction::Bond { - amount: Amount::from_uint( - amount, - NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(), + amount: Amount::from_uint(amount, 0).unwrap(), owner, validator, }); @@ -809,11 +803,7 @@ pub mod testing { // Unbond an arbitrary amount up to what's available (0..current_bond_amount).prop_map(move |amount| { ValidPosAction::Unbond { - amount: Amount::from_uint( - amount, - NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(), + amount: Amount::from_uint(amount, 0).unwrap(), owner: bond_id.source.clone(), validator: bond_id.validator.clone(), } diff --git a/tests/src/vm_host_env/ibc.rs b/tests/src/vm_host_env/ibc.rs index 23d59dfb01..45b4e364c9 100644 --- a/tests/src/vm_host_env/ibc.rs +++ b/tests/src/vm_host_env/ibc.rs @@ -202,7 +202,7 @@ pub fn init_storage() -> (Address, Address) { // initialize an account let account = tx::ctx().init_account(code).unwrap(); let key = token::balance_key(&token, &account); - let init_bal = Amount::native_whole(1_000_000_000u64); + let init_bal = Amount::from_uint(1_000_000_000u64, 0).unwrap(); tx::ctx().write(&key, init_bal).unwrap(); (token, account) } diff --git a/tests/src/vm_host_env/mod.rs b/tests/src/vm_host_env/mod.rs index 8754928b3e..e345d00956 100644 --- a/tests/src/vm_host_env/mod.rs +++ b/tests/src/vm_host_env/mod.rs @@ -35,7 +35,6 @@ mod tests { use namada::types::time::DateTimeUtc; use namada::types::token::{self, Amount}; use namada::types::{address, key}; - use namada_core::types::token::NATIVE_MAX_DECIMAL_PLACES; use namada_tx_prelude::{ BorshDeserialize, BorshSerialize, StorageRead, StorageWrite, }; @@ -1313,9 +1312,7 @@ mod tests { let denom = format!("{}/{}/{}", port_id, channel_id, token); let key_prefix = ibc_storage::ibc_token_prefix(denom).unwrap(); let key = token::multitoken_balance_key(&key_prefix, &sender); - let init_bal = - Amount::from_uint(1_000_000_000u64, NATIVE_MAX_DECIMAL_PLACES) - .unwrap(); + let init_bal = Amount::from_uint(1_000_000_000u64, 0).unwrap(); writes.insert(key, init_bal.try_to_vec().unwrap()); writes.into_iter().for_each(|(key, val)| { tx_host_env::with(|env| { @@ -1376,9 +1373,7 @@ mod tests { let denom = format!("{}/{}/{}", port_id, channel_id, token); let key_prefix = ibc_storage::ibc_token_prefix(denom).unwrap(); let key = token::multitoken_balance_key(&key_prefix, &receiver); - let init_bal = - Amount::from_uint(1_000_000_000u64, NATIVE_MAX_DECIMAL_PLACES) - .unwrap(); + let init_bal = Amount::from_uint(1_000_000_000u64, 0).unwrap(); writes.insert(key, init_bal.try_to_vec().unwrap()); writes.into_iter().for_each(|(key, val)| { @@ -1457,11 +1452,10 @@ mod tests { &key_prefix, &address::Address::Internal(address::InternalAddress::IbcEscrow), ); - let val = - Amount::from_uint(1_000_000_000u64, NATIVE_MAX_DECIMAL_PLACES) - .unwrap() - .try_to_vec() - .unwrap(); + let val = Amount::from_uint(1_000_000_000u64, 0) + .unwrap() + .try_to_vec() + .unwrap(); tx_host_env::with(|env| { env.wl_storage .storage diff --git a/wasm/checksums.json b/wasm/checksums.json index 9e821b3575..82e5672487 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,20 +1,20 @@ { - "tx_bond.wasm": "tx_bond.f7f4169dbd708a4776fa6f19c32c259402a7b7c34b9c1ac34029788c27f125fa.wasm", - "tx_change_validator_commission.wasm": "tx_change_validator_commission.49143933426925d549739dd06890f1c4709a27eca101596e34d5e0dbec1bd4c5.wasm", - "tx_ibc.wasm": "tx_ibc.e8081fbfb59dbdb42a2f66e89056fff3058bd7c3111e131b8ff84452752d94f5.wasm", - "tx_init_account.wasm": "tx_init_account.9ad3971335452cc7eada0dc35fb1e6310a05e8e2367e3067c0af8e21dad5705b.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.d0419c9cd9de905c92a0b9839ab94cdc3daf19b050375798f55503150ddc2bfa.wasm", - "tx_init_validator.wasm": "tx_init_validator.ef991c1019164b5d2432a3fba01e4b116825b024cc0dc3bcecdd1555ae7c6579.wasm", - "tx_reveal_pk.wasm": "tx_reveal_pk.b0509274d06dfe22988f03dd4c42e0a70bc40e21983f9670a603c057784dbd8f.wasm", - "tx_transfer.wasm": "tx_transfer.deae9d3955f0761331c21f253f4dc9b1bc93fe268a53049f9eb41d969848c62d.wasm", - "tx_unbond.wasm": "tx_unbond.8c63c856db5b608b44179abf29ec8996005d5a5e5467b11b1afad09b9052e69a.wasm", - "tx_update_vp.wasm": "tx_update_vp.287a8dde719b278b10c63384adbeacf40906b40e173b3ab0d0fac659e323951a.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.f86a66ce7c96be2ed4ee6b8d1fa0186264749ef88ec22575bf2c31ca82341c3e.wasm", - "tx_withdraw.wasm": "tx_withdraw.c9d451ccf7561db4a1113fa5c4c9d8266f185030c3ceb57e0204707de1489792.wasm", - "vp_implicit.wasm": "vp_implicit.03f75fbf6a2a4b343ba0d5691d381e643af1e592ed6dcc7f65b5554caf2f52df.wasm", - "vp_masp.wasm": "vp_masp.013f6e673ad10fcf233f1cda940fea7042c2ba4ac79b30c4fd9dc6cfa60a6080.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.a9bbcb7fbc484fe703e0bf18661701302bf2cc9425f4e8bcc8becc8ecc58a51c.wasm", - "vp_token.wasm": "vp_token.ac90ced308b618c6d991939a60735a1679e773bb5e76dd03a4dc4c9d56180ddd.wasm", - "vp_user.wasm": "vp_user.cf363aaf2fd13faa79501d8c8c251bafe22992b9989035b2c2edaf3edbe629fe.wasm", - "vp_validator.wasm": "vp_validator.4f7b0efb2f742b4b605738fe48ede23f57623ff083f23dc18c5bf8c5e26294cd.wasm" + "tx_bond.wasm": "tx_bond.4353af6713777b2738d2c8b5050e32687a63423ea4827c3dce31895f04cf17a8.wasm", + "tx_change_validator_commission.wasm": "tx_change_validator_commission.3012d6e8322ed0caf8f818f6f47ac70d40d7bf5366ab49b6594cbb160efd31a6.wasm", + "tx_ibc.wasm": "tx_ibc.91f160e0a518eab3e2b6adb54a19b7d50b30b6936c43de3e3cdd83c2b931cd06.wasm", + "tx_init_account.wasm": "tx_init_account.eee57525a9759281a0ed1579ff13440d09ccb6187ff8a7c9cc7d7027a1611f97.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.6b7dc8ddffad3ad01d666ef738e3839a54af7c73b23f24a9c1e6d27b68449dec.wasm", + "tx_init_validator.wasm": "tx_init_validator.f4c7d983bd6076dfe7ca9d6ad497eb352c047543ee01d577e6f56c3b9309e56e.wasm", + "tx_reveal_pk.wasm": "tx_reveal_pk.5a86cc0bd44d9755a4ce326ed1e178614311704468380ba7b297ed0f62018778.wasm", + "tx_transfer.wasm": "tx_transfer.06235371b028d3a4fafb3e35fd7b7b111763ed630647a39cd5e0655b91b37995.wasm", + "tx_unbond.wasm": "tx_unbond.f1f32820e024be0053abbaf0f5c30622cb0cc2470360b3e0894285fcf982e413.wasm", + "tx_update_vp.wasm": "tx_update_vp.2e319438adbc5256a29f1e8817c1cba69ffd5a8d3a51887f50383a471231d388.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.4cc5a9dde9dd1fef3f0dbc6cc2defb500c194c3934cd055d6e46f9b82c528368.wasm", + "tx_withdraw.wasm": "tx_withdraw.368f8bc90a2381169008590ba14a6cf6dce7cfd3f86a76e591eb4c5ed804d5a6.wasm", + "vp_implicit.wasm": "vp_implicit.64e6048d45d832360d59ac3437bebf947313d93ea75dd7538cf55f29b3e29cd6.wasm", + "vp_masp.wasm": "vp_masp.31688087bba04ca8b4c57a17ce7532ad2eb58f19221a94d3a7b4748cae79f420.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.21f0f5e0ed2d604fa291512fb6937aacef77c8e6303da306577a07a6db168564.wasm", + "vp_token.wasm": "vp_token.2ca9b0e8791315a8d29579011d194270bd007fe49e72874f3bbdcef58622361e.wasm", + "vp_user.wasm": "vp_user.225064a9c6b4f1f06d8843daa5fa8795f010d72320dee076b15a7c7e17b1bb3f.wasm", + "vp_validator.wasm": "vp_validator.0202d4cd90930910ffb0a9f12ada0952b4e095968e05ef9d636916115b4d5648.wasm" } \ No newline at end of file diff --git a/wasm/wasm_source/src/tx_change_validator_commission.rs b/wasm/wasm_source/src/tx_change_validator_commission.rs index 5e68993097..b8f2a856cc 100644 --- a/wasm/wasm_source/src/tx_change_validator_commission.rs +++ b/wasm/wasm_source/src/tx_change_validator_commission.rs @@ -68,11 +68,7 @@ mod tests { let consensus_key = key::testing::keypair_1().ref_to(); let genesis_validators = [GenesisValidator { address: commission_change.validator.clone(), - tokens: token::Amount::from_uint( - 1_000_000, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(), + tokens: token::Amount::from_uint(1_000_000, 0).unwrap(), consensus_key, commission_rate: initial_rate, max_commission_rate_change: max_change, diff --git a/wasm/wasm_source/src/vp_testnet_faucet.rs b/wasm/wasm_source/src/vp_testnet_faucet.rs index ab94e88c9f..61a80d681e 100644 --- a/wasm/wasm_source/src/vp_testnet_faucet.rs +++ b/wasm/wasm_source/src/vp_testnet_faucet.rs @@ -152,11 +152,7 @@ mod tests { let vp_owner = address::testing::established_address_1(); let source = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from_uint( - 10_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &source, &token]); @@ -299,12 +295,12 @@ mod tests { // Init the VP let vp_owner = address::testing::established_address_1(); let difficulty = testnet_pow::Difficulty::try_new(0).unwrap(); - let withdrawal_limit = token::Amount::from_uint(MAX_FREE_DEBIT as u64, token::NATIVE_MAX_DECIMAL_PLACES).unwrap(); + let withdrawal_limit = token::Amount::from_uint(MAX_FREE_DEBIT as u64, 0).unwrap(); testnet_pow::init_faucet_storage(&mut tx_env.wl_storage, &vp_owner, difficulty, withdrawal_limit).unwrap(); let target = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from_uint(amount, token::NATIVE_MAX_DECIMAL_PLACES).unwrap(); + let amount = token::Amount::from_uint(amount, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &target, &token]); @@ -343,13 +339,13 @@ mod tests { // Init the VP let vp_owner = address::testing::established_address_1(); let difficulty = testnet_pow::Difficulty::try_new(0).unwrap(); - let withdrawal_limit = token::Amount::from_uint(MAX_FREE_DEBIT as u64, token::NATIVE_MAX_DECIMAL_PLACES).unwrap(); + let withdrawal_limit = token::Amount::from_uint(MAX_FREE_DEBIT as u64, 0).unwrap(); testnet_pow::init_faucet_storage(&mut tx_env.wl_storage, &vp_owner, difficulty, withdrawal_limit).unwrap(); let target = address::testing::established_address_2(); let target_key = key::testing::keypair_1(); let token = address::nam(); - let amount = token::Amount::from_uint(amount, token::NATIVE_MAX_DECIMAL_PLACES).unwrap(); + let amount = token::Amount::from_uint(amount, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &target, &token]); @@ -410,7 +406,7 @@ mod tests { // Init the VP let difficulty = testnet_pow::Difficulty::try_new(0).unwrap(); - let withdrawal_limit = token::Amount::from_uint(MAX_FREE_DEBIT as u64, token::NATIVE_MAX_DECIMAL_PLACES).unwrap(); + let withdrawal_limit = token::Amount::from_uint(MAX_FREE_DEBIT as u64, 0).unwrap(); testnet_pow::init_faucet_storage(&mut tx_env.wl_storage, &vp_owner, difficulty, withdrawal_limit).unwrap(); let keypair = key::testing::keypair_1(); diff --git a/wasm/wasm_source/src/vp_user.rs b/wasm/wasm_source/src/vp_user.rs index 0e9c0556d9..a44d99ddaa 100644 --- a/wasm/wasm_source/src/vp_user.rs +++ b/wasm/wasm_source/src/vp_user.rs @@ -238,11 +238,7 @@ mod tests { let vp_owner = address::testing::established_address_1(); let source = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from_uint( - 10_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &source, &token]); @@ -292,11 +288,7 @@ mod tests { let vp_owner = address::testing::established_address_1(); let target = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from_uint( - 10_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &target, &token]); @@ -348,11 +340,7 @@ mod tests { let public_key = keypair.ref_to(); let target = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from_uint( - 10_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &target, &token]); @@ -406,11 +394,7 @@ mod tests { // Init PoS genesis let pos_params = PosParams::default(); let validator = address::testing::established_address_3(); - let initial_stake = token::Amount::from_uint( - 10_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); + let initial_stake = token::Amount::from_uint(10_098_123, 0).unwrap(); let consensus_key = key::testing::keypair_2().ref_to(); let commission_rate = rust_decimal::Decimal::new(5, 2); let max_commission_rate_change = rust_decimal::Decimal::new(1, 2); @@ -433,21 +417,9 @@ mod tests { let vp_owner: Address = address::testing::established_address_2(); let target = address::testing::established_address_3(); let token = address::nam(); - let amount = token::Amount::from_uint( - 10_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); - let bond_amount = token::Amount::from_uint( - 5_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); - let unbond_amount = token::Amount::from_uint( - 3_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); + let bond_amount = token::Amount::from_uint(5_098_123, 0).unwrap(); + let unbond_amount = token::Amount::from_uint(3_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&target, &token]); @@ -486,11 +458,7 @@ mod tests { // Init PoS genesis let pos_params = PosParams::default(); let validator = address::testing::established_address_3(); - let initial_stake = token::Amount::from_uint( - 10_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); + let initial_stake = token::Amount::from_uint(10_098_123, 0).unwrap(); let consensus_key = key::testing::keypair_2().ref_to(); let commission_rate = rust_decimal::Decimal::new(5, 2); let max_commission_rate_change = rust_decimal::Decimal::new(1, 2); @@ -513,21 +481,9 @@ mod tests { let vp_owner: Address = address::testing::established_address_2(); let target = address::testing::established_address_3(); let token = address::nam(); - let amount = token::Amount::from_uint( - 10_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); - let bond_amount = token::Amount::from_uint( - 5_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); - let unbond_amount = token::Amount::from_uint( - 3_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); + let bond_amount = token::Amount::from_uint(5_098_123, 0).unwrap(); + let unbond_amount = token::Amount::from_uint(3_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&target, &token]); @@ -574,11 +530,7 @@ mod tests { let source = address::testing::established_address_2(); let target = address::testing::established_address_3(); let token = address::nam(); - let amount = token::Amount::from_uint( - 10_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &source, &target, &token]); diff --git a/wasm/wasm_source/src/vp_validator.rs b/wasm/wasm_source/src/vp_validator.rs index f616e9dd31..18aa679fb3 100644 --- a/wasm/wasm_source/src/vp_validator.rs +++ b/wasm/wasm_source/src/vp_validator.rs @@ -246,11 +246,7 @@ mod tests { let vp_owner = address::testing::established_address_1(); let source = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from_uint( - 10_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &source, &token]); @@ -299,11 +295,7 @@ mod tests { let vp_owner = address::testing::established_address_1(); let target = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from_uint( - 10_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &target, &token]); @@ -355,11 +347,7 @@ mod tests { let public_key = keypair.ref_to(); let target = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from_uint( - 10_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &target, &token]); @@ -412,11 +400,7 @@ mod tests { // Init PoS genesis let pos_params = PosParams::default(); let validator = address::testing::established_address_3(); - let initial_stake = token::Amount::from_uint( - 10_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); + let initial_stake = token::Amount::from_uint(10_098_123, 0).unwrap(); let consensus_key = key::testing::keypair_2().ref_to(); let commission_rate = Decimal::new(5, 2); let max_commission_rate_change = Decimal::new(1, 2); @@ -439,21 +423,9 @@ mod tests { let vp_owner: Address = address::testing::established_address_2(); let target = address::testing::established_address_3(); let token = address::nam(); - let amount = token::Amount::from_uint( - 10_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); - let bond_amount = token::Amount::from_uint( - 5_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); - let unbond_amount = token::Amount::from_uint( - 3_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); + let bond_amount = token::Amount::from_uint(5_098_123, 0).unwrap(); + let unbond_amount = token::Amount::from_uint(3_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&target, &token]); @@ -498,11 +470,7 @@ mod tests { // Init PoS genesis let pos_params = PosParams::default(); let validator = address::testing::established_address_3(); - let initial_stake = token::Amount::from_uint( - 10_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); + let initial_stake = token::Amount::from_uint(10_098_123, 0).unwrap(); let consensus_key = key::testing::keypair_2().ref_to(); let commission_rate = Decimal::new(5, 2); let max_commission_rate_change = Decimal::new(1, 2); @@ -525,21 +493,9 @@ mod tests { let vp_owner: Address = address::testing::established_address_2(); let target = address::testing::established_address_3(); let token = address::nam(); - let amount = token::Amount::from_uint( - 10_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); - let bond_amount = token::Amount::from_uint( - 5_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); - let unbond_amount = token::Amount::from_uint( - 3_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); + let bond_amount = token::Amount::from_uint(5_098_123, 0).unwrap(); + let unbond_amount = token::Amount::from_uint(3_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&target, &token]); @@ -592,11 +548,7 @@ mod tests { let source = address::testing::established_address_2(); let target = address::testing::established_address_3(); let token = address::nam(); - let amount = token::Amount::from_uint( - 10_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &source, &target, &token]); From 86698a98c242461864a572aa2e0b398658234912 Mon Sep 17 00:00:00 2001 From: satan Date: Mon, 17 Apr 2023 11:59:54 +0200 Subject: [PATCH 019/151] [feat]: Added sub-prefixs to denom storage read/writes --- Makefile | 19 +- apps/src/lib/client/rpc.rs | 171 +++++++++++++----- apps/src/lib/client/tx.rs | 50 +++-- apps/src/lib/config/genesis.rs | 12 +- apps/src/lib/node/ledger/shell/init_chain.rs | 11 ++ apps/src/lib/wallet/defaults.rs | 2 +- core/src/ledger/storage_api/token.rs | 7 +- core/src/types/address.rs | 20 +- core/src/types/token.rs | 46 +++-- core/src/types/uint.rs | 4 +- rust-toolchain.toml | 2 +- shared/src/ledger/ibc/vp/token.rs | 13 +- shared/src/ledger/queries/vp/token.rs | 9 +- tx_prelude/src/token.rs | 10 +- wasm/checksums.json | 22 +-- .../vp_testnet_faucet.txt | 7 + wasm/wasm_source/src/vp_implicit.rs | 157 +++++++++------- wasm/wasm_source/src/vp_testnet_faucet.rs | 5 +- wasm/wasm_source/src/vp_user.rs | 32 +++- wasm/wasm_source/src/vp_validator.rs | 32 +++- 20 files changed, 424 insertions(+), 207 deletions(-) create mode 100644 wasm/wasm_source/proptest-regressions/vp_testnet_faucet.txt diff --git a/Makefile b/Makefile index d42b3fe3ec..0778e9e8a0 100644 --- a/Makefile +++ b/Makefile @@ -13,17 +13,23 @@ wasms_for_tests := wasm_for_tests/wasm_source # Paths for all the wasm templates wasm_templates := wasm/tx_template wasm/vp_template +ifdef JOBS +jobs := -j $(JOBS) +else +jobs := +endif + # TODO upgrade libp2p audit-ignores += RUSTSEC-2021-0076 build: - $(cargo) build + $(cargo) $(jobs) build build-test: - $(cargo) +$(nightly) build --tests -Z unstable-options + $(cargo) +$(nightly) build --tests $(jobs) -Z unstable-options build-release: - NAMADA_DEV=false $(cargo) build --release --package namada_apps --manifest-path Cargo.toml + NAMADA_DEV=false $(cargo) build $(jobs) --release --package namada_apps --manifest-path Cargo.toml install-release: NAMADA_DEV=false $(cargo) install --path ./apps --locked @@ -128,6 +134,7 @@ test-e2e: test-unit-abcipp: $(cargo) test \ --manifest-path ./apps/Cargo.toml \ + $(jobs) \ --no-default-features \ --features "testing std abcipp" \ -Z unstable-options \ @@ -136,12 +143,14 @@ test-unit-abcipp: $(cargo) test \ --manifest-path \ ./proof_of_stake/Cargo.toml \ + $(jobs) \ --features "testing" \ -Z unstable-options \ $(TEST_FILTER) -- \ -Z unstable-options --report-time && \ $(cargo) test \ --manifest-path ./shared/Cargo.toml \ + $(jobs) \ --no-default-features \ --features "testing wasm-runtime abcipp ibc-mocks-abcipp" \ -Z unstable-options \ @@ -149,6 +158,7 @@ test-unit-abcipp: -Z unstable-options --report-time && \ $(cargo) test \ --manifest-path ./vm_env/Cargo.toml \ + $(jobs) \ --no-default-features \ --features "abcipp" \ -Z unstable-options \ @@ -158,6 +168,7 @@ test-unit-abcipp: test-unit: $(cargo) +$(nightly) test \ $(TEST_FILTER) \ + $(jobs) \ -Z unstable-options \ -- --skip e2e \ -Z unstable-options --report-time @@ -166,12 +177,14 @@ test-unit-mainnet: $(cargo) +$(nightly) test \ --features "mainnet" \ $(TEST_FILTER) \ + $(jobs) -Z unstable-options \ -- --skip e2e \ -Z unstable-options --report-time test-unit-debug: $(debug-cargo) +$(nightly) test \ + $(jobs) $(TEST_FILTER) -- \ -Z unstable-options \ -- --skip e2e \ diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 443885857a..4cd02f9fdd 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -367,8 +367,11 @@ pub async fn query_transfers(mut ctx: Context, args: args::QueryTransfers) { print!(" {}:", account); for ((addr, denom), val) in amt.components() { let addr_enc = addr.encode(); - let readable = - tokens.get(addr).cloned().unwrap_or(addr_enc.as_str()); + let readable = tokens + .get(addr) + .cloned() + .map(|a| a.0) + .unwrap_or(addr_enc.as_str()); let sign = match val.cmp(&0) { Ordering::Greater => "+", Ordering::Less => "-", @@ -380,6 +383,9 @@ pub async fn query_transfers(mut ctx: Context, args: args::QueryTransfers) { format_denominated_amount( &client, addr, + // TODO: apparently MASP doesn't support + // multi-tokens? + &None, token::Amount::from_masp_denominated( val.unsigned_abs(), *denom @@ -399,8 +405,11 @@ pub async fn query_transfers(mut ctx: Context, args: args::QueryTransfers) { print!(" {}:", fvk_map[&account]); for ((addr, denom), val) in amt.components() { let addr_enc = addr.encode(); - let readable = - tokens.get(addr).cloned().unwrap_or(addr_enc.as_str()); + let readable = tokens + .get(addr) + .cloned() + .map(|a| a.0) + .unwrap_or(addr_enc.as_str()); let sign = match val.cmp(&0) { Ordering::Greater => "+", Ordering::Less => "-", @@ -412,6 +421,7 @@ pub async fn query_transfers(mut ctx: Context, args: args::QueryTransfers) { format_denominated_amount( &client, addr, + &None, token::Amount::from_masp_denominated( val.unsigned_abs(), *denom @@ -517,27 +527,39 @@ pub async fn query_transparent_balance( (Some(token), Some(owner)) => { let token = ctx.get(&token); let owner = ctx.get_cached(&owner); - let key = match &args.sub_prefix { + let (balance_key, sub_prefix) = match &args.sub_prefix { Some(sub_prefix) => { let sub_prefix = Key::parse(sub_prefix).unwrap(); let prefix = token::multitoken_balance_prefix(&token, &sub_prefix); - token::multitoken_balance_key( - &prefix, - &owner.address().unwrap(), + ( + token::multitoken_balance_key( + &prefix, + &owner.address().unwrap(), + ), + Some(sub_prefix), ) } - None => token::balance_key(&token, &owner.address().unwrap()), + None => ( + token::balance_key(&token, &owner.address().unwrap()), + None, + ), }; let currency_code = tokens .get(&token) - .map(|c| Cow::Borrowed(*c)) + .map(|(c, _)| Cow::Borrowed(*c)) .unwrap_or_else(|| Cow::Owned(token.to_string())); - match query_storage_value::(&client, &key).await { + match query_storage_value::(&client, &balance_key) + .await + { Some(balance) => { - let balance = - format_denominated_amount(&client, &token, balance) - .await; + let balance = format_denominated_amount( + &client, + &token, + &sub_prefix, + balance, + ) + .await; match &args.sub_prefix { Some(sub_prefix) => { println!( @@ -676,7 +698,7 @@ pub async fn query_pinned_balance(ctx: &mut Context, args: args::QueryBalance) { let token = ctx.get(token); let currency_code = tokens .get(&token) - .map(|c| Cow::Borrowed(*c)) + .map(|(c, _)| Cow::Borrowed(*c)) .unwrap_or_else(|| Cow::Owned(token.to_string())); let mut total_balance = token::Amount::default(); for denom in MaspDenom::iter() { @@ -698,6 +720,8 @@ pub async fn query_pinned_balance(ctx: &mut Context, args: args::QueryBalance) { let formatted = format_denominated_amount( &client, &token, + // TODO: Is this correct? + &None, total_balance, ) .await; @@ -729,12 +753,21 @@ pub async fn query_pinned_balance(ctx: &mut Context, args: args::QueryBalance) { found_any = true; } let addr_enc = addr.encode(); - let formatted = - format_denominated_amount(&client, addr, asset_value) - .await; + let formatted = format_denominated_amount( + &client, + addr, + // TODO: Is this correct? + &None, + asset_value, + ) + .await; println!( " {}: {}", - tokens.get(addr).cloned().unwrap_or(addr_enc.as_str()), + tokens + .get(addr) + .cloned() + .map(|a| a.0) + .unwrap_or(addr_enc.as_str()), formatted, ); } @@ -763,29 +796,39 @@ async fn print_balances( let tokens = address::tokens(); let currency_code = tokens .get(token) - .map(|c| Cow::Borrowed(*c)) + .map(|(c, _)| Cow::Borrowed(*c)) .unwrap_or_else(|| Cow::Owned(token.to_string())); writeln!(w, "Token {}", currency_code).unwrap(); let mut print_num = 0; for (key, balance) in balances { let (o, s) = match token::is_any_multitoken_balance_key(&key) { - Some((sub_prefix, owner)) => ( + Some((sub_prefix, [tok, owner])) => ( owner.clone(), format!( "with {}: {}, owned by {}", - sub_prefix, - format_denominated_amount(client, token, balance).await, + sub_prefix.clone(), + format_denominated_amount( + client, + tok, + &Some(sub_prefix), + balance + ) + .await, lookup_alias(ctx, owner) ), ), None => { - if let Some(owner) = token::is_any_token_balance_key(&key) { + if let Some([tok, owner]) = + token::is_any_token_balance_key(&key) + { ( owner.clone(), format!( ": {}, owned by {}", - format_denominated_amount(client, token, balance) - .await, + format_denominated_amount( + client, tok, &None, balance + ) + .await, lookup_alias(ctx, owner) ), ) @@ -1018,7 +1061,7 @@ pub async fn query_shielded_balance( let currency_code = tokens .get(&token) - .map(|c| Cow::Borrowed(*c)) + .map(|(c, _)| Cow::Borrowed(*c)) .unwrap_or_else(|| Cow::Owned(token.to_string())); if total_balance.is_zero() { println!( @@ -1029,8 +1072,14 @@ pub async fn query_shielded_balance( println!( "{}: {}", currency_code, - format_denominated_amount(&client, &token, total_balance) - .await + format_denominated_amount( + &client, + &token, + // TODO: Is this correct? + &None, + total_balance + ) + .await ); } } @@ -1083,6 +1132,7 @@ pub async fn query_shielded_balance( tokens .get(&addr) .cloned() + .map(|a| a.0) .unwrap_or(addr_enc.as_str()) ); read_tokens.insert(addr.clone()); @@ -1093,7 +1143,9 @@ pub async fn query_shielded_balance( denom, ); let formatted = format_denominated_amount( - &client, &addr, value, + &client, &addr, + // TODO: Is this correct? + &None, value, ) .await; println!(" {}, owned by {}", formatted, fvk); @@ -1111,7 +1163,7 @@ pub async fn query_shielded_balance( } } // Print zero balances for remaining assets - for (token, currency_code) in tokens { + for (token, (currency_code, _)) in tokens { if !read_tokens.contains(&token) { println!("Shielded Token {}:", currency_code); println!( @@ -1129,7 +1181,7 @@ pub async fn query_shielded_balance( let mut found_any = false; let currency_code = tokens .get(&token) - .map(|c| Cow::Borrowed(*c)) + .map(|(c, _)| Cow::Borrowed(*c)) .unwrap_or_else(|| Cow::Owned(token.to_string())); println!("Shielded Token {}:", currency_code); for fvk in viewing_keys { @@ -1166,8 +1218,11 @@ pub async fn query_shielded_balance( found_any = true; } } - let formatted = - format_denominated_amount(&client, &token, balance).await; + let formatted = format_denominated_amount( + &client, &token, // TODO: Is this correct? + &None, balance, + ) + .await; println!(" {}, owned by {}", formatted, fvk); } if !found_any { @@ -1237,8 +1292,16 @@ pub async fn print_decoded_balance( let addr_enc = addr.encode(); println!( "{} : {}", - tokens.get(addr).cloned().unwrap_or(addr_enc.as_str()), - format_denominated_amount(client, addr, amount).await, + tokens + .get(addr) + .cloned() + .map(|a| a.0) + .unwrap_or(addr_enc.as_str()), + format_denominated_amount( + client, addr, // TODO: Is this correct? + &None, amount, + ) + .await, ); } } @@ -1265,9 +1328,17 @@ pub async fn print_decoded_balance_with_epoch( let addr_enc = addr.encode(); println!( "{} | {} : {}", - tokens.get(addr).cloned().unwrap_or(addr_enc.as_str()), + tokens + .get(addr) + .cloned() + .map(|a| a.0) + .unwrap_or(addr_enc.as_str()), epoch, - format_denominated_amount(client, addr, amount).await, + format_denominated_amount( + client, addr, // TODO: Is this correct? + &None, amount, + ) + .await, ); } } @@ -2003,7 +2074,11 @@ pub async fn query_conversions(ctx: Context, args: args::QueryConversions) { let addr_enc = addr.encode(); print!( "{}[{}]: ", - tokens.get(addr).cloned().unwrap_or(addr_enc.as_str()), + tokens + .get(addr) + .cloned() + .map(|a| a.0) + .unwrap_or(addr_enc.as_str()), epoch, ); // Now print out the components of the allowed conversion @@ -2018,7 +2093,11 @@ pub async fn query_conversions(ctx: Context, args: args::QueryConversions) { "{}{} {}[{}]", prefix, val, - tokens.get(addr).cloned().unwrap_or(addr_enc.as_str()), + tokens + .get(addr) + .cloned() + .map(|a| a.0) + .unwrap_or(addr_enc.as_str()), epoch ); // Future iterations need to be prefixed with + @@ -2781,10 +2860,14 @@ pub(super) fn unwrap_client_response( pub(super) async fn format_denominated_amount( client: &HttpClient, token: &Address, + sub_prefix: &Option, amount: token::Amount, ) -> String { let denom = unwrap_client_response( - RPC.vp().token().denomination(client, token).await, + RPC.vp() + .token() + .denomination(client, token, sub_prefix) + .await, ) .unwrap_or_else(|| { println!( @@ -2815,13 +2898,17 @@ pub async fn validate_amount( client: &HttpClient, amount: InputAmount, token: &Address, + sub_prefix: &Option, ) -> token::DenominatedAmount { let input_amount = match amount { InputAmount::Unvalidated(amt) => amt.canonical(), InputAmount::Validated(amt) => return amt, }; let denom = unwrap_client_response( - RPC.vp().token().denomination(client, token).await, + RPC.vp() + .token() + .denomination(client, token, sub_prefix) + .await, ) .unwrap_or_else(|| { println!( diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 6e64df6114..433ee3423d 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -1607,17 +1607,6 @@ pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { safe_exit(1) } } - // validate the amount given - let validated_amount = - validate_amount(&client, parsed_args.amount, &parsed_args.token).await; - let validate_fee = validate_amount( - &client, - parsed_args.tx.fee_amount, - &parsed_args.tx.fee_token, - ) - .await; - parsed_args.amount = InputAmount::Validated(validated_amount); - parsed_args.tx.fee_amount = InputAmount::Validated(validate_fee); // Check source balance let (sub_prefix, balance_key) = match args.sub_prefix { Some(sub_prefix) => { @@ -1633,6 +1622,25 @@ pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { } None => (None, token::balance_key(&parsed_args.token, &source)), }; + // validate the amount given + let validated_amount = validate_amount( + &client, + parsed_args.amount, + &parsed_args.token, + &sub_prefix, + ) + .await; + let validate_fee = validate_amount( + &client, + parsed_args.tx.fee_amount, + &parsed_args.tx.fee_token, + // TODO: Currently multi-tokens cannot be used to pay fees + &None, + ) + .await; + parsed_args.amount = InputAmount::Validated(validated_amount); + parsed_args.tx.fee_amount = InputAmount::Validated(validate_fee); + match rpc::query_storage_value::(&client, &balance_key).await { Some(balance) => { @@ -1640,6 +1648,7 @@ pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { let balance_amount = format_denominated_amount( &client, &parsed_args.token, + &sub_prefix, balance, ) .await; @@ -1820,11 +1829,20 @@ pub async fn submit_ibc_transfer(ctx: Context, args: args::TxIbcTransfer) { { Some(balance) => { if balance < args.amount { - let formatted_amount = - format_denominated_amount(&client, &token, args.amount) - .await; - let formatted_balance = - format_denominated_amount(&client, &token, balance).await; + let formatted_amount = format_denominated_amount( + &client, + &token, + &sub_prefix, + args.amount, + ) + .await; + let formatted_balance = format_denominated_amount( + &client, + &token, + &sub_prefix, + balance, + ) + .await; eprintln!( "The balance of the source {} of token {} is lower than \ the amount to be transferred. Amount to transfer is {} \ diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index 7dde5fbe5b..52abc2f977 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -18,6 +18,7 @@ use namada::types::chain::ProposalBytes; use namada::types::key::dkg_session_keys::DkgPublicKey; use namada::types::key::*; use namada::types::time::{DateTimeUtc, DurationSecs}; +use namada::types::token::Denomination; use namada::types::{storage, token}; use rust_decimal::Decimal; @@ -41,6 +42,7 @@ pub mod genesis_config { use namada::types::key::dkg_session_keys::DkgPublicKey; use namada::types::key::*; use namada::types::time::Rfc3339String; + use namada::types::token::Denomination; use namada::types::{storage, token}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; @@ -207,6 +209,8 @@ pub mod genesis_config { pub struct TokenAccountConfig { // Address of token account (default: generate). pub address: Option, + // The number of decimal places amounts of this token has + pub denom: Denomination, // Filename of token account VP. (default: token VP) pub vp: Option, // Initial balances held by accounts defined elsewhere. @@ -400,6 +404,7 @@ pub mod genesis_config { TokenAccount { address: Address::decode(config.address.as_ref().unwrap()).unwrap(), + denom: config.denom, vp_code_path: token_vp_config.filename.to_owned(), vp_sha256: token_vp_config .sha256 @@ -799,6 +804,8 @@ pub struct EstablishedAccount { pub struct TokenAccount { /// Address pub address: Address, + /// The number of decimal places amounts of this token has + pub denom: Denomination, /// Validity predicate code WASM pub vp_code_path: String, /// Expected SHA-256 hash of the validity predicate wasm @@ -985,9 +992,10 @@ pub fn genesis() -> Genesis { ((&validator.account_key).into(), default_key_tokens), ]); let token_accounts = address::tokens() - .into_keys() - .map(|address| TokenAccount { + .into_iter() + .map(|(address, (_, denom))| TokenAccount { address, + denom, vp_code_path: vp_token_path.into(), vp_sha256: Default::default(), balances: balances.clone(), diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index 0bdaff27e2..3ac250536a 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -6,6 +6,7 @@ use std::hash::Hash; use namada::core::ledger::testnet_pow; use namada::ledger::parameters::Parameters; use namada::ledger::pos::into_tm_voting_power; +use namada::ledger::storage_api::token::write_denom; use namada::ledger::storage_api::StorageWrite; use namada::types::key::*; #[cfg(not(feature = "dev"))] @@ -240,11 +241,21 @@ where // Initialize genesis token accounts for genesis::TokenAccount { address, + denom, vp_code_path, vp_sha256, balances, } in genesis.token_accounts { + // associate a token with its denomination. + write_denom( + &mut self.wl_storage, + &address, + // TODO: Should we support multi-tokens at genesis? + None, + denom, + ) + .unwrap(); let vp_code = vp_code_cache.get_or_insert_with(vp_code_path.clone(), || { wasm_loader::read_wasm(&self.wasm_dir, &vp_code_path) diff --git a/apps/src/lib/wallet/defaults.rs b/apps/src/lib/wallet/defaults.rs index b0ae08ac83..cf4069e1cc 100644 --- a/apps/src/lib/wallet/defaults.rs +++ b/apps/src/lib/wallet/defaults.rs @@ -122,7 +122,7 @@ mod dev { ]; let token_addresses = address::tokens() .into_iter() - .map(|(addr, alias)| (alias.into(), addr)); + .map(|(addr, (alias, _))| (alias.into(), addr)); addresses.extend(token_addresses); addresses } diff --git a/core/src/ledger/storage_api/token.rs b/core/src/ledger/storage_api/token.rs index 793cba632b..2d24a4081c 100644 --- a/core/src/ledger/storage_api/token.rs +++ b/core/src/ledger/storage_api/token.rs @@ -3,6 +3,7 @@ use super::{StorageRead, StorageWrite}; use crate::ledger::storage_api; use crate::types::address::Address; +use crate::types::storage::Key; use crate::types::token; pub use crate::types::token::{Amount, Change}; @@ -26,11 +27,12 @@ where pub fn read_denom( storage: &S, token: &Address, + sub_prefix: Option<&Key>, ) -> storage_api::Result> where S: StorageRead, { - let key = token::denom_key(token); + let key = token::denom_key(token, sub_prefix); storage.read(&key) } @@ -38,12 +40,13 @@ where pub fn write_denom( storage: &mut S, token: &Address, + sub_prefix: Option<&Key>, denom: token::Denomination, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, { - let key = token::denom_key(token); + let key = token::denom_key(token, sub_prefix); storage.write(&key, denom) } diff --git a/core/src/types/address.rs b/core/src/types/address.rs index 9c3985ff81..b22a141678 100644 --- a/core/src/types/address.rs +++ b/core/src/types/address.rs @@ -14,7 +14,7 @@ use thiserror::Error; use crate::types::key; use crate::types::key::PublicKeyHash; -use crate::types::token::MaspDenom; +use crate::types::token::{Denomination, MaspDenom}; /// The length of an established [`Address`] encoded with Borsh. pub const ESTABLISHED_ADDRESS_BYTES_LEN: usize = 45; @@ -558,16 +558,16 @@ pub fn masp_tx_key() -> crate::types::key::common::SecretKey { } /// Temporary helper for testing, a hash map of tokens addresses with their -/// informal currency codes. -pub fn tokens() -> HashMap { +/// informal currency codes and number of decimal places. +pub fn tokens() -> HashMap { vec![ - (nam(), "NAM"), - (btc(), "BTC"), - (eth(), "ETH"), - (dot(), "DOT"), - (schnitzel(), "Schnitzel"), - (apfel(), "Apfel"), - (kartoffel(), "Kartoffel"), + (nam(), ("NAM", 6.into())), + (btc(), ("BTC", 8.into())), + (eth(), ("ETH", 18.into())), + (dot(), ("DOT", 10.into())), + (schnitzel(), ("Schnitzel", 6.into())), + (apfel(), ("Apfel", 6.into())), + (kartoffel(), ("Kartoffel", 6.into())), ] .into_iter() .collect() diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 0851d8463f..6da010c603 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -216,10 +216,11 @@ impl Amount { pub fn denominated( &self, token: &Address, + sub_prefix: Option<&Key>, storage: &impl StorageRead, ) -> Option { - let denom = - read_denom(storage, token).expect("Should be able to read storage"); + let denom = read_denom(storage, token, sub_prefix) + .expect("Should be able to read storage"); denom.map(|denom| DenominatedAmount { amount: *self, denom, @@ -248,7 +249,10 @@ impl Amount { BorshSerialize, BorshDeserialize, BorshSchema, + Serialize, + Deserialize, )] +#[serde(transparent)] pub struct Denomination(pub u8); impl From for Denomination { @@ -749,23 +753,29 @@ pub fn is_balance_key<'a>( } /// Check if the given storage key is balance key for unspecified token. If it -/// is, returns the owner. -pub fn is_any_token_balance_key(key: &Key) -> Option<&Address> { +/// is, returns the token and owner address. +pub fn is_any_token_balance_key(key: &Key) -> Option<[&Address; 2]> { match &key.segments[..] { [ - DbKeySeg::AddressSeg(_), + DbKeySeg::AddressSeg(token), DbKeySeg::StringSeg(key), DbKeySeg::AddressSeg(owner), - ] if key == BALANCE_STORAGE_KEY => Some(owner), + ] if key == BALANCE_STORAGE_KEY => Some([token, owner]), _ => None, } } /// Obtain a storage key denomination of a token. -pub fn denom_key(token_addr: &Address) -> Key { - Key::from(token_addr.to_db_key()) - .push(&DENOM_STORAGE_KEY.to_owned()) - .expect("Cannot obtain a storage key") +pub fn denom_key(token_addr: &Address, sub_prefix: Option<&Key>) -> Key { + match sub_prefix { + Some(sub) => Key::from(token_addr.to_db_key()) + .join(sub) + .push(&DENOM_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key"), + None => Key::from(token_addr.to_db_key()) + .push(&DENOM_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key"), + } } /// Check if the given storage key is a denomination key for the given token. @@ -773,6 +783,7 @@ pub fn is_denom_key(token_addr: &Address, key: &Key) -> bool { matches!(&key.segments[..], [ DbKeySeg::AddressSeg(addr), + .., DbKeySeg::StringSeg(key), ] if key == DENOM_STORAGE_KEY && addr == token_addr) } @@ -802,10 +813,13 @@ pub fn is_multitoken_balance_key<'a>( } /// Check if the given storage key is multitoken balance key for unspecified -/// token. If it is, returns the sub prefix and the owner. -pub fn is_any_multitoken_balance_key(key: &Key) -> Option<(Key, &Address)> { +/// token. If it is, returns the sub prefix and the token and owner addresses. +pub fn is_any_multitoken_balance_key( + key: &Key, +) -> Option<(Key, [&Address; 2])> { match key.segments.first() { - Some(DbKeySeg::AddressSeg(_)) => multitoken_balance_owner(key), + Some(DbKeySeg::AddressSeg(token)) => multitoken_balance_owner(key) + .map(|(sub, owner)| (sub, [token, owner])), _ => None, } } @@ -1085,12 +1099,12 @@ pub mod testing { /// Generate an arbitrary token amount pub fn arb_amount() -> impl Strategy { - any::().prop_map(Amount::native_whole) + any::().prop_map(|val| Amount::from_uint(val, 0).unwrap()) } /// Generate an arbitrary token amount up to and including given `max` value pub fn arb_amount_ceiled(max: u64) -> impl Strategy { - (0..=max).prop_map(Amount::native_whole) + (0..=max).prop_map(|val| Amount::from_uint(val, 0).unwrap()) } /// Generate an arbitrary non-zero token amount up to and including given @@ -1098,6 +1112,6 @@ pub mod testing { pub fn arb_amount_non_zero_ceiled( max: u64, ) -> impl Strategy { - (1..=max).prop_map(Amount::native_whole) + (1..=max).prop_map(|val| Amount::from_uint(val, 0).unwrap()) } } diff --git a/core/src/types/uint.rs b/core/src/types/uint.rs index 223044387a..fb6e72376c 100644 --- a/core/src/types/uint.rs +++ b/core/src/types/uint.rs @@ -106,7 +106,7 @@ impl SignedUint { } /// Adds two [`SignedUint`]'s if the absolute value does - /// not exceed [`MAX_SIGNED_AMOUNT`], else returns `None`. + /// not exceed [`MAX_SIGNED_VALUE`], else returns `None`. pub fn checked_add(&self, other: &Self) -> Option { if self.non_negative() == other.non_negative() { self.abs().checked_add(other.abs()).and_then(|val| { @@ -120,7 +120,7 @@ impl SignedUint { } /// Subtracts two [`SignedUint`]'s if the absolute value does - /// not exceed [`MAX_SIGNED_AMOUNT`], else returns `None`. + /// not exceed [`MAX_SIGNED_VALUE`], else returns `None`. pub fn checked_sub(&self, other: &Self) -> Option { self.checked_add(&other.neg()) } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 173d7f6b62..bb2965ded6 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.68.0" +channel = "1.65.0" components = ["rustc", "cargo", "rust-std", "rust-docs", "rls", "rust-src", "rust-analysis"] targets = ['wasm32-unknown-unknown'] \ No newline at end of file diff --git a/shared/src/ledger/ibc/vp/token.rs b/shared/src/ledger/ibc/vp/token.rs index b28019849f..19858e0255 100644 --- a/shared/src/ledger/ibc/vp/token.rs +++ b/shared/src/ledger/ibc/vp/token.rs @@ -92,11 +92,14 @@ where token::is_any_multitoken_balance_key(k), Some(( _, - Address::Internal( - InternalAddress::IbcEscrow - | InternalAddress::IbcBurn - | InternalAddress::IbcMint - ) + [ + _, + Address::Internal( + InternalAddress::IbcEscrow + | InternalAddress::IbcBurn + | InternalAddress::IbcMint + ) + ] )) ) }) diff --git a/shared/src/ledger/queries/vp/token.rs b/shared/src/ledger/queries/vp/token.rs index 930a9412c5..71ef3a52b4 100644 --- a/shared/src/ledger/queries/vp/token.rs +++ b/shared/src/ledger/queries/vp/token.rs @@ -2,13 +2,13 @@ use namada_core::ledger::storage::{DBIter, StorageHasher, DB}; use namada_core::ledger::storage_api; use namada_core::ledger::storage_api::token::read_denom; use namada_core::types::address::Address; +use namada_core::types::storage::Key; use namada_core::types::token; -use namada_core::types::token::Denomination; use crate::ledger::queries::RequestCtx; router! {TOKEN, - ( "denomination" / [addr: Address] ) -> Option = denomination, + ( "denomination" / [addr: Address] / [sub_prefix: opt Key] ) -> Option = denomination, } /// Get the number of decimal places (in base 10) for a @@ -16,10 +16,11 @@ router! {TOKEN, fn denomination( ctx: RequestCtx<'_, D, H>, addr: Address, -) -> storage_api::Result> + sub_prefix: Option, +) -> storage_api::Result> where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, { - read_denom(ctx.wl_storage, &addr) + read_denom(ctx.wl_storage, &addr, sub_prefix.as_ref()) } diff --git a/tx_prelude/src/token.rs b/tx_prelude/src/token.rs index 44d775843f..78335b7925 100644 --- a/tx_prelude/src/token.rs +++ b/tx_prelude/src/token.rs @@ -142,10 +142,10 @@ pub fn transfer_with_keys( ) -> TxResult { let src_owner = is_any_multitoken_balance_key(src_key).map(|(_, o)| o); let src_bal: Option = match src_owner { - Some(Address::Internal(InternalAddress::IbcMint)) => { + Some([_, Address::Internal(InternalAddress::IbcMint)]) => { Some(Amount::max_signed()) } - Some(Address::Internal(InternalAddress::IbcBurn)) => { + Some([_, Address::Internal(InternalAddress::IbcBurn)]) => { log_string("invalid transfer from the burn address"); unreachable!() } @@ -168,7 +168,7 @@ pub fn transfer_with_keys( src_bal.spend(&amount); let dest_owner = is_any_multitoken_balance_key(dest_key).map(|(_, o)| o); let mut dest_bal: Amount = match dest_owner { - Some(Address::Internal(InternalAddress::IbcMint)) => { + Some([_, Address::Internal(InternalAddress::IbcMint)]) => { log_string("invalid transfer to the mint address"); unreachable!() } @@ -183,13 +183,13 @@ pub fn transfer_with_keys( }; dest_bal.receive(&amount); match src_owner { - Some(Address::Internal(InternalAddress::IbcMint)) => { + Some([_, Address::Internal(InternalAddress::IbcMint)]) => { ctx.write_temp(src_key, src_bal)?; } _ => ctx.write(src_key, src_bal)?, } match dest_owner { - Some(Address::Internal(InternalAddress::IbcBurn)) => { + Some([_, Address::Internal(InternalAddress::IbcBurn)]) => { ctx.write_temp(dest_key, dest_bal)?; } _ => ctx.write(dest_key, dest_bal)?, diff --git a/wasm/checksums.json b/wasm/checksums.json index 82e5672487..c3eef316ff 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,20 +1,20 @@ { - "tx_bond.wasm": "tx_bond.4353af6713777b2738d2c8b5050e32687a63423ea4827c3dce31895f04cf17a8.wasm", + "tx_bond.wasm": "tx_bond.6875d76a927658665699a0c9e35a3505e8ccbfbc999cf1ea8d88ca8dd4550267.wasm", "tx_change_validator_commission.wasm": "tx_change_validator_commission.3012d6e8322ed0caf8f818f6f47ac70d40d7bf5366ab49b6594cbb160efd31a6.wasm", - "tx_ibc.wasm": "tx_ibc.91f160e0a518eab3e2b6adb54a19b7d50b30b6936c43de3e3cdd83c2b931cd06.wasm", + "tx_ibc.wasm": "tx_ibc.9f5d6878a77ab942fb6c479e4d777dd30b3b7c6b297fc4190bf7b7fd3317c648.wasm", "tx_init_account.wasm": "tx_init_account.eee57525a9759281a0ed1579ff13440d09ccb6187ff8a7c9cc7d7027a1611f97.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.6b7dc8ddffad3ad01d666ef738e3839a54af7c73b23f24a9c1e6d27b68449dec.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.0cbd682144335b3a112030a5d7eb9acead5c7547e54d7837299f48f42669f0a9.wasm", "tx_init_validator.wasm": "tx_init_validator.f4c7d983bd6076dfe7ca9d6ad497eb352c047543ee01d577e6f56c3b9309e56e.wasm", "tx_reveal_pk.wasm": "tx_reveal_pk.5a86cc0bd44d9755a4ce326ed1e178614311704468380ba7b297ed0f62018778.wasm", - "tx_transfer.wasm": "tx_transfer.06235371b028d3a4fafb3e35fd7b7b111763ed630647a39cd5e0655b91b37995.wasm", - "tx_unbond.wasm": "tx_unbond.f1f32820e024be0053abbaf0f5c30622cb0cc2470360b3e0894285fcf982e413.wasm", + "tx_transfer.wasm": "tx_transfer.a3a02481c513dfa142f4b25ea5177bbde60998e42dfc0f298ef7f38d15438922.wasm", + "tx_unbond.wasm": "tx_unbond.c691e7ccba1add857e8c1adf2b37d288ee09b2ca6403f6b6f14831185a854aa7.wasm", "tx_update_vp.wasm": "tx_update_vp.2e319438adbc5256a29f1e8817c1cba69ffd5a8d3a51887f50383a471231d388.wasm", "tx_vote_proposal.wasm": "tx_vote_proposal.4cc5a9dde9dd1fef3f0dbc6cc2defb500c194c3934cd055d6e46f9b82c528368.wasm", - "tx_withdraw.wasm": "tx_withdraw.368f8bc90a2381169008590ba14a6cf6dce7cfd3f86a76e591eb4c5ed804d5a6.wasm", - "vp_implicit.wasm": "vp_implicit.64e6048d45d832360d59ac3437bebf947313d93ea75dd7538cf55f29b3e29cd6.wasm", - "vp_masp.wasm": "vp_masp.31688087bba04ca8b4c57a17ce7532ad2eb58f19221a94d3a7b4748cae79f420.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.21f0f5e0ed2d604fa291512fb6937aacef77c8e6303da306577a07a6db168564.wasm", + "tx_withdraw.wasm": "tx_withdraw.d47ef25ea19d1f696ede4f9be5bfbcad89dda8d5901e3e09686c82b90df779f0.wasm", + "vp_implicit.wasm": "vp_implicit.1d3e362e5e27bfc6fdd19ade86125a70529d245605fd9e843ec9f7efcb12e7b0.wasm", + "vp_masp.wasm": "vp_masp.59bc45054f964c3ed044215928b0760cf529c3669429453da4c0f16b287e4632.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.4409133d316355e5701e3e049283e059933e281719f6b6820cc6ed3130be5999.wasm", "vp_token.wasm": "vp_token.2ca9b0e8791315a8d29579011d194270bd007fe49e72874f3bbdcef58622361e.wasm", - "vp_user.wasm": "vp_user.225064a9c6b4f1f06d8843daa5fa8795f010d72320dee076b15a7c7e17b1bb3f.wasm", - "vp_validator.wasm": "vp_validator.0202d4cd90930910ffb0a9f12ada0952b4e095968e05ef9d636916115b4d5648.wasm" + "vp_user.wasm": "vp_user.c002d586a8741bc8ccadfbc0bb9b7e2e05e80ef0accbe9f78c273e1acf1a1462.wasm", + "vp_validator.wasm": "vp_validator.4ec929399961906f2499fb39991ef46dbb4101ed59d346ef9b373bbc609982d6.wasm" } \ No newline at end of file diff --git a/wasm/wasm_source/proptest-regressions/vp_testnet_faucet.txt b/wasm/wasm_source/proptest-regressions/vp_testnet_faucet.txt new file mode 100644 index 0000000000..ed127f296a --- /dev/null +++ b/wasm/wasm_source/proptest-regressions/vp_testnet_faucet.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc a2303427ef50a7c459fcdc71279f4ab37a6ad1854c0d999d3bdaf0e4b3039e9f # shrinks to amount = 0 diff --git a/wasm/wasm_source/src/vp_implicit.rs b/wasm/wasm_source/src/vp_implicit.rs index 97b50c2511..e43e598d37 100644 --- a/wasm/wasm_source/src/vp_implicit.rs +++ b/wasm/wasm_source/src/vp_implicit.rs @@ -11,14 +11,18 @@ //! //! Any other storage key changes are allowed only with a valid signature. -use namada_vp_prelude::storage::KeySeg; +use namada_vp_prelude::storage::{Key, KeySeg}; use namada_vp_prelude::*; use once_cell::unsync::Lazy; enum KeyType<'a> { /// Public key - written once revealed Pk(&'a Address), - Token(&'a Address), + Token { + token: &'a Address, + sub_prefix: Option, + owner: &'a Address, + }, PoS, GovernanceVote(&'a Address), Unknown, @@ -28,12 +32,22 @@ impl<'a> From<&'a storage::Key> for KeyType<'a> { fn from(key: &'a storage::Key) -> KeyType<'a> { if let Some(address) = key::is_pk_key(key) { Self::Pk(address) - } else if let Some(address) = token::is_any_token_balance_key(key) { - Self::Token(address) - } else if let Some((_, address)) = + } else if let Some([token, owner]) = + token::is_any_token_balance_key(key) + { + Self::Token { + token, + owner, + sub_prefix: None, + } + } else if let Some((sub, [token, owner])) = token::is_any_multitoken_balance_key(key) { - Self::Token(address) + Self::Token { + token, + owner, + sub_prefix: Some(sub), + } } else if proof_of_stake::is_pos_key(key) { Self::PoS } else if gov_storage::is_vote_key(key) { @@ -115,7 +129,11 @@ fn validate_tx( } true } - KeyType::Token(owner) => { + KeyType::Token { + token, + owner, + sub_prefix, + } => { if owner == &addr { let pre: token::Amount = ctx.read_pre(key)?.unwrap_or_default(); @@ -126,7 +144,7 @@ fn validate_tx( let valid = change.non_negative() || *valid_sig; let sign = if change.non_negative() { "" } else { "-" }; let denom_amount = token::Amount::from_change(change) - .denominated(owner, &ctx.pre()) + .denominated(token, sub_prefix.as_ref(), &ctx.pre()) .unwrap(); debug_log!( "token key: {}, change: {}{}, valid_sig: {}, valid \ @@ -213,7 +231,7 @@ mod tests { use namada_tests::tx::{self, tx_host_env, TestTxEnv}; use namada_tests::vp::vp_host_env::storage::Key; use namada_tests::vp::*; - use namada_tx_prelude::{StorageWrite, TxEnv}; + use namada_tx_prelude::{storage_api, StorageWrite, TxEnv}; use namada_vp_prelude::key::RefTo; use proptest::prelude::*; use storage::testing::arb_account_storage_key_no_vp; @@ -343,13 +361,17 @@ mod tests { let vp_owner: Address = (&public_key).into(); let source = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from_uint( - 10_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &source, &token]); + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); // Credit the tokens to the source before running the transaction to be // able to transfer from it @@ -394,11 +416,7 @@ mod tests { // Init PoS genesis let pos_params = PosParams::default(); let validator = address::testing::established_address_3(); - let initial_stake = token::Amount::from_uint( - 10_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); + let initial_stake = token::Amount::from_uint(10_098_123, 0).unwrap(); let consensus_key = key::testing::keypair_2().ref_to(); let commission_rate = rust_decimal::Decimal::new(5, 2); let max_commission_rate_change = rust_decimal::Decimal::new(1, 2); @@ -421,21 +439,9 @@ mod tests { let vp_owner: Address = (&public_key).into(); let target = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from_uint( - 10_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); - let bond_amount = token::Amount::from_uint( - 5_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); - let unbond_amount = token::Amount::from_uint( - 3_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); + let bond_amount = token::Amount::from_uint(5_098_123, 0).unwrap(); + let unbond_amount = token::Amount::from_uint(3_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&target, &token]); @@ -443,7 +449,14 @@ mod tests { // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); - + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { // Bond the tokens, then unbond some of them @@ -474,11 +487,7 @@ mod tests { // Init PoS genesis let pos_params = PosParams::default(); let validator = address::testing::established_address_3(); - let initial_stake = token::Amount::from_uint( - 10_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); + let initial_stake = token::Amount::from_uint(10_098_123, 0).unwrap(); let consensus_key = key::testing::keypair_2().ref_to(); let commission_rate = rust_decimal::Decimal::new(5, 2); let max_commission_rate_change = rust_decimal::Decimal::new(1, 2); @@ -501,21 +510,9 @@ mod tests { let vp_owner: Address = (&public_key).into(); let target = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from_uint( - 10_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); - let bond_amount = token::Amount::from_uint( - 5_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); - let unbond_amount = token::Amount::from_uint( - 3_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); + let bond_amount = token::Amount::from_uint(5_098_123, 0).unwrap(); + let unbond_amount = token::Amount::from_uint(3_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&target, &token]); @@ -523,6 +520,14 @@ mod tests { // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); tx_env.write_public_key(&vp_owner, &public_key); @@ -563,11 +568,7 @@ mod tests { let vp_owner: Address = (&public_key).into(); let target = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from_uint( - 10_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &target, &token]); @@ -575,6 +576,14 @@ mod tests { // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); let amount = token::DenominatedAmount { amount, @@ -620,11 +629,7 @@ mod tests { let vp_owner: Address = (&public_key).into(); let target = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from_uint( - 10_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &target, &token]); @@ -632,6 +637,14 @@ mod tests { // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); tx_env.write_public_key(&vp_owner, &public_key); @@ -682,11 +695,7 @@ mod tests { let source = address::testing::established_address_2(); let target = address::testing::established_address_3(); let token = address::nam(); - let amount = token::Amount::from_uint( - 10_098_123, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .unwrap(); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &source, &target, &token]); @@ -694,6 +703,14 @@ mod tests { // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it tx_env.credit_tokens(&source, &token, None, amount); + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); let amount = token::DenominatedAmount { amount, diff --git a/wasm/wasm_source/src/vp_testnet_faucet.rs b/wasm/wasm_source/src/vp_testnet_faucet.rs index 61a80d681e..b116be4ad2 100644 --- a/wasm/wasm_source/src/vp_testnet_faucet.rs +++ b/wasm/wasm_source/src/vp_testnet_faucet.rs @@ -49,7 +49,8 @@ fn validate_tx( } for key in keys_changed.iter() { - let is_valid = if let Some(owner) = token::is_any_token_balance_key(key) + let is_valid = if let Some([_, owner]) = + token::is_any_token_balance_key(key) { if owner == &addr { let pre: token::Amount = ctx.read_pre(key)?.unwrap_or_default(); @@ -353,6 +354,8 @@ mod tests { // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); + // write the denomination of NAM into storage + storage_api::token::write_denom(&mut tx_env.wl_storage, &token, None, token::NATIVE_MAX_DECIMAL_PLACES.into()).unwrap(); tx_env.commit_genesis(); // Construct a PoW solution like a client would diff --git a/wasm/wasm_source/src/vp_user.rs b/wasm/wasm_source/src/vp_user.rs index a44d99ddaa..f7c59f2dc8 100644 --- a/wasm/wasm_source/src/vp_user.rs +++ b/wasm/wasm_source/src/vp_user.rs @@ -9,12 +9,16 @@ //! Any other storage key changes are allowed only with a valid signature. use namada_vp_prelude::address::masp; -use namada_vp_prelude::storage::KeySeg; +use namada_vp_prelude::storage::{Key, KeySeg}; use namada_vp_prelude::*; use once_cell::unsync::Lazy; enum KeyType<'a> { - Token(&'a Address), + Token { + token: &'a Address, + sub_prefix: Option, + owner: &'a Address, + }, PoS, Vp(&'a Address), Masp, @@ -24,12 +28,20 @@ enum KeyType<'a> { impl<'a> From<&'a storage::Key> for KeyType<'a> { fn from(key: &'a storage::Key) -> KeyType<'a> { - if let Some(address) = token::is_any_token_balance_key(key) { - Self::Token(address) - } else if let Some((_, address)) = + if let Some([token, owner]) = token::is_any_token_balance_key(key) { + Self::Token { + token, + owner, + sub_prefix: None, + } + } else if let Some((sub, [token, owner])) = token::is_any_multitoken_balance_key(key) { - Self::Token(address) + Self::Token { + token, + owner, + sub_prefix: Some(sub), + } } else if proof_of_stake::is_pos_key(key) { Self::PoS } else if gov_storage::is_vote_key(key) { @@ -90,7 +102,11 @@ fn validate_tx( for key in keys_changed.iter() { let key_type: KeyType = key.into(); let is_valid = match key_type { - KeyType::Token(owner) => { + KeyType::Token { + token, + owner, + sub_prefix, + } => { if owner == &addr { let pre: token::Amount = ctx.read_pre(key)?.unwrap_or_default(); @@ -101,7 +117,7 @@ fn validate_tx( let valid = change.non_negative() || addr == masp() || *valid_sig; let denom_amount = token::Amount::from(change) - .denominated(owner, &ctx.pre()) + .denominated(token, sub_prefix.as_ref(), &ctx.pre()) .unwrap(); debug_log!( "token key: {}, change: {}, valid_sig: {}, valid \ diff --git a/wasm/wasm_source/src/vp_validator.rs b/wasm/wasm_source/src/vp_validator.rs index 18aa679fb3..9f9a152159 100644 --- a/wasm/wasm_source/src/vp_validator.rs +++ b/wasm/wasm_source/src/vp_validator.rs @@ -12,12 +12,16 @@ //! //! Any other storage key changes are allowed only with a valid signature. -use namada_vp_prelude::storage::KeySeg; +use namada_vp_prelude::storage::{Key, KeySeg}; use namada_vp_prelude::*; use once_cell::unsync::Lazy; enum KeyType<'a> { - Token(&'a Address), + Token { + token: &'a Address, + sub_prefix: Option, + owner: &'a Address, + }, PoS, Vp(&'a Address), GovernanceVote(&'a Address), @@ -26,12 +30,20 @@ enum KeyType<'a> { impl<'a> From<&'a storage::Key> for KeyType<'a> { fn from(key: &'a storage::Key) -> KeyType<'a> { - if let Some(address) = token::is_any_token_balance_key(key) { - Self::Token(address) - } else if let Some((_, address)) = + if let Some([token, owner]) = token::is_any_token_balance_key(key) { + Self::Token { + token, + owner, + sub_prefix: None, + } + } else if let Some((sub, [token, owner])) = token::is_any_multitoken_balance_key(key) { - Self::Token(address) + Self::Token { + token, + owner, + sub_prefix: Some(sub), + } } else if proof_of_stake::is_pos_key(key) { Self::PoS } else if gov_storage::is_vote_key(key) { @@ -90,7 +102,11 @@ fn validate_tx( for key in keys_changed.iter() { let key_type: KeyType = key.into(); let is_valid = match key_type { - KeyType::Token(owner) => { + KeyType::Token { + token, + owner, + sub_prefix, + } => { if owner == &addr { let pre: token::Amount = ctx.read_pre(key)?.unwrap_or_default(); @@ -100,7 +116,7 @@ fn validate_tx( // debit has to signed, credit doesn't let valid = change.non_negative() || *valid_sig; let amount = token::Amount::from(change) - .denominated(owner, &ctx.pre()) + .denominated(token, sub_prefix.as_ref(), &ctx.pre()) .unwrap(); debug_log!( "token key: {}, change: {}, valid_sig: {}, valid \ From 5d10c6c6d67429bc54879d92f5cba1856f952a7d Mon Sep 17 00:00:00 2001 From: satan Date: Mon, 17 Apr 2023 15:13:03 +0200 Subject: [PATCH 020/151] [fix]: Fixed wasm tests --- core/src/types/uint.rs | 22 ++++++++++++++- wasm/wasm_source/src/vp_user.rs | 40 +++++++++++++++++++++++++++ wasm/wasm_source/src/vp_validator.rs | 41 ++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 1 deletion(-) diff --git a/core/src/types/uint.rs b/core/src/types/uint.rs index fb6e72376c..e91c497fba 100644 --- a/core/src/types/uint.rs +++ b/core/src/types/uint.rs @@ -165,11 +165,16 @@ impl PartialOrd for SignedUint { match (self.non_negative(), other.non_negative()) { (true, false) => Some(Ordering::Greater), (false, true) => Some(Ordering::Less), - _ => { + (true, true) => { let this = self.abs(); let that = other.abs(); this.0.partial_cmp(&that.0) } + (false, false) => { + let this = self.abs(); + let that = other.abs(); + that.0.partial_cmp(&this.0) + } } } } @@ -359,4 +364,19 @@ mod test_uint { assert_eq!(neg_eight - two, -ten); assert!((two - two).is_zero()); } + + /// Test that ordering is correctly implemented + #[test] + fn test_ord() { + let this = Amount::from_uint(1, 0).unwrap().change(); + let that = Amount::native_whole(1000).change(); + assert!(this <= that); + assert!(-this <= that); + assert!(-this >= -that); + assert!(this >= -that); + assert!(that >= this); + assert!(that >= -this); + assert!(-that <= -this); + assert!(-that <= this); + } } diff --git a/wasm/wasm_source/src/vp_user.rs b/wasm/wasm_source/src/vp_user.rs index f7c59f2dc8..d76792521f 100644 --- a/wasm/wasm_source/src/vp_user.rs +++ b/wasm/wasm_source/src/vp_user.rs @@ -262,6 +262,14 @@ mod tests { // Credit the tokens to the source before running the transaction to be // able to transfer from it tx_env.credit_tokens(&source, &token, None, amount); + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); let amount = token::DenominatedAmount { amount, @@ -308,6 +316,14 @@ mod tests { // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &target, &token]); + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it @@ -364,6 +380,14 @@ mod tests { // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); tx_env.write_public_key(&vp_owner, &public_key); @@ -439,6 +463,14 @@ mod tests { // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&target, &token]); + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it @@ -503,6 +535,14 @@ mod tests { // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&target, &token]); + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it diff --git a/wasm/wasm_source/src/vp_validator.rs b/wasm/wasm_source/src/vp_validator.rs index 9f9a152159..271b87e8a1 100644 --- a/wasm/wasm_source/src/vp_validator.rs +++ b/wasm/wasm_source/src/vp_validator.rs @@ -270,6 +270,15 @@ mod tests { // Credit the tokens to the source before running the transaction to be // able to transfer from it tx_env.credit_tokens(&source, &token, None, amount); + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); + let amount = token::DenominatedAmount { amount, denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), @@ -323,6 +332,14 @@ mod tests { amount, denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), }; + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -371,6 +388,14 @@ mod tests { // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); tx_env.write_public_key(&vp_owner, &public_key); let amount = token::DenominatedAmount { @@ -449,6 +474,14 @@ mod tests { // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { @@ -519,6 +552,14 @@ mod tests { // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); tx_env.write_public_key(&vp_owner, &public_key); From a8d9b72536e2b372492c2085b21b3acf4426335c Mon Sep 17 00:00:00 2001 From: satan Date: Fri, 21 Apr 2023 12:00:11 +0200 Subject: [PATCH 021/151] [feat]: Added multitoken support to MASP --- apps/src/lib/cli.rs | 9 + apps/src/lib/client/rpc.rs | 253 ++++++++++++-------- apps/src/lib/client/tx.rs | 60 +++-- core/src/ledger/storage/masp_conversions.rs | 57 +++-- core/src/types/address.rs | 18 +- shared/src/ledger/queries/shell.rs | 6 +- tests/src/e2e/ledger_tests.rs | 47 ++-- wasm/checksums.json | 36 +-- wasm/wasm_source/src/vp_masp.rs | 5 +- 9 files changed, 305 insertions(+), 186 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 08816f6d5a..6cfb99269d 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -2778,6 +2778,8 @@ pub mod args { pub owner: Option, /// Address of a token pub token: Option, + /// sub-prefix if querying a multi-token + pub sub_prefix: Option, } impl Args for QueryTransfers { @@ -2785,10 +2787,12 @@ pub mod args { let query = Query::parse(matches); let owner = BALANCE_OWNER.parse(matches); let token = TOKEN_OPT.parse(matches); + let sub_prefix = SUB_PREFIX.parse(matches); Self { query, owner, token, + sub_prefix, } } @@ -2800,6 +2804,11 @@ pub mod args { .arg(TOKEN_OPT.def().about( "The token address that queried transfers must involve.", )) + .arg( + SUB_PREFIX.def().about( + "The token's sub prefix whose balance to query.", + ), + ) } } diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 7c2212fd86..1fd77f109c 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -257,7 +257,11 @@ pub async fn query_tx_deltas( denom.denominate(&transfer.amount); if denominated != 0 { let tfer_delta = Amount::from_nonnegative( - (transfer.token.clone(), denom), + ( + transfer.token.clone(), + transfer.sub_prefix.clone(), + denom, + ), denominated, ) .expect("invalid value for amount"); @@ -291,6 +295,7 @@ pub async fn query_tx_deltas( /// Query the specified accepted transfers from the ledger pub async fn query_transfers(mut ctx: Context, args: args::QueryTransfers) { let query_token = args.token.as_ref().map(|x| ctx.get(x)); + let sub_prefix = args.sub_prefix.map(|s| Key::parse(s).unwrap()); let query_owner = args.owner.as_ref().map(|x| ctx.get_cached(x)); // Obtain the effects of all shielded and transparent transactions let transfers = query_tx_deltas( @@ -340,19 +345,21 @@ pub async fn query_transfers(mut ctx: Context, args: args::QueryTransfers) { shielded_accounts.insert(acc, dec); } // Check if this transfer pertains to the supplied token - relevant &= match &query_token { - Some(token) => { - tfer_delta - .values() - .zip(MaspDenom::iter()) - .any(|(x, denom)| x[&(token.clone(), denom)] != 0) - || shielded_accounts - .values() - .zip(MaspDenom::iter()) - .any(|(x, denom)| x[&(token.clone(), denom)] != 0) - } - None => true, - }; + relevant &= + match &query_token { + Some(token) => { + tfer_delta.values().zip(MaspDenom::iter()).any( + |(x, denom)| { + x[&(token.clone(), sub_prefix.clone(), denom)] != 0 + }, + ) || shielded_accounts.values().zip(MaspDenom::iter()).any( + |(x, denom)| { + x[&(token.clone(), sub_prefix.clone(), denom)] != 0 + }, + ) + } + None => true, + }; // Filter out those entries that do not satisfy user query if !relevant { continue; @@ -362,7 +369,7 @@ pub async fn query_transfers(mut ctx: Context, args: args::QueryTransfers) { for (account, amt) in tfer_delta { if account != masp() { print!(" {}:", account); - for ((addr, denom), val) in amt.components() { + for ((addr, sub_prefix, denom), val) in amt.components() { let token_alias = lookup_alias(&ctx, addr); let sign = match val.cmp(&0) { Ordering::Greater => "+", @@ -370,14 +377,12 @@ pub async fn query_transfers(mut ctx: Context, args: args::QueryTransfers) { Ordering::Equal => "", }; print!( - " {}{} {}", + " {}{} {}{}", sign, format_denominated_amount( &client, addr, - // TODO: apparently MASP doesn't support - // multi-tokens? - &None, + sub_prefix, token::Amount::from_masp_denominated( val.unsigned_abs(), *denom @@ -385,6 +390,10 @@ pub async fn query_transfers(mut ctx: Context, args: args::QueryTransfers) { ) .await, token_alias, + sub_prefix + .as_ref() + .map(|k| format!("/{}", k)) + .unwrap_or_default(), ); } println!(); @@ -395,7 +404,7 @@ pub async fn query_transfers(mut ctx: Context, args: args::QueryTransfers) { for (account, amt) in shielded_accounts { if fvk_map.contains_key(&account) { print!(" {}:", fvk_map[&account]); - for ((addr, denom), val) in amt.components() { + for ((addr, sub_prefix, denom), val) in amt.components() { let token_alias = lookup_alias(&ctx, addr); let sign = match val.cmp(&0) { Ordering::Greater => "+", @@ -403,19 +412,23 @@ pub async fn query_transfers(mut ctx: Context, args: args::QueryTransfers) { Ordering::Equal => "", }; print!( - " {}{} {}", + " {}{} {}{}", sign, format_denominated_amount( &client, addr, - &None, + sub_prefix, token::Amount::from_masp_denominated( val.unsigned_abs(), *denom ) ) .await, - token_alias + token_alias, + sub_prefix + .as_ref() + .map(|k| format!("/{}", k)) + .unwrap_or_default() ); } println!(); @@ -670,22 +683,27 @@ pub async fn query_pinned_balance(ctx: &mut Context, args: args::QueryBalance) { .await } // Now print out the received quantities according to CLI arguments - match (balance, args.token.as_ref()) { - (Err(PinnedBalanceError::InvalidViewingKey), _) => println!( + match (balance, args.token.as_ref(), args.sub_prefix.as_ref()) { + (Err(PinnedBalanceError::InvalidViewingKey), _, _) => println!( "Supplied viewing key cannot decode transactions to given \ payment address." ), - (Err(PinnedBalanceError::NoTransactionPinned), _) => { + (Err(PinnedBalanceError::NoTransactionPinned), _, _) => { println!("Payment address {} has not yet been consumed.", owner) } - (Ok((balance, epoch)), Some(token)) => { + (Ok((balance, epoch)), Some(token), sub_prefix) => { let token = ctx.get(token); let token_alias = lookup_alias(ctx, &token); let mut total_balance = token::Amount::default(); for denom in MaspDenom::iter() { // Extract and print only the specified token from the total - let (_asset_type, value) = - value_by_address(&balance, token.clone(), denom, epoch); + let (_asset_type, value) = value_by_address( + &balance, + token.clone(), + sub_prefix, + denom, + epoch, + ); total_balance += token::Amount::from_masp_denominated( value as u64, denom, @@ -694,33 +712,43 @@ pub async fn query_pinned_balance(ctx: &mut Context, args: args::QueryBalance) { if total_balance.is_zero() { println!( "Payment address {} was consumed during epoch {}. \ - Received no shielded {}", - owner, epoch, token_alias + Received no shielded {}{}", + owner, + epoch, + token_alias, + sub_prefix + .map(|k| format!("/{}", k)) + .unwrap_or_default() ); } else { let formatted = format_denominated_amount( &client, &token, - // TODO: Is this correct? - &None, + &sub_prefix.map(|k| Key::parse(k).unwrap()), total_balance, ) .await; println!( "Payment address {} was consumed during epoch {}. \ - Received {} {}", - owner, epoch, formatted, token_alias + Received {} {}{}", + owner, + epoch, + formatted, + token_alias, + sub_prefix + .map(|k| format!("/{}", k)) + .unwrap_or_default() ); } } - (Ok((balance, epoch)), None) => { + (Ok((balance, epoch)), None, _) => { let mut found_any = false; // Print balances by human-readable token names let balance = ctx .shielded .decode_amount(client.clone(), balance, epoch) .await; - for ((addr, denom), value) in balance.components() { + for ((addr, sub_prefix, denom), value) in balance.components() { let asset_value = token::Amount::from_masp_denominated( *value as u64, *denom, @@ -736,17 +764,20 @@ pub async fn query_pinned_balance(ctx: &mut Context, args: args::QueryBalance) { let formatted = format_denominated_amount( &client, addr, - // TODO: Is this correct? - &None, + sub_prefix, asset_value, ) .await; println!( - " {}: {}", + " {}{}: {}", tokens .get(addr) .cloned() .unwrap_or_else(|| addr.clone()), + sub_prefix + .as_ref() + .map(|k| format!("/{}", k)) + .unwrap_or_default(), formatted, ); } @@ -966,17 +997,12 @@ pub async fn query_proposal(_ctx: Context, args: args::QueryProposal) { pub fn value_by_address( amt: &masp_primitives::transaction::components::Amount, token: Address, + sub_prefix: Option<&String>, denom: MaspDenom, epoch: Epoch, ) -> (AssetType, i64) { // Compute the unique asset identifier from the token address - let asset_type = AssetType::new( - (token, denom, epoch.0) - .try_to_vec() - .expect("token addresses should serialize") - .as_ref(), - ) - .unwrap(); + let asset_type = make_asset_type(epoch, &token, &sub_prefix, denom); (asset_type, amt[&asset_type]) } @@ -1039,13 +1065,8 @@ pub async fn query_shielded_balance( .expect("context should contain viewing key") }; // Compute the unique asset identifier from the token address - let asset_type = AssetType::new( - (token.clone(), denom, epoch.0) - .try_to_vec() - .expect("token addresses should serialize") - .as_ref(), - ) - .unwrap(); + let asset_type = + make_asset_type(epoch, &token, &args.sub_prefix, denom); total_balance += token::Amount::from_masp_denominated( balance[&asset_type] as u64, denom, @@ -1055,18 +1076,25 @@ pub async fn query_shielded_balance( let token_alias = lookup_alias(ctx, &token); if total_balance.is_zero() { println!( - "No shielded {} balance found for given key", - token_alias + "No shielded {}{} balance found for given key", + token_alias, + args.sub_prefix + .as_ref() + .map(|k| format!("/{}", k)) + .unwrap_or_default(), ); } else { println!( - "{}: {}", + "{}{}: {}", token_alias, + args.sub_prefix + .as_ref() + .map(|k| format!("/{}", k)) + .unwrap_or_default(), format_denominated_amount( &client, &token, - // TODO: Is this correct? - &None, + &args.sub_prefix.map(|k| Key::parse(k).unwrap()), total_balance ) .await @@ -1112,16 +1140,20 @@ pub async fn query_shielded_balance( .decode_asset_type(client.clone(), asset_type) .await; match decoded { - Some((addr, denom, asset_epoch)) + Some((addr, sub_prefix, denom, asset_epoch)) if asset_epoch == epoch => { // Only assets with the current timestamp count println!( - "Shielded Token {}:", + "Shielded Token {}{}:", tokens .get(&addr) .cloned() - .unwrap_or_else(|| addr.clone()) + .unwrap_or_else(|| addr.clone()), + sub_prefix + .as_ref() + .map(|k| format!("/{}", k)) + .unwrap_or_default(), ); read_tokens.insert(addr.clone()); let mut found_any = false; @@ -1131,9 +1163,10 @@ pub async fn query_shielded_balance( denom, ); let formatted = format_denominated_amount( - &client, &addr, - // TODO: Is this correct? - &None, value, + &client, + &addr, + &sub_prefix, + value, ) .await; println!(" {}, owned by {}", formatted, fvk); @@ -1169,17 +1202,19 @@ pub async fn query_shielded_balance( let token = ctx.get(&token); let mut found_any = false; let token_alias = lookup_alias(ctx, &token); - println!("Shielded Token {}:", token_alias); + println!( + "Shielded Token {}{}:", + token_alias, + args.sub_prefix + .as_ref() + .map(|k| format!("/{}", k)) + .unwrap_or_default() + ); for fvk in viewing_keys { let mut balance = token::Amount::default(); for denom in MaspDenom::iter() { - let asset_type = AssetType::new( - (token.clone(), denom, epoch.0) - .try_to_vec() - .expect("token addresses should serialize") - .as_ref(), - ) - .unwrap(); + let asset_type = + make_asset_type(epoch, &token, &args.sub_prefix, denom); // Query the multi-asset balance at the given spending key let viewing_key = ExtendedFullViewingKey::from(fvk).fvk.vk; let denom_balance = if no_conversions { @@ -1205,8 +1240,10 @@ pub async fn query_shielded_balance( } } let formatted = format_denominated_amount( - &client, &token, // TODO: Is this correct? - &None, balance, + &client, + &token, + &args.sub_prefix.as_ref().map(|k| Key::parse(k).unwrap()), + balance, ) .await; println!(" {}, owned by {}", formatted, fvk); @@ -1263,26 +1300,27 @@ pub async fn print_decoded_balance( decoded_balance: MaspDenominatedAmount, ) { let mut balances = HashMap::new(); - for ((addr, denom), value) in decoded_balance.components() { + for ((addr, sub_prefix, denom), value) in decoded_balance.components() { let asset_value = token::Amount::from_masp_denominated(*value as u64, *denom); balances - .entry(addr) + .entry((addr, sub_prefix)) .and_modify(|val| *val += asset_value) .or_insert(asset_value); } if balances.is_empty() { println!("No shielded balance found for given key"); } else { - for (addr, amount) in balances { + for ((addr, sub_prefix), amount) in balances { println!( - "{} : {}", + "{}{} : {}", lookup_alias(ctx, addr), - format_denominated_amount( - client, addr, // TODO: Is this correct? - &None, amount, - ) - .await, + sub_prefix + .as_ref() + .map(|k| format!("/{}", k)) + .unwrap_or_default(), + format_denominated_amount(client, addr, sub_prefix, amount,) + .await, ); } } @@ -1291,31 +1329,34 @@ pub async fn print_decoded_balance( pub async fn print_decoded_balance_with_epoch( ctx: &mut Context, client: &HttpClient, - decoded_balance: Amount<(Address, MaspDenom, Epoch)>, + decoded_balance: Amount<(Address, Option, MaspDenom, Epoch)>, ) { let tokens = ctx.tokens(); let mut balances = HashMap::new(); - for ((addr, denom, epoch), value) in decoded_balance.components() { + for ((addr, sub_prefix, denom, epoch), value) in + decoded_balance.components() + { let asset_value = token::Amount::from_masp_denominated(*value as u64, *denom); balances - .entry((addr, epoch)) + .entry((addr, sub_prefix, epoch)) .and_modify(|val| *val += asset_value) .or_insert(asset_value); } if balances.is_empty() { println!("No shielded balance found for given key"); } else { - for ((addr, epoch), amount) in balances { + for ((addr, sub_prefix, epoch), amount) in balances { println!( - "{} | {} : {}", + "{}{} | {} : {}", tokens.get(addr).cloned().unwrap_or_else(|| addr.clone()), + sub_prefix + .as_ref() + .map(|k| format!("/{}", k)) + .unwrap_or_default(), epoch, - format_denominated_amount( - client, addr, // TODO: Is this correct? - &None, amount, - ) - .await, + format_denominated_amount(client, addr, sub_prefix, amount,) + .await, ); } } @@ -2082,7 +2123,7 @@ pub async fn query_conversions(ctx: Context, args: args::QueryConversions) { .expect("Conversions should be defined"); // Track whether any non-sentinel conversions are found let mut conversions_found = false; - for ((addr, _), epoch, conv, _) in conv_state.assets.values() { + for ((addr, sub, _), epoch, conv, _) in conv_state.assets.values() { let amt: masp_primitives::transaction::components::Amount = conv.clone().into(); // If the user has specified any targets, then meet them @@ -2096,8 +2137,9 @@ pub async fn query_conversions(ctx: Context, args: args::QueryConversions) { conversions_found = true; // Print the asset to which the conversion applies print!( - "{}[{}]: ", + "{}{}[{}]: ", tokens.get(addr).cloned().unwrap_or_else(|| addr.clone()), + sub.as_ref().map(|k| format!("/{}", k)).unwrap_or_default(), epoch, ); // Now print out the components of the allowed conversion @@ -2105,13 +2147,14 @@ pub async fn query_conversions(ctx: Context, args: args::QueryConversions) { for (asset_type, val) in amt.components() { // Look up the address and epoch of asset to facilitate pretty // printing - let ((addr, _), epoch, _, _) = &conv_state.assets[asset_type]; + let ((addr, sub, _), epoch, _, _) = &conv_state.assets[asset_type]; // Now print out this component of the conversion print!( - "{}{} {}[{}]", + "{}{} {}{}[{}]", prefix, val, tokens.get(addr).cloned().unwrap_or_else(|| addr.clone()), + sub.as_ref().map(|k| format!("/{}", k)).unwrap_or_default(), epoch ); // Future iterations need to be prefixed with + @@ -2131,6 +2174,7 @@ pub async fn query_conversion( asset_type: AssetType, ) -> Option<( Address, + Option, MaspDenom, Epoch, masp_primitives::transaction::components::Amount, @@ -2845,13 +2889,22 @@ pub(super) async fn format_denominated_amount( } /// Make asset type corresponding to given address and epoch -pub fn make_asset_type( +pub fn make_asset_type( epoch: Epoch, token: &Address, + sub_prefix: &Option, denom: MaspDenom, ) -> AssetType { // Typestamp the chosen token with the current epoch - let token_bytes = (token, denom, epoch.0) + let token_bytes = ( + token, + sub_prefix + .as_ref() + .map(|k| k.to_string()) + .unwrap_or_default(), + denom, + epoch.0, + ) .try_to_vec() .expect("token should serialize"); // Generate the unique asset identifier from the unique token address diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 3470e3b5c7..2ce1771afd 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -512,10 +512,11 @@ pub type Conversions = HashMap, i64)>; /// Represents an amount that is -pub type MaspDenominatedAmount = Amount<(Address, MaspDenom)>; +pub type MaspDenominatedAmount = Amount<(Address, Option, MaspDenom)>; /// Represents the changes that were made to a list of transparent accounts -pub type TransferDelta = HashMap>; +pub type TransferDelta = + HashMap, MaspDenom)>>; /// Represents the changes that were made to a list of shielded accounts pub type TransactionDelta = HashMap; @@ -551,7 +552,7 @@ pub struct ShieldedContext { /// The set of note positions that have been spent spents: HashSet, /// Maps asset types to their decodings - asset_types: HashMap, + asset_types: HashMap, MaspDenom, Epoch)>, /// Maps note positions to their corresponding viewing keys vk_map: HashMap, } @@ -881,7 +882,7 @@ impl ShieldedContext { let mut transfer_delta = TransferDelta::new(); for denom in MaspDenom::iter() { let transparent_delta = Amount::from_nonnegative( - (tx.token.clone(), denom), + (tx.token.clone(), tx.sub_prefix.clone(), denom), denom.denominate(&tx.amount.amount), ) .expect("invalid value for amount"); @@ -943,22 +944,23 @@ impl ShieldedContext { &mut self, client: HttpClient, asset_type: AssetType, - ) -> Option<(Address, MaspDenom, Epoch)> { + ) -> Option<(Address, Option, MaspDenom, Epoch)> { // 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, sub_prefix, denom, ep, _conv, _path): ( Address, + Option, MaspDenom, _, Amount, MerklePath, ) = query_conversion(client, asset_type).await?; self.asset_types - .insert(asset_type, (addr.clone(), denom, ep)); - Some((addr, denom, ep)) + .insert(asset_type, (addr.clone(), sub_prefix.clone(), denom, ep)); + Some((addr, sub_prefix, denom, ep)) } /// Query the ledger for the conversion that is allowed for the given asset @@ -973,9 +975,16 @@ impl ShieldedContext { Entry::Occupied(conv_entry) => Some(conv_entry.into_mut()), Entry::Vacant(conv_entry) => { // Query for the ID of the last accepted transaction - let (addr, denom, ep, conv, path): (Address, _, _, _, _) = - query_conversion(client, asset_type).await?; - self.asset_types.insert(asset_type, (addr, denom, ep)); + let (addr, sub_prefix, denom, ep, conv, path): ( + Address, + _, + _, + _, + _, + _, + ) = query_conversion(client, asset_type).await?; + self.asset_types + .insert(asset_type, (addr, sub_prefix, denom, ep)); // If the conversion is 0, then we just have a pure decoding if conv == Amount::zero() { None @@ -1071,8 +1080,8 @@ impl ShieldedContext { let target_asset_type = self .decode_asset_type(client.clone(), asset_type) .await - .map(|(addr, denom, _epoch)| { - make_asset_type(target_epoch, &addr, denom) + .map(|(addr, sub, denom, _epoch)| { + make_asset_type(target_epoch, &addr, &sub, denom) }) .unwrap_or(asset_type); let at_target_asset_type = asset_type == target_asset_type; @@ -1317,8 +1326,8 @@ impl ShieldedContext { self.decode_asset_type(client.clone(), *asset_type).await; // Only assets with the target timestamp count match decoded { - Some((addr, denom, epoch)) if epoch == target_epoch => { - res += &Amount::from_pair((addr, denom), *val).unwrap() + Some((addr, sub, denom, epoch)) if epoch == target_epoch => { + res += &Amount::from_pair((addr, sub, denom), *val).unwrap() } _ => {} } @@ -1332,15 +1341,16 @@ impl ShieldedContext { &mut self, client: HttpClient, amt: Amount, - ) -> Amount<(Address, MaspDenom, Epoch)> { + ) -> Amount<(Address, Option, MaspDenom, Epoch)> { let mut res = Amount::zero(); for (asset_type, val) in amt.components() { // Decode the asset type let decoded = self.decode_asset_type(client.clone(), *asset_type).await; // Only assets with the target timestamp count - if let Some((addr, denom, epoch)) = decoded { - res += &Amount::from_pair((addr, denom, epoch), *val).unwrap() + if let Some((addr, sub, denom, epoch)) = decoded { + res += + &Amount::from_pair((addr, sub, denom, epoch), *val).unwrap() } } res @@ -1351,10 +1361,11 @@ impl ShieldedContext { fn convert_amount( epoch: Epoch, token: &Address, + sub_prefix: &Option<&String>, val: u64, denom: MaspDenom, ) -> (AssetType, Amount) { - let asset_type = make_asset_type(epoch, token, denom); + let asset_type = make_asset_type(epoch, token, sub_prefix, denom); // Combine the value and unit into one amount let amount = Amount::from_nonnegative(asset_type, val) .expect("invalid value for amount"); @@ -1424,6 +1435,7 @@ async fn gen_shielded_transfer( let (_, fee) = convert_amount( epoch, &ctx.get(&args.tx.fee_token), + &args.sub_prefix.as_ref(), MaspDenom::Zero.denominate(&fee.amount), MaspDenom::Zero, ); @@ -1446,8 +1458,13 @@ async fn gen_shielded_transfer( continue; } // Convert transaction amount into MASP types - let (asset_type, amount) = - convert_amount(epoch, &ctx.get(&args.token), denom_amount, denom); + let (asset_type, amount) = convert_amount( + epoch, + &ctx.get(&args.token), + &args.sub_prefix.as_ref(), + denom_amount, + denom, + ); // If there are shielded inputs if let Some(sk) = spending_key { @@ -1546,7 +1563,6 @@ async fn gen_shielded_transfer( } } // Build and return the constructed transaction - // Build and return the constructed transaction builder .build(consensus_branch_id, &prover) .map(|(a, b)| Some((a, b, epoch))) diff --git a/core/src/ledger/storage/masp_conversions.rs b/core/src/ledger/storage/masp_conversions.rs index 903a4088ac..73f11e0520 100644 --- a/core/src/ledger/storage/masp_conversions.rs +++ b/core/src/ledger/storage/masp_conversions.rs @@ -9,7 +9,7 @@ use masp_primitives::merkle_tree::FrozenCommitmentTree; use masp_primitives::sapling::Node; use crate::types::address::Address; -use crate::types::storage::Epoch; +use crate::types::storage::{Epoch, Key}; use crate::types::token::MaspDenom; /// A representation of the conversion state @@ -20,9 +20,15 @@ pub struct ConversionState { /// The tree currently containing all the conversions pub tree: FrozenCommitmentTree, /// Map assets to their latest conversion and position in Merkle tree + #[allow(clippy::type_complexity)] pub assets: BTreeMap< AssetType, - ((Address, MaspDenom), Epoch, AllowedConversion, usize), + ( + (Address, Option, MaspDenom), + Epoch, + AllowedConversion, + usize, + ), >, } @@ -59,20 +65,29 @@ where // with the zeroth epoch to minimize the number of convert notes clients // have to use. This trick works under the assumption that reward tokens // from different epochs are exactly equivalent. - let reward_asset_bytes = (address::nam(), MaspDenom::Zero, 0u64) - .try_to_vec() - .expect("unable to serialize address and epoch"); + let reward_asset_bytes = + (address::nam(), None::, MaspDenom::Zero, 0u64) + .try_to_vec() + .expect("unable to serialize address and epoch"); let reward_asset = AssetType::new(reward_asset_bytes.as_ref()) .expect("unable to derive asset identifier"); // Conversions from the previous to current asset for each address let mut current_convs = - BTreeMap::<(Address, MaspDenom), AllowedConversion>::new(); + BTreeMap::<(Address, Option, MaspDenom), AllowedConversion>::new(); // Reward all tokens according to above reward rates - for ((addr, denom), reward) in &masp_rewards { + for ((addr, sub_prefix, denom), reward) in &masp_rewards { // Dispense a transparent reward in parallel to the shielded rewards - let addr_bal: token::Amount = wl_storage - .read(&token::balance_key(addr, &masp_addr))? - .unwrap_or_default(); + let addr_bal: token::Amount = match sub_prefix { + None => wl_storage + .read(&token::balance_key(addr, &masp_addr))? + .unwrap_or_default(), + Some(sub) => wl_storage + .read(&token::multitoken_balance_key( + &token::multitoken_balance_prefix(addr, sub), + &masp_addr, + ))? + .unwrap_or_default(), + }; // The reward for each reward.1 units of the current asset is // reward.0 units of the reward token // Since floor(a) + floor(b) <= floor(a+b), there will always be @@ -83,16 +98,18 @@ where // cancelled out/replaced with the new asset let old_asset = encode_asset_type( addr.clone(), + sub_prefix, *denom, wl_storage.storage.last_epoch, ); let new_asset = encode_asset_type( addr.clone(), + sub_prefix, *denom, wl_storage.storage.block.epoch, ); current_convs.insert( - (addr.clone(), *denom), + (addr.clone(), sub_prefix.clone(), *denom), (MaspAmount::from_pair(old_asset, -(reward.1 as i64)).unwrap() + MaspAmount::from_pair(new_asset, reward.1).unwrap() + MaspAmount::from_pair(reward_asset, reward.0).unwrap()) @@ -102,7 +119,7 @@ where wl_storage.storage.conversion_state.assets.insert( old_asset, ( - (addr.clone(), *denom), + (addr.clone(), sub_prefix.clone(), *denom), wl_storage.storage.last_epoch, MaspAmount::zero().into(), 0, @@ -173,18 +190,19 @@ where // Add purely decoding entries to the assets map. These will be // overwritten before the creation of the next commitment tree - for (addr, denom) in masp_rewards.keys() { + for (addr, sub_prefix, denom) in masp_rewards.keys() { // Add the decoding entry for the new asset type. An uncommited // node position is used since this is not a conversion. let new_asset = encode_asset_type( addr.clone(), + sub_prefix, *denom, wl_storage.storage.block.epoch, ); wl_storage.storage.conversion_state.assets.insert( new_asset, ( - (addr.clone(), *denom), + (addr.clone(), sub_prefix.clone(), *denom), wl_storage.storage.block.epoch, MaspAmount::zero().into(), wl_storage.storage.conversion_state.tree.size(), @@ -211,10 +229,19 @@ where /// Construct MASP asset type with given epoch for given token pub fn encode_asset_type( addr: Address, + sub_prefix: &Option, denom: MaspDenom, epoch: Epoch, ) -> AssetType { - let new_asset_bytes = (addr, denom, epoch.0) + let new_asset_bytes = ( + addr, + sub_prefix + .as_ref() + .map(|k| k.to_string()) + .unwrap_or_default(), + denom, + epoch.0, + ) .try_to_vec() .expect("unable to serialize address and epoch"); AssetType::new(new_asset_bytes.as_ref()) diff --git a/core/src/types/address.rs b/core/src/types/address.rs index 84fe2aac18..eea8692ac3 100644 --- a/core/src/types/address.rs +++ b/core/src/types/address.rs @@ -14,6 +14,7 @@ use thiserror::Error; use crate::types::key; use crate::types::key::PublicKeyHash; +use crate::types::storage::Key; use crate::types::token::{Denomination, MaspDenom}; /// The length of an established [`Address`] encoded with Borsh. @@ -596,15 +597,16 @@ pub fn tokens() -> HashMap { /// Temporary helper for testing, a hash map of tokens addresses with their /// MASP XAN incentive schedules. If the reward is (a, b) then a rewarded tokens /// are dispensed for every b possessed tokens. -pub fn masp_rewards() -> HashMap<(Address, MaspDenom), (u64, u64)> { +pub fn masp_rewards() -> HashMap<(Address, Option, MaspDenom), (u64, u64)> +{ vec![ - ((nam(), MaspDenom::Zero), (0, 100)), - ((btc(), MaspDenom::Zero), (1, 100)), - ((eth(), MaspDenom::Zero), (2, 100)), - ((dot(), MaspDenom::Zero), (3, 100)), - ((schnitzel(), MaspDenom::Zero), (4, 100)), - ((apfel(), MaspDenom::Zero), (5, 100)), - ((kartoffel(), MaspDenom::Zero), (6, 100)), + ((nam(), None, MaspDenom::Zero), (0, 100)), + ((btc(), None, MaspDenom::Zero), (1, 100)), + ((eth(), None, MaspDenom::Zero), (2, 100)), + ((dot(), None, MaspDenom::Zero), (3, 100)), + ((schnitzel(), None, MaspDenom::Zero), (4, 100)), + ((apfel(), None, MaspDenom::Zero), (5, 100)), + ((kartoffel(), None, MaspDenom::Zero), (6, 100)), ] .into_iter() .collect() diff --git a/shared/src/ledger/queries/shell.rs b/shared/src/ledger/queries/shell.rs index 1b28741e97..bfb98e9be7 100644 --- a/shared/src/ledger/queries/shell.rs +++ b/shared/src/ledger/queries/shell.rs @@ -4,7 +4,7 @@ use masp_primitives::merkle_tree::MerklePath; use masp_primitives::sapling::Node; use namada_core::types::address::Address; use namada_core::types::hash::Hash; -use namada_core::types::storage::{BlockHeight, BlockResults}; +use namada_core::types::storage::{BlockHeight, BlockResults, Key}; use namada_core::types::token::MaspDenom; use crate::ledger::events::log::dumb_queries; @@ -21,6 +21,7 @@ use crate::types::transaction::TxResult; type Conversion = ( Address, + Option, MaspDenom, Epoch, masp_primitives::transaction::components::Amount, @@ -142,7 +143,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, sub_prefix, denom), epoch, conv, pos)) = ctx .wl_storage .storage .conversion_state @@ -151,6 +152,7 @@ where { Ok(( addr.clone(), + sub_prefix.clone(), *denom, *epoch, Into::::into( diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 336b968c18..8ec44c5e9f 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -1153,7 +1153,8 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "nam: {}", - ((amt20 * masp_rewards[&(btc(), MaspDenom::Zero)]).0 * (ep1.0 - ep0.0)) + ((amt20 * masp_rewards[&(btc(), None, MaspDenom::Zero)]).0 + * (ep1.0 - ep0.0)) .to_string_native() ))?; client.assert_success(); @@ -1175,7 +1176,8 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "nam: {}", - ((amt20 * masp_rewards[&(btc(), MaspDenom::Zero)]).0 * (ep1.0 - ep0.0)) + ((amt20 * masp_rewards[&(btc(), None, MaspDenom::Zero)]).0 + * (ep1.0 - ep0.0)) .to_string_native() ))?; client.assert_success(); @@ -1218,7 +1220,8 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "nam: {}", - ((amt20 * masp_rewards[&(btc(), MaspDenom::Zero)]).0 * (ep2.0 - ep0.0)) + ((amt20 * masp_rewards[&(btc(), None, MaspDenom::Zero)]).0 + * (ep2.0 - ep0.0)) .to_string_native() ))?; client.assert_success(); @@ -1240,7 +1243,8 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "nam: {}", - ((amt20 * masp_rewards[&(btc(), MaspDenom::Zero)]).0 * (ep2.0 - ep0.0)) + ((amt20 * masp_rewards[&(btc(), None, MaspDenom::Zero)]).0 + * (ep2.0 - ep0.0)) .to_string_native(), ))?; client.assert_success(); @@ -1344,7 +1348,8 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "nam: {}", - ((amt30 * masp_rewards[&(eth(), MaspDenom::Zero)]).0 * (ep4.0 - ep3.0)) + ((amt30 * masp_rewards[&(eth(), None, MaspDenom::Zero)]).0 + * (ep4.0 - ep3.0)) .to_string_native(), ))?; client.assert_success(); @@ -1367,9 +1372,9 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "nam: {}", - (((amt20 * masp_rewards[&(btc(), MaspDenom::Zero)]).0 + (((amt20 * masp_rewards[&(btc(), None, MaspDenom::Zero)]).0 * (ep4.0 - ep0.0)) - + ((amt30 * masp_rewards[&(eth(), MaspDenom::Zero)]).0 + + ((amt30 * masp_rewards[&(eth(), None, MaspDenom::Zero)]).0 * (ep4.0 - ep3.0))) .to_string_native() ))?; @@ -1439,7 +1444,8 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "nam: {}", - ((amt30 * masp_rewards[&(eth(), MaspDenom::Zero)]).0 * (ep.0 - ep3.0)) + ((amt30 * masp_rewards[&(eth(), None, MaspDenom::Zero)]).0 + * (ep.0 - ep3.0)) .to_string_native() ))?; client.assert_success(); @@ -1463,9 +1469,9 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "nam: {}", - (((amt20 * masp_rewards[&(btc(), MaspDenom::Zero)]).0 + (((amt20 * masp_rewards[&(btc(), None, MaspDenom::Zero)]).0 * (ep.0 - ep0.0)) - + ((amt30 * masp_rewards[&(eth(), MaspDenom::Zero)]).0 + + ((amt30 * masp_rewards[&(eth(), None, MaspDenom::Zero)]).0 * (ep.0 - ep3.0))) .to_string_native() ))?; @@ -1533,7 +1539,8 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "nam: {}", - ((amt20 * masp_rewards[&(btc(), MaspDenom::Zero)]).0 * (ep6.0 - ep0.0)) + ((amt20 * masp_rewards[&(btc(), None, MaspDenom::Zero)]).0 + * (ep6.0 - ep0.0)) .to_string_native() ))?; client.assert_success(); @@ -1556,9 +1563,9 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "nam: {}", - (((amt20 * masp_rewards[&(btc(), MaspDenom::Zero)]).0 + (((amt20 * masp_rewards[&(btc(), None, MaspDenom::Zero)]).0 * (ep6.0 - ep0.0)) - + ((amt30 * masp_rewards[&(eth(), MaspDenom::Zero)]).0 + + ((amt30 * masp_rewards[&(eth(), None, MaspDenom::Zero)]).0 * (ep5.0 - ep3.0))) .to_string_native() ))?; @@ -1584,7 +1591,8 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "nam: {}", - ((amt20 * masp_rewards[&(btc(), MaspDenom::Zero)]).0 * (ep6.0 - ep0.0)) + ((amt20 * masp_rewards[&(btc(), None, MaspDenom::Zero)]).0 + * (ep6.0 - ep0.0)) .to_string_native() ))?; client.assert_success(); @@ -1606,7 +1614,8 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "nam: {}", - ((amt30 * masp_rewards[&(eth(), MaspDenom::Zero)]).0 * (ep5.0 - ep3.0)) + ((amt30 * masp_rewards[&(eth(), None, MaspDenom::Zero)]).0 + * (ep5.0 - ep3.0)) .to_string_native() ))?; client.assert_success(); @@ -1629,9 +1638,9 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "nam: {}", - (((amt20 * masp_rewards[&(btc(), MaspDenom::Zero)]).0 + (((amt20 * masp_rewards[&(btc(), None, MaspDenom::Zero)]).0 * (ep6.0 - ep0.0)) - + ((amt30 * masp_rewards[&(eth(), MaspDenom::Zero)]).0 + + ((amt30 * masp_rewards[&(eth(), None, MaspDenom::Zero)]).0 * (ep5.0 - ep3.0))) .to_string_native() ))?; @@ -1654,7 +1663,7 @@ fn masp_incentives() -> Result<()> { "--token", NAM, "--amount", - &((amt30 * masp_rewards[&(eth(), MaspDenom::Zero)]).0 + &((amt30 * masp_rewards[&(eth(), None, MaspDenom::Zero)]).0 * (ep5.0 - ep3.0)) .to_string_native(), "--signer", @@ -1683,7 +1692,7 @@ fn masp_incentives() -> Result<()> { "--token", NAM, "--amount", - &((amt20 * masp_rewards[&(btc(), MaspDenom::Zero)]).0 + &((amt20 * masp_rewards[&(btc(), None, MaspDenom::Zero)]).0 * (ep6.0 - ep0.0)) .to_string_native(), "--signer", diff --git a/wasm/checksums.json b/wasm/checksums.json index 6fc3ef593a..470a405e0a 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,20 +1,20 @@ { - "tx_bond.wasm": "tx_bond.b9bc8b99b99b8cec29ad84e2b8013a295e2853c92faa853a62fb97726d909cef.wasm", - "tx_change_validator_commission.wasm": "tx_change_validator_commission.8cae1cd2f5853e47110dac6881bdf96f763b70b265bbe6718470c05268926991.wasm", - "tx_ibc.wasm": "tx_ibc.e900dd9f63efed368f276b37ae3a23d39d2d2e414f27e1661beb694d3962384b.wasm", - "tx_init_account.wasm": "tx_init_account.13c6195f839adb0fc90b34f0a7e3427252bc85cb0822547fa9ef9eef820b1003.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.0731458ba3c1deff1455e01d20d5668356120192daae971da5cfe65a0eb8426e.wasm", - "tx_init_validator.wasm": "tx_init_validator.f361859a37f50affde8c513bdbe7fc45ff1585428569513a07f9ad873b39aba4.wasm", - "tx_reveal_pk.wasm": "tx_reveal_pk.4043bcfcf456b927fd31f43a3804963462c1ca6e33d1e81a9bac8d80b0dd0e06.wasm", - "tx_transfer.wasm": "tx_transfer.2f349031d24d9221d411e50f3ee083586cc1b528e3d828d971b7ca3e85eff5b2.wasm", - "tx_unbond.wasm": "tx_unbond.f8f6070d9df88837745a4ee97a11388ade03b961c095dfb49966fa08d6769123.wasm", - "tx_update_vp.wasm": "tx_update_vp.633b6611bde975ca59ce2e770d4e94ce00c21f7dac1eb7e0e152741657cf1e60.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.c999d4fd180741a2fc8d6d204d9c3bd9bbebcba816175381378d054d52d08ee2.wasm", - "tx_withdraw.wasm": "tx_withdraw.8489877bb8593646bd5192ea5287a2e4eabe99e21655d704e361cba7ca9b0d53.wasm", - "vp_implicit.wasm": "vp_implicit.30795d13881147198344387748a5b26eb947dc7b94b81f69beef4b9ac0cddc55.wasm", - "vp_masp.wasm": "vp_masp.0f7fa1acb7927e9dc4e7b36272b9b7cb74f883d6b9404d47330cac9c3d3320a8.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.c3caefef87bdfd3d6adb453ace9a95b6b50be4ba5d0e1b6c70b836cfe626a677.wasm", - "vp_token.wasm": "vp_token.4c8ff7e2a4fd19d0aea5e40acceaf9ff05c134abe27626a6a796491ba55ab763.wasm", - "vp_user.wasm": "vp_user.2774adf1449643a7eba6b67f691e242c9e593b4efd5db53fac13fed05b68705f.wasm", - "vp_validator.wasm": "vp_validator.610c4d76872e9e2f5d6fa1cae61fd685e0cd4e0f54653d1c49dc36d004c99090.wasm" + "tx_bond.wasm": "tx_bond.204dd016d48999ce2bd65a2cc8ba7ba6eec6a2e9878b544e4d508ce8a6c4619e.wasm", + "tx_change_validator_commission.wasm": "tx_change_validator_commission.820da4e9d5cd00200e1a88808c5d0cce79bbf9c6a42c4bcf69cc0094d44053eb.wasm", + "tx_ibc.wasm": "tx_ibc.9d95be7b97770cee6665bfb9a53cfb10a4bb162be3bd813fb0720c5b70bf75d7.wasm", + "tx_init_account.wasm": "tx_init_account.92adf7dd170cde441f34001738e4854f6febeba416c73489ae31a75a09be9603.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.e76dcfdea228ac37745de999feefa56ae881b6be101565b86d832d27d1598323.wasm", + "tx_init_validator.wasm": "tx_init_validator.087a519a8e9e0a88e7ff71556b9cab8296ba879e0da3dfe7dfb66ccf407db92e.wasm", + "tx_reveal_pk.wasm": "tx_reveal_pk.fcd7aca657cd5e6aac3af9752c24d31078a3b8bbccba845e843980fec677dea0.wasm", + "tx_transfer.wasm": "tx_transfer.4ac65052b5a699df823b773dc943dd582701f3e4b1a6b8e52da4e39c9fa31195.wasm", + "tx_unbond.wasm": "tx_unbond.558c7d3e0d46766be957187b9c6d3d44490c29c9a354e7297be831fe48383452.wasm", + "tx_update_vp.wasm": "tx_update_vp.b7791369274dea718756434ba323396a471ca27c94a52d6c182da94953d9b22b.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.ca9bff7f78c3accf84725cd4bcdaf3872022fd5f6390fc22a5e20d24f53d88b7.wasm", + "tx_withdraw.wasm": "tx_withdraw.650cfc67a10136eb73bc3ceaaad9e2b2b06e2b423989a86604222187be4a5865.wasm", + "vp_implicit.wasm": "vp_implicit.14afbabd825733511d404013f4bc1dd9078ff0b801d3dd1ebd5bf3b4e5b41dfd.wasm", + "vp_masp.wasm": "vp_masp.b9d93452eb037e411a9e103aef6de33a27ddf9578b0125b7044aae00ebcfcd93.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.3e2f78bfa703c54e28d41ee12ca7869e60f0fe37b936ccc246a7aef39c95807a.wasm", + "vp_token.wasm": "vp_token.189080cb6137da24bc2ce9173ecac54e46b1dcb341d79aa66c9b6891fed92eb7.wasm", + "vp_user.wasm": "vp_user.909808779f6442ff5d08a4b195bb39b75402c5fe3d710c38ea17469c56cc9824.wasm", + "vp_validator.wasm": "vp_validator.ce440d41274ebc487f88a64843c27bb693093a3c2828a197b7975e82d27b295c.wasm" } \ No newline at end of file diff --git a/wasm/wasm_source/src/vp_masp.rs b/wasm/wasm_source/src/vp_masp.rs index 07c1bf41d8..5dd02a95b5 100644 --- a/wasm/wasm_source/src/vp_masp.rs +++ b/wasm/wasm_source/src/vp_masp.rs @@ -149,7 +149,7 @@ fn validate_tx( debug_log!( "Transparent output to a transaction to the masp must be \ beteween 1 and 4 but is {}", - shielded_tx.vin.len() + shielded_tx.vout.len() ); return reject(); } @@ -224,12 +224,13 @@ fn validate_tx( // Handle shielded output // The following boundary conditions must be satisfied // 1. Zero transparent output + // Satisfies 1. if !shielded_tx.vout.is_empty() { debug_log!( "Transparent output to a transaction from the masp must \ be 0 but is {}", - shielded_tx.vin.len() + shielded_tx.vout.len() ); return reject(); } From 33b2bf0d161c62f28fcca6d6fadda458f6a6d9bc Mon Sep 17 00:00:00 2001 From: satan Date: Fri, 21 Apr 2023 12:26:23 +0200 Subject: [PATCH 022/151] [fix]: Fixed makefile recipes for abcipp --- Makefile | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 7722745f59..b65e8b698d 100644 --- a/Makefile +++ b/Makefile @@ -132,7 +132,7 @@ test-e2e: -Z unstable-options --report-time test-unit-abcipp: - $(cargo) test \ + $(cargo) +$(nightly) test \ --manifest-path ./apps/Cargo.toml \ $(jobs) \ --no-default-features \ @@ -140,7 +140,7 @@ test-unit-abcipp: -Z unstable-options \ $(TEST_FILTER) -- \ -Z unstable-options --report-time && \ - $(cargo) test \ + $(cargo) +$(nightly) test \ --manifest-path \ ./proof_of_stake/Cargo.toml \ $(jobs) \ @@ -148,19 +148,11 @@ test-unit-abcipp: -Z unstable-options \ $(TEST_FILTER) -- \ -Z unstable-options --report-time && \ - $(cargo) test \ + $(cargo) +$(nightly) test \ --manifest-path ./shared/Cargo.toml \ $(jobs) \ --no-default-features \ - --features "testing wasm-runtime abcipp ibc-mocks-abcipp" \ - -Z unstable-options \ - $(TEST_FILTER) -- \ - -Z unstable-options --report-time && \ - $(cargo) test \ - --manifest-path ./vm_env/Cargo.toml \ - $(jobs) \ - --no-default-features \ - --features "abcipp" \ + --features "testing wasm-runtime abcipp ibc-mocks-abcipp ferveo-tpke" \ -Z unstable-options \ $(TEST_FILTER) -- \ -Z unstable-options --report-time From 9d20a96461ee9be35ce748be54d98110520ed3cd Mon Sep 17 00:00:00 2001 From: satan Date: Mon, 24 Apr 2023 12:11:23 +0200 Subject: [PATCH 023/151] [feat]: Replaced u128 in governance with u256 --- apps/src/lib/client/tx.rs | 47 ++++++-- core/src/ledger/storage/masp_conversions.rs | 102 +++++++++--------- core/src/types/address.rs | 19 ++-- core/src/types/governance.rs | 37 ++++--- core/src/types/uint.rs | 42 ++++++++ .../src/ledger/native_vp/governance/utils.rs | 36 ++++--- tests/src/e2e/ledger_tests.rs | 59 ++++------ 7 files changed, 206 insertions(+), 136 deletions(-) diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 2ce1771afd..50db5ec0fc 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -1435,7 +1435,7 @@ async fn gen_shielded_transfer( let (_, fee) = convert_amount( epoch, &ctx.get(&args.tx.fee_token), - &args.sub_prefix.as_ref(), + &None, MaspDenom::Zero.denominate(&fee.amount), MaspDenom::Zero, ); @@ -1470,7 +1470,9 @@ async fn gen_shielded_transfer( if let Some(sk) = spending_key { // If the gas is coming from the shielded pool, then our shielded // inputs must also cover the gas fee - let required_amt = if shielded_gas { + let required_amt = if shielded_gas && denom == MaspDenom::Zero { + // TODO: This could overflow the amount. Need carry logic. + // TODO: Move this up out of the loop. amount + fee.clone() } else { amount @@ -1649,10 +1651,17 @@ pub async fn submit_transfer(mut ctx: Context, mut args: args::TxTransfer) { ) .await; eprintln!( - "The balance of the source {} of token {} is lower than \ + "The balance of the source {} of token {}{} is lower than \ the amount to be transferred. Amount to transfer is {} \ and the balance is {}.", - source, token, validated_amount, balance_amount + source, + token, + validated_amount, + args.sub_prefix + .as_ref() + .map(|s| format!("/{}", s)) + .unwrap_or_default(), + balance_amount ); if !args.tx.force { safe_exit(1) @@ -1661,8 +1670,13 @@ pub async fn submit_transfer(mut ctx: Context, mut args: args::TxTransfer) { } None => { eprintln!( - "No balance found for the source {} of token {}", - source, token + "No balance found for the source {} of token {}{}", + source, + token, + args.sub_prefix + .as_ref() + .map(|s| format!("/{}", s)) + .unwrap_or_default(), ); if !args.tx.force { safe_exit(1) @@ -1729,11 +1743,15 @@ pub async fn submit_transfer(mut ctx: Context, mut args: args::TxTransfer) { Err(builder::Error::ChangeIsNegative(_)) => { eprintln!( "The balance of the source {} is lower than the amount to \ - be transferred and fees. Amount to transfer is {} {} and \ - fees are {} {}.", + be transferred and fees. Amount to transfer is {} {}{} \ + and fees are {} {}.", source.clone(), validated_amount, token, + args.sub_prefix + .as_ref() + .map(|s| format!("/{}", s)) + .unwrap_or_default(), validate_fee, ctx.get(&args.tx.fee_token), ); @@ -1830,7 +1848,7 @@ pub async fn submit_ibc_transfer(ctx: Context, args: args::TxIbcTransfer) { } // Check source balance let (sub_prefix, balance_key) = match args.sub_prefix { - Some(sub_prefix) => { + Some(ref sub_prefix) => { let sub_prefix = storage::Key::parse(sub_prefix).unwrap(); let prefix = token::multitoken_balance_prefix(&token, &sub_prefix); ( @@ -1860,10 +1878,17 @@ pub async fn submit_ibc_transfer(ctx: Context, args: args::TxIbcTransfer) { ) .await; eprintln!( - "The balance of the source {} of token {} is lower than \ + "The balance of the source {} of token {}{} is lower than \ the amount to be transferred. Amount to transfer is {} \ and the balance is {}.", - source, token, formatted_amount, formatted_balance + source, + token, + args.sub_prefix + .as_ref() + .map(|s| format!("/{}", s)) + .unwrap_or_default(), + formatted_amount, + formatted_balance ); if !args.tx.force { safe_exit(1) diff --git a/core/src/ledger/storage/masp_conversions.rs b/core/src/ledger/storage/masp_conversions.rs index 73f11e0520..37ce49892f 100644 --- a/core/src/ledger/storage/masp_conversions.rs +++ b/core/src/ledger/storage/masp_conversions.rs @@ -75,7 +75,7 @@ where let mut current_convs = BTreeMap::<(Address, Option, MaspDenom), AllowedConversion>::new(); // Reward all tokens according to above reward rates - for ((addr, sub_prefix, denom), reward) in &masp_rewards { + for ((addr, sub_prefix), reward) in &masp_rewards { // Dispense a transparent reward in parallel to the shielded rewards let addr_bal: token::Amount = match sub_prefix { None => wl_storage @@ -93,38 +93,40 @@ where // Since floor(a) + floor(b) <= floor(a+b), there will always be // enough rewards to reimburse users total_reward += (addr_bal * *reward).0; - // 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 - let old_asset = encode_asset_type( - addr.clone(), - sub_prefix, - *denom, - wl_storage.storage.last_epoch, - ); - let new_asset = encode_asset_type( - addr.clone(), - sub_prefix, - *denom, - wl_storage.storage.block.epoch, - ); - current_convs.insert( - (addr.clone(), sub_prefix.clone(), *denom), - (MaspAmount::from_pair(old_asset, -(reward.1 as i64)).unwrap() - + MaspAmount::from_pair(new_asset, reward.1).unwrap() - + MaspAmount::from_pair(reward_asset, reward.0).unwrap()) - .into(), - ); - // Add a conversion from the previous asset type - wl_storage.storage.conversion_state.assets.insert( - old_asset, - ( - (addr.clone(), sub_prefix.clone(), *denom), + for denom 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 + let old_asset = encode_asset_type( + addr.clone(), + sub_prefix, + denom, wl_storage.storage.last_epoch, - MaspAmount::zero().into(), - 0, - ), - ); + ); + let new_asset = encode_asset_type( + addr.clone(), + sub_prefix, + denom, + wl_storage.storage.block.epoch, + ); + current_convs.insert( + (addr.clone(), sub_prefix.clone(), denom), + (MaspAmount::from_pair(old_asset, -(reward.1 as i64)).unwrap() + + MaspAmount::from_pair(new_asset, reward.1).unwrap() + + MaspAmount::from_pair(reward_asset, reward.0).unwrap()) + .into(), + ); + // Add a conversion from the previous asset type + wl_storage.storage.conversion_state.assets.insert( + old_asset, + ( + (addr.clone(), sub_prefix.clone(), denom), + wl_storage.storage.last_epoch, + MaspAmount::zero().into(), + 0, + ), + ); + } } // Try to distribute Merkle leaf updating as evenly as possible across @@ -190,24 +192,26 @@ where // Add purely decoding entries to the assets map. These will be // overwritten before the creation of the next commitment tree - for (addr, sub_prefix, denom) in masp_rewards.keys() { - // Add the decoding entry for the new asset type. An uncommited - // node position is used since this is not a conversion. - let new_asset = encode_asset_type( - addr.clone(), - sub_prefix, - *denom, - wl_storage.storage.block.epoch, - ); - wl_storage.storage.conversion_state.assets.insert( - new_asset, - ( - (addr.clone(), sub_prefix.clone(), *denom), + for (addr, sub_prefix) in masp_rewards.keys() { + for denom in token::MaspDenom::iter() { + // Add the decoding entry for the new asset type. An uncommited + // node position is used since this is not a conversion. + let new_asset = encode_asset_type( + addr.clone(), + sub_prefix, + denom, wl_storage.storage.block.epoch, - MaspAmount::zero().into(), - wl_storage.storage.conversion_state.tree.size(), - ), - ); + ); + wl_storage.storage.conversion_state.assets.insert( + new_asset, + ( + (addr.clone(), sub_prefix.clone(), denom), + wl_storage.storage.block.epoch, + MaspAmount::zero().into(), + wl_storage.storage.conversion_state.tree.size(), + ), + ); + } } // Save the current conversion state in order to avoid computing diff --git a/core/src/types/address.rs b/core/src/types/address.rs index eea8692ac3..79f65b014c 100644 --- a/core/src/types/address.rs +++ b/core/src/types/address.rs @@ -15,7 +15,7 @@ use thiserror::Error; use crate::types::key; use crate::types::key::PublicKeyHash; use crate::types::storage::Key; -use crate::types::token::{Denomination, MaspDenom}; +use crate::types::token::Denomination; /// The length of an established [`Address`] encoded with Borsh. pub const ESTABLISHED_ADDRESS_BYTES_LEN: usize = 45; @@ -597,16 +597,15 @@ pub fn tokens() -> HashMap { /// Temporary helper for testing, a hash map of tokens addresses with their /// MASP XAN incentive schedules. If the reward is (a, b) then a rewarded tokens /// are dispensed for every b possessed tokens. -pub fn masp_rewards() -> HashMap<(Address, Option, MaspDenom), (u64, u64)> -{ +pub fn masp_rewards() -> HashMap<(Address, Option), (u64, u64)> { vec![ - ((nam(), None, MaspDenom::Zero), (0, 100)), - ((btc(), None, MaspDenom::Zero), (1, 100)), - ((eth(), None, MaspDenom::Zero), (2, 100)), - ((dot(), None, MaspDenom::Zero), (3, 100)), - ((schnitzel(), None, MaspDenom::Zero), (4, 100)), - ((apfel(), None, MaspDenom::Zero), (5, 100)), - ((kartoffel(), None, MaspDenom::Zero), (6, 100)), + ((nam(), None), (0, 100)), + ((btc(), None), (1, 100)), + ((eth(), None), (2, 100)), + ((dot(), None), (3, 100)), + ((schnitzel(), None), (4, 100)), + ((apfel(), None), (5, 100)), + ((kartoffel(), None), (6, 100)), ] .into_iter() .collect() diff --git a/core/src/types/governance.rs b/core/src/types/governance.rs index e599a2e10f..d70cb58f51 100644 --- a/core/src/types/governance.rs +++ b/core/src/types/governance.rs @@ -4,7 +4,6 @@ use std::collections::{BTreeMap, HashSet}; use std::fmt::{self, Display}; use borsh::{BorshDeserialize, BorshSerialize}; -use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -13,10 +12,13 @@ use crate::types::hash::Hash; use crate::types::key::common::{self, Signature}; use crate::types::key::SigScheme; use crate::types::storage::Epoch; -use crate::types::token::{Amount, NATIVE_SCALE}; +use crate::types::token::{ + Amount, DenominatedAmount, NATIVE_MAX_DECIMAL_PLACES, +}; +use crate::types::uint::Uint; /// Type alias for vote power -pub type VotePower = u128; +pub type VotePower = Uint; /// A PGF cocuncil composed of the address and spending cap pub type Council = (Address, Amount); @@ -141,19 +143,30 @@ pub struct ProposalResult { impl Display for ProposalResult { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let percentage = Decimal::checked_div( - self.total_yay_power.into(), - self.total_voting_power.into(), - ) - .unwrap_or_default(); + let percentage = DenominatedAmount { + amount: Amount::from_uint( + self.total_yay_power + .fixed_precision_div(&self.total_voting_power, 4) + .unwrap_or_default(), + 0, + ) + .unwrap(), + denom: 2.into(), + }; write!( f, - "{} with {} yay votes over {} ({:.2}%)", + "{} with {} yay votes over {} ({}%)", self.result, - self.total_yay_power / NATIVE_SCALE as u128, - self.total_voting_power / NATIVE_SCALE as u128, - percentage.checked_mul(100.into()).unwrap_or_default() + DenominatedAmount { + amount: Amount::from_uint(self.total_yay_power, 0).unwrap(), + denom: NATIVE_MAX_DECIMAL_PLACES.into() + }, + DenominatedAmount { + amount: Amount::from_uint(self.total_voting_power, 0).unwrap(), + denom: NATIVE_MAX_DECIMAL_PLACES.into() + }, + percentage ) } } diff --git a/core/src/types/uint.rs b/core/src/types/uint.rs index e91c497fba..daf6925835 100644 --- a/core/src/types/uint.rs +++ b/core/src/types/uint.rs @@ -32,6 +32,19 @@ impl_uint_num_traits!(Uint, 4); pub const MAX_VALUE: Uint = Uint([u64::MAX; 4]); impl Uint { + /// Divide two [`Uint`]s with scaled to allow the `denom` number + /// of decimal places. + /// + /// This method is checked and will return `None` if + /// * `self` * 10^(`denom`) overflows 256 bits + /// * `other` is zero (`checked_div` will return `None`). + pub fn fixed_precision_div(&self, rhs: &Self, denom: u8) -> Option { + let lhs = Uint::from(10) + .checked_pow(Uint::from(denom)) + .and_then(|res| res.checked_mul(*self))?; + lhs.checked_div(*rhs) + } + /// Compute the two's complement of a number. fn negate(&self) -> Self { Self( @@ -264,6 +277,35 @@ impl TryFrom for i128 { mod test_uint { use super::*; + /// Test that dividing two [`Uint`]s with the specified precision + /// works correctly and performs correct checks. + #[test] + fn test_fixed_precision_div() { + let zero = Uint::zero(); + let two = Uint::from(2); + let three = Uint::from(3); + + assert_eq!( + zero.fixed_precision_div(&two, 10).expect("Test failed"), + zero + ); + assert!(two.fixed_precision_div(&zero, 3).is_none()); + assert_eq!( + three.fixed_precision_div(&two, 1).expect("Test failed"), + Uint::from(15) + ); + assert_eq!( + two.fixed_precision_div(&three, 2).expect("Test failed"), + Uint::from(66) + ); + assert_eq!( + two.fixed_precision_div(&three, 3).expect("Satan lives"), + Uint::from(666) + ); + assert!(two.fixed_precision_div(&three, 77).is_none()); + assert!(Uint::from(20).fixed_precision_div(&three, 76).is_none()); + } + /// Test that adding one to the max signed /// value gives zero. #[test] diff --git a/shared/src/ledger/native_vp/governance/utils.rs b/shared/src/ledger/native_vp/governance/utils.rs index c97c13f6b0..8397cb51af 100644 --- a/shared/src/ledger/native_vp/governance/utils.rs +++ b/shared/src/ledger/native_vp/governance/utils.rs @@ -98,7 +98,7 @@ pub fn compute_tally( for (_, (amount, validator_vote)) in yay_validators.iter() { if let ProposalVote::Yay(vote_type) = validator_vote { if proposal_type == vote_type { - total_yay_staked_tokens += amount; + total_yay_staked_tokens += *amount; } else { // Log the error and continue tracing::error!( @@ -129,7 +129,7 @@ pub fn compute_tally( if !yay_validators.contains_key(validator_address) { // YAY: Add delegator amount whose validator // didn't vote / voted nay - total_yay_staked_tokens += vote_power; + total_yay_staked_tokens += *vote_power; } } ProposalVote::Nay => { @@ -137,7 +137,7 @@ pub fn compute_tally( // validator vote yay if yay_validators.contains_key(validator_address) { - total_yay_staked_tokens -= vote_power; + total_yay_staked_tokens -= *vote_power; } } @@ -175,14 +175,14 @@ pub fn compute_tally( result: tally_result, total_voting_power: total_stake, total_yay_power: total_yay_staked_tokens, - total_nay_power: 0, + total_nay_power: 0.into(), }) } else { Ok(ProposalResult { result: TallyResult::Rejected, total_voting_power: total_stake, total_yay_power: total_yay_staked_tokens, - total_nay_power: 0, + total_nay_power: 0.into(), }) } } @@ -193,8 +193,9 @@ pub fn compute_tally( validator_vote { for v in votes { - *total_yay_staked_tokens.entry(v).or_insert(0) += - amount; + *total_yay_staked_tokens + .entry(v) + .or_insert(VotePower::zero()) += *amount; } } else { // Log the error and continue @@ -235,7 +236,7 @@ pub fn compute_tally( total_yay_staked_tokens .get_mut(vote) { - *power -= vote_power; + *power -= *vote_power; } else { return Err(Error::Tally( format!( @@ -251,7 +252,9 @@ pub fn compute_tally( // this, add voting power *total_yay_staked_tokens .entry(vote) - .or_insert(0) += vote_power; + .or_insert( + VotePower::zero(), + ) += *vote_power; } } } else { @@ -271,7 +274,8 @@ pub fn compute_tally( for vote in delegator_votes { *total_yay_staked_tokens .entry(vote) - .or_insert(0) += vote_power; + .or_insert(VotePower::zero()) += + *vote_power; } } } @@ -294,7 +298,7 @@ pub fn compute_tally( total_yay_staked_tokens .get_mut(vote) { - *power -= vote_power; + *power -= *vote_power; } else { return Err(Error::Tally( format!( @@ -333,14 +337,16 @@ pub fn compute_tally( // At least 1/3 of the total voting power must vote Yay let total_yay_voted_power = total_yay_staked_tokens .iter() - .fold(0, |acc, (_, vote_power)| acc + vote_power); + .fold(VotePower::zero(), |acc, (_, vote_power)| { + acc + *vote_power + }); - match total_yay_voted_power.checked_mul(3) { + match total_yay_voted_power.checked_mul(3.into()) { Some(v) if v < total_stake => Ok(ProposalResult { result: TallyResult::Rejected, total_voting_power: total_stake, total_yay_power: total_yay_voted_power, - total_nay_power: 0, + total_nay_power: VotePower::zero(), }), _ => { // Select the winner council based on approval voting @@ -359,7 +365,7 @@ pub fn compute_tally( result: TallyResult::Passed(Tally::PGFCouncil(council)), total_voting_power: total_stake, total_yay_power: total_yay_voted_power, - total_nay_power: 0, + total_nay_power: VotePower::zero(), }) } } diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 8ec44c5e9f..2418deb884 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -27,7 +27,7 @@ use namada_apps::client::tx::ShieldedContext; use namada_apps::config::genesis::genesis_config::{ GenesisConfig, ParametersConfig, PosParamsConfig, }; -use namada_core::types::token::{MaspDenom, NATIVE_MAX_DECIMAL_PLACES}; +use namada_core::types::token::NATIVE_MAX_DECIMAL_PLACES; use namada_test_utils::TestWasms; use serde_json::json; use setup::constants::*; @@ -1153,8 +1153,7 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "nam: {}", - ((amt20 * masp_rewards[&(btc(), None, MaspDenom::Zero)]).0 - * (ep1.0 - ep0.0)) + ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep1.0 - ep0.0)) .to_string_native() ))?; client.assert_success(); @@ -1176,8 +1175,7 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "nam: {}", - ((amt20 * masp_rewards[&(btc(), None, MaspDenom::Zero)]).0 - * (ep1.0 - ep0.0)) + ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep1.0 - ep0.0)) .to_string_native() ))?; client.assert_success(); @@ -1220,8 +1218,7 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "nam: {}", - ((amt20 * masp_rewards[&(btc(), None, MaspDenom::Zero)]).0 - * (ep2.0 - ep0.0)) + ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep2.0 - ep0.0)) .to_string_native() ))?; client.assert_success(); @@ -1243,8 +1240,7 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "nam: {}", - ((amt20 * masp_rewards[&(btc(), None, MaspDenom::Zero)]).0 - * (ep2.0 - ep0.0)) + ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep2.0 - ep0.0)) .to_string_native(), ))?; client.assert_success(); @@ -1348,8 +1344,7 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "nam: {}", - ((amt30 * masp_rewards[&(eth(), None, MaspDenom::Zero)]).0 - * (ep4.0 - ep3.0)) + ((amt30 * masp_rewards[&(eth(), None)]).0 * (ep4.0 - ep3.0)) .to_string_native(), ))?; client.assert_success(); @@ -1372,10 +1367,8 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "nam: {}", - (((amt20 * masp_rewards[&(btc(), None, MaspDenom::Zero)]).0 - * (ep4.0 - ep0.0)) - + ((amt30 * masp_rewards[&(eth(), None, MaspDenom::Zero)]).0 - * (ep4.0 - ep3.0))) + (((amt20 * masp_rewards[&(btc(), None)]).0 * (ep4.0 - ep0.0)) + + ((amt30 * masp_rewards[&(eth(), None)]).0 * (ep4.0 - ep3.0))) .to_string_native() ))?; client.assert_success(); @@ -1444,8 +1437,7 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "nam: {}", - ((amt30 * masp_rewards[&(eth(), None, MaspDenom::Zero)]).0 - * (ep.0 - ep3.0)) + ((amt30 * masp_rewards[&(eth(), None)]).0 * (ep.0 - ep3.0)) .to_string_native() ))?; client.assert_success(); @@ -1469,10 +1461,8 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "nam: {}", - (((amt20 * masp_rewards[&(btc(), None, MaspDenom::Zero)]).0 - * (ep.0 - ep0.0)) - + ((amt30 * masp_rewards[&(eth(), None, MaspDenom::Zero)]).0 - * (ep.0 - ep3.0))) + (((amt20 * masp_rewards[&(btc(), None)]).0 * (ep.0 - ep0.0)) + + ((amt30 * masp_rewards[&(eth(), None)]).0 * (ep.0 - ep3.0))) .to_string_native() ))?; client.assert_success(); @@ -1539,8 +1529,7 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "nam: {}", - ((amt20 * masp_rewards[&(btc(), None, MaspDenom::Zero)]).0 - * (ep6.0 - ep0.0)) + ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep6.0 - ep0.0)) .to_string_native() ))?; client.assert_success(); @@ -1563,10 +1552,8 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "nam: {}", - (((amt20 * masp_rewards[&(btc(), None, MaspDenom::Zero)]).0 - * (ep6.0 - ep0.0)) - + ((amt30 * masp_rewards[&(eth(), None, MaspDenom::Zero)]).0 - * (ep5.0 - ep3.0))) + (((amt20 * masp_rewards[&(btc(), None)]).0 * (ep6.0 - ep0.0)) + + ((amt30 * masp_rewards[&(eth(), None)]).0 * (ep5.0 - ep3.0))) .to_string_native() ))?; client.assert_success(); @@ -1591,8 +1578,7 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "nam: {}", - ((amt20 * masp_rewards[&(btc(), None, MaspDenom::Zero)]).0 - * (ep6.0 - ep0.0)) + ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep6.0 - ep0.0)) .to_string_native() ))?; client.assert_success(); @@ -1614,8 +1600,7 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "nam: {}", - ((amt30 * masp_rewards[&(eth(), None, MaspDenom::Zero)]).0 - * (ep5.0 - ep3.0)) + ((amt30 * masp_rewards[&(eth(), None)]).0 * (ep5.0 - ep3.0)) .to_string_native() ))?; client.assert_success(); @@ -1638,10 +1623,8 @@ fn masp_incentives() -> Result<()> { )?; client.exp_string(&format!( "nam: {}", - (((amt20 * masp_rewards[&(btc(), None, MaspDenom::Zero)]).0 - * (ep6.0 - ep0.0)) - + ((amt30 * masp_rewards[&(eth(), None, MaspDenom::Zero)]).0 - * (ep5.0 - ep3.0))) + (((amt20 * masp_rewards[&(btc(), None)]).0 * (ep6.0 - ep0.0)) + + ((amt30 * masp_rewards[&(eth(), None)]).0 * (ep5.0 - ep3.0))) .to_string_native() ))?; client.assert_success(); @@ -1663,8 +1646,7 @@ fn masp_incentives() -> Result<()> { "--token", NAM, "--amount", - &((amt30 * masp_rewards[&(eth(), None, MaspDenom::Zero)]).0 - * (ep5.0 - ep3.0)) + &((amt30 * masp_rewards[&(eth(), None)]).0 * (ep5.0 - ep3.0)) .to_string_native(), "--signer", BERTHA, @@ -1692,8 +1674,7 @@ fn masp_incentives() -> Result<()> { "--token", NAM, "--amount", - &((amt20 * masp_rewards[&(btc(), None, MaspDenom::Zero)]).0 - * (ep6.0 - ep0.0)) + &((amt20 * masp_rewards[&(btc(), None)]).0 * (ep6.0 - ep0.0)) .to_string_native(), "--signer", ALBERT, From adaf3a145bf9f6f15f130243f7d32d6cdc3a4e64 Mon Sep 17 00:00:00 2001 From: satan Date: Tue, 25 Apr 2023 10:54:23 +0200 Subject: [PATCH 024/151] [fix]: Incorporating in review suggestions --- apps/src/lib/client/tx.rs | 27 +++++------ core/src/types/token.rs | 6 +-- core/src/types/uint.rs | 98 +++++++++++++++++++-------------------- 3 files changed, 63 insertions(+), 68 deletions(-) diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 50db5ec0fc..cd457ddede 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -1426,26 +1426,30 @@ async fn gen_shielded_transfer( LocalTxProver::with_default_location() .expect("unable to load MASP Parameters") }; - let fee = if spending_key.is_some() { + let fee_amount = if spending_key.is_some() { let InputAmount::Validated(fee) = args.tx.fee_amount else { unreachable!("The function `gen_shielded_transfer` is only called by `submit_tx` which validates amounts.") }; // Transaction fees need to match the amount in the wrapper Transfer // when MASP source is used. This amount should be <= `u64::MAX`. - let (_, fee) = convert_amount( + let (_, shielded_fee) = convert_amount( epoch, &ctx.get(&args.tx.fee_token), &None, MaspDenom::Zero.denominate(&fee.amount), MaspDenom::Zero, ); - builder.set_fee(fee.clone())?; - fee + builder.set_fee(shielded_fee)?; + if shielded_gas { + fee.amount + } else { + token::Amount::zero() + } } else { // No transfer fees come from the shielded transaction for non-MASP // sources builder.set_fee(Amount::zero())?; - Amount::zero() + token::Amount::zero() }; // break up a transfer into a number of transfers with suitable // denominations @@ -1453,12 +1457,12 @@ async fn gen_shielded_transfer( unreachable!("The function `gen_shielded_transfer` is only called by `submit_tx` which validates amounts.") }; for denom in MaspDenom::iter() { - let denom_amount = denom.denominate(&amt.amount); + let denom_amount = denom.denominate(&(amt.amount + fee_amount)); if denom_amount == 0 { continue; } // Convert transaction amount into MASP types - let (asset_type, amount) = convert_amount( + let (asset_type, required_amt) = convert_amount( epoch, &ctx.get(&args.token), &args.sub_prefix.as_ref(), @@ -1468,15 +1472,6 @@ async fn gen_shielded_transfer( // If there are shielded inputs if let Some(sk) = spending_key { - // If the gas is coming from the shielded pool, then our shielded - // inputs must also cover the gas fee - let required_amt = if shielded_gas && denom == MaspDenom::Zero { - // TODO: This could overflow the amount. Need carry logic. - // TODO: Move this up out of the loop. - amount + fee.clone() - } else { - amount - }; // Locate unspent notes that can help us meet the transaction amount let (_, unspent_notes, used_convs) = ctx .shielded diff --git a/core/src/types/token.rs b/core/src/types/token.rs index cdbe8ef32d..7e7ea3b20e 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -16,7 +16,7 @@ use crate::ledger::storage_api::StorageRead; use crate::types::address::{masp, Address, DecodeError as AddressError}; use crate::types::storage; use crate::types::storage::{DbKeySeg, Key, KeySeg}; -use crate::types::uint::{self, SignedUint, Uint}; +use crate::types::uint::{self, I256, Uint}; /// Amount in micro units. For different granularity another representation /// might be more appropriate. @@ -49,7 +49,7 @@ pub const NATIVE_MAX_DECIMAL_PLACES: u8 = 6; pub const NATIVE_SCALE: u64 = 1_000_000; /// A change in tokens amount -pub type Change = SignedUint; +pub type Change = I256; impl Amount { /// Get the amount as a [`Change`] @@ -207,7 +207,7 @@ impl Amount { pub fn to_string_native(&self) -> String { DenominatedAmount { amount: *self, - denom: 6.into(), + denom:NATIVE_MAX_DECIMAL_PLACES.into(), } .to_string_precise() } diff --git a/core/src/types/uint.rs b/core/src/types/uint.rs index daf6925835..cb88c96844 100644 --- a/core/src/types/uint.rs +++ b/core/src/types/uint.rs @@ -71,7 +71,7 @@ impl Uint { } } -/// The maximum absolute value a [`SignedUint`] may have. +/// The maximum absolute value a [`I256`] may have. /// Note the the last digit is 2^63 - 1. We add this cap so /// we can use two's complement. pub const MAX_SIGNED_VALUE: Uint = @@ -83,9 +83,9 @@ const MINUS_ZERO: Uint = Uint([0u64, 0u64, 0u64, 9223372036854775808]); #[derive( Copy, Clone, Debug, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize, )] -pub struct SignedUint(Uint); +pub struct I256(Uint); -impl SignedUint { +impl I256 { /// Check if the amount is not negative (greater /// than or equal to zero) pub fn non_negative(&self) -> bool { @@ -118,7 +118,7 @@ impl SignedUint { sign } - /// Adds two [`SignedUint`]'s if the absolute value does + /// Adds two [`I256`]'s if the absolute value does /// not exceed [`MAX_SIGNED_VALUE`], else returns `None`. pub fn checked_add(&self, other: &Self) -> Option { if self.non_negative() == other.non_negative() { @@ -132,7 +132,7 @@ impl SignedUint { } } - /// Subtracts two [`SignedUint`]'s if the absolute value does + /// Subtracts two [`I256`]'s if the absolute value does /// not exceed [`MAX_SIGNED_VALUE`], else returns `None`. pub fn checked_sub(&self, other: &Self) -> Option { self.checked_add(&other.neg()) @@ -144,14 +144,14 @@ impl SignedUint { } } -impl From for SignedUint { +impl From for I256 { fn from(val: u64) -> Self { - SignedUint::try_from(Uint::from(val)) + I256::try_from(Uint::from(val)) .expect("A u64 will always fit in this type") } } -impl TryFrom for SignedUint { +impl TryFrom for I256 { type Error = Box; fn try_from(value: Uint) -> Result { @@ -165,7 +165,7 @@ impl TryFrom for SignedUint { } } -impl Neg for SignedUint { +impl Neg for I256 { type Output = Self; fn neg(self) -> Self::Output { @@ -173,7 +173,7 @@ impl Neg for SignedUint { } } -impl PartialOrd for SignedUint { +impl PartialOrd for I256 { fn partial_cmp(&self, other: &Self) -> Option { match (self.non_negative(), other.non_negative()) { (true, false) => Some(Ordering::Greater), @@ -192,16 +192,16 @@ impl PartialOrd for SignedUint { } } -impl Ord for SignedUint { +impl Ord for I256 { fn cmp(&self, other: &Self) -> Ordering { self.partial_cmp(other).unwrap() } } -impl Add for SignedUint { +impl Add for I256 { type Output = Self; - fn add(self, rhs: SignedUint) -> Self::Output { + fn add(self, rhs: I256) -> Self::Output { match (self.non_negative(), rhs.non_negative()) { (true, true) => Self(self.0 + rhs.0), (false, false) => -Self(self.abs() + rhs.abs()), @@ -224,13 +224,13 @@ impl Add for SignedUint { } } -impl AddAssign for SignedUint { +impl AddAssign for I256 { fn add_assign(&mut self, rhs: Self) { *self = *self + rhs; } } -impl Sub for SignedUint { +impl Sub for I256 { type Output = Self; fn sub(self, rhs: Self) -> Self::Output { @@ -238,7 +238,7 @@ impl Sub for SignedUint { } } -impl From for SignedUint { +impl From for I256 { fn from(val: i128) -> Self { if val < 0 { let abs = Self((-val).into()); @@ -249,22 +249,22 @@ impl From for SignedUint { } } -impl From for SignedUint { +impl From for I256 { fn from(val: i64) -> Self { Self::from(val as i128) } } -impl From for SignedUint { +impl From for I256 { fn from(val: i32) -> Self { Self::from(val as i128) } } -impl TryFrom for i128 { +impl TryFrom for i128 { type Error = std::io::Error; - fn try_from(value: SignedUint) -> Result { + fn try_from(value: I256) -> Result { if !value.non_negative() { Ok(-(u128::try_from(Amount::from_change(value))? as i128)) } else { @@ -311,12 +311,12 @@ mod test_uint { #[test] fn test_max_signed_value() { let signed = - SignedUint::try_from(MAX_SIGNED_VALUE).expect("Test failed"); - let one = SignedUint::try_from(Uint::from(1u64)).expect("Test failed"); + I256::try_from(MAX_SIGNED_VALUE).expect("Test failed"); + let one = I256::try_from(Uint::from(1u64)).expect("Test failed"); let overflow = signed + one; assert_eq!( overflow, - SignedUint::try_from(Uint::zero()).expect("Test failed") + I256::try_from(Uint::zero()).expect("Test failed") ); assert!(signed.checked_add(&one).is_none()); assert!((-signed).checked_sub(&one).is_none()); @@ -331,7 +331,7 @@ mod test_uint { assert!(larger > smaller); assert_eq!(smaller, MAX_SIGNED_VALUE); assert_eq!(larger, MINUS_ZERO); - assert!(SignedUint::try_from(MINUS_ZERO).is_err()); + assert!(I256::try_from(MINUS_ZERO).is_err()); let zero = Uint::zero(); assert_eq!(zero, zero.negate()); } @@ -340,25 +340,25 @@ mod test_uint { /// sign. #[test] fn test_non_negative() { - let zero = SignedUint::try_from(Uint::zero()).expect("Test failed"); + let zero = I256::try_from(Uint::zero()).expect("Test failed"); assert!(zero.non_negative()); assert!((-zero).non_negative()); - let negative = SignedUint(Uint([1u64, 0, 0, 2u64.pow(63)])); + let negative = I256(Uint([1u64, 0, 0, 2u64.pow(63)])); assert!(!negative.non_negative()); assert!((-negative).non_negative()); - let positive = SignedUint(MAX_SIGNED_VALUE); + let positive = I256(MAX_SIGNED_VALUE); assert!(positive.non_negative()); assert!(!(-positive).non_negative()); } - /// Test that the absolute vale is computed correctly + /// Test that the absolute value is computed correctly. #[test] fn test_abs() { - let zero = SignedUint::try_from(Uint::zero()).expect("Test failed"); - let neg_one = SignedUint(Uint::max_value()); - let neg_eight = SignedUint(Uint::max_value() - Uint::from(7)); - let two = SignedUint(Uint::from(2)); - let ten = SignedUint(Uint::from(10)); + let zero = I256::try_from(Uint::zero()).expect("Test failed"); + let neg_one = I256(Uint::max_value()); + let neg_eight = I256(Uint::max_value() - Uint::from(7)); + let two = I256(Uint::from(2)); + let ten = I256(Uint::from(10)); assert_eq!(zero.abs(), Uint::zero()); assert_eq!(neg_one.abs(), Uint::from(1)); @@ -367,15 +367,15 @@ mod test_uint { assert_eq!(ten.abs(), Uint::from(10)); } - /// Test that the absolute vale is computed correctly + /// Test that the string representation is created correctly. #[test] fn test_to_string_native() { let native_scaling = Uint::exp10(6); - let zero = SignedUint::try_from(Uint::zero()).expect("Test failed"); - let neg_one = -SignedUint(native_scaling); - let neg_eight = -SignedUint(Uint::from(8) * native_scaling); - let two = SignedUint(Uint::from(2) * native_scaling); - let ten = SignedUint(Uint::from(10) * native_scaling); + let zero = I256::try_from(Uint::zero()).expect("Test failed"); + let neg_one = -I256(native_scaling); + let neg_eight = -I256(Uint::from(8) * native_scaling); + let two = I256(Uint::from(2) * native_scaling); + let ten = I256(Uint::from(10) * native_scaling); assert_eq!(zero.to_string_native(), "0.000000"); assert_eq!(neg_one.to_string_native(), "-1.000000"); @@ -387,22 +387,22 @@ mod test_uint { /// Test that we correctly handle arithmetic with two's complement #[test] fn test_arithmetic() { - let zero = SignedUint::try_from(Uint::zero()).expect("Test failed"); - let neg_one = SignedUint(Uint::max_value()); - let neg_eight = SignedUint(Uint::max_value() - Uint::from(7)); - let two = SignedUint(Uint::from(2)); - let ten = SignedUint(Uint::from(10)); + let zero = I256::try_from(Uint::zero()).expect("Test failed"); + let neg_one = I256(Uint::max_value()); + let neg_eight = I256(Uint::max_value() - Uint::from(7)); + let two = I256(Uint::from(2)); + let ten = I256(Uint::from(10)); assert_eq!(zero + neg_one, neg_one); assert_eq!(neg_one - zero, neg_one); - assert_eq!(zero - neg_one, SignedUint(Uint::one())); + assert_eq!(zero - neg_one, I256(Uint::one())); assert_eq!(two - neg_eight, ten); - assert_eq!(two + ten, SignedUint(Uint::from(12))); + assert_eq!(two + ten, I256(Uint::from(12))); assert_eq!(ten - two, -neg_eight); assert_eq!(two - ten, neg_eight); - assert_eq!(neg_eight + neg_one, -SignedUint(Uint::from(9))); - assert_eq!(neg_one - neg_eight, SignedUint(Uint::from(7))); - assert_eq!(neg_eight - neg_one, -SignedUint(Uint::from(7))); + assert_eq!(neg_eight + neg_one, -I256(Uint::from(9))); + assert_eq!(neg_one - neg_eight, I256(Uint::from(7))); + assert_eq!(neg_eight - neg_one, -I256(Uint::from(7))); assert_eq!(neg_eight - two, -ten); assert!((two - two).is_zero()); } From acc79d2f28cec29db39f3b0fa952664c08d20b48 Mon Sep 17 00:00:00 2001 From: satan Date: Tue, 25 Apr 2023 14:47:40 +0200 Subject: [PATCH 025/151] [fix]: Refactored gen_sheilded_transfer to only denominate amounts at the masp crate boundary --- apps/src/lib/client/tx.rs | 198 +++++++++++++++++++------------------- 1 file changed, 97 insertions(+), 101 deletions(-) diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index cd457ddede..43093543a0 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -1362,14 +1362,20 @@ fn convert_amount( epoch: Epoch, token: &Address, sub_prefix: &Option<&String>, - val: u64, - denom: MaspDenom, -) -> (AssetType, Amount) { - let asset_type = make_asset_type(epoch, token, sub_prefix, denom); - // Combine the value and unit into one amount - let amount = Amount::from_nonnegative(asset_type, val) - .expect("invalid value for amount"); - (asset_type, amount) + val: &token::Amount, +) -> ([AssetType; 4], Amount) { + let mut amount = Amount::zero(); + let asset_types: [AssetType; 4] = MaspDenom::iter().map(|denom| { + let asset_type = make_asset_type(epoch, token, sub_prefix, denom); + // Combine the value and unit into one amount + amount += Amount::from_nonnegative(asset_type, denom.denominate(val)) + .expect("invalid value for amount"); + asset_type + }) + .collect() + .try_into() + .expect("This can't fail"); + (asset_types, amount) } /// Make shielded components to embed within a Transfer object. If no shielded @@ -1426,7 +1432,23 @@ async fn gen_shielded_transfer( LocalTxProver::with_default_location() .expect("unable to load MASP Parameters") }; - let fee_amount = if spending_key.is_some() { + + // break up a transfer into a number of transfers with suitable + // denominations + let InputAmount::Validated(amt) = args.amount else { + unreachable!("The function `gen_shielded_transfer` is only called by `submit_tx` which validates amounts.") + }; + + // Convert transaction amount into MASP types + let (asset_types, amount) = convert_amount( + epoch, + &ctx.get(&args.token), + &args.sub_prefix.as_ref(), + &amt.amount, + ); + + // If there are shielded inputs + if let Some(sk) = spending_key { let InputAmount::Validated(fee) = args.tx.fee_amount else { unreachable!("The function `gen_shielded_transfer` is only called by `submit_tx` which validates amounts.") }; @@ -1436,111 +1458,83 @@ async fn gen_shielded_transfer( epoch, &ctx.get(&args.tx.fee_token), &None, - MaspDenom::Zero.denominate(&fee.amount), - MaspDenom::Zero, + &fee.amount, ); - builder.set_fee(shielded_fee)?; - if shielded_gas { - fee.amount - } else { - token::Amount::zero() + builder.set_fee(shielded_fee.clone())?; + let required_amt = if shielded_gas { amount + shielded_fee } else { amount }; + // Locate unspent notes that can help us meet the transaction amount + let (_, unspent_notes, used_convs) = ctx + .shielded + .collect_unspent_notes( + args.tx.ledger_address.clone(), + &to_viewing_key(&sk).vk, + required_amt, + epoch, + ) + .await; + // Commit the notes found to our transaction + for (diversifier, note, merkle_path) in unspent_notes { + builder.add_sapling_spend( + sk, + diversifier, + note, + merkle_path, + )?; + } + // Commit the conversion notes used during summation + for (conv, wit, value) in used_convs.values() { + if *value > 0 { + builder.add_convert( + conv.clone(), + *value as u64, + wit.clone(), + )?; + } } } else { // No transfer fees come from the shielded transaction for non-MASP // sources builder.set_fee(Amount::zero())?; - token::Amount::zero() - }; - // break up a transfer into a number of transfers with suitable - // denominations - let InputAmount::Validated(amt) = args.amount else { - unreachable!("The function `gen_shielded_transfer` is only called by `submit_tx` which validates amounts.") - }; - for denom in MaspDenom::iter() { - let denom_amount = denom.denominate(&(amt.amount + fee_amount)); - if denom_amount == 0 { - continue; - } - // Convert transaction amount into MASP types - let (asset_type, required_amt) = convert_amount( - epoch, - &ctx.get(&args.token), - &args.sub_prefix.as_ref(), - denom_amount, - denom, - ); - - // If there are shielded inputs - if let Some(sk) = spending_key { - // Locate unspent notes that can help us meet the transaction amount - let (_, unspent_notes, used_convs) = ctx - .shielded - .collect_unspent_notes( - args.tx.ledger_address.clone(), - &to_viewing_key(&sk).vk, - required_amt, - epoch, - ) - .await; - // Commit the notes found to our transaction - for (diversifier, note, merkle_path) in unspent_notes { - builder.add_sapling_spend( - sk, - diversifier, - note, - merkle_path, - )?; - } - // Commit the conversion notes used during summation - for (conv, wit, value) in used_convs.values() { - if *value > 0 { - builder.add_convert( - conv.clone(), - *value as u64, - wit.clone(), - )?; - } - } - } else { - // No transfer fees come from the shielded transaction for non-MASP - // sources - builder.set_fee(Amount::zero())?; - // We add a dummy UTXO to our transaction, but only the source of - // the parent Transfer object is used to validate fund - // availability - let secp_sk = secp256k1::SecretKey::from_slice(&[0xcd; 32]) - .expect("secret key"); - let secp_ctx = - secp256k1::Secp256k1::::gen_new(); - let secp_pk = - secp256k1::PublicKey::from_secret_key(&secp_ctx, &secp_sk) - .serialize(); - let hash = - ripemd160::Ripemd160::digest(&sha2::Sha256::digest(&secp_pk)); - let script = TransparentAddress::PublicKey(hash.into()).script(); + // We add a dummy UTXO to our transaction, but only the source of + // the parent Transfer object is used to validate fund + // availability + let secp_sk = secp256k1::SecretKey::from_slice(&[0xcd; 32]) + .expect("secret key"); + let secp_ctx = + secp256k1::Secp256k1::::gen_new(); + let secp_pk = + secp256k1::PublicKey::from_secret_key(&secp_ctx, &secp_sk) + .serialize(); + let hash = + ripemd160::Ripemd160::digest(&sha2::Sha256::digest(&secp_pk)); + let script = TransparentAddress::PublicKey(hash.into()).script(); + for (denom, asset_type) in MaspDenom::iter().zip(asset_types.iter()) { builder.add_transparent_input( secp_sk, OutPoint::new([0u8; 32], 0), TxOut { - asset_type, - value: denom_amount, - script_pubkey: script, + asset_type: *asset_type, + value: denom.denominate(&amt), + script_pubkey: script.clone(), }, )?; } + } - // Now handle the outputs of this transaction - // If there is a shielded output - if let Some(pa) = payment_address { - let ovk_opt = spending_key.map(|x| x.expsk.ovk); + // Now handle the outputs of this transaction + // If there is a shielded output + if let Some(pa) = payment_address { + let ovk_opt = spending_key.map(|x| x.expsk.ovk); + for (denom, asset_type) in MaspDenom::iter().zip(asset_types.iter()) { builder.add_sapling_output( ovk_opt, pa.into(), - asset_type, - denom_amount, + *asset_type, + denom.denominate(&amt), memo.clone(), )?; - } else { + } + } else { // Embed the transparent target address into the shielded // transaction so that it can be signed let target = ctx.get(&args.target); @@ -1552,13 +1546,15 @@ async fn gen_shielded_transfer( let hash = ripemd160::Ripemd160::digest(&sha2::Sha256::digest( target_enc.as_ref(), )); - builder.add_transparent_output( - &TransparentAddress::PublicKey(hash.into()), - asset_type, - denom_amount, - )?; + for (denom, asset_type) in MaspDenom::iter().zip(asset_types.iter()) { + builder.add_transparent_output( + &TransparentAddress::PublicKey(hash.into()), + *asset_type, + denom.denominate(&amt), + )?; + } } - } + // Build and return the constructed transaction builder .build(consensus_branch_id, &prover) From 1e3e77dbbeae4d9984554d23ef560f85dbad181d Mon Sep 17 00:00:00 2001 From: mariari Date: Tue, 25 Apr 2023 21:55:37 +0800 Subject: [PATCH 026/151] Update query_sheilded_balance to handle the multiple denominations --- apps/src/lib/client/rpc.rs | 97 +++++++++++++++++++++++--------------- 1 file changed, 58 insertions(+), 39 deletions(-) diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 1fd77f109c..807c523f22 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -1131,67 +1131,86 @@ pub async fn query_shielded_balance( } // These are the asset types for which we have human-readable names - let mut read_tokens = HashSet::new(); + let mut read_tokens: HashMap>> = HashMap::new(); // Print non-zero balances whose asset types can be decoded + // TODO Implement a function for this + + let mut balance_map = HashMap::new(); for (asset_type, balances) in balances { - // Decode the asset type let decoded = ctx .shielded .decode_asset_type(client.clone(), asset_type) .await; + match decoded { Some((addr, sub_prefix, denom, asset_epoch)) if asset_epoch == epoch => { - // Only assets with the current timestamp count - println!( - "Shielded Token {}{}:", - tokens - .get(&addr) - .cloned() - .unwrap_or_else(|| addr.clone()), - sub_prefix - .as_ref() - .map(|k| format!("/{}", k)) - .unwrap_or_default(), - ); - read_tokens.insert(addr.clone()); - let mut found_any = false; - for (fvk, value) in balances { - let value = token::Amount::from_masp_denominated( - value as u64, - denom, - ); - let formatted = format_denominated_amount( - &client, - &addr, - &sub_prefix, - value, - ) - .await; - println!(" {}, owned by {}", formatted, fvk); - found_any = true; - } - if !found_any { + // remove this from here, should not be making the hashtable creation any uglier + if balances.is_empty() { println!( "No shielded {} balance found for any wallet \ key", asset_type ); } + let asset_type = (addr, sub_prefix); + for (fvk, value) in balances { + let token_value = token::Amount::from_masp_denominated(value as u64, denom); + balance_map.entry((fvk, asset_type.clone())).and_modify(|value_2| *value_2 += token_value).or_insert(token_value); + } + } _ => {} - } + }; + } + for ((fvk, (addr, sub_prefix)), token_balance) in balance_map { + read_tokens.entry(addr.clone()).and_modify(|addr_vec| addr_vec.push(sub_prefix.clone())).or_insert(vec![sub_prefix.clone()]); + // Only assets with the current timestamp count + println!( + "Shielded Token {}{}:", + tokens + .get(&addr) + .cloned() + .unwrap_or_else(|| addr.clone()), + sub_prefix + .as_ref() + .map(|k| format!("/{}", k)) + .unwrap_or_default(), + ); + let formatted = format_denominated_amount( + &client, + &addr, + &sub_prefix, + token_balance, + ) + .await; + println!(" {}, owned by {}", formatted, fvk); } // Print zero balances for remaining assets for token in tokens { - if !read_tokens.contains(&token) { + if let Some(sub_addrs) = read_tokens.get(&token) { let token_alias = lookup_alias(ctx, &token); - println!("Shielded Token {}:", token_alias); - println!( - "No shielded {} balance found for any wallet key", - token_alias - ); + for sub_addr in sub_addrs { + match sub_addr { + // abstract out these prints + Some(sub_addr) => { + println!("Shielded Token {}/{}:", token_alias, sub_addr); + println!( + "No shielded {}/{} balance found for any wallet key", + token_alias, sub_addr + ); + + } + None => { + println!("Shielded Token {}:", token_alias, ); + println!( + "No shielded {} balance found for any wallet key", + token_alias + ); + } + } + } } } } From e71cd44b2ff3b55fa64a7ad29f6be0923881ee81 Mon Sep 17 00:00:00 2001 From: mariari Date: Tue, 25 Apr 2023 21:55:37 +0800 Subject: [PATCH 027/151] Update query_sheilded_balance to handle the multiple denominations --- apps/src/lib/client/rpc.rs | 109 ++++++++++++++++++++++++------------- 1 file changed, 70 insertions(+), 39 deletions(-) diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 1fd77f109c..dc8c406810 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -1131,67 +1131,98 @@ pub async fn query_shielded_balance( } // These are the asset types for which we have human-readable names - let mut read_tokens = HashSet::new(); + let mut read_tokens: HashMap>> = + HashMap::new(); // Print non-zero balances whose asset types can be decoded + // TODO Implement a function for this + + let mut balance_map = HashMap::new(); for (asset_type, balances) in balances { - // Decode the asset type let decoded = ctx .shielded .decode_asset_type(client.clone(), asset_type) .await; + match decoded { Some((addr, sub_prefix, denom, asset_epoch)) if asset_epoch == epoch => { - // Only assets with the current timestamp count - println!( - "Shielded Token {}{}:", - tokens - .get(&addr) - .cloned() - .unwrap_or_else(|| addr.clone()), - sub_prefix - .as_ref() - .map(|k| format!("/{}", k)) - .unwrap_or_default(), - ); - read_tokens.insert(addr.clone()); - let mut found_any = false; - for (fvk, value) in balances { - let value = token::Amount::from_masp_denominated( - value as u64, - denom, - ); - let formatted = format_denominated_amount( - &client, - &addr, - &sub_prefix, - value, - ) - .await; - println!(" {}, owned by {}", formatted, fvk); - found_any = true; - } - if !found_any { + // remove this from here, should not be making the + // hashtable creation any uglier + if balances.is_empty() { println!( "No shielded {} balance found for any wallet \ key", asset_type ); } + let asset_type = (addr, sub_prefix); + for (fvk, value) in balances { + let token_value = + token::Amount::from_masp_denominated( + value as u64, + denom, + ); + balance_map + .entry((fvk, asset_type.clone())) + .and_modify(|value_2| *value_2 += token_value) + .or_insert(token_value); + } } _ => {} - } + }; + } + for ((fvk, (addr, sub_prefix)), token_balance) in balance_map { + read_tokens + .entry(addr.clone()) + .and_modify(|addr_vec| addr_vec.push(sub_prefix.clone())) + .or_insert(vec![sub_prefix.clone()]); + // Only assets with the current timestamp count + println!( + "Shielded Token {}{}:", + tokens.get(&addr).cloned().unwrap_or_else(|| addr.clone()), + sub_prefix + .as_ref() + .map(|k| format!("/{}", k)) + .unwrap_or_default(), + ); + let formatted = format_denominated_amount( + &client, + &addr, + &sub_prefix, + token_balance, + ) + .await; + println!(" {}, owned by {}", formatted, fvk); } // Print zero balances for remaining assets for token in tokens { - if !read_tokens.contains(&token) { + if let Some(sub_addrs) = read_tokens.get(&token) { let token_alias = lookup_alias(ctx, &token); - println!("Shielded Token {}:", token_alias); - println!( - "No shielded {} balance found for any wallet key", - token_alias - ); + for sub_addr in sub_addrs { + match sub_addr { + // abstract out these prints + Some(sub_addr) => { + println!( + "Shielded Token {}/{}:", + token_alias, sub_addr + ); + println!( + "No shielded {}/{} balance found for any \ + wallet key", + token_alias, sub_addr + ); + } + None => { + println!("Shielded Token {}:", token_alias,); + println!( + "No shielded {} balance found for any \ + wallet key", + token_alias + ); + } + } + } } } } From 8de6a448c8176f053bf90134e81c9faf71ce524e Mon Sep 17 00:00:00 2001 From: satan Date: Tue, 25 Apr 2023 17:58:14 +0200 Subject: [PATCH 028/151] Starting replacing rust_decimal crate in PoS --- core/src/types/token.rs | 8 ++ proof_of_stake/src/lib.rs | 49 ++++--- proof_of_stake/src/parameters.rs | 33 ++--- proof_of_stake/src/rewards.rs | 42 +++--- proof_of_stake/src/tests.rs | 10 +- proof_of_stake/src/types.rs | 222 +++++++++++++++++++++++++------ 6 files changed, 252 insertions(+), 112 deletions(-) diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 7e7ea3b20e..fe16161a74 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -518,6 +518,14 @@ impl Add for Amount { } } +impl Add for Amount { + type Output = Self; + + fn add(self, rhs: u64) -> Self::Output { + Self { raw: self.raw + Uint::from(rhs)} + } +} + impl std::iter::Sum for Amount { fn sum>(iter: I) -> Self { iter.fold(Amount::zero(), |acc, amt| acc + amt) diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 612be66c25..75e84cd87c 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -44,22 +44,22 @@ use namada_core::types::key::{ }; pub use namada_core::types::storage::Epoch; use namada_core::types::token; -use namada_core::types::token::Amount; +use namada_core::types::token::{Amount, DenominatedAmount}; use once_cell::unsync::Lazy; use parameters::PosParams; use rewards::PosRewardsCalculator; -use rust_decimal::Decimal; use storage::{ bonds_for_source_prefix, bonds_prefix, consensus_keys_key, get_validator_address_from_bond, into_tm_voting_power, is_bond_key, is_unbond_key, is_validator_slashes_key, last_block_proposer_key, - mult_amount, mult_change_to_amount, num_consensus_validators_key, - params_key, slashes_prefix, unbonds_for_source_prefix, unbonds_prefix, + num_consensus_validators_key, params_key, slashes_prefix, + unbonds_for_source_prefix, unbonds_prefix, validator_address_raw_hash_key, validator_max_commission_rate_change_key, BondDetails, BondsAndUnbondsDetail, BondsAndUnbondsDetails, ReverseOrdTokenAmount, RewardsAccumulator, UnbondDetails, }; use thiserror::Error; +use namada_core::types::uint::Uint; use types::{ BelowCapacityValidatorSet, BelowCapacityValidatorSets, BondId, Bonds, CommissionRates, ConsensusValidator, ConsensusValidatorSet, @@ -70,7 +70,7 @@ use types::{ WeightedValidator, }; -use crate::types::decimal_mult_u128; +use crate::types::Dec; /// Address of the PoS account implemented as a native VP pub const ADDRESS: Address = Address::Internal(InternalAddress::PoS); @@ -168,9 +168,9 @@ pub enum SlashError { #[derive(Error, Debug)] pub enum CommissionRateChangeError { #[error("Unexpected negative commission rate {0} for validator {1}")] - NegativeRate(Decimal, Address), + NegativeRate(DenominatedAmount, Address), #[error("Rate change of {0} is too large for validator {1}")] - RateChangeTooLarge(Decimal, Address), + RateChangeTooLarge(DenominatedAmount, Address), #[error( "There is no maximum rate change written in storage for validator {0}" )] @@ -477,7 +477,7 @@ where pub fn read_validator_max_commission_rate_change( storage: &S, validator: &Address, -) -> storage_api::Result> +) -> storage_api::Result> where S: StorageRead, { @@ -489,7 +489,7 @@ where pub fn write_validator_max_commission_rate_change( storage: &mut S, validator: &Address, - change: Decimal, + change: DenominatedAmount, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, @@ -1585,8 +1585,8 @@ pub fn become_validator( address: &Address, consensus_key: &common::PublicKey, current_epoch: Epoch, - commission_rate: Decimal, - max_commission_rate_change: Decimal, + commission_rate: DenominatedAmount, + max_commission_rate_change: DenominatedAmount, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, @@ -1739,19 +1739,19 @@ where pub fn change_validator_commission_rate( storage: &mut S, validator: &Address, - new_rate: Decimal, + new_rate: DenominatedAmount, current_epoch: Epoch, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, { - if new_rate < Decimal::ZERO { - return Err(CommissionRateChangeError::NegativeRate( - new_rate, - validator.clone(), - ) - .into()); - } + // if new_rate < Uint::zero() { + // return Err(CommissionRateChangeError::NegativeRate( + // new_rate, + // validator.clone(), + // ) + // .into()); + // } let max_change = read_validator_max_commission_rate_change(storage, validator)?; @@ -2688,10 +2688,9 @@ where // Compute the fractional block rewards for each consensus validator and // update the reward accumulators - let consensus_stake_unscaled: Decimal = - total_consensus_stake.as_dec_unscaled(); - let signing_stake_unscaled: Decimal = total_signing_stake.as_dec_unscaled(); - let mut values: HashMap = HashMap::new(); + let consensus_stake_unscaled: Dec = total_consensus_stake.into(); + let signing_stake_unscaled: Dec = total_signing_stake.into(); + let mut values: HashMap= HashMap::new(); for validator in consensus_validators.iter(storage)? { let ( NestedSubKey::Data { @@ -2709,8 +2708,8 @@ where continue; } - let mut rewards_frac = Decimal::default(); - let stake_unscaled: Decimal = stake.as_dec_unscaled(); + let mut rewards_frac = Dec::zero(); + let stake_unscaled: Dec = stake.into(); // println!( // "NAMADA VALIDATOR STAKE (LOGGING BLOCK REWARDS) OF EPOCH {} = // {}", epoch, stake diff --git a/proof_of_stake/src/parameters.rs b/proof_of_stake/src/parameters.rs index 1422971089..9eb5b6de05 100644 --- a/proof_of_stake/src/parameters.rs +++ b/proof_of_stake/src/parameters.rs @@ -5,6 +5,7 @@ use rust_decimal::prelude::ToPrimitive; use rust_decimal::Decimal; use rust_decimal_macros::dec; use thiserror::Error; +use crate::types::Dec; /// Proof-of-Stake system parameters, set at genesis and can only be changed via /// governance @@ -23,22 +24,22 @@ pub struct PosParams { /// The voting power per fundamental unit of the staking token (namnam). /// Used in validators' voting power calculation to interface with /// tendermint. - pub tm_votes_per_token: Decimal, + pub tm_votes_per_token: Dec, /// Amount of tokens rewarded to a validator for proposing a block - pub block_proposer_reward: Decimal, + pub block_proposer_reward: Dec, /// Amount of tokens rewarded to each validator that voted on a block /// proposal - pub block_vote_reward: Decimal, + pub block_vote_reward: Dec, /// Maximum staking rewards rate per annum - pub max_inflation_rate: Decimal, + pub max_inflation_rate: Dec, /// Target ratio of staked NAM tokens to total NAM tokens - pub target_staked_ratio: Decimal, + pub target_staked_ratio: Dec, /// Fraction of validator's stake that should be slashed on a duplicate /// vote. - pub duplicate_vote_min_slash_rate: Decimal, + pub duplicate_vote_min_slash_rate: Dec, /// Fraction of validator's stake that should be slashed on a light client /// attack. - pub light_client_attack_min_slash_rate: Decimal, + pub light_client_attack_min_slash_rate: Dec, } impl Default for PosParams { @@ -49,17 +50,17 @@ impl Default for PosParams { unbonding_len: 21, // 1 voting power per 1 fundamental token (10^6 per NAM or 1 per // namnam) - tm_votes_per_token: dec!(1.0), - block_proposer_reward: dec!(0.125), - block_vote_reward: dec!(0.1), + tm_votes_per_token: Dec::one(), + block_proposer_reward: Dec::new(125, 3).expect("Test failed"), + block_vote_reward: Dec::new(1, 1).expect("Test failed"), // PoS inflation of 10% - max_inflation_rate: dec!(0.1), + max_inflation_rate: Dec::new(1, 1).expect("Test failed"), // target staked ratio of 2/3 - target_staked_ratio: dec!(0.6667), + target_staked_ratio: Dec::new(6667, 4).expect("Test failed"), // slash 0.1% - duplicate_vote_min_slash_rate: dec!(0.001), + duplicate_vote_min_slash_rate: Dec::new(1, 3).expect("Test failed"), // slash 0.1% - light_client_attack_min_slash_rate: dec!(0.001), + light_client_attack_min_slash_rate: Dec::new(1, 3).expect("Test failed"), } } } @@ -73,7 +74,7 @@ pub enum ValidationError { )] TotalVotingPowerTooLarge(u64), #[error("Votes per token cannot be greater than 1, got {0}")] - VotesPerTokenGreaterThanOne(Decimal), + VotesPerTokenGreaterThanOne(Dec), #[error("Pipeline length must be >= 2, got {0}")] PipelineLenTooShort(u64), #[error( @@ -188,7 +189,7 @@ pub mod testing { max_validator_slots, pipeline_len, unbonding_len, - tm_votes_per_token: Decimal::from(tm_votes_per_token) / dec!(10_000), + tm_votes_per_token: Dec(Uint::from(tm_votes_per_token)) / Dec(Uint::from(10_000)), // The rest of the parameters that are not being used in the PoS // VP are constant for now ..Default::default() diff --git a/proof_of_stake/src/rewards.rs b/proof_of_stake/src/rewards.rs index 6f830d9c52..e189cbc1a7 100644 --- a/proof_of_stake/src/rewards.rs +++ b/proof_of_stake/src/rewards.rs @@ -1,10 +1,12 @@ //! PoS rewards distribution. -use rust_decimal::Decimal; -use rust_decimal_macros::dec; use thiserror::Error; +use namada_core::types::token::{Amount, DenominatedAmount}; +use namada_core::types::uint::Uint; +use crate::types::Dec; -const MIN_PROPOSER_REWARD: Decimal = dec!(0.01); +/// This is equal to 0.01. +const MIN_PROPOSER_REWARD: Dec = Dec(Uint([10000u64, 0u64, 0u64, 0u64])); /// Errors during rewards calculation #[derive(Debug, Error)] @@ -16,8 +18,8 @@ pub enum RewardsError { least 2/3 of the total bonded stake)." )] InsufficientVotes { - votes_needed: u64, - signing_stake: u64, + votes_needed: Uint, + signing_stake: Uint, }, /// rewards coefficients are not set #[error("Rewards coefficients are not properly set.")] @@ -28,9 +30,9 @@ pub enum RewardsError { #[derive(Debug, Copy, Clone)] #[allow(missing_docs)] pub struct PosRewards { - pub proposer_coeff: Decimal, - pub signer_coeff: Decimal, - pub active_val_coeff: Decimal, + pub proposer_coeff: Dec, + pub signer_coeff: Dec, + pub active_val_coeff: Dec, } /// Holds relevant PoS parameters and is used to calculate the coefficients for @@ -38,13 +40,13 @@ pub struct PosRewards { #[derive(Debug, Copy, Clone)] pub struct PosRewardsCalculator { /// Rewards fraction that goes to the block proposer - pub proposer_reward: Decimal, + pub proposer_reward: Dec, /// Rewards fraction that goes to the block signers - pub signer_reward: Decimal, + pub signer_reward: Dec, /// Total stake of validators who signed the block - pub signing_stake: u64, + pub signing_stake: Amount, /// Total stake of the whole consensus set - pub total_stake: u64, + pub total_stake: Amount, } impl PosRewardsCalculator { @@ -64,18 +66,18 @@ impl PosRewardsCalculator { if signing_stake < votes_needed { return Err(RewardsError::InsufficientVotes { - votes_needed, - signing_stake, + votes_needed: votes_needed.into(), + signing_stake: signing_stake.into(), }); } // Logic for determining the coefficients. - let proposer_coeff = proposer_reward - * Decimal::from(signing_stake - votes_needed) - / Decimal::from(total_stake) + let proposer_coeff = Dec::from(proposer_reward + * (signing_stake - votes_needed)) + / Dec::from(total_stake) + MIN_PROPOSER_REWARD; let signer_coeff = signer_reward; - let active_val_coeff = dec!(1.0) - proposer_coeff - signer_coeff; + let active_val_coeff = Dec::one() - proposer_coeff - signer_coeff; let coeffs = PosRewards { proposer_coeff, @@ -87,7 +89,7 @@ impl PosRewardsCalculator { } /// Implement as ceiling of (2/3) * validator set stake - fn get_min_required_votes(&self) -> u64 { - ((2 * self.total_stake) + 3 - 1) / 3 + fn get_min_required_votes(&self) -> Amount { + ((self.total_stake * 2u64) + (3u64 - 1u64)) / 3u64 } } diff --git a/proof_of_stake/src/tests.rs b/proof_of_stake/src/tests.rs index fd24ce2042..e7a1ef4d75 100644 --- a/proof_of_stake/src/tests.rs +++ b/proof_of_stake/src/tests.rs @@ -29,11 +29,7 @@ use test_log::test; use crate::parameters::testing::arb_pos_params; use crate::parameters::PosParams; -use crate::types::{ - into_tm_voting_power, BondDetails, BondId, BondsAndUnbondsDetails, - ConsensusValidator, GenesisValidator, Position, ReverseOrdTokenAmount, - UnbondDetails, ValidatorSetUpdate, ValidatorState, WeightedValidator, -}; +use crate::types::{into_tm_voting_power, BondDetails, BondId, BondsAndUnbondsDetails, ConsensusValidator, GenesisValidator, Position, ReverseOrdTokenAmount, UnbondDetails, ValidatorSetUpdate, ValidatorState, WeightedValidator, Dec}; use crate::{ become_validator, below_capacity_validator_set_handle, bond_handle, bond_tokens, bonds_and_unbonds, consensus_validator_set_handle, @@ -1708,8 +1704,8 @@ fn arb_genesis_validators( let consensus_sk = common_sk_from_simple_seed(seed); let consensus_key = consensus_sk.to_public(); - let commission_rate = Decimal::new(5, 2); - let max_commission_rate_change = Decimal::new(1, 3); + let commission_rate = Dec::new(5, 2).expect("Test failed"); + let max_commission_rate_change = Dec::new(1, 3).expect("Test failed"); GenesisValidator { address, tokens, diff --git a/proof_of_stake/src/types.rs b/proof_of_stake/src/types.rs index fee3197c71..3d0ae8e489 100644 --- a/proof_of_stake/src/types.rs +++ b/proof_of_stake/src/types.rs @@ -5,7 +5,7 @@ mod rev_order; use core::fmt::Debug; use std::collections::{HashMap, HashSet}; use std::convert::TryFrom; -use std::fmt::Display; +use std::fmt::{Display, Formatter, Write}; use std::hash::Hash; use std::ops::Sub; @@ -21,8 +21,7 @@ use namada_core::types::storage::{Epoch, KeySeg}; use namada_core::types::token; use namada_core::types::token::{Amount, NATIVE_MAX_DECIMAL_PLACES}; pub use rev_order::ReverseOrdTokenAmount; -use rust_decimal::prelude::{Decimal, ToPrimitive}; -use rust_decimal::RoundingStrategy; +use namada_core::types::uint::Uint; use crate::parameters::PosParams; @@ -124,7 +123,7 @@ pub type TotalDeltas = crate::epoched::EpochedDelta< /// Epoched validator commission rate pub type CommissionRates = - crate::epoched::Epoched; + crate::epoched::Epoched; /// Epoched validator's bonds pub type Bonds = crate::epoched::EpochedDelta< @@ -143,17 +142,17 @@ pub type ConsensusKeys = LazySet; /// Commission rate and max commission rate change per epoch for a validator pub struct CommissionPair { /// Validator commission rate - pub commission_rate: Decimal, + pub commission_rate: Dec, /// Validator max commission rate change per epoch - pub max_commission_change_per_epoch: Decimal, + pub max_commission_change_per_epoch: Dec, } /// Epoched rewards products -pub type RewardsProducts = LazyMap; +pub type RewardsProducts = LazyMap; /// Consensus validator rewards accumulator (for tracking the fractional block /// rewards owed over the course of an epoch) -pub type RewardsAccumulator = LazyMap; +pub type RewardsAccumulator = LazyMap; // -------------------------------------------------------------------------------------------- @@ -177,9 +176,9 @@ pub struct GenesisValidator { /// A public key used for signing validator's consensus actions pub consensus_key: common::PublicKey, /// Commission rate charged on rewards for delegators (bounded inside 0-1) - pub commission_rate: Decimal, + pub commission_rate: Dec, /// Maximum change in commission rate permitted per epoch - pub max_commission_rate_change: Decimal, + pub max_commission_rate_change: Dec, } /// An update of the consensus and below-capacity validator set. @@ -446,7 +445,7 @@ impl Display for BondId { impl SlashType { /// Get the slash rate applicable to the given slash type from the PoS /// parameters. - pub fn get_slash_rate(&self, params: &PosParams) -> Decimal { + pub fn get_slash_rate(&self, params: &PosParams) -> Dec { match self { SlashType::DuplicateVote => params.duplicate_vote_min_slash_rate, SlashType::LightClientAttack => { @@ -465,51 +464,186 @@ impl Display for SlashType { } } -/// Multiply a value of type Decimal with one of type u64 and then return the -/// truncated u64 -pub fn decimal_mult_u128(dec: Decimal, int: u128) -> u128 { - let prod = dec * Decimal::from(int); - // truncate the number to the floor - prod.to_u128().expect("Product is out of bounds") +// -------------------------------------------------------------------------------------------- + +/// The numbrer of Dec places for PoS rational calculations +pub const POS_DECIMAL_PRECISION: u8 = 6; + +/// A 256 bit number with [`POS_DECIMAL_PRECISION`] number of Dec places. +/// +/// To be precise, an instance X of this type should be interpeted as the Dec +/// X * 10 ^ (-[`POS_DECIMAL_PRECISION`]) +#[derive( + Clone, + Copy, + Default, + BorshSerialize, + BorshDeserialize, + BorshSchema, + PartialEq, + Eq, + PartialOrd, + Ord, + Debug, + Hash, +)] +pub struct Dec(pub Uint); + +impl std::ops::Deref for Dec { + type Target = Uint; + + fn deref(&self) -> &Self::Target { + &self.0 + } } -/// Multiply a value of type Decimal with one of type i128 and then return the -/// truncated i128 -pub fn decimal_mult_i128(dec: Decimal, int: i128) -> i128 { - let prod = dec * Decimal::from(int); - // truncate the number to the floor - prod.to_i128().expect("Product is out of bounds") +impl std::ops::DerefMut for Dec { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } } -/// Multiply a value of type Decimal with one of type Uint and then convert it -/// to an Amount type -pub fn mult_change_to_amount( - dec: Decimal, - change: token::Change, -) -> token::Amount { - // this function is used for slashing calculations. We want to err - // on the side of slashing more, not less. - let dec = - dec.round_dp_with_strategy(6, RoundingStrategy::ToPositiveInfinity); - Amount::from_decimal(dec, NATIVE_MAX_DECIMAL_PLACES).unwrap() * change.abs() +impl Dec { + pub fn trunc_div(&self, rhs: &Self) -> Option { + self.0.fixed_precision_div(rhs, POS_DECIMAL_PRECISION) + .map(Self) + } + + pub fn zero() -> Self { + Self(Uint::zero()) + } + + pub fn one() -> Self { + Self(Uint::one()) + } + + pub fn new(matissa: u64, scale: u8) -> Option { + if scale > POS_DECIMAL_PRECISION { + None + } else { + Uint::exp10((POS_DECIMAL_PRECISION - scale) as usize) + .checked_mul(Uint::from(matissa)) + .map(Self) + } + } +} + +impl From for Dec { + fn from(amt: Amount) -> Self { + Self(amt.into()) + } } -/// Multiply a value of type Decimal with one of type Amount and then return the -/// truncated Amount -pub fn mult_amount(dec: Decimal, amount: token::Amount) -> token::Amount { - let dec = - dec.round_dp_with_strategy(6, RoundingStrategy::ToPositiveInfinity); - Amount::from_decimal(dec, NATIVE_MAX_DECIMAL_PLACES).unwrap() * amount +impl std::ops::Add for Dec { + type Output = Self; + + fn add(self, rhs: Dec) -> Self::Output { + Self(self.0 + rhs.0) + } } +impl std::ops::Add for Dec { + type Output = Self; + + fn add(self, rhs: u64) -> Self::Output { + Self(self.0 + Uint::from(rhs)) + } +} + +impl std::ops::Sub for Dec { + type Output = Self; + + fn sub(self, rhs: Dec) -> Self::Output { + Self(self.0 - rhs.0) + } +} + +impl std::ops::Mul for Dec { + type Output = Dec; + + fn mul(self, rhs: u128) -> Self::Output { + Self(self.0 * Uint::from(rhs)) + } +} + +impl std::ops::Mul for Dec { + type Output = Amount; + + fn mul(self, rhs: Amount) -> Self::Output { + self.0 * rhs + } +} + +impl std::ops::Mul for Dec { + type Output = Self; + + fn mul(self, rhs: Dec) -> Self::Output { + Self(self.0 * rhs.0) + } +} + +impl std::ops::Div for Dec { + type Output = Self; + + fn div(self, rhs: Dec) -> Self::Output { + Self(self.0 / rhs.0) + } +} + +impl Display for Dec { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let string = self.0.to_string(); + f.write_str(&string) + } +} + +// -------------------------------------------------------------------------------------------- + +// /// Multiply a value of type Dec with one of type u64 and then return the +// /// truncated u64 +// pub fn decimal_mult_u128(dec: Dec, int: u128) -> u128 { +// let prod = dec * Dec::from(int); +// // truncate the number to the floor +// prod.to_u128().expect("Product is out of bounds") +// } +// +// /// Multiply a value of type Dec with one of type i128 and then return the +// /// truncated i128 +// pub fn decimal_mult_i128(dec: Dec, int: i128) -> i128 { +// let prod = dec * Dec::from(int); +// // truncate the number to the floor +// prod.to_i128().expect("Product is out of bounds") +// } + +// /// Multiply a value of type Dec with one of type Uint and then convert it +// /// to an Amount type +// pub fn mult_change_to_amount( +// dec: Dec, +// change: token::Change, +// ) -> token::Amount { +// // this function is used for slashing calculations. We want to err +// // on the side of slashing more, not less. +// let dec = +// dec.round_dp_with_strategy(6, RoundingStrategy::ToPositiveInfinity); +// Amount::from_Dec(dec, NATIVE_MAX_Dec_PLACES).unwrap() * change.abs() +// } + +// /// Multiply a value of type Dec with one of type Amount and then return the +// /// truncated Amount +// pub fn mult_amount(dec: Dec, amount: token::Amount) -> token::Amount { +// let dec = +// dec.round_dp_with_strategy(6, RoundingStrategy::ToPositiveInfinity); +// Amount::from_Dec(dec, NATIVE_MAX_Dec_PLACES).unwrap() * amount +// } + /// Calculate voting power in the tendermint context (which is stored as i64) /// from the number of tokens pub fn into_tm_voting_power( - votes_per_token: Decimal, - tokens: impl Into, + votes_per_token: Dec, + tokens: Amount, ) -> i64 { - let prod = decimal_mult_u128(votes_per_token, tokens.into() as u128); - i64::try_from(prod).expect("Invalid voting power") + let pow = votes_per_token * u128::try_from(tokens).expect("Voting power out of bounds"); + i64::try_from(pow.0).expect("Invalid voting power") } #[cfg(test)] From 8920d0ad3949b98202990e5a43130bbfadfc6622 Mon Sep 17 00:00:00 2001 From: brentstone Date: Tue, 25 Apr 2023 16:28:27 -0400 Subject: [PATCH 029/151] WIP continue replacing rust_decimal crate in PoS --- Cargo.lock | 1 - apps/src/lib/cli.rs | 16 +- apps/src/lib/client/tx.rs | 90 +++--- .../lib/node/ledger/shell/finalize_block.rs | 89 +++--- core/src/types/dec.rs | 271 ++++++++++++++++++ core/src/types/mod.rs | 1 + core/src/types/token.rs | 15 +- core/src/types/transaction/mod.rs | 6 +- core/src/types/transaction/pos.rs | 4 +- core/src/types/uint.rs | 33 ++- proof_of_stake/src/lib.rs | 95 +++--- proof_of_stake/src/parameters.rs | 34 ++- proof_of_stake/src/rewards.rs | 14 +- proof_of_stake/src/tests.rs | 64 ++--- proof_of_stake/src/tests/state_machine.rs | 6 +- proof_of_stake/src/types.rs | 148 +--------- shared/src/ledger/inflation.rs | 33 +-- shared/src/ledger/pos/mod.rs | 13 +- shared/src/types/mod.rs | 4 +- tx_prelude/Cargo.toml | 1 - tx_prelude/src/proof_of_stake.rs | 4 +- 21 files changed, 538 insertions(+), 404 deletions(-) create mode 100644 core/src/types/dec.rs diff --git a/Cargo.lock b/Cargo.lock index ff1836db36..8bba1dde6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4078,7 +4078,6 @@ dependencies = [ "namada_macros", "namada_proof_of_stake", "namada_vm_env", - "rust_decimal", "sha2 0.10.6", "thiserror", ] diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 6cfb99269d..26babc1538 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -1622,6 +1622,7 @@ pub mod args { use namada::ibc::core::ics24_host::identifier::{ChannelId, PortId}; use namada::types::address::Address; use namada::types::chain::{ChainId, ChainIdPrefix}; + use namada::types::dec::Dec; use namada::types::key::*; use namada::types::masp::MaspValue; use namada::types::storage::{self, BlockHeight, Epoch}; @@ -1629,7 +1630,6 @@ pub mod args { use namada::types::token; use namada::types::token::NATIVE_MAX_DECIMAL_PLACES; use namada::types::transaction::GasLimit; - use rust_decimal::Decimal; use super::context::*; use super::utils::*; @@ -1662,7 +1662,7 @@ pub mod args { const CHANNEL_ID: Arg = arg("channel-id"); const CODE_PATH: Arg = arg("code-path"); const CODE_PATH_OPT: ArgOpt = CODE_PATH.opt(); - const COMMISSION_RATE: Arg = arg("commission-rate"); + const COMMISSION_RATE: Arg = arg("commission-rate"); const CONSENSUS_TIMEOUT_COMMIT: ArgDefault = arg_default( "consensus-timeout-commit", DefaultFn(|| Timeout::from_str("1s").unwrap()), @@ -1709,7 +1709,7 @@ pub mod args { const LEDGER_ADDRESS: Arg = arg("node"); const LOCALHOST: ArgFlag = flag("localhost"); const MASP_VALUE: Arg = arg("value"); - const MAX_COMMISSION_RATE_CHANGE: Arg = + const MAX_COMMISSION_RATE_CHANGE: Arg = arg("max-commission-rate-change"); const MODE: ArgOpt = arg_opt("mode"); const NET_ADDRESS: Arg = arg("net-address"); @@ -2183,8 +2183,8 @@ pub mod args { pub account_key: Option, pub consensus_key: Option, pub protocol_key: Option, - pub commission_rate: Decimal, - pub max_commission_rate_change: Decimal, + pub commission_rate: Dec, + pub max_commission_rate_change: Dec, pub validator_vp_code_path: Option, pub unsafe_dont_encrypt: bool, } @@ -2893,7 +2893,7 @@ pub mod args { /// Validator address (should be self) pub validator: WalletAddress, /// Value to which the tx changes the commission rate - pub rate: Decimal, + pub rate: Dec, } impl Args for TxCommissionRateChange { @@ -3674,8 +3674,8 @@ pub mod args { #[derive(Clone, Debug)] pub struct InitGenesisValidator { pub alias: String, - pub commission_rate: Decimal, - pub max_commission_rate_change: Decimal, + pub commission_rate: Dec, + pub max_commission_rate_change: Dec, pub net_address: SocketAddr, pub unsafe_dont_encrypt: bool, pub key_scheme: SchemeType, diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 43093543a0..97e97094cf 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -42,6 +42,7 @@ use namada::ledger::masp; use namada::ledger::pos::{CommissionPair, PosParams}; use namada::proto::Tx; use namada::types::address::{masp, masp_tx_key, Address}; +use namada::types::dec::Dec; use namada::types::governance::{ OfflineProposal, OfflineVote, Proposal, ProposalVote, VoteType, }; @@ -61,7 +62,6 @@ use namada::types::transaction::governance::{ use namada::types::transaction::{pos, InitAccount, InitValidator, UpdateVp}; use namada::types::{storage, token}; use rand_core::{CryptoRng, OsRng, RngCore}; -use rust_decimal::Decimal; use sha2::Digest; use tokio::time::{Duration, Instant}; @@ -332,7 +332,7 @@ pub async fn submit_init_validator( .unwrap(); // Validate the commission rate data - if commission_rate > Decimal::ONE || commission_rate < Decimal::ZERO { + if commission_rate > Dec::one() || commission_rate < Dec::zero() { eprintln!( "The validator commission rate must not exceed 1.0 or 100%, and \ it must be 0 or positive" @@ -341,8 +341,8 @@ pub async fn submit_init_validator( safe_exit(1) } } - if max_commission_rate_change > Decimal::ONE - || max_commission_rate_change < Decimal::ZERO + if max_commission_rate_change > Dec::one() + || max_commission_rate_change < Dec::zero() { eprintln!( "The validator maximum change in commission rate per epoch must \ @@ -1365,16 +1365,18 @@ fn convert_amount( val: &token::Amount, ) -> ([AssetType; 4], Amount) { let mut amount = Amount::zero(); - let asset_types: [AssetType; 4] = MaspDenom::iter().map(|denom| { - let asset_type = make_asset_type(epoch, token, sub_prefix, denom); - // Combine the value and unit into one amount - amount += Amount::from_nonnegative(asset_type, denom.denominate(val)) - .expect("invalid value for amount"); - asset_type - }) - .collect() - .try_into() - .expect("This can't fail"); + let asset_types: [AssetType; 4] = MaspDenom::iter() + .map(|denom| { + let asset_type = make_asset_type(epoch, token, sub_prefix, denom); + // Combine the value and unit into one amount + amount += + Amount::from_nonnegative(asset_type, denom.denominate(val)) + .expect("invalid value for amount"); + asset_type + }) + .collect() + .try_into() + .expect("This can't fail"); (asset_types, amount) } @@ -1461,7 +1463,11 @@ async fn gen_shielded_transfer( &fee.amount, ); builder.set_fee(shielded_fee.clone())?; - let required_amt = if shielded_gas { amount + shielded_fee } else { amount }; + let required_amt = if shielded_gas { + amount + shielded_fee + } else { + amount + }; // Locate unspent notes that can help us meet the transaction amount let (_, unspent_notes, used_convs) = ctx .shielded @@ -1474,12 +1480,7 @@ async fn gen_shielded_transfer( .await; // Commit the notes found to our transaction for (diversifier, note, merkle_path) in unspent_notes { - builder.add_sapling_spend( - sk, - diversifier, - note, - merkle_path, - )?; + builder.add_sapling_spend(sk, diversifier, note, merkle_path)?; } // Commit the conversion notes used during summation for (conv, wit, value) in used_convs.values() { @@ -1498,10 +1499,9 @@ async fn gen_shielded_transfer( // We add a dummy UTXO to our transaction, but only the source of // the parent Transfer object is used to validate fund // availability - let secp_sk = secp256k1::SecretKey::from_slice(&[0xcd; 32]) - .expect("secret key"); - let secp_ctx = - secp256k1::Secp256k1::::gen_new(); + let secp_sk = + secp256k1::SecretKey::from_slice(&[0xcd; 32]).expect("secret key"); + let secp_ctx = secp256k1::Secp256k1::::gen_new(); let secp_pk = secp256k1::PublicKey::from_secret_key(&secp_ctx, &secp_sk) .serialize(); @@ -1535,25 +1535,25 @@ async fn gen_shielded_transfer( )?; } } else { - // Embed the transparent target address into the shielded - // transaction so that it can be signed - let target = ctx.get(&args.target); - let target_enc = target - .address() - .expect("target address should be transparent") - .try_to_vec() - .expect("target address encoding"); - let hash = ripemd160::Ripemd160::digest(&sha2::Sha256::digest( - target_enc.as_ref(), - )); - for (denom, asset_type) in MaspDenom::iter().zip(asset_types.iter()) { - builder.add_transparent_output( - &TransparentAddress::PublicKey(hash.into()), - *asset_type, - denom.denominate(&amt), - )?; - } + // Embed the transparent target address into the shielded + // transaction so that it can be signed + let target = ctx.get(&args.target); + let target_enc = target + .address() + .expect("target address should be transparent") + .try_to_vec() + .expect("target address encoding"); + let hash = ripemd160::Ripemd160::digest(&sha2::Sha256::digest( + target_enc.as_ref(), + )); + for (denom, asset_type) in MaspDenom::iter().zip(asset_types.iter()) { + builder.add_transparent_output( + &TransparentAddress::PublicKey(hash.into()), + *asset_type, + denom.denominate(&amt), + )?; } + } // Build and return the constructed transaction builder @@ -2912,7 +2912,7 @@ pub async fn submit_validator_commission_change( let validator = ctx.get(&args.validator); if rpc::is_validator(&client, &validator).await { - if args.rate < Decimal::ZERO || args.rate > Decimal::ONE { + if args.rate < Dec::zero() || args.rate > Dec::one() { eprintln!("Invalid new commission rate, received {}", args.rate); if !args.tx.force { safe_exit(1) @@ -2932,7 +2932,7 @@ pub async fn submit_validator_commission_change( commission_rate, max_commission_change_per_epoch, }) => { - if (args.rate - commission_rate).abs() + if args.rate.abs_diff(&commission_rate) > max_commission_change_per_epoch { eprintln!( diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index e1b3665be2..ffb3db21fe 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -5,9 +5,7 @@ use std::collections::HashMap; use data_encoding::HEXUPPER; use namada::ledger::parameters::storage as params_storage; use namada::ledger::pos::types::into_tm_voting_power; -use namada::ledger::pos::{ - decimal_mult_u128, namada_proof_of_stake, staking_token_address, -}; +use namada::ledger::pos::{namada_proof_of_stake, staking_token_address}; use namada::ledger::storage::EPOCH_SWITCH_BLOCKS_DELAY; use namada::ledger::storage_api::token::credit_tokens; use namada::ledger::storage_api::{StorageRead, StorageWrite}; @@ -20,10 +18,10 @@ use namada::proof_of_stake::{ write_last_block_proposer_address, }; use namada::types::address::Address; +use namada::types::dec::Dec; use namada::types::key::tm_raw_hash_to_string; use namada::types::storage::{BlockHash, BlockResults, Epoch, Header}; use namada::types::token::{total_supply_key, Amount}; -use rust_decimal::prelude::Decimal; use super::governance::execute_governance_proposals; use super::*; @@ -616,14 +614,14 @@ where let epochs_per_year: u64 = self .read_storage_key(¶ms_storage::get_epochs_per_year_key()) .expect("Epochs per year should exist in storage"); - let pos_p_gain_nom: Decimal = self + let pos_p_gain_nom: Dec = self .read_storage_key(¶ms_storage::get_pos_gain_p_key()) .expect("PoS P-gain factor should exist in storage"); - let pos_d_gain_nom: Decimal = self + let pos_d_gain_nom: Dec = self .read_storage_key(¶ms_storage::get_pos_gain_d_key()) .expect("PoS D-gain factor should exist in storage"); - let pos_last_staked_ratio: Decimal = self + let pos_last_staked_ratio: Dec = self .read_storage_key(¶ms_storage::get_staked_ratio_key()) .expect("PoS staked ratio should exist in storage"); let pos_last_inflation_amount: u64 = self @@ -642,12 +640,12 @@ where // TODO: properly fetch these values (arbitrary for now) let masp_locked_supply: Amount = Amount::default(); - let masp_locked_ratio_target = Decimal::new(5, 1); - let masp_locked_ratio_last = Decimal::new(5, 1); - let masp_max_inflation_rate = Decimal::new(2, 1); - let masp_last_inflation_rate = Decimal::new(12, 2); - let masp_p_gain = Decimal::new(1, 1); - let masp_d_gain = Decimal::new(1, 1); + let masp_locked_ratio_target = Dec::new(5, 1); + let masp_locked_ratio_last = Dec::new(5, 1); + let masp_max_inflation_rate = Dec::new(2, 1); + let masp_last_inflation_rate = Dec::new(12, 2); + let masp_p_gain = Dec::new(1, 1); + let masp_d_gain = Dec::new(1, 1); // Run rewards PD controller let pos_controller = inflation::RewardsController { @@ -707,15 +705,14 @@ where // // TODO: think about changing the reward to Decimal let mut reward_tokens_remaining = inflation; - let mut new_rewards_products: HashMap = + let mut new_rewards_products: HashMap = HashMap::new(); for acc in rewards_accumulator_handle().iter(&self.wl_storage)? { let (address, value) = acc?; // Get reward token amount for this validator - let fractional_claim = - value / Decimal::from(num_blocks_in_last_epoch); - let reward = decimal_mult_u128(fractional_claim, inflation as u128); + let fractional_claim = value / Dec::from(num_blocks_in_last_epoch); + let reward = fractional_claim * inflation; // Get validator data at the last epoch let stake = read_validator_stake( @@ -724,25 +721,25 @@ where &address, last_epoch, )? - .map(|v| Decimal::try_from(v).unwrap_or_default()) + .map(Dec::from) .unwrap_or_default(); let last_rewards_product = validator_rewards_products_handle(&address) .get(&self.wl_storage, &last_epoch)? - .unwrap_or(Decimal::ONE); + .unwrap_or(Dec::one()); let last_delegation_product = delegator_rewards_products_handle(&address) .get(&self.wl_storage, &last_epoch)? - .unwrap_or(Decimal::ONE); + .unwrap_or(Dec::one()); let commission_rate = validator_commission_rate_handle(&address) .get(&self.wl_storage, last_epoch, ¶ms)? .expect("Should be able to find validator commission rate"); - let new_product = last_rewards_product - * (Decimal::ONE + Decimal::from(reward) / stake); + let new_product = + last_rewards_product * (Dec::one() + Dec::from(reward) / stake); let new_delegation_product = last_delegation_product - * (Decimal::ONE - + (Decimal::ONE - commission_rate) * Decimal::from(reward) + * (Dec::one() + + (Dec::one() - commission_rate) * Dec::from(reward) / stake); new_rewards_products .insert(address, (new_product, new_delegation_product)); @@ -768,8 +765,7 @@ where let staking_token = staking_token_address(&self.wl_storage); // Mint tokens to the PoS account for the last epoch's inflation - let pos_reward_tokens = - Amount::from_uint(inflation - reward_tokens_remaining, 0).unwrap(); + let pos_reward_tokens = inflation - reward_tokens_remaining; tracing::info!( "Minting tokens for PoS rewards distribution into the PoS \ account. Amount: {}.", @@ -782,7 +778,7 @@ where pos_reward_tokens, )?; - if reward_tokens_remaining > 0 { + if reward_tokens_remaining > token::Amount::zero() { let amount = Amount::from_uint(reward_tokens_remaining, 0).unwrap(); tracing::info!( "Minting tokens remaining from PoS rewards distribution into \ @@ -907,6 +903,7 @@ mod test_finalize_block { rewards_accumulator_handle, validator_consensus_key_handle, validator_rewards_products_handle, }; + use namada::types::dec::POS_DECIMAL_PRECISION; use namada::types::governance::ProposalVote; use namada::types::key::tm_consensus_key_raw_hash; use namada::types::storage::Epoch; @@ -1509,18 +1506,18 @@ mod test_finalize_block { let rewards_prod_3 = validator_rewards_products_handle(&val3.address); let rewards_prod_4 = validator_rewards_products_handle(&val4.address); - let is_decimal_equal_enough = - |target: Decimal, to_compare: Decimal| -> bool { - // also return false if to_compare > target since this should - // never happen for the use cases - if to_compare < target { - let tolerance = Decimal::new(1, 9); - let res = Decimal::ONE - to_compare / target; - res < tolerance - } else { - to_compare == target - } - }; + let is_decimal_equal_enough = |target: Dec, to_compare: Dec| -> bool { + // also return false if to_compare > target since this should + // never happen for the use cases + if to_compare < target { + let tolerance = Dec::new(1, POS_DECIMAL_PRECISION) + .expect("Dec creation failed"); + let res = Dec::one() - to_compare / target; + res < tolerance + } else { + to_compare == target + } + }; // NOTE: Want to manually set the block proposer and the vote // information in a FinalizeBlock object. In non-abcipp mode, @@ -1553,7 +1550,7 @@ mod test_finalize_block { // Val1 was the proposer, so its reward should be larger than all // others, which should themselves all be equal let acc_sum = get_rewards_sum(&shell.wl_storage); - assert!(is_decimal_equal_enough(Decimal::ONE, acc_sum)); + assert!(is_decimal_equal_enough(Dec::one(), acc_sum)); let acc = get_rewards_acc(&shell.wl_storage); assert_eq!(acc.get(&val2.address), acc.get(&val3.address)); assert_eq!(acc.get(&val2.address), acc.get(&val4.address)); @@ -1572,7 +1569,7 @@ mod test_finalize_block { // should be the same as val1 now. Val3 and val4 should be equal as // well. let acc_sum = get_rewards_sum(&shell.wl_storage); - assert!(is_decimal_equal_enough(Decimal::TWO, acc_sum)); + assert!(is_decimal_equal_enough(Dec::two(), acc_sum)); let acc = get_rewards_acc(&shell.wl_storage); assert_eq!(acc.get(&val1.address), acc.get(&val2.address)); assert_eq!(acc.get(&val3.address), acc.get(&val4.address)); @@ -1684,7 +1681,7 @@ mod test_finalize_block { assert!(rp3 > rp4); } - fn get_rewards_acc(storage: &S) -> HashMap + fn get_rewards_acc(storage: &S) -> HashMap where S: StorageRead, { @@ -1692,18 +1689,18 @@ mod test_finalize_block { .iter(storage) .unwrap() .map(|elem| elem.unwrap()) - .collect::>() + .collect::>() } - fn get_rewards_sum(storage: &S) -> Decimal + fn get_rewards_sum(storage: &S) -> Dec where S: StorageRead, { let acc = get_rewards_acc(storage); if acc.is_empty() { - Decimal::ZERO + Dec::zero() } else { - acc.iter().fold(Decimal::default(), |sum, elm| sum + *elm.1) + acc.iter().fold(Dec::zero(), |sum, elm| sum + *elm.1) } } diff --git a/core/src/types/dec.rs b/core/src/types/dec.rs new file mode 100644 index 0000000000..0b7b0f20b1 --- /dev/null +++ b/core/src/types/dec.rs @@ -0,0 +1,271 @@ +//! A non-negative fixed precision decimal type for computation primarily in the +//! PoS module. +use core::fmt::{Debug, Formatter}; +use std::fmt::Display; +use std::ops::{Add, AddAssign, Div, Mul, Sub}; +use std::str::FromStr; + +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::types::token::{Amount, Change}; +use crate::types::uint::Uint; + +/// The number of Dec places for PoS rational calculations +pub const POS_DECIMAL_PRECISION: u8 = 6; + +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum Error { + #[error("{error}")] + First { error: String }, +} + +/// A 256 bit number with [`POS_DECIMAL_PRECISION`] number of Dec places. +/// +/// To be precise, an instance X of this type should be interpeted as the Dec +/// X * 10 ^ (-[`POS_DECIMAL_PRECISION`]) +#[derive( + Clone, + Copy, + Default, + BorshSerialize, + BorshDeserialize, + Serialize, + Deserialize, + BorshSchema, + PartialEq, + Eq, + PartialOrd, + Ord, + Debug, + Hash, +)] +pub struct Dec(pub Uint); + +impl std::ops::Deref for Dec { + type Target = Uint; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for Dec { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Dec { + /// Division with truncation (TDO: better description) + pub fn trunc_div(&self, rhs: &Self) -> Option { + self.0 + .fixed_precision_div(rhs, POS_DECIMAL_PRECISION) + .map(Self) + } + + /// The representation of 0 + pub fn zero() -> Self { + Self(Uint::zero()) + } + + /// The representation of 1 + pub fn one() -> Self { + Self(Uint::one()) + } + + /// The representation of 2 + pub fn two() -> Self { + Self(Uint::one() + Uint::one()) + } + + /// Create a new [`Dec`] using a mantissa and a scale. + pub fn new(mantissa: u64, scale: u8) -> Option { + if scale > POS_DECIMAL_PRECISION { + None + } else { + Uint::exp10((POS_DECIMAL_PRECISION - scale) as usize) + .checked_mul(Uint::from(mantissa)) + .map(Self) + } + } + + /// Get the non-negative difference between two [`Dec`]s. + pub fn abs_diff(&self, other: &Self) -> Self { + if self > other { + *self - *other + } else { + *other - *self + } + } +} + +// TODO: improve (actualyl do) error handling! +impl FromStr for Dec { + type Err = self::Error; + + fn from_str(s: &str) -> Result { + if s.starts_with('-') { + return Err(self::Error::First { + error: "Dec cannot be negative".to_string(), + }); + } + if let Some((large, mut small)) = s.split_once('.') { + let num_large = + u64::from_str(large).map_err(|_| self::Error::First { + error: "Error".to_string(), + })?; + let mut num_small = + u64::from_str(small).map_err(|_| self::Error::First { + error: "Error".to_string(), + })?; + + if num_small == 0u64 { + return Ok(Dec(Uint::from(num_large))); + } + + small = small.trim_end_matches('0'); + let mut num_dec_places = small.len(); + if num_dec_places > POS_DECIMAL_PRECISION as usize { + // truncate to the first `POS_DECIMAL_PRECISION` places + num_dec_places = POS_DECIMAL_PRECISION as usize; + small = &small[..POS_DECIMAL_PRECISION as usize]; + num_small = + u64::from_str(small).map_err(|_| self::Error::First { + error: "Error".to_string(), + })?; + } + if num_large == 0u64 { + return Ok(Dec::new(num_small, num_dec_places as u8) + .expect("Dec creation failed")); + } + let tot_num = format!("{}{}", num_large, num_small); + let tot_num = u64::from_str(tot_num.as_str()).map_err(|_| { + self::Error::First { + error: "Error".to_string(), + } + })?; + Ok(Dec::new(tot_num, num_dec_places as u8) + .expect("Dec creation failed")) + } else { + Err(self::Error::First { + error: "Error".to_string(), + }) + } + } +} + +impl From for Dec { + fn from(amt: Amount) -> Self { + Self(amt.into()) + } +} + +impl From for Dec { + fn from(num: u64) -> Self { + Self(Uint::from(num * 10u64.pow(POS_DECIMAL_PRECISION as u32))) + } +} + +impl Add for Dec { + type Output = Self; + + fn add(self, rhs: Dec) -> Self::Output { + Self(self.0 + rhs.0) + } +} + +impl Add for Dec { + type Output = Self; + + fn add(self, rhs: u64) -> Self::Output { + Self(self.0 + Uint::from(rhs)) + } +} + +impl AddAssign for Dec { + fn add_assign(&mut self, rhs: Dec) { + *self = *self + rhs; + } +} + +impl Sub for Dec { + type Output = Self; + + fn sub(self, rhs: Dec) -> Self::Output { + Self(self.0 - rhs.0) + } +} + +impl Mul for Dec { + type Output = Uint; + + fn mul(self, rhs: Uint) -> Self::Output { + self.0 * rhs + } +} + +impl Mul for Dec { + type Output = Dec; + + fn mul(self, rhs: u128) -> Self::Output { + Self(self.0 * Uint::from(rhs)) + } +} + +impl Mul for Dec { + type Output = Amount; + + fn mul(self, rhs: Amount) -> Self::Output { + (rhs * self.0) / 10u64.pow(POS_DECIMAL_PRECISION as u32) + } +} + +impl Mul for Dec { + type Output = Change; + + fn mul(self, rhs: Change) -> Self::Output { + let tot = rhs * self.0; + let denom = Uint::from(10u64.pow(POS_DECIMAL_PRECISION as u32)); + tot / denom + } +} + +impl Mul for Dec { + type Output = Self; + + fn mul(self, rhs: Dec) -> Self::Output { + Self(self.0 * rhs.0) + } +} + +impl Div for Dec { + type Output = Self; + + fn div(self, rhs: Dec) -> Self::Output { + Self(self.0 / rhs.0) + } +} + +impl Display for Dec { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let string = self.0.to_string(); + f.write_str(&string) + } +} + +#[cfg(test)] +mod test_dec { + use super::*; + + /// Fill in tests later + #[test] + fn test_basic() { + assert_eq!( + Dec::one() + Dec::new(3, 0).unwrap() / Dec::new(5, 0).unwrap(), + Dec::new(16, 1).unwrap() + ); + } +} diff --git a/core/src/types/mod.rs b/core/src/types/mod.rs index b414efc89a..8bbd73eaf2 100644 --- a/core/src/types/mod.rs +++ b/core/src/types/mod.rs @@ -2,6 +2,7 @@ pub mod address; pub mod chain; +pub mod dec; pub mod governance; pub mod hash; pub mod ibc; diff --git a/core/src/types/token.rs b/core/src/types/token.rs index fe16161a74..b28b2af399 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -14,9 +14,10 @@ use thiserror::Error; use crate::ledger::storage_api::token::read_denom; use crate::ledger::storage_api::StorageRead; use crate::types::address::{masp, Address, DecodeError as AddressError}; +use crate::types::dec::Dec; use crate::types::storage; use crate::types::storage::{DbKeySeg, Key, KeySeg}; -use crate::types::uint::{self, I256, Uint}; +use crate::types::uint::{self, Uint, I256}; /// Amount in micro units. For different granularity another representation /// might be more appropriate. @@ -207,7 +208,7 @@ impl Amount { pub fn to_string_native(&self) -> String { DenominatedAmount { amount: *self, - denom:NATIVE_MAX_DECIMAL_PLACES.into(), + denom: NATIVE_MAX_DECIMAL_PLACES.into(), } .to_string_precise() } @@ -492,6 +493,12 @@ impl From for Amount { } } +impl From for Amount { + fn from(dec: Dec) -> Amount { + Amount { raw: dec.0 } + } +} + impl TryFrom for u128 { type Error = std::io::Error; @@ -522,7 +529,9 @@ impl Add for Amount { type Output = Self; fn add(self, rhs: u64) -> Self::Output { - Self { raw: self.raw + Uint::from(rhs)} + Self { + raw: self.raw + Uint::from(rhs), + } } } diff --git a/core/src/types/transaction/mod.rs b/core/src/types/transaction/mod.rs index 7d8e587011..8bf80b032a 100644 --- a/core/src/types/transaction/mod.rs +++ b/core/src/types/transaction/mod.rs @@ -21,13 +21,13 @@ pub use decrypted::*; #[cfg(feature = "ferveo-tpke")] pub use encrypted::EncryptionKey; pub use protocol::UpdateDkgSessionKey; -use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; pub use wrapper::*; use crate::ledger::gas::VpsGas; use crate::types::address::Address; +use crate::types::dec::Dec; use crate::types::hash::Hash; use crate::types::ibc::IbcEvent; use crate::types::key::*; @@ -190,10 +190,10 @@ pub struct InitValidator { /// Serialization of the public session key used in the DKG pub dkg_key: crate::types::key::dkg_session_keys::DkgPublicKey, /// The initial commission rate charged for delegation rewards - pub commission_rate: Decimal, + pub commission_rate: Dec, /// The maximum change allowed per epoch to the commission rate. This is /// immutable once set here. - pub max_commission_rate_change: Decimal, + pub max_commission_rate_change: Dec, /// The VP code for validator account pub validator_vp_code_hash: Hash, } diff --git a/core/src/types/transaction/pos.rs b/core/src/types/transaction/pos.rs index 8119eb2310..d334047ffe 100644 --- a/core/src/types/transaction/pos.rs +++ b/core/src/types/transaction/pos.rs @@ -1,10 +1,10 @@ //! Types used for PoS system transactions use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use crate::types::address::Address; +use crate::types::dec::Dec; use crate::types::token; /// A bond is a validator's self-bond or a delegation from non-validator to a @@ -72,5 +72,5 @@ pub struct CommissionChange { /// Validator address pub validator: Address, /// The new commission rate - pub new_rate: Decimal, + pub new_rate: Dec, } diff --git a/core/src/types/uint.rs b/core/src/types/uint.rs index cb88c96844..39d0839c71 100644 --- a/core/src/types/uint.rs +++ b/core/src/types/uint.rs @@ -2,7 +2,7 @@ //! An unsigned 256 integer type. Used for, among other things, //! the backing type of token amounts. use std::cmp::Ordering; -use std::ops::{Add, AddAssign, BitXor, Neg, Sub}; +use std::ops::{Add, AddAssign, BitXor, Div, Mul, Neg, Sub}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use impl_num_traits::impl_uint_num_traits; @@ -92,6 +92,11 @@ impl I256 { self.0.0[3].leading_zeros() > 0 } + /// Check if the amount is negative (less than zero) + pub fn is_negative(&self) -> bool { + !self.non_negative() + } + /// Get the absolute value pub fn abs(&self) -> Uint { if self.non_negative() { @@ -238,6 +243,29 @@ impl Sub for I256 { } } +impl Mul for I256 { + type Output = Self; + + fn mul(self, rhs: Uint) -> Self::Output { + let is_neg = self.is_negative(); + let prod = self.abs() * rhs; + if is_neg { -Self(prod) } else { Self(prod) } + } +} + +impl Div for I256 { + type Output = Self; + + fn div(self, rhs: Uint) -> Self::Output { + let is_neg = self.is_negative(); + let quot = self + .abs() + .fixed_precision_div(&rhs, 0u8) + .unwrap_or_default(); + if is_neg { -Self(quot) } else { Self(quot) } + } +} + impl From for I256 { fn from(val: i128) -> Self { if val < 0 { @@ -310,8 +338,7 @@ mod test_uint { /// value gives zero. #[test] fn test_max_signed_value() { - let signed = - I256::try_from(MAX_SIGNED_VALUE).expect("Test failed"); + let signed = I256::try_from(MAX_SIGNED_VALUE).expect("Test failed"); let one = I256::try_from(Uint::from(1u64)).expect("Test failed"); let overflow = signed + one; assert_eq!( diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 75e84cd87c..eed29cdfe1 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -39,12 +39,12 @@ use namada_core::ledger::storage_api::{ self, OptionExt, ResultExt, StorageRead, StorageWrite, }; use namada_core::types::address::{Address, InternalAddress}; +use namada_core::types::dec::Dec; use namada_core::types::key::{ common, tm_consensus_key_raw_hash, PublicKeyTmRawHash, }; pub use namada_core::types::storage::Epoch; use namada_core::types::token; -use namada_core::types::token::{Amount, DenominatedAmount}; use once_cell::unsync::Lazy; use parameters::PosParams; use rewards::PosRewardsCalculator; @@ -53,13 +53,12 @@ use storage::{ get_validator_address_from_bond, into_tm_voting_power, is_bond_key, is_unbond_key, is_validator_slashes_key, last_block_proposer_key, num_consensus_validators_key, params_key, slashes_prefix, - unbonds_for_source_prefix, unbonds_prefix, - validator_address_raw_hash_key, validator_max_commission_rate_change_key, - BondDetails, BondsAndUnbondsDetail, BondsAndUnbondsDetails, - ReverseOrdTokenAmount, RewardsAccumulator, UnbondDetails, + unbonds_for_source_prefix, unbonds_prefix, validator_address_raw_hash_key, + validator_max_commission_rate_change_key, BondDetails, + BondsAndUnbondsDetail, BondsAndUnbondsDetails, ReverseOrdTokenAmount, + RewardsAccumulator, UnbondDetails, }; use thiserror::Error; -use namada_core::types::uint::Uint; use types::{ BelowCapacityValidatorSet, BelowCapacityValidatorSets, BondId, Bonds, CommissionRates, ConsensusValidator, ConsensusValidatorSet, @@ -70,8 +69,6 @@ use types::{ WeightedValidator, }; -use crate::types::Dec; - /// Address of the PoS account implemented as a native VP pub const ADDRESS: Address = Address::Internal(InternalAddress::PoS); @@ -168,9 +165,9 @@ pub enum SlashError { #[derive(Error, Debug)] pub enum CommissionRateChangeError { #[error("Unexpected negative commission rate {0} for validator {1}")] - NegativeRate(DenominatedAmount, Address), + NegativeRate(Dec, Address), #[error("Rate change of {0} is too large for validator {1}")] - RateChangeTooLarge(DenominatedAmount, Address), + RateChangeTooLarge(Dec, Address), #[error( "There is no maximum rate change written in storage for validator {0}" )] @@ -477,7 +474,7 @@ where pub fn read_validator_max_commission_rate_change( storage: &S, validator: &Address, -) -> storage_api::Result> +) -> storage_api::Result> where S: StorageRead, { @@ -489,7 +486,7 @@ where pub fn write_validator_max_commission_rate_change( storage: &mut S, validator: &Address, - change: DenominatedAmount, + change: Dec, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, @@ -1585,8 +1582,8 @@ pub fn become_validator( address: &Address, consensus_key: &common::PublicKey, current_epoch: Epoch, - commission_rate: DenominatedAmount, - max_commission_rate_change: DenominatedAmount, + commission_rate: Dec, + max_commission_rate_change: Dec, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, @@ -1692,14 +1689,7 @@ where .unwrap_or_default() { let slash_rate = slash_type.get_slash_rate(¶ms); - let to_slash = token::Amount::from_uint( - decimal_mult_u128( - slash_rate, - u128::try_from(amount).expect("Amount out of bounds"), - ), - 0, - ) - .expect("Amount out of bounds"); + let to_slash = slash_rate * amount; slashed += to_slash; } } @@ -1739,7 +1729,7 @@ where pub fn change_validator_commission_rate( storage: &mut S, validator: &Address, - new_rate: DenominatedAmount, + new_rate: Dec, current_epoch: Epoch, ) -> storage_api::Result<()> where @@ -1775,8 +1765,16 @@ where let rate_before_pipeline = commission_handle .get(storage, pipeline_epoch - 1, ¶ms)? .expect("Could not find a rate in given epoch"); - let change_from_prev = new_rate - rate_before_pipeline; - if change_from_prev.abs() > max_change.unwrap() { + + // TODO: change this back if we use `Dec` type with a signed integer + // let change_from_prev = new_rate - rate_before_pipeline; + // if change_from_prev.abs() > max_change.unwrap() { + let change_from_prev = if new_rate > rate_before_pipeline { + new_rate - rate_before_pipeline + } else { + rate_before_pipeline - new_rate + }; + if change_from_prev > max_change.unwrap() { return Err(CommissionRateChangeError::RateChangeTooLarge( change_from_prev, validator.clone(), @@ -1810,14 +1808,7 @@ where let current_stake = read_validator_stake(storage, params, validator, current_epoch)? .unwrap_or_default(); - let slashed_amount = Amount::from_uint( - decimal_mult_u128( - rate, - u128::try_from(current_stake).expect("Amount out of bounds"), - ), - 0, - ) - .expect("Amount out of bounds"); + let slashed_amount = rate * current_stake; let token_change = -token::Change::from(slashed_amount); // Update validator sets and deltas at the pipeline length @@ -1958,9 +1949,7 @@ where if slash_epoch > &bond_epoch { continue; } - let current_slashed = - mult_change_to_amount(slash_type.get_slash_rate(params), delta) - .change(); + let current_slashed = slash_type.get_slash_rate(params) * delta; let delta = token::Amount::from_change(delta - current_slashed); total += delta; if bond_epoch <= epoch { @@ -2024,17 +2013,11 @@ where .unwrap_or_default(); into_tm_voting_power( params.tm_votes_per_token, - u128::try_from(prev_validator_stake) - .expect("Amount out of bounds") - as u64, + prev_validator_stake, ) }); let cur_tm_voting_power = Lazy::new(|| { - into_tm_voting_power( - params.tm_votes_per_token, - u128::try_from(cur_stake).expect("Amount out of bounds") - as u64, - ) + into_tm_voting_power(params.tm_votes_per_token, cur_stake) }); // If it was in `Consensus` before and voting power has not @@ -2104,8 +2087,7 @@ where .unwrap_or_default(); let prev_tm_voting_power = into_tm_voting_power( params.tm_votes_per_token, - u128::try_from(prev_validator_stake) - .expect("Amount out of bounds") as u64, + prev_validator_stake, ); // If the validator previously had no voting power, it wasn't in @@ -2544,10 +2526,8 @@ fn make_bond_details( } return Some( acc.unwrap_or_default() - + mult_change_to_amount( - slash.r#type.get_slash_rate(params), - change, - ), + + slash.r#type.get_slash_rate(params) + * token::Amount::from_change(change), ); } None @@ -2585,10 +2565,7 @@ fn make_unbond_details( } return Some( acc.unwrap_or_default() - + mult_amount( - slash.r#type.get_slash_rate(params), - amount, - ), + + slash.r#type.get_slash_rate(params) * amount, ); } None @@ -2655,7 +2632,7 @@ where debug_assert_eq!( into_tm_voting_power( params.tm_votes_per_token, - u128::try_from(stake_from_deltas).unwrap() as u64, + stake_from_deltas, ), i64::try_from(validator_vp).unwrap_or_default(), ); @@ -2670,8 +2647,8 @@ where let rewards_calculator = PosRewardsCalculator { proposer_reward: params.block_proposer_reward, signer_reward: params.block_vote_reward, - signing_stake: u128::try_from(total_signing_stake).unwrap() as u64, - total_stake: u128::try_from(total_consensus_stake).unwrap() as u64, + signing_stake: total_signing_stake, + total_stake: total_consensus_stake, }; let coeffs = rewards_calculator .get_reward_coeffs() @@ -2688,9 +2665,9 @@ where // Compute the fractional block rewards for each consensus validator and // update the reward accumulators - let consensus_stake_unscaled: Dec = total_consensus_stake.into(); + let consensus_stake_unscaled: Dec = total_consensus_stake.into(); let signing_stake_unscaled: Dec = total_signing_stake.into(); - let mut values: HashMap= HashMap::new(); + let mut values: HashMap = HashMap::new(); for validator in consensus_validators.iter(storage)? { let ( NestedSubKey::Data { diff --git a/proof_of_stake/src/parameters.rs b/proof_of_stake/src/parameters.rs index 9eb5b6de05..a5b75ebd80 100644 --- a/proof_of_stake/src/parameters.rs +++ b/proof_of_stake/src/parameters.rs @@ -1,11 +1,9 @@ //! Proof-of-Stake system parameters use borsh::{BorshDeserialize, BorshSerialize}; -use rust_decimal::prelude::ToPrimitive; -use rust_decimal::Decimal; -use rust_decimal_macros::dec; +use namada_core::types::dec::Dec; +use namada_core::types::uint::Uint; use thiserror::Error; -use crate::types::Dec; /// Proof-of-Stake system parameters, set at genesis and can only be changed via /// governance @@ -60,7 +58,8 @@ impl Default for PosParams { // slash 0.1% duplicate_vote_min_slash_rate: Dec::new(1, 3).expect("Test failed"), // slash 0.1% - light_client_attack_min_slash_rate: Dec::new(1, 3).expect("Test failed"), + light_client_attack_min_slash_rate: Dec::new(1, 3) + .expect("Test failed"), } } } @@ -72,7 +71,7 @@ pub enum ValidationError { "Maximum total voting power is too large: got {0}, expected at most \ {MAX_TOTAL_VOTING_POWER}" )] - TotalVotingPowerTooLarge(u64), + TotalVotingPowerTooLarge(Uint), #[error("Votes per token cannot be greater than 1, got {0}")] VotesPerTokenGreaterThanOne(Dec), #[error("Pipeline length must be >= 2, got {0}")] @@ -118,24 +117,24 @@ impl PosParams { // TODO: decide if this is still a check we want to do (in its current // state with our latest voting power conventions, it will fail // always) - let max_total_voting_power = Decimal::from(self.max_validator_slots) - * self.tm_votes_per_token - * Decimal::from(TOKEN_MAX_AMOUNT); + let max_total_voting_power = *self.tm_votes_per_token + * Uint::from(TOKEN_MAX_AMOUNT) + * Uint::from(self.max_validator_slots); match i64::try_from(max_total_voting_power) { Ok(max_total_voting_power_i64) => { if max_total_voting_power_i64 > MAX_TOTAL_VOTING_POWER { errors.push(ValidationError::TotalVotingPowerTooLarge( - max_total_voting_power.to_u64().unwrap(), + max_total_voting_power, )) } } Err(_) => errors.push(ValidationError::TotalVotingPowerTooLarge( - max_total_voting_power.to_u64().unwrap(), + max_total_voting_power, )), } // Check that there is no more than 1 vote per token - if self.tm_votes_per_token > dec!(1.0) { + if self.tm_votes_per_token > Dec::one() { errors.push(ValidationError::VotesPerTokenGreaterThanOne( self.tm_votes_per_token, )) @@ -170,8 +169,8 @@ mod tests { /// Testing helpers #[cfg(any(test, feature = "testing"))] pub mod testing { + use namada_core::types::dec::Dec; use proptest::prelude::*; - use rust_decimal::Decimal; use super::*; @@ -189,7 +188,7 @@ pub mod testing { max_validator_slots, pipeline_len, unbonding_len, - tm_votes_per_token: Dec(Uint::from(tm_votes_per_token)) / Dec(Uint::from(10_000)), + tm_votes_per_token: Dec::new(tm_votes_per_token, 4).expect("Test failed"), // The rest of the parameters that are not being used in the PoS // VP are constant for now ..Default::default() @@ -197,10 +196,9 @@ pub mod testing { } } - /// Get an arbitrary rate - a Decimal value between 0 and 1 inclusive, with + /// Get an arbitrary rate - a Dec value between 0 and 1 inclusive, with /// some fixed precision - pub fn arb_rate() -> impl Strategy { - (0..=100_000_u64) - .prop_map(|num| Decimal::from(num) / Decimal::from(100_000_u64)) + pub fn arb_rate() -> impl Strategy { + (0..=100_000_u64).prop_map(|num| Dec::new(num, 5).expect("Test failed")) } } diff --git a/proof_of_stake/src/rewards.rs b/proof_of_stake/src/rewards.rs index e189cbc1a7..acfca50673 100644 --- a/proof_of_stake/src/rewards.rs +++ b/proof_of_stake/src/rewards.rs @@ -1,9 +1,9 @@ //! PoS rewards distribution. -use thiserror::Error; -use namada_core::types::token::{Amount, DenominatedAmount}; +use namada_core::types::dec::Dec; +use namada_core::types::token::Amount; use namada_core::types::uint::Uint; -use crate::types::Dec; +use thiserror::Error; /// This is equal to 0.01. const MIN_PROPOSER_REWARD: Dec = Dec(Uint([10000u64, 0u64, 0u64, 0u64])); @@ -72,10 +72,10 @@ impl PosRewardsCalculator { } // Logic for determining the coefficients. - let proposer_coeff = Dec::from(proposer_reward - * (signing_stake - votes_needed)) - / Dec::from(total_stake) - + MIN_PROPOSER_REWARD; + let proposer_coeff = + Dec::from(proposer_reward * (signing_stake - votes_needed)) + / Dec::from(total_stake) + + MIN_PROPOSER_REWARD; let signer_coeff = signer_reward; let active_val_coeff = Dec::one() - proposer_coeff - signer_coeff; diff --git a/proof_of_stake/src/tests.rs b/proof_of_stake/src/tests.rs index e7a1ef4d75..63d2b84eb2 100644 --- a/proof_of_stake/src/tests.rs +++ b/proof_of_stake/src/tests.rs @@ -13,6 +13,7 @@ use namada_core::types::address::testing::{ address_from_simple_seed, arb_established_address, }; use namada_core::types::address::{Address, EstablishedAddressGen}; +use namada_core::types::dec::Dec; use namada_core::types::key::common::{PublicKey, SecretKey}; use namada_core::types::key::testing::{ arb_common_keypair, common_sk_from_simple_seed, @@ -21,15 +22,17 @@ use namada_core::types::storage::Epoch; use namada_core::types::{address, key, token}; use proptest::prelude::*; use proptest::test_runner::Config; -use rust_decimal::Decimal; -use rust_decimal_macros::dec; // Use `RUST_LOG=info` (or another tracing level) and `--nocapture` to see // `tracing` logs from tests use test_log::test; use crate::parameters::testing::arb_pos_params; use crate::parameters::PosParams; -use crate::types::{into_tm_voting_power, BondDetails, BondId, BondsAndUnbondsDetails, ConsensusValidator, GenesisValidator, Position, ReverseOrdTokenAmount, UnbondDetails, ValidatorSetUpdate, ValidatorState, WeightedValidator, Dec}; +use crate::types::{ + into_tm_voting_power, BondDetails, BondId, BondsAndUnbondsDetails, + ConsensusValidator, GenesisValidator, Position, ReverseOrdTokenAmount, + UnbondDetails, ValidatorSetUpdate, ValidatorState, WeightedValidator, +}; use crate::{ become_validator, below_capacity_validator_set_handle, bond_handle, bond_tokens, bonds_and_unbonds, consensus_validator_set_handle, @@ -741,8 +744,8 @@ fn test_become_validator_aux( &new_validator, &consensus_key, current_epoch, - Decimal::new(5, 2), - Decimal::new(5, 2), + Dec::new(5, 2).expect("Dec creation failed"), + Dec::new(5, 2).expect("Dec creation failed"), ) .unwrap(); @@ -927,15 +930,17 @@ fn test_validator_sets() { address: val1.clone(), tokens: stake1, consensus_key: pk1.clone(), - commission_rate: Decimal::new(1, 1), - max_commission_rate_change: Decimal::new(1, 1), + commission_rate: Dec::new(1, 1).expect("Dec creation failed"), + max_commission_rate_change: Dec::new(1, 1) + .expect("Dec creation failed"), }, GenesisValidator { address: val2.clone(), tokens: stake2, consensus_key: pk2.clone(), - commission_rate: Decimal::new(1, 1), - max_commission_rate_change: Decimal::new(1, 1), + commission_rate: Dec::new(1, 1).expect("Dec creation failed"), + max_commission_rate_change: Dec::new(1, 1) + .expect("Dec creation failed"), }, ] .into_iter(), @@ -1472,7 +1477,7 @@ fn test_validator_sets_swap() { let params = PosParams { max_validator_slots: 2, // Set 0.1 votes per token - tm_votes_per_token: dec!(0.1), + tm_votes_per_token: Dec::new(1, 1).expect("Dec creation failed"), ..Default::default() }; let addr_seed = "seed"; @@ -1537,15 +1542,17 @@ fn test_validator_sets_swap() { address: val1, tokens: stake1, consensus_key: pk1, - commission_rate: Decimal::new(1, 1), - max_commission_rate_change: Decimal::new(1, 1), + commission_rate: Dec::new(1, 1).expect("Dec creation failed"), + max_commission_rate_change: Dec::new(1, 1) + .expect("Dec creation failed"), }, GenesisValidator { address: val2.clone(), tokens: stake2, consensus_key: pk2, - commission_rate: Decimal::new(1, 1), - max_commission_rate_change: Decimal::new(1, 1), + commission_rate: Dec::new(1, 1).expect("Dec creation failed"), + max_commission_rate_change: Dec::new(1, 1) + .expect("Dec creation failed"), }, ] .into_iter(), @@ -1570,20 +1577,8 @@ fn test_validator_sets_swap() { let stake3 = stake3 + bond3; assert!(stake2 < stake3); - assert_eq!( - into_tm_voting_power( - params.tm_votes_per_token, - u128::try_from(stake2).unwrap() as u64 - ), - 0 - ); - assert_eq!( - into_tm_voting_power( - params.tm_votes_per_token, - u128::try_from(stake3).unwrap() as u64 - ), - 0 - ); + assert_eq!(into_tm_voting_power(params.tm_votes_per_token, stake2), 0); + assert_eq!(into_tm_voting_power(params.tm_votes_per_token, stake3), 0); update_validator_set(&mut s, ¶ms, &val2, bond2.change(), epoch) .unwrap(); @@ -1606,14 +1601,8 @@ fn test_validator_sets_swap() { let stake3 = stake3 + bonds; assert!(stake2 < stake3); assert_eq!( - into_tm_voting_power( - params.tm_votes_per_token, - u128::try_from(stake2).unwrap() as u64 - ), - into_tm_voting_power( - params.tm_votes_per_token, - u128::try_from(stake3).unwrap() as u64 - ) + into_tm_voting_power(params.tm_votes_per_token, stake2), + into_tm_voting_power(params.tm_votes_per_token, stake3) ); update_validator_set(&mut s, ¶ms, &val2, bonds.change(), epoch) @@ -1705,7 +1694,8 @@ fn arb_genesis_validators( let consensus_key = consensus_sk.to_public(); let commission_rate = Dec::new(5, 2).expect("Test failed"); - let max_commission_rate_change = Dec::new(1, 3).expect("Test failed"); + let max_commission_rate_change = + Dec::new(1, 3).expect("Test failed"); GenesisValidator { address, tokens, diff --git a/proof_of_stake/src/tests/state_machine.rs b/proof_of_stake/src/tests/state_machine.rs index 3662f4e382..879e99403c 100644 --- a/proof_of_stake/src/tests/state_machine.rs +++ b/proof_of_stake/src/tests/state_machine.rs @@ -6,6 +6,7 @@ use itertools::Itertools; use namada_core::ledger::storage::testing::TestWlStorage; use namada_core::ledger::storage_api::{token, StorageRead}; use namada_core::types::address::{self, Address}; +use namada_core::types::dec::Dec; use namada_core::types::key; use namada_core::types::key::common::PublicKey; use namada_core::types::storage::Epoch; @@ -14,7 +15,6 @@ use proptest::prelude::*; use proptest::prop_state_machine; use proptest::state_machine::{AbstractStateMachine, StateMachineTest}; use proptest::test_runner::Config; -use rust_decimal::Decimal; // Use `RUST_LOG=info` (or another tracing level) and `--nocapture` to see // `tracing` logs from tests use test_log::test; @@ -81,8 +81,8 @@ enum Transition { InitValidator { address: Address, consensus_key: PublicKey, - commission_rate: Decimal, - max_commission_rate_change: Decimal, + commission_rate: Dec, + max_commission_rate_change: Dec, }, Bond { id: BondId, diff --git a/proof_of_stake/src/types.rs b/proof_of_stake/src/types.rs index 3d0ae8e489..6d1a0bdf3b 100644 --- a/proof_of_stake/src/types.rs +++ b/proof_of_stake/src/types.rs @@ -5,7 +5,7 @@ mod rev_order; use core::fmt::Debug; use std::collections::{HashMap, HashSet}; use std::convert::TryFrom; -use std::fmt::{Display, Formatter, Write}; +use std::fmt::Display; use std::hash::Hash; use std::ops::Sub; @@ -16,15 +16,18 @@ use namada_core::ledger::storage_api::collections::{ }; use namada_core::ledger::storage_api::{self, StorageRead}; use namada_core::types::address::Address; +use namada_core::types::dec::Dec; use namada_core::types::key::common; use namada_core::types::storage::{Epoch, KeySeg}; use namada_core::types::token; -use namada_core::types::token::{Amount, NATIVE_MAX_DECIMAL_PLACES}; +use namada_core::types::token::Amount; pub use rev_order::ReverseOrdTokenAmount; -use namada_core::types::uint::Uint; use crate::parameters::PosParams; +// TODO: replace `POS_MAX_DECIMAL_PLACES` with +// core::types::token::NATIVE_MAX_DECIMAL_PLACES?? + // const U64_MAX: u64 = u64::MAX; // TODO: add this to the spec @@ -466,137 +469,6 @@ impl Display for SlashType { // -------------------------------------------------------------------------------------------- -/// The numbrer of Dec places for PoS rational calculations -pub const POS_DECIMAL_PRECISION: u8 = 6; - -/// A 256 bit number with [`POS_DECIMAL_PRECISION`] number of Dec places. -/// -/// To be precise, an instance X of this type should be interpeted as the Dec -/// X * 10 ^ (-[`POS_DECIMAL_PRECISION`]) -#[derive( - Clone, - Copy, - Default, - BorshSerialize, - BorshDeserialize, - BorshSchema, - PartialEq, - Eq, - PartialOrd, - Ord, - Debug, - Hash, -)] -pub struct Dec(pub Uint); - -impl std::ops::Deref for Dec { - type Target = Uint; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl std::ops::DerefMut for Dec { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl Dec { - pub fn trunc_div(&self, rhs: &Self) -> Option { - self.0.fixed_precision_div(rhs, POS_DECIMAL_PRECISION) - .map(Self) - } - - pub fn zero() -> Self { - Self(Uint::zero()) - } - - pub fn one() -> Self { - Self(Uint::one()) - } - - pub fn new(matissa: u64, scale: u8) -> Option { - if scale > POS_DECIMAL_PRECISION { - None - } else { - Uint::exp10((POS_DECIMAL_PRECISION - scale) as usize) - .checked_mul(Uint::from(matissa)) - .map(Self) - } - } -} - -impl From for Dec { - fn from(amt: Amount) -> Self { - Self(amt.into()) - } -} - -impl std::ops::Add for Dec { - type Output = Self; - - fn add(self, rhs: Dec) -> Self::Output { - Self(self.0 + rhs.0) - } -} - -impl std::ops::Add for Dec { - type Output = Self; - - fn add(self, rhs: u64) -> Self::Output { - Self(self.0 + Uint::from(rhs)) - } -} - -impl std::ops::Sub for Dec { - type Output = Self; - - fn sub(self, rhs: Dec) -> Self::Output { - Self(self.0 - rhs.0) - } -} - -impl std::ops::Mul for Dec { - type Output = Dec; - - fn mul(self, rhs: u128) -> Self::Output { - Self(self.0 * Uint::from(rhs)) - } -} - -impl std::ops::Mul for Dec { - type Output = Amount; - - fn mul(self, rhs: Amount) -> Self::Output { - self.0 * rhs - } -} - -impl std::ops::Mul for Dec { - type Output = Self; - - fn mul(self, rhs: Dec) -> Self::Output { - Self(self.0 * rhs.0) - } -} - -impl std::ops::Div for Dec { - type Output = Self; - - fn div(self, rhs: Dec) -> Self::Output { - Self(self.0 / rhs.0) - } -} - -impl Display for Dec { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let string = self.0.to_string(); - f.write_str(&string) - } -} - // -------------------------------------------------------------------------------------------- // /// Multiply a value of type Dec with one of type u64 and then return the @@ -638,11 +510,9 @@ impl Display for Dec { /// Calculate voting power in the tendermint context (which is stored as i64) /// from the number of tokens -pub fn into_tm_voting_power( - votes_per_token: Dec, - tokens: Amount, -) -> i64 { - let pow = votes_per_token * u128::try_from(tokens).expect("Voting power out of bounds"); +pub fn into_tm_voting_power(votes_per_token: Dec, tokens: Amount) -> i64 { + let pow = votes_per_token + * u128::try_from(tokens).expect("Voting power out of bounds"); i64::try_from(pow.0).expect("Invalid voting power") } diff --git a/shared/src/ledger/inflation.rs b/shared/src/ledger/inflation.rs index c935c0b019..11415ae96e 100644 --- a/shared/src/ledger/inflation.rs +++ b/shared/src/ledger/inflation.rs @@ -2,9 +2,7 @@ //! proof-of-stake, providing liquity to shielded asset pools, and public goods //! funding. -use rust_decimal::prelude::ToPrimitive; -use rust_decimal::Decimal; -use rust_decimal_macros::dec; +use namada_core::types::dec::Dec; use crate::types::token; @@ -21,8 +19,8 @@ pub enum RewardsType { /// Holds the PD controller values that should be updated in storage #[allow(missing_docs)] pub struct ValsToUpdate { - pub locked_ratio: Decimal, - pub inflation: u64, + pub locked_ratio: Dec, + pub inflation: token::Amount, } /// PD controller used to dynamically adjust the rewards rates @@ -33,17 +31,17 @@ pub struct RewardsController { /// Total token supply pub total_tokens: token::Amount, /// PD target locked ratio - pub locked_ratio_target: Decimal, + pub locked_ratio_target: Dec, /// PD last locked ratio - pub locked_ratio_last: Decimal, + pub locked_ratio_last: Dec, /// Maximum reward rate - pub max_reward_rate: Decimal, + pub max_reward_rate: Dec, /// Last inflation amount pub last_inflation_amount: token::Amount, /// Nominal proportional gain - pub p_gain_nom: Decimal, + pub p_gain_nom: Dec, /// Nominal derivative gain - pub d_gain_nom: Decimal, + pub d_gain_nom: Dec, /// Number of epochs per year pub epochs_per_year: u64, } @@ -63,9 +61,9 @@ impl RewardsController { epochs_per_year, } = self; - let locked = locked_tokens.as_dec_unscaled(); - let total = total_tokens.as_dec_unscaled(); - let epochs_py: Decimal = (epochs_per_year).into(); + let locked = Dec::from(locked_tokens); + let total = Dec::from(total_tokens); + let epochs_py: Dec = epochs_per_year.into(); let locked_ratio = locked / total; let max_inflation = total * max_reward_rate / epochs_py; @@ -76,16 +74,15 @@ impl RewardsController { let delta_error = locked_ratio_last - locked_ratio; let control_val = p_gain * error - d_gain * delta_error; - let last_inflation_amount = - Decimal::try_from(last_inflation_amount).unwrap(); + let last_inflation_amount = Dec::from(last_inflation_amount); let inflation = if last_inflation_amount + control_val > max_inflation { max_inflation - } else if last_inflation_amount + control_val > dec!(0.0) { + } else if last_inflation_amount + control_val > Dec::zero() { last_inflation_amount + control_val } else { - dec!(0.0) + Dec::zero() }; - let inflation: u64 = inflation.to_u64().unwrap(); + let inflation = token::Amount::from(inflation); ValsToUpdate { locked_ratio, diff --git a/shared/src/ledger/pos/mod.rs b/shared/src/ledger/pos/mod.rs index fc3c32233b..152112874b 100644 --- a/shared/src/ledger/pos/mod.rs +++ b/shared/src/ledger/pos/mod.rs @@ -5,13 +5,13 @@ pub mod vp; pub use namada_core::ledger::storage_api; use namada_core::ledger::storage_api::{StorageRead, StorageWrite}; use namada_core::types::address; +pub use namada_core::types::dec::Dec; pub use namada_core::types::key::common; pub use namada_core::types::token; pub use namada_proof_of_stake; pub use namada_proof_of_stake::parameters::PosParams; pub use namada_proof_of_stake::storage::*; pub use namada_proof_of_stake::{staking_token_address, types}; -use rust_decimal::Decimal; pub use vp::PosVP; use crate::types::address::{Address, InternalAddress}; @@ -27,14 +27,13 @@ pub const SLASH_POOL_ADDRESS: Address = /// Calculate voting power in the tendermint context (which is stored as i64) /// from the number of tokens pub fn into_tm_voting_power( - votes_per_token: Decimal, + votes_per_token: Dec, tokens: token::Amount, ) -> i64 { - let prod = decimal_mult_u128( - votes_per_token, - u128::try_from(tokens).expect("TODO(Tomas): handle overflow"), - ); - i64::try_from(prod).expect("Invalid validator voting power (i64)") + let tokens = tokens.change(); + let prod = votes_per_token * tokens; + let res = i128::try_from(prod).expect("Failed conversion to i128"); + i64::try_from(res).expect("Invalid validator voting power (i64)") } /// Initialize storage in the genesis block. diff --git a/shared/src/types/mod.rs b/shared/src/types/mod.rs index 1b73329efe..ba1265025b 100644 --- a/shared/src/types/mod.rs +++ b/shared/src/types/mod.rs @@ -4,6 +4,6 @@ pub mod ibc; pub mod key; pub use namada_core::types::{ - address, chain, governance, hash, internal, masp, storage, time, token, - transaction, validity_predicate, + address, chain, dec, governance, hash, internal, masp, storage, time, + token, transaction, validity_predicate, }; diff --git a/tx_prelude/Cargo.toml b/tx_prelude/Cargo.toml index 7359f528bf..e669fdefee 100644 --- a/tx_prelude/Cargo.toml +++ b/tx_prelude/Cargo.toml @@ -23,4 +23,3 @@ namada_vm_env = {path = "../vm_env", default-features = false} borsh = "0.9.0" sha2 = "0.10.1" thiserror = "1.0.30" -rust_decimal = "1.26.1" diff --git a/tx_prelude/src/proof_of_stake.rs b/tx_prelude/src/proof_of_stake.rs index 22c00ca599..f002b4dc37 100644 --- a/tx_prelude/src/proof_of_stake.rs +++ b/tx_prelude/src/proof_of_stake.rs @@ -1,5 +1,6 @@ //! Proof of Stake system integration with functions for transactions +use namada_core::types::dec::Dec; use namada_core::types::transaction::InitValidator; use namada_core::types::{key, token}; pub use namada_proof_of_stake::parameters::PosParams; @@ -8,7 +9,6 @@ use namada_proof_of_stake::{ read_pos_params, unbond_tokens, withdraw_tokens, }; pub use namada_proof_of_stake::{parameters, types}; -use rust_decimal::Decimal; use super::*; @@ -55,7 +55,7 @@ impl Ctx { pub fn change_validator_commission_rate( &mut self, validator: &Address, - rate: &Decimal, + rate: &Dec, ) -> TxResult { let current_epoch = self.get_block_epoch()?; change_validator_commission_rate(self, validator, *rate, current_epoch) From b0419f5342d7b117d202053f554bacc8e261fec1 Mon Sep 17 00:00:00 2001 From: satan Date: Wed, 26 Apr 2023 11:53:26 +0200 Subject: [PATCH 030/151] [fix]: Added tests for Dec, fixed division bug, cleaned up from_str method, added tests, removed some more Decimal types --- Cargo.lock | 1 + apps/src/lib/client/tx.rs | 2 +- apps/src/lib/client/utils.rs | 13 +-- apps/src/lib/config/genesis.rs | 58 +++++------ core/Cargo.toml | 1 + core/src/types/dec.rs | 169 +++++++++++++++++++++------------ proof_of_stake/src/types.rs | 4 - 7 files changed, 142 insertions(+), 106 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8bba1dde6d..448eb364e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3931,6 +3931,7 @@ dependencies = [ "derivative", "ed25519-consensus", "ethabi", + "eyre", "ferveo", "ferveo-common", "group-threshold-cryptography", diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 97e97094cf..743bb4c600 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -1374,7 +1374,7 @@ fn convert_amount( .expect("invalid value for amount"); asset_type }) - .collect() + .collect::>() .try_into() .expect("This can't fail"); (asset_types, amount) diff --git a/apps/src/lib/client/utils.rs b/apps/src/lib/client/utils.rs index 8e774ca572..9e901cfaf1 100644 --- a/apps/src/lib/client/utils.rs +++ b/apps/src/lib/client/utils.rs @@ -12,11 +12,11 @@ use flate2::write::GzEncoder; use flate2::Compression; use namada::types::address; use namada::types::chain::ChainId; +use namada::types::dec::Dec; use namada::types::key::*; use prost::bytes::Bytes; use rand::prelude::ThreadRng; use rand::thread_rng; -use rust_decimal::Decimal; use serde_json::json; use sha2::{Digest, Sha256}; @@ -897,16 +897,11 @@ pub fn init_genesis_validator( }: args::InitGenesisValidator, ) { // Validate the commission rate data - if commission_rate > Decimal::ONE || commission_rate < Decimal::ZERO { - eprintln!( - "The validator commission rate must not exceed 1.0 or 100%, and \ - it must be 0 or positive" - ); + if commission_rate > Dec::one() { + eprintln!("The validator commission rate must not exceed 1.0 or 100%"); cli::safe_exit(1) } - if max_commission_rate_change > Decimal::ONE - || max_commission_rate_change < Decimal::ZERO - { + if max_commission_rate_change > Dec::one() { eprintln!( "The validator maximum change in commission rate per epoch must \ not exceed 1.0 or 100%" diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index aa0330b0a8..b4ef132ca4 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -10,7 +10,7 @@ use derivative::Derivative; use namada::core::ledger::testnet_pow; use namada::ledger::governance::parameters::GovParams; use namada::ledger::parameters::EpochDuration; -use namada::ledger::pos::{GenesisValidator, PosParams}; +use namada::ledger::pos::{Dec, GenesisValidator, PosParams}; use namada::types::address::Address; #[cfg(not(feature = "dev"))] use namada::types::chain::ChainId; @@ -20,7 +20,6 @@ use namada::types::key::*; use namada::types::time::{DateTimeUtc, DurationSecs}; use namada::types::token::Denomination; use namada::types::{storage, token}; -use rust_decimal::Decimal; /// Genesis configuration file format pub mod genesis_config { @@ -36,7 +35,7 @@ pub mod genesis_config { use namada::core::ledger::testnet_pow; use namada::ledger::governance::parameters::GovParams; use namada::ledger::parameters::EpochDuration; - use namada::ledger::pos::{GenesisValidator, PosParams}; + use namada::ledger::pos::{Dec, GenesisValidator, PosParams}; use namada::types::address::Address; use namada::types::chain::ProposalBytes; use namada::types::key::dkg_session_keys::DkgPublicKey; @@ -193,9 +192,9 @@ pub mod genesis_config { pub non_staked_balance: Option, /// Commission rate charged on rewards for delegators (bounded inside /// 0-1) - pub commission_rate: Option, + pub commission_rate: Option, /// Maximum change in commission rate permitted per epoch - pub max_commission_rate_change: Option, + pub max_commission_rate_change: Option, // Filename of validator VP. (default: default validator VP) pub validator_vp: Option, // IP:port of the validator. (used in generation only) @@ -289,26 +288,26 @@ pub mod genesis_config { pub unbonding_len: u64, // Votes per token. // XXX: u64 doesn't work with toml-rs! - pub tm_votes_per_token: Decimal, + pub tm_votes_per_token: Dec, // Reward for proposing a block. // XXX: u64 doesn't work with toml-rs! - pub block_proposer_reward: Decimal, + pub block_proposer_reward: Dec, // Reward for voting on a block. // XXX: u64 doesn't work with toml-rs! - pub block_vote_reward: Decimal, + pub block_vote_reward: Dec, // Maximum staking APY // XXX: u64 doesn't work with toml-rs! - pub max_inflation_rate: Decimal, + pub max_inflation_rate: Dec, // Target ratio of staked NAM tokens to total NAM tokens - pub target_staked_ratio: Decimal, + pub target_staked_ratio: Dec, // Portion of a validator's stake that should be slashed on a // duplicate vote. // XXX: u64 doesn't work with toml-rs! - pub duplicate_vote_min_slash_rate: Decimal, + pub duplicate_vote_min_slash_rate: Dec, // Portion of a validator's stake that should be slashed on a // light client attack. // XXX: u64 doesn't work with toml-rs! - pub light_client_attack_min_slash_rate: Decimal, + pub light_client_attack_min_slash_rate: Dec, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -340,21 +339,13 @@ pub mod genesis_config { commission_rate: config .commission_rate .and_then(|rate| { - if rate >= Decimal::ZERO && rate <= Decimal::ONE { - Some(rate) - } else { - None - } + if rate <= Dec::one() { Some(rate) } else { None } }) .expect("Commission rate must be between 0.0 and 1.0"), max_commission_rate_change: config .max_commission_rate_change .and_then(|rate| { - if rate >= Decimal::ZERO && rate <= Decimal::ONE { - Some(rate) - } else { - None - } + if rate <= Dec::one() { Some(rate) } else { None } }) .expect( "Max commission rate change must be between 0.0 and \ @@ -864,11 +855,11 @@ pub struct Parameters { /// Expected number of epochs per year (read only) pub epochs_per_year: u64, /// PoS gain p (read only) - pub pos_gain_p: Decimal, + pub pos_gain_p: Dec, /// PoS gain d (read only) - pub pos_gain_d: Decimal, + pub pos_gain_d: Dec, /// PoS staked ratio (read + write for every epoch) - pub staked_ratio: Decimal, + pub staked_ratio: Dec, /// PoS inflation amount from the last epoch (read + write for every epoch) pub pos_inflation_amount: u64, /// Fixed Wrapper tx fees @@ -888,7 +879,6 @@ pub fn genesis(num_validators: u64) -> Genesis { use namada::types::address::{ self, apfel, btc, dot, eth, kartoffel, nam, schnitzel, }; - use rust_decimal_macros::dec; use crate::wallet; @@ -911,8 +901,9 @@ pub fn genesis(num_validators: u64) -> Genesis { address, tokens: token::Amount::native_whole(200_000), consensus_key: consensus_keypair.ref_to(), - commission_rate: dec!(0.05), - max_commission_rate_change: dec!(0.01), + commission_rate: Dec::new(5, 2).expect("This can't fail"), + max_commission_rate_change: Dec::new(1, 2) + .expect("This can't fail"), }, account_key: account_keypair.ref_to(), protocol_key: protocol_keypair.ref_to(), @@ -939,8 +930,9 @@ pub fn genesis(num_validators: u64) -> Genesis { address, tokens: token::Amount::native_whole(200_000), consensus_key: consensus_keypair.ref_to(), - commission_rate: dec!(0.05), - max_commission_rate_change: dec!(0.01), + commission_rate: Dec::new(5, 2).expect("This can't fail"), + max_commission_rate_change: Dec::new(1, 2) + .expect("This can't fail"), }, account_key: account_keypair.ref_to(), protocol_key: protocol_keypair.ref_to(), @@ -966,9 +958,9 @@ pub fn genesis(num_validators: u64) -> Genesis { implicit_vp_sha256: Default::default(), epochs_per_year: 525_600, /* seconds in yr (60*60*24*365) div seconds * per epoch (60 = min_duration) */ - pos_gain_p: dec!(0.1), - pos_gain_d: dec!(0.1), - staked_ratio: dec!(0.0), + pos_gain_p: Dec::new(1, 2).expect("This can't fail"), + pos_gain_d: Dec::new(1, 2).expect("This can't fail"), + staked_ratio: Dec::zero(), pos_inflation_amount: 0, wrapper_tx_fees: Some(token::Amount::native_whole(0)), }; diff --git a/core/Cargo.toml b/core/Cargo.toml index 2849e8c9b2..5e97f59a41 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -68,6 +68,7 @@ chrono = {version = "0.4.22", default-features = false, features = ["clock", "st data-encoding = "2.3.2" derivative = "2.2.0" ed25519-consensus = "1.2.0" +eyre = "0.6.8" ethabi = "18.0.0" ferveo = {optional = true, git = "https://github.com/anoma/ferveo"} ferveo-common = {git = "https://github.com/anoma/ferveo"} diff --git a/core/src/types/dec.rs b/core/src/types/dec.rs index 0b7b0f20b1..4dfb00f72e 100644 --- a/core/src/types/dec.rs +++ b/core/src/types/dec.rs @@ -1,13 +1,12 @@ //! A non-negative fixed precision decimal type for computation primarily in the //! PoS module. -use core::fmt::{Debug, Formatter}; -use std::fmt::Display; +use std::fmt::{Debug, Display, Formatter}; use std::ops::{Add, AddAssign, Div, Mul, Sub}; use std::str::FromStr; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use eyre::eyre; use serde::{Deserialize, Serialize}; -use thiserror::Error; use crate::types::token::{Amount, Change}; use crate::types::uint::Uint; @@ -15,12 +14,13 @@ use crate::types::uint::Uint; /// The number of Dec places for PoS rational calculations pub const POS_DECIMAL_PRECISION: u8 = 6; -#[allow(missing_docs)] -#[derive(Error, Debug)] -pub enum Error { - #[error("{error}")] - First { error: String }, -} +#[derive(thiserror::Error, Debug)] +#[error(transparent)] +/// Generic error [`Dec`] operations can return +pub struct Error(#[from] eyre::Error); + +/// Generic result type for fallible [`Dec`] operations +pub type Result = std::result::Result; /// A 256 bit number with [`POS_DECIMAL_PRECISION`] number of Dec places. /// @@ -59,7 +59,7 @@ impl std::ops::DerefMut for Dec { } impl Dec { - /// Division with truncation (TDO: better description) + /// Division with truncation (TODO: better description) pub fn trunc_div(&self, rhs: &Self) -> Option { self.0 .fixed_precision_div(rhs, POS_DECIMAL_PRECISION) @@ -102,58 +102,50 @@ impl Dec { } } -// TODO: improve (actualyl do) error handling! impl FromStr for Dec { - type Err = self::Error; + type Err = Error; - fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> Result { if s.starts_with('-') { - return Err(self::Error::First { - error: "Dec cannot be negative".to_string(), - }); + return Err(eyre!("Dec cannot be negative").into()); } - if let Some((large, mut small)) = s.split_once('.') { - let num_large = - u64::from_str(large).map_err(|_| self::Error::First { - error: "Error".to_string(), - })?; - let mut num_small = - u64::from_str(small).map_err(|_| self::Error::First { - error: "Error".to_string(), - })?; - - if num_small == 0u64 { - return Ok(Dec(Uint::from(num_large))); - } - - small = small.trim_end_matches('0'); - let mut num_dec_places = small.len(); - if num_dec_places > POS_DECIMAL_PRECISION as usize { - // truncate to the first `POS_DECIMAL_PRECISION` places - num_dec_places = POS_DECIMAL_PRECISION as usize; - small = &small[..POS_DECIMAL_PRECISION as usize]; - num_small = - u64::from_str(small).map_err(|_| self::Error::First { - error: "Error".to_string(), - })?; - } - if num_large == 0u64 { - return Ok(Dec::new(num_small, num_dec_places as u8) - .expect("Dec creation failed")); - } - let tot_num = format!("{}{}", num_large, num_small); - let tot_num = u64::from_str(tot_num.as_str()).map_err(|_| { - self::Error::First { - error: "Error".to_string(), - } - })?; - Ok(Dec::new(tot_num, num_dec_places as u8) - .expect("Dec creation failed")) - } else { - Err(self::Error::First { - error: "Error".to_string(), - }) + + let (large, small) = s.split_once('.').unwrap_or((s, "0")); + let num_large = Uint::from_str_radix(large, 10).map_err(|e| { + eyre!("Could not parse {} as an integer: {}", large, e) + })?; + + // In theory we could allow this, but it is aesthetically offensive. + // Thus we don't. + if small.is_empty() { + return Err(eyre!( + "Failed to parse Dec from string as there were no numbers \ + following the decimal point." + ) + .into()); } + + let trimmed = small + .trim_end_matches('0') + .chars() + .take(POS_DECIMAL_PRECISION as usize) + .collect::(); + let decimal_part = if trimmed.is_empty() { + Uint::zero() + } else { + Uint::from_str_radix(&trimmed, 10).map_err(|e| { + eyre!("Could not parse .{} as decimals: {}", small, e) + })? * Uint::exp10(POS_DECIMAL_PRECISION as usize - trimmed.len()) + }; + let int_part = Uint::exp10(POS_DECIMAL_PRECISION as usize) + .checked_mul(num_large) + .ok_or_else(|| { + eyre!( + "The number {} is too large to fit in the Dec type.", + num_large + ) + })?; + Ok(Dec(int_part + decimal_part)) } } @@ -244,8 +236,15 @@ impl Mul for Dec { impl Div for Dec { type Output = Self; + /// Unchecked fixed precision division. + /// + /// # Panics: + /// + /// * Denominator is zero + /// * Scaling the left hand side by 10^([`POS_DECIMAL_PRECISION`]) + /// overflows 256 bits fn div(self, rhs: Dec) -> Self::Output { - Self(self.0 / rhs.0) + self.trunc_div(&rhs).unwrap() } } @@ -264,8 +263,60 @@ mod test_dec { #[test] fn test_basic() { assert_eq!( - Dec::one() + Dec::new(3, 0).unwrap() / Dec::new(5, 0).unwrap(), + Dec::new(1, 0).unwrap() + + Dec::new(3, 0).unwrap() / Dec::new(5, 0).unwrap(), Dec::new(16, 1).unwrap() ); } + + /// Test that parsing from string is correct. + #[test] + fn test_from_string() { + // Fewer than six decimal places and non-zero integer part + assert_eq!( + Dec::from_str("3.14").expect("Test failed"), + Dec::new(314, 2).expect("Test failed"), + ); + + // more than six decimal places and zero integer part + assert_eq!( + Dec::from_str("0.1234567").expect("Test failed"), + Dec::new(123456, 6).expect("Test failed"), + ); + + // No zero before the decimal + assert_eq!( + Dec::from_str(".333333").expect("Test failed"), + Dec::new(333333, 6).expect("Test failed"), + ); + + // No decimal places + assert_eq!( + Dec::from_str("50").expect("Test failed"), + Dec::new(50, 0).expect("Test failed"), + ); + + // Test zero representations + assert_eq!(Dec::from_str("0").expect("Test failed"), Dec::zero()); + assert_eq!(Dec::from_str("0.0").expect("Test failed"), Dec::zero()); + assert_eq!(Dec::from_str(".0").expect("Test failed"), Dec::zero()); + + // Error conditions + + // Test that a decimal point must be followed by numbers + assert!(Dec::from_str("0.").is_err()); + // Test that multiple decimal points get caught + assert!(Dec::from_str("1.2.3").is_err()); + // Test that negative numbers are rejected + assert!(Dec::from_str("-1").is_err()); + // Test that non-numerics are caught + assert!(Dec::from_str("DEADBEEF.12").is_err()); + assert!(Dec::from_str("23.DEADBEEF").is_err()); + // Test that we catch strings overflowing 256 bits + let mut yuge = String::from("1"); + for _ in 0..80 { + yuge.push('0'); + } + assert!(Dec::from_str(&yuge).is_err()); + } } diff --git a/proof_of_stake/src/types.rs b/proof_of_stake/src/types.rs index 6d1a0bdf3b..a441c9312e 100644 --- a/proof_of_stake/src/types.rs +++ b/proof_of_stake/src/types.rs @@ -467,10 +467,6 @@ impl Display for SlashType { } } -// -------------------------------------------------------------------------------------------- - -// -------------------------------------------------------------------------------------------- - // /// Multiply a value of type Dec with one of type u64 and then return the // /// truncated u64 // pub fn decimal_mult_u128(dec: Dec, int: u128) -> u128 { From bb93c858221875ee582a8a5f2abc3904d57fedbc Mon Sep 17 00:00:00 2001 From: satan Date: Wed, 26 Apr 2023 15:23:01 +0200 Subject: [PATCH 031/151] [chore]: Added more tests and fixes --- core/src/types/dec.rs | 11 +++++++++++ core/src/types/token.rs | 11 ----------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/core/src/types/dec.rs b/core/src/types/dec.rs index 4dfb00f72e..59074e7506 100644 --- a/core/src/types/dec.rs +++ b/core/src/types/dec.rs @@ -267,6 +267,17 @@ mod test_dec { + Dec::new(3, 0).unwrap() / Dec::new(5, 0).unwrap(), Dec::new(16, 1).unwrap() ); + + // Fixed precision division is more thoroughly tested for the `Uint` type. These + // are sanity checks that the precision is correct. + assert_eq!( + Dec::new(1, 6).expect("Test failed") / Dec::new(1, 0).expect("Test failed"), + Dec::one(), + ); + assert_eq!( + Dec::new(1, 6).expect("Test failed") / (Dec::new(1, 0).expect("Test failed") + Dec::one()), + Dec::zero(), + ); } /// Test that parsing from string is correct. diff --git a/core/src/types/token.rs b/core/src/types/token.rs index b28b2af399..1608e6de28 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -233,17 +233,6 @@ impl Amount { pub fn from_string_precise(string: &str) -> Result { DenominatedAmount::from_str(string).map(|den| den.amount) } - - /// Convert the amount to [`Decimal`] ignoring its scale (i.e. as an integer - /// in micro units). - /// - /// # Panics - /// - /// Panics if the stored amount overflows either a u128 or the [`Decimal`] - /// type. - pub fn as_dec_unscaled(&self) -> Decimal { - Into::::into(u128::try_from(self.raw).unwrap()) - } } /// Given a number represented as `M*B^D`, then From 1968660466809d369005cc3974fc5c3798eba5cf Mon Sep 17 00:00:00 2001 From: brentstone Date: Wed, 26 Apr 2023 13:06:39 -0400 Subject: [PATCH 032/151] WIP more `Dec` integration and testing (compiling!) --- apps/src/lib/client/rpc.rs | 2 +- apps/src/lib/config/genesis.rs | 13 ++- .../lib/node/ledger/shell/finalize_block.rs | 28 +++--- apps/src/lib/node/ledger/shell/init_chain.rs | 5 +- core/src/ledger/parameters/mod.rs | 24 ++--- core/src/ledger/storage/mod.rs | 10 +- core/src/types/dec.rs | 94 ++++++++++++++++--- core/src/types/token.rs | 13 ++- proof_of_stake/src/lib.rs | 4 +- proof_of_stake/src/tests.rs | 10 +- proof_of_stake/src/types.rs | 4 +- shared/src/types/mod.rs | 2 +- tests/src/native_vp/pos.rs | 10 +- wasm/Cargo.lock | 2 +- wasm/wasm_source/src/tx_bond.rs | 6 +- .../src/tx_change_validator_commission.rs | 47 +++++----- wasm/wasm_source/src/tx_unbond.rs | 5 +- wasm/wasm_source/src/tx_withdraw.rs | 5 +- wasm/wasm_source/src/vp_implicit.rs | 9 +- wasm/wasm_source/src/vp_user.rs | 9 +- wasm/wasm_source/src/vp_validator.rs | 14 +-- wasm_for_tests/wasm_source/Cargo.lock | 2 +- 22 files changed, 199 insertions(+), 119 deletions(-) diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index dc8c406810..f574661cf1 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -1176,7 +1176,7 @@ pub async fn query_shielded_balance( read_tokens .entry(addr.clone()) .and_modify(|addr_vec| addr_vec.push(sub_prefix.clone())) - .or_insert(vec![sub_prefix.clone()]); + .or_insert_with(|| vec![sub_prefix.clone()]); // Only assets with the current timestamp count println!( "Shielded Token {}{}:", diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index b4ef132ca4..38762bbd76 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -43,7 +43,6 @@ pub mod genesis_config { use namada::types::time::Rfc3339String; use namada::types::token::Denomination; use namada::types::{storage, token}; - use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -267,9 +266,9 @@ pub mod genesis_config { /// Expected number of epochs per year pub epochs_per_year: u64, /// PoS gain p - pub pos_gain_p: Decimal, + pub pos_gain_p: Dec, /// PoS gain d - pub pos_gain_d: Decimal, + pub pos_gain_d: Dec, #[cfg(not(feature = "mainnet"))] /// Fix wrapper tx fees pub wrapper_tx_fees: Option, @@ -612,8 +611,8 @@ pub mod genesis_config { epochs_per_year: parameters.epochs_per_year, pos_gain_p: parameters.pos_gain_p, pos_gain_d: parameters.pos_gain_d, - staked_ratio: Decimal::ZERO, - pos_inflation_amount: 0, + staked_ratio: Dec::zero(), + pos_inflation_amount: token::Amount::zero(), wrapper_tx_fees: parameters.wrapper_tx_fees, }; @@ -861,7 +860,7 @@ pub struct Parameters { /// PoS staked ratio (read + write for every epoch) pub staked_ratio: Dec, /// PoS inflation amount from the last epoch (read + write for every epoch) - pub pos_inflation_amount: u64, + pub pos_inflation_amount: token::Amount, /// Fixed Wrapper tx fees #[cfg(not(feature = "mainnet"))] pub wrapper_tx_fees: Option, @@ -961,7 +960,7 @@ pub fn genesis(num_validators: u64) -> Genesis { pos_gain_p: Dec::new(1, 2).expect("This can't fail"), pos_gain_d: Dec::new(1, 2).expect("This can't fail"), staked_ratio: Dec::zero(), - pos_inflation_amount: 0, + pos_inflation_amount: token::Amount::zero(), wrapper_tx_fees: Some(token::Amount::native_whole(0)), }; let albert = EstablishedAccount { diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index ffb3db21fe..242ac3d619 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -640,12 +640,12 @@ where // TODO: properly fetch these values (arbitrary for now) let masp_locked_supply: Amount = Amount::default(); - let masp_locked_ratio_target = Dec::new(5, 1); - let masp_locked_ratio_last = Dec::new(5, 1); - let masp_max_inflation_rate = Dec::new(2, 1); - let masp_last_inflation_rate = Dec::new(12, 2); - let masp_p_gain = Dec::new(1, 1); - let masp_d_gain = Dec::new(1, 1); + let masp_locked_ratio_target = Dec::new(5, 1).expect("Cannot fail"); + let masp_locked_ratio_last = Dec::new(5, 1).expect("Cannot fail"); + let masp_max_inflation_rate = Dec::new(2, 1).expect("Cannot fail"); + let masp_last_inflation_rate = Dec::new(12, 2).expect("Cannot fail"); + let masp_p_gain = Dec::new(1, 1).expect("Cannot fail"); + let masp_d_gain = Dec::new(1, 1).expect("Cannot fail"); // Run rewards PD controller let pos_controller = inflation::RewardsController { @@ -669,11 +669,9 @@ where locked_ratio_target: masp_locked_ratio_target, locked_ratio_last: masp_locked_ratio_last, max_reward_rate: masp_max_inflation_rate, - last_inflation_amount: token::Amount::from_decimal( + last_inflation_amount: token::Amount::from( masp_last_inflation_rate, - token::NATIVE_MAX_DECIMAL_PLACES, - ) - .expect("Amount out of bounds"), + ), p_gain_nom: masp_p_gain, d_gain_nom: masp_d_gain, epochs_per_year, @@ -726,11 +724,11 @@ where let last_rewards_product = validator_rewards_products_handle(&address) .get(&self.wl_storage, &last_epoch)? - .unwrap_or(Dec::one()); + .unwrap_or_else(Dec::one); let last_delegation_product = delegator_rewards_products_handle(&address) .get(&self.wl_storage, &last_epoch)? - .unwrap_or(Dec::one()); + .unwrap_or_else(Dec::one); let commission_rate = validator_commission_rate_handle(&address) .get(&self.wl_storage, last_epoch, ¶ms)? .expect("Should be able to find validator commission rate"); @@ -743,7 +741,7 @@ where / stake); new_rewards_products .insert(address, (new_product, new_delegation_product)); - reward_tokens_remaining -= reward as u64; + reward_tokens_remaining -= reward; } for ( address, @@ -912,8 +910,8 @@ mod test_finalize_block { InitProposalData, ProposalType, VoteProposalData, }; use namada::types::transaction::{EncryptionKey, Fee, WrapperTx, MIN_FEE}; + use namada::types::uint::Uint; use namada_test_utils::TestWasms; - use rust_decimal_macros::dec; use test_log::test; use super::*; @@ -1626,7 +1624,7 @@ mod test_finalize_block { assert!(rewards_prod_3.is_empty(&shell.wl_storage).unwrap()); assert!(rewards_prod_4.is_empty(&shell.wl_storage).unwrap()); let acc_sum = get_rewards_sum(&shell.wl_storage); - assert!(is_decimal_equal_enough(dec!(3), acc_sum)); + assert!(is_decimal_equal_enough(Dec(Uint::from(3)), acc_sum)); let acc = get_rewards_acc(&shell.wl_storage); assert!( acc.get(&val1.address).cloned().unwrap() diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index 1bd9ff9e6d..3eb891b6d6 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -10,9 +10,9 @@ use namada::ledger::storage_api::token::{ credit_tokens, read_balance, read_total_supply, write_denom, }; use namada::ledger::storage_api::{ResultExt, StorageRead, StorageWrite}; +use namada::types::dec::Dec; use namada::types::hash::Hash as CodeHash; use namada::types::key::*; -use rust_decimal::Decimal; use super::*; use crate::facade::tendermint_proto::abci; @@ -407,8 +407,7 @@ where // Set the ratio of staked to total NAM tokens in the parameters storage parameters::update_staked_ratio_parameter( &mut self.wl_storage, - &(Decimal::try_from(total_staked_nam).unwrap() - / Decimal::try_from(total_nam).unwrap()), + &(Dec::from(total_staked_nam) / Dec::from(total_nam)), ) .expect("unable to set staked ratio of NAM in storage"); diff --git a/core/src/ledger/parameters/mod.rs b/core/src/ledger/parameters/mod.rs index d835e1878f..ab72527522 100644 --- a/core/src/ledger/parameters/mod.rs +++ b/core/src/ledger/parameters/mod.rs @@ -2,7 +2,6 @@ pub mod storage; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -use rust_decimal::Decimal; use thiserror::Error; use super::storage::types; @@ -10,6 +9,7 @@ use super::storage_api::{self, ResultExt, StorageRead, StorageWrite}; use crate::ledger::storage::{self as ledger_storage}; use crate::types::address::{Address, InternalAddress}; use crate::types::chain::ProposalBytes; +use crate::types::dec::Dec; use crate::types::hash::Hash; use crate::types::time::DurationSecs; use crate::types::token; @@ -45,13 +45,13 @@ pub struct Parameters { /// Expected number of epochs per year (read only) pub epochs_per_year: u64, /// PoS gain p (read only) - pub pos_gain_p: Decimal, + pub pos_gain_p: Dec, /// PoS gain d (read only) - pub pos_gain_d: Decimal, + pub pos_gain_d: Dec, /// PoS staked ratio (read + write for every epoch) - pub staked_ratio: Decimal, + pub staked_ratio: Dec, /// PoS inflation amount from the last epoch (read + write for every epoch) - pub pos_inflation_amount: u64, + pub pos_inflation_amount: token::Amount, #[cfg(not(feature = "mainnet"))] /// Faucet account for free token withdrawal pub faucet_account: Option
, @@ -276,7 +276,7 @@ where /// cost. pub fn update_pos_gain_p_parameter( storage: &mut S, - value: &Decimal, + value: &Dec, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, @@ -289,7 +289,7 @@ where /// cost. pub fn update_pos_gain_d_parameter( storage: &mut S, - value: &Decimal, + value: &Dec, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, @@ -302,7 +302,7 @@ where /// gas cost. pub fn update_staked_ratio_parameter( storage: &mut S, - value: &Decimal, + value: &Dec, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, @@ -435,28 +435,28 @@ where // read PoS gain P let pos_gain_p_key = storage::get_pos_gain_p_key(); let value = storage.read(&pos_gain_p_key)?; - let pos_gain_p: Decimal = value + let pos_gain_p = value .ok_or(ReadError::ParametersMissing) .into_storage_result()?; // read PoS gain D let pos_gain_d_key = storage::get_pos_gain_d_key(); let value = storage.read(&pos_gain_d_key)?; - let pos_gain_d: Decimal = value + let pos_gain_d = value .ok_or(ReadError::ParametersMissing) .into_storage_result()?; // read staked ratio let staked_ratio_key = storage::get_staked_ratio_key(); let value = storage.read(&staked_ratio_key)?; - let staked_ratio: Decimal = value + let staked_ratio = value .ok_or(ReadError::ParametersMissing) .into_storage_result()?; // read PoS inflation rate let pos_inflation_key = storage::get_pos_inflation_amount_key(); let value = storage.read(&pos_inflation_key)?; - let pos_inflation_amount: u64 = value + let pos_inflation_amount = value .ok_or(ReadError::ParametersMissing) .into_storage_result()?; diff --git a/core/src/ledger/storage/mod.rs b/core/src/ledger/storage/mod.rs index 69c159b1e1..0560844d6a 100644 --- a/core/src/ledger/storage/mod.rs +++ b/core/src/ledger/storage/mod.rs @@ -1003,11 +1003,11 @@ mod tests { use chrono::{TimeZone, Utc}; use proptest::prelude::*; use proptest::test_runner::Config; - use rust_decimal_macros::dec; use super::testing::*; use super::*; use crate::ledger::parameters::{self, Parameters}; + use crate::types::dec::Dec; use crate::types::time::{self, Duration}; prop_compose! { @@ -1086,10 +1086,10 @@ mod tests { tx_whitelist: vec![], implicit_vp_code_hash: Hash::zero(), epochs_per_year: 100, - pos_gain_p: dec!(0.1), - pos_gain_d: dec!(0.1), - staked_ratio: dec!(0.1), - pos_inflation_amount: 0, + pos_gain_p: Dec::new(1,1).expect("Cannot fail"), + pos_gain_d: Dec::new(1,1).expect("Cannot fail"), + staked_ratio: Dec::new(1,1).expect("Cannot fail"), + pos_inflation_amount: token::Amount::zero(), #[cfg(not(feature = "mainnet"))] faucet_account: None, #[cfg(not(feature = "mainnet"))] diff --git a/core/src/types/dec.rs b/core/src/types/dec.rs index 59074e7506..493d5c3321 100644 --- a/core/src/types/dec.rs +++ b/core/src/types/dec.rs @@ -73,12 +73,15 @@ impl Dec { /// The representation of 1 pub fn one() -> Self { - Self(Uint::one()) + Self(Uint::one() * Uint::exp10(POS_DECIMAL_PRECISION as usize)) } /// The representation of 2 pub fn two() -> Self { - Self(Uint::one() + Uint::one()) + Self( + (Uint::one() + Uint::one()) + * Uint::exp10(POS_DECIMAL_PRECISION as usize), + ) } /// Create a new [`Dec`] using a mantissa and a scale. @@ -100,6 +103,11 @@ impl Dec { *other - *self } } + + /// Convert the Dec type into a Uint with truncation + pub fn to_uint(&self) -> Uint { + self.0 / Uint::exp10(POS_DECIMAL_PRECISION as usize) + } } impl FromStr for Dec { @@ -161,6 +169,19 @@ impl From for Dec { } } +// impl TryFrom for u64 { +// type Error = Error; + +// fn try_from(value: Dec) -> std::result::Result { +// let int = value.0 / Uint::exp10(POS_DECIMAL_PRECISION as usize); +// if int > Uint::from(u64::MAX) { +// Err(eyre!("Dec value is too large to fit in a u64").into()) +// } else { +// Ok(int.into()) +// } +// } +// } + impl Add for Dec { type Output = Self; @@ -250,7 +271,18 @@ impl Div for Dec { impl Display for Dec { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let string = self.0.to_string(); + let mut string = self.0.to_string(); + if string.len() > POS_DECIMAL_PRECISION as usize { + let idx = string.len() - POS_DECIMAL_PRECISION as usize; + string.insert(idx, '.'); + } else { + let mut str_pre = "0.".to_string(); + for _ in 0..(POS_DECIMAL_PRECISION as usize - string.len()) { + str_pre.push('0'); + } + str_pre.push_str(string.as_str()); + string = str_pre; + }; f.write_str(&string) } } @@ -258,31 +290,71 @@ impl Display for Dec { #[cfg(test)] mod test_dec { use super::*; + use crate::types::token::{Amount, Change}; /// Fill in tests later #[test] - fn test_basic() { + fn test_dec_basics() { assert_eq!( - Dec::new(1, 0).unwrap() - + Dec::new(3, 0).unwrap() / Dec::new(5, 0).unwrap(), + Dec::one() + Dec::new(3, 0).unwrap() / Dec::new(5, 0).unwrap(), Dec::new(16, 1).unwrap() ); + assert_eq!(Dec::new(1, 0).expect("Test failed"), Dec::one()); + assert_eq!(Dec::new(2, 0).expect("Test failed"), Dec::two()); + assert_eq!( + Dec(Uint::from(1653)), + Dec::new(1653, POS_DECIMAL_PRECISION).expect("Test failed") + ); + assert_eq!( + Dec::new(123456789, 4).expect("Test failed").to_uint(), + Uint::from(12345) + ); + assert_eq!( + Dec::new(123, 4).expect("Test failed").to_uint(), + Uint::zero() + ); + assert_eq!( + Dec::from_str("4876.3855").expect("Test failed").to_uint(), + Uint::from(4876) + ); - // Fixed precision division is more thoroughly tested for the `Uint` type. These - // are sanity checks that the precision is correct. + // Fixed precision division is more thoroughly tested for the `Uint` + // type. These are sanity checks that the precision is correct. assert_eq!( - Dec::new(1, 6).expect("Test failed") / Dec::new(1, 0).expect("Test failed"), + Dec::new(1, 6).expect("Test failed") + / Dec::new(1, 6).expect("Test failed"), Dec::one(), ); assert_eq!( - Dec::new(1, 6).expect("Test failed") / (Dec::new(1, 0).expect("Test failed") + Dec::one()), + Dec::new(1, 6).expect("Test failed") + / (Dec::new(1, 0).expect("Test failed") + Dec::one()), + Dec::zero(), + ); + assert_eq!( + Dec::new(1, 6).expect("Test failed") / Dec::two(), Dec::zero(), ); } + /// Test the `Dec` and `Amount` interplay + #[test] + fn test_dec_and_amount() { + let amt = Amount::from(1018u64); + let dec = Dec::from_str("2.76").unwrap(); + + debug_assert_eq!( + Dec::from(amt), + Dec::new(1018, 0).expect("Test failed") + ); + debug_assert_eq!(dec * amt, Amount::from(2809u64)); + + let chg = -amt.change(); + debug_assert_eq!(dec * chg, Change::from(-2809i64)); + } + /// Test that parsing from string is correct. #[test] - fn test_from_string() { + fn test_dec_from_string() { // Fewer than six decimal places and non-zero integer part assert_eq!( Dec::from_str("3.14").expect("Test failed"), diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 1608e6de28..734ca998c0 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -11,6 +11,7 @@ use rust_decimal::prelude::Decimal; use serde::{Deserialize, Serialize}; use thiserror::Error; +use super::dec::POS_DECIMAL_PRECISION; use crate::ledger::storage_api::token::read_denom; use crate::ledger::storage_api::StorageRead; use crate::types::address::{masp, Address, DecodeError as AddressError}; @@ -482,9 +483,19 @@ impl From for Amount { } } +impl From for Amount { + fn from(val: u64) -> Amount { + Amount { + raw: Uint::from(val), + } + } +} + impl From for Amount { fn from(dec: Dec) -> Amount { - Amount { raw: dec.0 } + Amount { + raw: dec.0 / Uint::exp10(POS_DECIMAL_PRECISION as usize), + } } } diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index eed29cdfe1..e3ea5a2077 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -2052,9 +2052,7 @@ where ); Some(ValidatorSetUpdate::Consensus(ConsensusValidator { consensus_key, - bonded_stake: u128::try_from(cur_stake) - .expect("Amount out of bounds") - as u64, + bonded_stake: cur_stake, })) }); let cur_below_capacity_validators = diff --git a/proof_of_stake/src/tests.rs b/proof_of_stake/src/tests.rs index 63d2b84eb2..cfc4d244a0 100644 --- a/proof_of_stake/src/tests.rs +++ b/proof_of_stake/src/tests.rs @@ -1091,7 +1091,7 @@ fn test_validator_sets() { tm_updates[0], ValidatorSetUpdate::Consensus(ConsensusValidator { consensus_key: pk3, - bonded_stake: u128::try_from(stake3).unwrap() as u64, + bonded_stake: stake3, }) ); @@ -1146,7 +1146,7 @@ fn test_validator_sets() { tm_updates[0], ValidatorSetUpdate::Consensus(ConsensusValidator { consensus_key: pk5, - bonded_stake: u128::try_from(stake5).unwrap() as u64, + bonded_stake: stake5, }) ); assert_eq!(tm_updates[1], ValidatorSetUpdate::Deactivated(pk2)); @@ -1342,7 +1342,7 @@ fn test_validator_sets() { tm_updates[0], ValidatorSetUpdate::Consensus(ConsensusValidator { consensus_key: pk4.clone(), - bonded_stake: u128::try_from(stake4).unwrap() as u64, + bonded_stake: stake4, }) ); assert_eq!(tm_updates[1], ValidatorSetUpdate::Deactivated(pk1)); @@ -1458,7 +1458,7 @@ fn test_validator_sets() { tm_updates[0], ValidatorSetUpdate::Consensus(ConsensusValidator { consensus_key: pk6, - bonded_stake: u128::try_from(stake6).unwrap() as u64, + bonded_stake: stake6, }) ); assert_eq!(tm_updates[1], ValidatorSetUpdate::Deactivated(pk4)); @@ -1640,7 +1640,7 @@ fn test_validator_sets_swap() { tm_updates[0], ValidatorSetUpdate::Consensus(ConsensusValidator { consensus_key: pk3, - bonded_stake: u128::try_from(stake3).unwrap() as u64, + bonded_stake: stake3, }) ); } diff --git a/proof_of_stake/src/types.rs b/proof_of_stake/src/types.rs index a441c9312e..eb87cb22f1 100644 --- a/proof_of_stake/src/types.rs +++ b/proof_of_stake/src/types.rs @@ -200,7 +200,7 @@ pub struct ConsensusValidator { /// A public key used for signing validator's consensus actions pub consensus_key: common::PublicKey, /// Total bonded stake of the validator - pub bonded_stake: u64, + pub bonded_stake: token::Amount, } /// ID of a bond and/or an unbond. @@ -509,7 +509,7 @@ impl Display for SlashType { pub fn into_tm_voting_power(votes_per_token: Dec, tokens: Amount) -> i64 { let pow = votes_per_token * u128::try_from(tokens).expect("Voting power out of bounds"); - i64::try_from(pow.0).expect("Invalid voting power") + i64::try_from(pow.to_uint()).expect("Invalid voting power") } #[cfg(test)] diff --git a/shared/src/types/mod.rs b/shared/src/types/mod.rs index ba1265025b..4d50ee4ac7 100644 --- a/shared/src/types/mod.rs +++ b/shared/src/types/mod.rs @@ -5,5 +5,5 @@ pub mod key; pub use namada_core::types::{ address, chain, dec, governance, hash, internal, masp, storage, time, - token, transaction, validity_predicate, + token, transaction, uint, validity_predicate, }; diff --git a/tests/src/native_vp/pos.rs b/tests/src/native_vp/pos.rs index 5a59ce0494..3e7fff1cb7 100644 --- a/tests/src/native_vp/pos.rs +++ b/tests/src/native_vp/pos.rs @@ -588,10 +588,10 @@ pub mod testing { use namada::types::key::RefTo; use namada::types::storage::Epoch; use namada::types::{address, key, token}; + use namada_core::types::dec::Dec; use namada_core::types::token::{Amount, Change}; use namada_tx_prelude::{Address, StorageRead, StorageWrite}; use proptest::prelude::*; - use rust_decimal::Decimal; use crate::tx::{self, tx_host_env}; @@ -612,8 +612,8 @@ pub mod testing { InitValidator { address: Address, consensus_key: PublicKey, - commission_rate: Decimal, - max_commission_rate_change: Decimal, + commission_rate: Dec, + max_commission_rate_change: Dec, }, Bond { amount: token::Amount, @@ -700,11 +700,11 @@ pub mod testing { }, ValidatorCommissionRate { address: Address, - rate: Decimal, + rate: Dec, }, ValidatorMaxCommissionRateChange { address: Address, - change: Decimal, + change: Dec, }, } diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index 200bba3eab..73cf3f057b 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -2657,6 +2657,7 @@ dependencies = [ "derivative", "ed25519-consensus", "ethabi", + "eyre", "ferveo-common", "ibc", "ibc-proto", @@ -2765,7 +2766,6 @@ dependencies = [ "namada_macros", "namada_proof_of_stake", "namada_vm_env", - "rust_decimal", "sha2 0.10.6", "thiserror", ] diff --git a/wasm/wasm_source/src/tx_bond.rs b/wasm/wasm_source/src/tx_bond.rs index a4c459c873..c4d4602985 100644 --- a/wasm/wasm_source/src/tx_bond.rs +++ b/wasm/wasm_source/src/tx_bond.rs @@ -26,6 +26,7 @@ mod tests { }; use namada::proto::Tx; use namada::types::chain::ChainId; + use namada::types::dec::Dec; use namada::types::storage::Epoch; use namada_tests::log::test; use namada_tests::native_vp::pos::init_pos; @@ -41,7 +42,6 @@ mod tests { use namada_tx_prelude::token; use namada_vp_prelude::proof_of_stake::WeightedValidator; use proptest::prelude::*; - use rust_decimal; use super::*; @@ -73,8 +73,8 @@ mod tests { let is_delegation = matches!(&bond.source, Some(source) if *source != bond.validator); let consensus_key = key::testing::keypair_1().ref_to(); - let commission_rate = rust_decimal::Decimal::new(5, 2); - let max_commission_rate_change = rust_decimal::Decimal::new(1, 2); + let commission_rate = Dec::new(5, 2).expect("Cannot fail"); + let max_commission_rate_change = Dec::new(1, 2).expect("Cannot fail"); let genesis_validators = [GenesisValidator { address: bond.validator.clone(), diff --git a/wasm/wasm_source/src/tx_change_validator_commission.rs b/wasm/wasm_source/src/tx_change_validator_commission.rs index 2ab0e25f11..e2a41bc69e 100644 --- a/wasm/wasm_source/src/tx_change_validator_commission.rs +++ b/wasm/wasm_source/src/tx_change_validator_commission.rs @@ -12,7 +12,7 @@ fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { validator, new_rate, } = transaction::pos::CommissionChange::try_from_slice(&data[..]) - .wrap_err("failed to decode Decimal value")?; + .wrap_err("failed to decode Dec value")?; ctx.change_validator_commission_rate(&validator, &new_rate) } @@ -24,6 +24,7 @@ mod tests { use namada::proof_of_stake::validator_commission_rate_handle; use namada::proto::Tx; use namada::types::chain::ChainId; + use namada::types::dec::Dec; use namada::types::storage::Epoch; use namada_tests::log::test; use namada_tests::native_vp::pos::init_pos; @@ -36,8 +37,6 @@ mod tests { use namada_tx_prelude::token; use namada_vp_prelude::proof_of_stake::GenesisValidator; use proptest::prelude::*; - use rust_decimal::prelude::ToPrimitive; - use rust_decimal::Decimal; use super::*; @@ -61,8 +60,8 @@ mod tests { fn test_tx_change_validator_commission_aux( commission_change: transaction::pos::CommissionChange, - initial_rate: Decimal, - max_change: Decimal, + initial_rate: Dec, + max_change: Dec, key: key::common::SecretKey, pos_params: PosParams, ) -> TxResult { @@ -87,7 +86,7 @@ mod tests { let commission_rate_handle = validator_commission_rate_handle(&commission_change.validator); - let mut commission_rates_pre = Vec::>::new(); + let mut commission_rates_pre = Vec::>::new(); for epoch in Epoch::default().iter_range(pos_params.unbonding_len + 1) { commission_rates_pre.push(commission_rate_handle.get( ctx(), @@ -152,20 +151,20 @@ mod tests { Ok(()) } - fn arb_rate(min: Decimal, max: Decimal) -> impl Strategy { - let int_min: u64 = (min * Decimal::from(100_000_u64)) - .to_u64() - .unwrap_or_default(); - let int_max: u64 = (max * Decimal::from(100_000_u64)).to_u64().unwrap(); - (int_min..=int_max) - .prop_map(|num| Decimal::from(num) / Decimal::from(100_000_u64)) + fn arb_rate(min: Dec, max: Dec) -> impl Strategy { + let scale = Dec::new(100_000, 0).expect("Test failed"); + let int_min = (min * scale).to_uint(); + let int_min = u64::try_from(int_min).unwrap(); + let int_max = (max * scale).to_uint(); + let int_max = u64::try_from(int_max).unwrap(); + (int_min..=int_max).prop_map(move |num| Dec::from(num) / scale) } fn arb_new_rate( - min: Decimal, - max: Decimal, - rate_pre: Decimal, - ) -> impl Strategy { + min: Dec, + max: Dec, + rate_pre: Dec, + ) -> impl Strategy { arb_rate(min, max).prop_filter( "New rate must not be equal to the previous epoch's rate", move |v| v != &rate_pre, @@ -173,11 +172,11 @@ mod tests { } fn arb_commission_change( - rate_pre: Decimal, - max_change: Decimal, + rate_pre: Dec, + max_change: Dec, ) -> impl Strategy { - let min = cmp::max(rate_pre - max_change, Decimal::ZERO); - let max = cmp::min(rate_pre + max_change, Decimal::ONE); + let min = cmp::max(rate_pre - max_change, Dec::zero()); + let max = cmp::min(rate_pre + max_change, Dec::one()); (arb_established_address(), arb_new_rate(min, max, rate_pre)).prop_map( |(validator, new_rate)| transaction::pos::CommissionChange { validator: Address::Established(validator), @@ -187,10 +186,10 @@ mod tests { } fn arb_commission_info() - -> impl Strategy + -> impl Strategy { - let min = Decimal::ZERO; - let max = Decimal::ONE; + let min = Dec::zero(); + let max = Dec::one(); (arb_rate(min, max), arb_rate(min, max)).prop_flat_map( |(rate, change)| { ( diff --git a/wasm/wasm_source/src/tx_unbond.rs b/wasm/wasm_source/src/tx_unbond.rs index 82fb25ff6f..9d3b467098 100644 --- a/wasm/wasm_source/src/tx_unbond.rs +++ b/wasm/wasm_source/src/tx_unbond.rs @@ -26,6 +26,7 @@ mod tests { }; use namada::proto::Tx; use namada::types::chain::ChainId; + use namada::types::dec::Dec; use namada::types::storage::Epoch; use namada_tests::log::test; use namada_tests::native_vp::pos::init_pos; @@ -70,8 +71,8 @@ mod tests { &unbond.source, Some(source) if *source != unbond.validator); let consensus_key = key::testing::keypair_1().ref_to(); - let commission_rate = rust_decimal::Decimal::new(5, 2); - let max_commission_rate_change = rust_decimal::Decimal::new(1, 2); + let commission_rate = Dec::new(5, 2).expect("Cannot fail"); + let max_commission_rate_change = Dec::new(1, 2).expect("Cannot fail"); let genesis_validators = [GenesisValidator { address: unbond.validator.clone(), diff --git a/wasm/wasm_source/src/tx_withdraw.rs b/wasm/wasm_source/src/tx_withdraw.rs index f6386f104c..242ce0fcf1 100644 --- a/wasm/wasm_source/src/tx_withdraw.rs +++ b/wasm/wasm_source/src/tx_withdraw.rs @@ -25,6 +25,7 @@ mod tests { use namada::proof_of_stake::unbond_handle; use namada::proto::Tx; use namada::types::chain::ChainId; + use namada::types::dec::Dec; use namada::types::storage::Epoch; use namada_tests::log::test; use namada_tests::native_vp::pos::init_pos; @@ -73,8 +74,8 @@ mod tests { let is_delegation = matches!( &withdraw.source, Some(source) if *source != withdraw.validator); let consensus_key = key::testing::keypair_1().ref_to(); - let commission_rate = rust_decimal::Decimal::new(5, 2); - let max_commission_rate_change = rust_decimal::Decimal::new(1, 2); + let commission_rate = Dec::new(5, 2).expect("Cannot fail"); + let max_commission_rate_change = Dec::new(1, 2).expect("Cannot fail"); let genesis_validators = [GenesisValidator { address: withdraw.validator.clone(), diff --git a/wasm/wasm_source/src/vp_implicit.rs b/wasm/wasm_source/src/vp_implicit.rs index 985f4f7a80..3f1ea331f2 100644 --- a/wasm/wasm_source/src/vp_implicit.rs +++ b/wasm/wasm_source/src/vp_implicit.rs @@ -225,6 +225,7 @@ fn validate_tx( mod tests { // Use this as `#[test]` annotation to enable logging use namada::ledger::pos::{GenesisValidator, PosParams}; + use namada::types::dec::Dec; use namada::types::storage::Epoch; use namada_test_utils::TestWasms; use namada_tests::log::test; @@ -416,8 +417,8 @@ mod tests { let validator = address::testing::established_address_3(); let initial_stake = token::Amount::from_uint(10_098_123, 0).unwrap(); let consensus_key = key::testing::keypair_2().ref_to(); - let commission_rate = rust_decimal::Decimal::new(5, 2); - let max_commission_rate_change = rust_decimal::Decimal::new(1, 2); + let commission_rate = Dec::new(5, 2).unwrap(); + let max_commission_rate_change = Dec::new(1, 2).unwrap(); let genesis_validators = [GenesisValidator { address: validator.clone(), @@ -487,8 +488,8 @@ mod tests { let validator = address::testing::established_address_3(); let initial_stake = token::Amount::from_uint(10_098_123, 0).unwrap(); let consensus_key = key::testing::keypair_2().ref_to(); - let commission_rate = rust_decimal::Decimal::new(5, 2); - let max_commission_rate_change = rust_decimal::Decimal::new(1, 2); + let commission_rate = Dec::new(5, 2).unwrap(); + let max_commission_rate_change = Dec::new(1, 2).unwrap(); let genesis_validators = [GenesisValidator { address: validator.clone(), diff --git a/wasm/wasm_source/src/vp_user.rs b/wasm/wasm_source/src/vp_user.rs index 812bbd7bee..1cbb2b4d94 100644 --- a/wasm/wasm_source/src/vp_user.rs +++ b/wasm/wasm_source/src/vp_user.rs @@ -213,6 +213,7 @@ fn validate_tx( mod tests { use address::testing::arb_non_internal_address; use namada::ledger::pos::{GenesisValidator, PosParams}; + use namada::types::dec::Dec; use namada::types::storage::Epoch; use namada_test_utils::TestWasms; // Use this as `#[test]` annotation to enable logging @@ -435,8 +436,8 @@ mod tests { let validator = address::testing::established_address_3(); let initial_stake = token::Amount::from_uint(10_098_123, 0).unwrap(); let consensus_key = key::testing::keypair_2().ref_to(); - let commission_rate = rust_decimal::Decimal::new(5, 2); - let max_commission_rate_change = rust_decimal::Decimal::new(1, 2); + let commission_rate = Dec::new(5, 2).unwrap(); + let max_commission_rate_change = Dec::new(1, 2).unwrap(); let genesis_validators = [GenesisValidator { address: validator.clone(), @@ -507,8 +508,8 @@ mod tests { let validator = address::testing::established_address_3(); let initial_stake = token::Amount::from_uint(10_098_123, 0).unwrap(); let consensus_key = key::testing::keypair_2().ref_to(); - let commission_rate = rust_decimal::Decimal::new(5, 2); - let max_commission_rate_change = rust_decimal::Decimal::new(1, 2); + let commission_rate = Dec::new(5, 2).unwrap(); + let max_commission_rate_change = Dec::new(1, 2).unwrap(); let genesis_validators = [GenesisValidator { address: validator.clone(), diff --git a/wasm/wasm_source/src/vp_validator.rs b/wasm/wasm_source/src/vp_validator.rs index 17cbb5e51f..24169fb17d 100644 --- a/wasm/wasm_source/src/vp_validator.rs +++ b/wasm/wasm_source/src/vp_validator.rs @@ -220,6 +220,7 @@ fn validate_tx( mod tests { use address::testing::arb_non_internal_address; use namada::ledger::pos::{GenesisValidator, PosParams}; + use namada::types::dec::Dec; use namada::types::storage::Epoch; use namada_test_utils::TestWasms; // Use this as `#[test]` annotation to enable logging @@ -231,7 +232,6 @@ mod tests { use namada_tx_prelude::{StorageWrite, TxEnv}; use namada_vp_prelude::key::RefTo; use proptest::prelude::*; - use rust_decimal::Decimal; use storage::testing::arb_account_storage_key_no_vp; use super::*; @@ -442,8 +442,8 @@ mod tests { let validator = address::testing::established_address_3(); let initial_stake = token::Amount::from_uint(10_098_123, 0).unwrap(); let consensus_key = key::testing::keypair_2().ref_to(); - let commission_rate = Decimal::new(5, 2); - let max_commission_rate_change = Decimal::new(1, 2); + let commission_rate = Dec::new(5, 2).unwrap(); + let max_commission_rate_change = Dec::new(1, 2).unwrap(); let genesis_validators = [GenesisValidator { address: validator.clone(), @@ -494,7 +494,7 @@ mod tests { tx::ctx() .change_validator_commission_rate( &validator, - &Decimal::new(6, 2), + &Dec::new(6, 2).unwrap(), ) .unwrap(); }); @@ -520,8 +520,8 @@ mod tests { let validator = address::testing::established_address_3(); let initial_stake = token::Amount::from_uint(10_098_123, 0).unwrap(); let consensus_key = key::testing::keypair_2().ref_to(); - let commission_rate = Decimal::new(5, 2); - let max_commission_rate_change = Decimal::new(1, 2); + let commission_rate = Dec::new(5, 2).unwrap(); + let max_commission_rate_change = Dec::new(1, 2).unwrap(); let genesis_validators = [GenesisValidator { address: validator.clone(), @@ -574,7 +574,7 @@ mod tests { tx::ctx() .change_validator_commission_rate( &validator, - &Decimal::new(6, 2), + &Dec::new(6, 2).unwrap(), ) .unwrap(); }); diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index 6918cb3513..f1b38f02a6 100644 --- a/wasm_for_tests/wasm_source/Cargo.lock +++ b/wasm_for_tests/wasm_source/Cargo.lock @@ -2657,6 +2657,7 @@ dependencies = [ "derivative", "ed25519-consensus", "ethabi", + "eyre", "ferveo-common", "ibc", "ibc-proto", @@ -2765,7 +2766,6 @@ dependencies = [ "namada_macros", "namada_proof_of_stake", "namada_vm_env", - "rust_decimal", "sha2 0.10.6", "thiserror", ] From 7fbd9c68d3ab556d520b5d15cd6d57dfb4646a7a Mon Sep 17 00:00:00 2001 From: brentstone Date: Wed, 26 Apr 2023 17:07:39 -0400 Subject: [PATCH 033/151] WIP fixing pos unit tests --- apps/src/lib/config/genesis.rs | 4 ++-- .../lib/node/ledger/shell/finalize_block.rs | 24 ++++++++----------- core/src/types/dec.rs | 15 ++++++++++-- core/src/types/token.rs | 5 ++++ 4 files changed, 30 insertions(+), 18 deletions(-) diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index 38762bbd76..4136f5c665 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -957,8 +957,8 @@ pub fn genesis(num_validators: u64) -> Genesis { implicit_vp_sha256: Default::default(), epochs_per_year: 525_600, /* seconds in yr (60*60*24*365) div seconds * per epoch (60 = min_duration) */ - pos_gain_p: Dec::new(1, 2).expect("This can't fail"), - pos_gain_d: Dec::new(1, 2).expect("This can't fail"), + pos_gain_p: Dec::new(1, 1).expect("This can't fail"), + pos_gain_d: Dec::new(1, 1).expect("This can't fail"), staked_ratio: Dec::zero(), pos_inflation_amount: token::Amount::zero(), wrapper_tx_fees: Some(token::Amount::native_whole(0)), diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index 242ac3d619..6e4adac915 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -624,9 +624,9 @@ where let pos_last_staked_ratio: Dec = self .read_storage_key(¶ms_storage::get_staked_ratio_key()) .expect("PoS staked ratio should exist in storage"); - let pos_last_inflation_amount: u64 = self + let pos_last_inflation_amount: token::Amount = self .read_storage_key(¶ms_storage::get_pos_inflation_amount_key()) - .expect("PoS inflation rate should exist in storage"); + .expect("PoS inflation amount should exist in storage"); // Read from PoS storage let total_tokens = self .read_storage_key(&total_supply_key(&staking_token_address( @@ -654,11 +654,7 @@ where locked_ratio_target: pos_locked_ratio_target, locked_ratio_last: pos_last_staked_ratio, max_reward_rate: pos_max_inflation_rate, - last_inflation_amount: token::Amount::from_uint( - pos_last_inflation_amount, - 0, - ) - .expect("Amount out of bounds"), + last_inflation_amount: pos_last_inflation_amount, p_gain_nom: pos_p_gain_nom, d_gain_nom: pos_d_gain_nom, epochs_per_year, @@ -709,7 +705,7 @@ where let (address, value) = acc?; // Get reward token amount for this validator - let fractional_claim = value / Dec::from(num_blocks_in_last_epoch); + let fractional_claim = value / num_blocks_in_last_epoch; let reward = fractional_claim * inflation; // Get validator data at the last epoch @@ -1508,7 +1504,7 @@ mod test_finalize_block { // also return false if to_compare > target since this should // never happen for the use cases if to_compare < target { - let tolerance = Dec::new(1, POS_DECIMAL_PRECISION) + let tolerance = Dec::new(1, POS_DECIMAL_PRECISION / 2) .expect("Dec creation failed"); let res = Dec::one() - to_compare / target; res < tolerance @@ -1624,7 +1620,7 @@ mod test_finalize_block { assert!(rewards_prod_3.is_empty(&shell.wl_storage).unwrap()); assert!(rewards_prod_4.is_empty(&shell.wl_storage).unwrap()); let acc_sum = get_rewards_sum(&shell.wl_storage); - assert!(is_decimal_equal_enough(Dec(Uint::from(3)), acc_sum)); + assert!(is_decimal_equal_enough(Dec::new(3, 0).unwrap(), acc_sum)); let acc = get_rewards_acc(&shell.wl_storage); assert!( acc.get(&val1.address).cloned().unwrap() @@ -1647,10 +1643,10 @@ mod test_finalize_block { assert_eq!(current_height, shell.wl_storage.storage.block.height.0); for _ in current_height..height_of_next_epoch.0 + 2 { - dbg!( - get_rewards_acc(&shell.wl_storage), - get_rewards_sum(&shell.wl_storage), - ); + // dbg!( + // get_rewards_acc(&shell.wl_storage), + // get_rewards_sum(&shell.wl_storage), + // ); next_block_for_inflation(&mut shell, pkh1.clone(), votes.clone()); } assert!( diff --git a/core/src/types/dec.rs b/core/src/types/dec.rs index 493d5c3321..34ff067a49 100644 --- a/core/src/types/dec.rs +++ b/core/src/types/dec.rs @@ -159,7 +159,7 @@ impl FromStr for Dec { impl From for Dec { fn from(amt: Amount) -> Self { - Self(amt.into()) + Self(amt.raw_amount()) } } @@ -246,11 +246,14 @@ impl Mul for Dec { } } +// TODO: is some checked arithmetic needed here to prevent overflows? +// Truncates down to the `POS_DECIMAL_PRECISION`th decimal place. impl Mul for Dec { type Output = Self; fn mul(self, rhs: Dec) -> Self::Output { - Self(self.0 * rhs.0) + let prod = self.0 * rhs.0; + Self(prod / Uint::exp10(POS_DECIMAL_PRECISION as usize)) } } @@ -269,6 +272,14 @@ impl Div for Dec { } } +impl Div for Dec { + type Output = Self; + + fn div(self, rhs: u64) -> Self::Output { + Self(self.0 / Uint::from(rhs)) + } +} + impl Display for Dec { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let mut string = self.0.to_string(); diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 734ca998c0..3ae675003c 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -78,6 +78,11 @@ impl Amount { } } + /// Get the raw [`Uint`] value, which represents namnam + pub fn raw_amount(&self) -> Uint { + self.raw + } + /// Create a new amount with the maximum value pub fn max() -> Self { Self { From 32b69e906908bf9e3a218af0b45d57d248db4f20 Mon Sep 17 00:00:00 2001 From: brentstone Date: Fri, 28 Apr 2023 16:13:12 -0400 Subject: [PATCH 034/151] Fix pos unit tests, change `Dec` precision to 12 places --- core/src/types/dec.rs | 63 ++++++++++++++++++++-------------- core/src/types/token.rs | 1 + shared/src/ledger/inflation.rs | 6 ++-- 3 files changed, 43 insertions(+), 27 deletions(-) diff --git a/core/src/types/dec.rs b/core/src/types/dec.rs index 34ff067a49..e725f2f3a5 100644 --- a/core/src/types/dec.rs +++ b/core/src/types/dec.rs @@ -1,5 +1,8 @@ //! A non-negative fixed precision decimal type for computation primarily in the -//! PoS module. +//! PoS module. For rounding, any computation that exceeds the specified +//! precision is truncated down to the closest value with the specified +//! precision. + use std::fmt::{Debug, Display, Formatter}; use std::ops::{Add, AddAssign, Div, Mul, Sub}; use std::str::FromStr; @@ -8,11 +11,12 @@ use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use eyre::eyre; use serde::{Deserialize, Serialize}; +use super::token::NATIVE_MAX_DECIMAL_PLACES; use crate::types::token::{Amount, Change}; use crate::types::uint::Uint; /// The number of Dec places for PoS rational calculations -pub const POS_DECIMAL_PRECISION: u8 = 6; +pub const POS_DECIMAL_PRECISION: u8 = 12; #[derive(thiserror::Error, Debug)] #[error(transparent)] @@ -159,7 +163,13 @@ impl FromStr for Dec { impl From for Dec { fn from(amt: Amount) -> Self { - Self(amt.raw_amount()) + Self( + amt.raw_amount() + * Uint::exp10( + (POS_DECIMAL_PRECISION - NATIVE_MAX_DECIMAL_PLACES) + as usize, + ), + ) } } @@ -169,18 +179,12 @@ impl From for Dec { } } -// impl TryFrom for u64 { -// type Error = Error; - -// fn try_from(value: Dec) -> std::result::Result { -// let int = value.0 / Uint::exp10(POS_DECIMAL_PRECISION as usize); -// if int > Uint::from(u64::MAX) { -// Err(eyre!("Dec value is too large to fit in a u64").into()) -// } else { -// Ok(int.into()) -// } -// } -// } +// Is error handling needed for this? +impl From for Dec { + fn from(num: Uint) -> Self { + Self(num * Uint::exp10(POS_DECIMAL_PRECISION as usize)) + } +} impl Add for Dec { type Output = Self; @@ -216,7 +220,7 @@ impl Mul for Dec { type Output = Uint; fn mul(self, rhs: Uint) -> Self::Output { - self.0 * rhs + self.0 * rhs / Uint::exp10(POS_DECIMAL_PRECISION as usize) } } @@ -247,7 +251,6 @@ impl Mul for Dec { } // TODO: is some checked arithmetic needed here to prevent overflows? -// Truncates down to the `POS_DECIMAL_PRECISION`th decimal place. impl Mul for Dec { type Output = Self; @@ -332,19 +335,29 @@ mod test_dec { // Fixed precision division is more thoroughly tested for the `Uint` // type. These are sanity checks that the precision is correct. assert_eq!( - Dec::new(1, 6).expect("Test failed") - / Dec::new(1, 6).expect("Test failed"), + Dec::new(1, POS_DECIMAL_PRECISION).expect("Test failed") + / Dec::new(1, POS_DECIMAL_PRECISION).expect("Test failed"), Dec::one(), ); assert_eq!( - Dec::new(1, 6).expect("Test failed") + Dec::new(1, POS_DECIMAL_PRECISION).expect("Test failed") / (Dec::new(1, 0).expect("Test failed") + Dec::one()), Dec::zero(), ); assert_eq!( - Dec::new(1, 6).expect("Test failed") / Dec::two(), + Dec::new(1, POS_DECIMAL_PRECISION).expect("Test failed") + / Dec::two(), Dec::zero(), ); + + // Test Dec * Dec multiplication + assert!(Dec::new(32353, POS_DECIMAL_PRECISION + 1u8).is_none()); + let dec1 = Dec::new(12345654321, 12).expect("Test failed"); + let dec2 = Dec::new(9876789, 12).expect("Test failed"); + let exp_prod = Dec::new(121935, 12).expect("Test failed"); + let exp_quot = Dec::new(1249966393025101, 12).expect("Test failed"); + assert_eq!(dec1 * dec2, exp_prod); + assert_eq!(dec1 / dec2, exp_quot); } /// Test the `Dec` and `Amount` interplay @@ -355,7 +368,7 @@ mod test_dec { debug_assert_eq!( Dec::from(amt), - Dec::new(1018, 0).expect("Test failed") + Dec::new(1018, 6).expect("Test failed") ); debug_assert_eq!(dec * amt, Amount::from(2809u64)); @@ -372,10 +385,10 @@ mod test_dec { Dec::new(314, 2).expect("Test failed"), ); - // more than six decimal places and zero integer part + // more than 12 decimal places and zero integer part assert_eq!( - Dec::from_str("0.1234567").expect("Test failed"), - Dec::new(123456, 6).expect("Test failed"), + Dec::from_str("0.1234567654321").expect("Test failed"), + Dec::new(123456765432, 12).expect("Test failed"), ); // No zero before the decimal diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 3ae675003c..f07e8eda72 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -488,6 +488,7 @@ impl From for Amount { } } +// Treats the u64 as a value of the raw amount (namnam) impl From for Amount { fn from(val: u64) -> Amount { Amount { diff --git a/shared/src/ledger/inflation.rs b/shared/src/ledger/inflation.rs index 11415ae96e..b5b1de321a 100644 --- a/shared/src/ledger/inflation.rs +++ b/shared/src/ledger/inflation.rs @@ -61,8 +61,10 @@ impl RewardsController { epochs_per_year, } = self; - let locked = Dec::from(locked_tokens); - let total = Dec::from(total_tokens); + // Token amounts must be expressed in terms of the raw amount (namnam) + // to properly run the PD controller + let locked = Dec::from(locked_tokens.raw_amount()); + let total = Dec::from(total_tokens.raw_amount()); let epochs_py: Dec = epochs_per_year.into(); let locked_ratio = locked / total; From 62cc6ec6b5e8c70929ed666d7f190e5ebe504042 Mon Sep 17 00:00:00 2001 From: satan Date: Mon, 1 May 2023 10:36:44 +0200 Subject: [PATCH 035/151] [fix]: Fixed proptest in PoS, removed rust_decimal from core and pos crates --- Cargo.lock | 5 -- .../lib/node/ledger/shell/finalize_block.rs | 1 - core/Cargo.toml | 4 +- core/src/types/token.rs | 73 ------------------- proof_of_stake/Cargo.toml | 2 - proof_of_stake/src/parameters.rs | 6 +- wasm/Cargo.lock | 5 -- wasm/checksums.json | 36 ++++----- wasm_for_tests/wasm_source/Cargo.lock | 5 -- 9 files changed, 21 insertions(+), 116 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 448eb364e8..bc12a7c447 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3954,8 +3954,6 @@ dependencies = [ "rand 0.8.5", "rand_core 0.6.4", "rayon", - "rust_decimal", - "rust_decimal_macros", "serde 1.0.145", "serde_json", "sha2 0.9.9", @@ -4005,8 +4003,6 @@ dependencies = [ "namada_core", "once_cell", "proptest", - "rust_decimal", - "rust_decimal_macros", "test-log", "thiserror", "tracing 0.1.37", @@ -5557,7 +5553,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee9164faf726e4f3ece4978b25ca877ddc6802fa77f38cdccb32c7f805ecd70c" dependencies = [ "arrayvec 0.7.2", - "borsh", "num-traits 0.2.15", "serde 1.0.145", ] diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index 6e4adac915..e9870d8ff1 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -906,7 +906,6 @@ mod test_finalize_block { InitProposalData, ProposalType, VoteProposalData, }; use namada::types::transaction::{EncryptionKey, Fee, WrapperTx, MIN_FEE}; - use namada::types::uint::Uint; use namada_test_utils::TestWasms; use test_log::test; diff --git a/core/Cargo.toml b/core/Cargo.toml index 5e97f59a41..0a0b08ec54 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -90,8 +90,8 @@ prost-types = "0.9.0" rand = {version = "0.8", optional = true} rand_core = {version = "0.6", optional = true} rayon = {version = "=1.5.3", optional = true} -rust_decimal = { version = "1.26.1", features = ["borsh"] } -rust_decimal_macros = "1.26.1" +# rust_decimal = { version = "1.26.1", features = ["borsh"] } +# rust_decimal_macros = "1.26.1" serde = {version = "1.0.125", features = ["derive"]} serde_json = "1.0.62" sha2 = "0.9.3" diff --git a/core/src/types/token.rs b/core/src/types/token.rs index f07e8eda72..7cc997684e 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -7,7 +7,6 @@ use std::str::FromStr; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use data_encoding::BASE32HEX_NOPAD; use masp_primitives::transaction::Transaction; -use rust_decimal::prelude::Decimal; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -143,27 +142,6 @@ impl Amount { Self { raw: change.abs() } } - /// Attempt to convert a `Decimal` to an `DenominatedAmount` with the - /// specified precision. - pub fn from_decimal( - decimal: Decimal, - denom: impl Into, - ) -> Result { - let denom = denom.into(); - if (denom as u32) < decimal.scale() { - Err(AmountParseError::ScaleTooLarge(decimal.scale(), denom)) - } else { - let value = Uint::from(decimal.mantissa().unsigned_abs()); - match Uint::from(10) - .checked_pow(Uint::from((denom as u32) - decimal.scale())) - .and_then(|scaling| scaling.checked_mul(value)) - { - Some(amount) => Ok(Self { raw: amount }), - None => Err(AmountParseError::ConvertToDecimal), - } - } - } - /// Given a string and a denomination, parse an amount from string. pub fn from_str( string: impl AsRef, @@ -174,18 +152,6 @@ impl Amount { .map(Into::into) } - /// Attempt to convert a float to an `Amount` with the specified - /// precision. - pub fn from_float( - float: impl Into, - denom: impl Into, - ) -> Result { - match Decimal::try_from(float.into()) { - Err(e) => Err(AmountParseError::InvalidDecimal(e)), - Ok(decimal) => Self::from_decimal(decimal, denom), - } - } - /// Attempt to convert an unsigned integer to an `Amount` with the /// specified precision. pub fn from_uint( @@ -463,19 +429,6 @@ impl<'de> serde::Deserialize<'de> for DenominatedAmount { } } -impl TryFrom for Decimal { - type Error = AmountParseError; - - fn try_from(amount: Amount) -> Result { - if amount.raw > Uint([u64::MAX, u64::MAX, 0, 0]) { - Err(AmountParseError::ConvertToDecimal) - } else { - Ok(Into::::into(amount.raw.as_u128()) - / Into::::into(NATIVE_SCALE)) - } - } -} - impl<'a> From<&'a DenominatedAmount> for &'a Amount { fn from(denom: &'a DenominatedAmount) -> Self { &denom.amount @@ -649,8 +602,6 @@ impl KeySeg for Amount { #[allow(missing_docs)] #[derive(Error, Debug)] pub enum AmountParseError { - #[error("Error decoding token amount: {0}")] - InvalidDecimal(rust_decimal::Error), #[error( "Error decoding token amount, too many decimal places: {0}. Maximum \ {1}" @@ -982,24 +933,8 @@ impl TryFrom for Transfer { #[cfg(test)] mod tests { - use proptest::prelude::*; - use rust_decimal_macros::dec; - use super::*; - proptest! { - /// The upper limit is set to `2^51`, because then the float is - /// starting to lose precision. - #[test] - fn test_token_amount_decimal_conversion(raw_amount in 0..2_u64.pow(51)) { - let amount = Amount::from_uint(raw_amount, NATIVE_MAX_DECIMAL_PLACES).expect("Test failed"); - // A round-trip conversion to and from Decimal should be an identity - let decimal = Decimal::from(raw_amount); - let identity = Amount::from_decimal(decimal, NATIVE_MAX_DECIMAL_PLACES).expect("Test failed"); - assert_eq!(amount, identity); - } - } - #[test] fn test_token_display() { let max = Amount::from_uint(u64::MAX, 0).expect("Test failed"); @@ -1095,14 +1030,6 @@ mod tests { assert_eq!(max_signed.checked_signed_add(max_signed), None); } - #[test] - fn test_amount_from_decimal() { - assert!(Amount::from_decimal(dec!(1.12), 1).is_err()); - assert!(Amount::from_decimal(dec!(1.12), 80).is_err()); - let amount = Amount::from_decimal(dec!(1.12), 3).expect("Test failed"); - assert_eq!(amount, Amount::from_uint(1120, 0).expect("Test failed")); - } - #[test] fn test_amount_from_string() { assert!(Amount::from_str("1.12", 1).is_err()); diff --git a/proof_of_stake/Cargo.toml b/proof_of_stake/Cargo.toml index faab68dfd6..cab2920820 100644 --- a/proof_of_stake/Cargo.toml +++ b/proof_of_stake/Cargo.toml @@ -24,8 +24,6 @@ hex = "0.4.3" once_cell = "1.8.0" # A fork with state machine testing proptest = {git = "https://github.com/heliaxdev/proptest", branch = "tomas/sm", optional = true} -rust_decimal = { version = "1.26.1", features = ["borsh"] } -rust_decimal_macros = "1.26.1" thiserror = "1.0.30" tracing = "0.1.30" data-encoding = "2.3.2" diff --git a/proof_of_stake/src/parameters.rs b/proof_of_stake/src/parameters.rs index a5b75ebd80..a67878b8d1 100644 --- a/proof_of_stake/src/parameters.rs +++ b/proof_of_stake/src/parameters.rs @@ -113,11 +113,7 @@ impl PosParams { // Check maximum total voting power cannot get larger than what // Tendermint allows - // - // TODO: decide if this is still a check we want to do (in its current - // state with our latest voting power conventions, it will fail - // always) - let max_total_voting_power = *self.tm_votes_per_token + let max_total_voting_power = self.tm_votes_per_token * Uint::from(TOKEN_MAX_AMOUNT) * Uint::from(self.max_validator_slots); match i64::try_from(max_total_voting_power) { diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index 73cf3f057b..2c887ffc54 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -2674,8 +2674,6 @@ dependencies = [ "rand 0.8.5", "rand_core 0.6.4", "rayon", - "rust_decimal", - "rust_decimal_macros", "serde", "serde_json", "sha2 0.9.9", @@ -2709,8 +2707,6 @@ dependencies = [ "namada_core", "once_cell", "proptest", - "rust_decimal", - "rust_decimal_macros", "thiserror", "tracing", ] @@ -3729,7 +3725,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee9164faf726e4f3ece4978b25ca877ddc6802fa77f38cdccb32c7f805ecd70c" dependencies = [ "arrayvec 0.7.2", - "borsh", "num-traits", "serde", ] diff --git a/wasm/checksums.json b/wasm/checksums.json index 470a405e0a..32215c5ba0 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,20 +1,20 @@ { - "tx_bond.wasm": "tx_bond.204dd016d48999ce2bd65a2cc8ba7ba6eec6a2e9878b544e4d508ce8a6c4619e.wasm", - "tx_change_validator_commission.wasm": "tx_change_validator_commission.820da4e9d5cd00200e1a88808c5d0cce79bbf9c6a42c4bcf69cc0094d44053eb.wasm", - "tx_ibc.wasm": "tx_ibc.9d95be7b97770cee6665bfb9a53cfb10a4bb162be3bd813fb0720c5b70bf75d7.wasm", - "tx_init_account.wasm": "tx_init_account.92adf7dd170cde441f34001738e4854f6febeba416c73489ae31a75a09be9603.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.e76dcfdea228ac37745de999feefa56ae881b6be101565b86d832d27d1598323.wasm", - "tx_init_validator.wasm": "tx_init_validator.087a519a8e9e0a88e7ff71556b9cab8296ba879e0da3dfe7dfb66ccf407db92e.wasm", - "tx_reveal_pk.wasm": "tx_reveal_pk.fcd7aca657cd5e6aac3af9752c24d31078a3b8bbccba845e843980fec677dea0.wasm", - "tx_transfer.wasm": "tx_transfer.4ac65052b5a699df823b773dc943dd582701f3e4b1a6b8e52da4e39c9fa31195.wasm", - "tx_unbond.wasm": "tx_unbond.558c7d3e0d46766be957187b9c6d3d44490c29c9a354e7297be831fe48383452.wasm", - "tx_update_vp.wasm": "tx_update_vp.b7791369274dea718756434ba323396a471ca27c94a52d6c182da94953d9b22b.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.ca9bff7f78c3accf84725cd4bcdaf3872022fd5f6390fc22a5e20d24f53d88b7.wasm", - "tx_withdraw.wasm": "tx_withdraw.650cfc67a10136eb73bc3ceaaad9e2b2b06e2b423989a86604222187be4a5865.wasm", - "vp_implicit.wasm": "vp_implicit.14afbabd825733511d404013f4bc1dd9078ff0b801d3dd1ebd5bf3b4e5b41dfd.wasm", - "vp_masp.wasm": "vp_masp.b9d93452eb037e411a9e103aef6de33a27ddf9578b0125b7044aae00ebcfcd93.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.3e2f78bfa703c54e28d41ee12ca7869e60f0fe37b936ccc246a7aef39c95807a.wasm", - "vp_token.wasm": "vp_token.189080cb6137da24bc2ce9173ecac54e46b1dcb341d79aa66c9b6891fed92eb7.wasm", - "vp_user.wasm": "vp_user.909808779f6442ff5d08a4b195bb39b75402c5fe3d710c38ea17469c56cc9824.wasm", - "vp_validator.wasm": "vp_validator.ce440d41274ebc487f88a64843c27bb693093a3c2828a197b7975e82d27b295c.wasm" + "tx_bond.wasm": "tx_bond.91275d24c59a883c629c5b26ff5f7b57cf0f18009cbed190050d752a2e96a0a8.wasm", + "tx_change_validator_commission.wasm": "tx_change_validator_commission.73aa36169a1366c89fdd7f24b6d25f2931b96ed2bf8f2cdb2f690a4132c99788.wasm", + "tx_ibc.wasm": "tx_ibc.bc180e0716d5e28f0395abe8c21ab05c98d85011c377633f91e1d0291e969ef8.wasm", + "tx_init_account.wasm": "tx_init_account.bad8964f803f6e369ccc911027deb78e48a7896131296172147c92666d27227d.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.c70f106d3c26f0f2c5c2ac6a9b5ff851b67ecfb19f76fc57d0563aa4a9e012f5.wasm", + "tx_init_validator.wasm": "tx_init_validator.0538743e4433d9657670307828c5df727bc936bad9f751653f9a914fbd29c983.wasm", + "tx_reveal_pk.wasm": "tx_reveal_pk.e4806260ff21c1a441ddbdba4f8640a18db03a5687f0ab5dc2e08a72423b808b.wasm", + "tx_transfer.wasm": "tx_transfer.c564498c1d419e566e03a1b5f39927ceda4b3efc1e78128c574d823d1f7cfaeb.wasm", + "tx_unbond.wasm": "tx_unbond.e800fac2d595b7ac3892b1e1efced00ee1f4b5fec22c08a90ee9838725104b8f.wasm", + "tx_update_vp.wasm": "tx_update_vp.6ff4bd7fbf11449abef4f3632243d6e8cf93949e76be206be6eaf8d88e15ccd2.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.4defe4f78e6aee8b6c6faabaf4a8cf6c2939abd9edcc2a772a02f0f6bcc4b8cf.wasm", + "tx_withdraw.wasm": "tx_withdraw.4a31fad26cf1e34d0b7ac5b47d78ccdc79de4666840c81b441592a1b5fe9d4b5.wasm", + "vp_implicit.wasm": "vp_implicit.d014edfd0b81d679c918d033962c347d0f833cd86642b91205e1705e33caddb5.wasm", + "vp_masp.wasm": "vp_masp.8e725e075108905e316a1f86fc696ee9f1f2ba4b0e7646505b3046443bb22d0d.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.99f470dbb03b51f2082b8ba31c6cd1580ff5504752b04b8243ffbfc1b60b5c99.wasm", + "vp_token.wasm": "vp_token.d642c37273de047a4d3bd126ad0bbd4f7b16b2efb1a766fcec0104d7ca231a64.wasm", + "vp_user.wasm": "vp_user.113f37df167b59d278b24f9c1a4c64f2e1b03641cb3d55942e02fe8ba8e461f2.wasm", + "vp_validator.wasm": "vp_validator.f02cf20e2cfda256bc3deb5458de8aa6cb614c5cf2524c4be33495b30a8ef91d.wasm" } \ No newline at end of file diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index f1b38f02a6..1c677a8592 100644 --- a/wasm_for_tests/wasm_source/Cargo.lock +++ b/wasm_for_tests/wasm_source/Cargo.lock @@ -2674,8 +2674,6 @@ dependencies = [ "rand 0.8.5", "rand_core 0.6.4", "rayon", - "rust_decimal", - "rust_decimal_macros", "serde", "serde_json", "sha2 0.9.9", @@ -2709,8 +2707,6 @@ dependencies = [ "namada_core", "once_cell", "proptest", - "rust_decimal", - "rust_decimal_macros", "thiserror", "tracing", ] @@ -3711,7 +3707,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee9164faf726e4f3ece4978b25ca877ddc6802fa77f38cdccb32c7f805ecd70c" dependencies = [ "arrayvec 0.7.2", - "borsh", "num-traits", "serde", ] From c2c66cc5b8c4193d1ce10f9a0d3d58dd0a0786db Mon Sep 17 00:00:00 2001 From: satan Date: Mon, 1 May 2023 10:46:42 +0200 Subject: [PATCH 036/151] [chore]: Removed rust_decimal and rust_decimal_macro deps from project --- Cargo.lock | 27 --------------------------- apps/Cargo.toml | 2 -- core/Cargo.toml | 2 -- shared/Cargo.toml | 2 -- tests/Cargo.toml | 2 -- wasm/Cargo.lock | 14 -------------- wasm_for_tests/wasm_source/Cargo.lock | 25 ------------------------- 7 files changed, 74 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bc12a7c447..21e402a594 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3800,8 +3800,6 @@ dependencies = [ "prost", "pwasm-utils", "rayon", - "rust_decimal", - "rust_decimal_macros", "serde_json", "sha2 0.9.9", "tempfile", @@ -3880,8 +3878,6 @@ dependencies = [ "rlimit", "rocksdb", "rpassword", - "rust_decimal", - "rust_decimal_macros", "serde 1.0.145", "serde_bytes", "serde_json", @@ -4049,8 +4045,6 @@ dependencies = [ "prost", "rand 0.8.5", "regex", - "rust_decimal", - "rust_decimal_macros", "serde_json", "sha2 0.9.9", "tempfile", @@ -5546,27 +5540,6 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e52c148ef37f8c375d49d5a73aa70713125b7f19095948a923f80afdeb22ec2" -[[package]] -name = "rust_decimal" -version = "1.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee9164faf726e4f3ece4978b25ca877ddc6802fa77f38cdccb32c7f805ecd70c" -dependencies = [ - "arrayvec 0.7.2", - "num-traits 0.2.15", - "serde 1.0.145", -] - -[[package]] -name = "rust_decimal_macros" -version = "1.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4903d8db81d2321699ca8318035d6ff805c548868df435813968795a802171b2" -dependencies = [ - "quote", - "rust_decimal", -] - [[package]] name = "rustc-demangle" version = "0.1.21" diff --git a/apps/Cargo.toml b/apps/Cargo.toml index c05a17a7aa..ee1e99ea4b 100644 --- a/apps/Cargo.toml +++ b/apps/Cargo.toml @@ -151,8 +151,6 @@ winapi = "0.3.9" masp_primitives = { git = "https://github.com/anoma/masp", rev = "bee40fc465f6afbd10558d12fe96eb1742eee45c", features = ["transparent-inputs"] } masp_proofs = { git = "https://github.com/anoma/masp", rev = "bee40fc465f6afbd10558d12fe96eb1742eee45c", features = ["bundled-prover", "download-params"] } bimap = {version = "0.6.2", features = ["serde"]} -rust_decimal = "1.26.1" -rust_decimal_macros = "1.26.1" [dev-dependencies] namada = {path = "../shared", default-features = false, features = ["testing", "wasm-runtime"]} diff --git a/core/Cargo.toml b/core/Cargo.toml index 0a0b08ec54..a074390bcc 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -90,8 +90,6 @@ prost-types = "0.9.0" rand = {version = "0.8", optional = true} rand_core = {version = "0.6", optional = true} rayon = {version = "=1.5.3", optional = true} -# rust_decimal = { version = "1.26.1", features = ["borsh"] } -# rust_decimal_macros = "1.26.1" serde = {version = "1.0.125", features = ["derive"]} serde_json = "1.0.62" sha2 = "0.9.3" diff --git a/shared/Cargo.toml b/shared/Cargo.toml index d4e0bf019e..d9dc8ad02a 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -105,8 +105,6 @@ proptest = {git = "https://github.com/heliaxdev/proptest", branch = "tomas/sm", prost = "0.9.0" pwasm-utils = {git = "https://github.com/heliaxdev/wasm-utils", tag = "v0.20.0", features = ["sign_ext"], optional = true} rayon = {version = "=1.5.3", optional = true} -rust_decimal = "1.26.1" -rust_decimal_macros = "1.26.1" serde_json = "1.0.62" sha2 = "0.9.3" # We switch off "blake2b" because it cannot be compiled to wasm diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 9941de357b..f141f363f6 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -46,8 +46,6 @@ tokio = {version = "1.8.2", features = ["full"]} tracing = "0.1.30" tracing-subscriber = {version = "0.3.7", default-features = false, features = ["env-filter", "fmt"]} derivative = "2.2.0" -rust_decimal = "1.26.1" -rust_decimal_macros = "1.26.1" [dev-dependencies] namada_apps = {path = "../apps", default-features = false, features = ["testing"]} diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index 2c887ffc54..c7edff1984 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -2624,8 +2624,6 @@ dependencies = [ "prost", "pwasm-utils", "rayon", - "rust_decimal", - "rust_decimal_macros", "serde_json", "sha2 0.9.9", "tempfile", @@ -2737,8 +2735,6 @@ dependencies = [ "namada_vp_prelude", "prost", "regex", - "rust_decimal", - "rust_decimal_macros", "serde_json", "sha2 0.9.9", "tempfile", @@ -3729,16 +3725,6 @@ dependencies = [ "serde", ] -[[package]] -name = "rust_decimal_macros" -version = "1.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4903d8db81d2321699ca8318035d6ff805c548868df435813968795a802171b2" -dependencies = [ - "quote", - "rust_decimal", -] - [[package]] name = "rustc-demangle" version = "0.1.21" diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index 1c677a8592..04f4c6989b 100644 --- a/wasm_for_tests/wasm_source/Cargo.lock +++ b/wasm_for_tests/wasm_source/Cargo.lock @@ -2624,8 +2624,6 @@ dependencies = [ "prost", "pwasm-utils", "rayon", - "rust_decimal", - "rust_decimal_macros", "serde_json", "sha2 0.9.9", "tempfile", @@ -2737,8 +2735,6 @@ dependencies = [ "namada_vp_prelude", "prost", "regex", - "rust_decimal", - "rust_decimal_macros", "serde_json", "sha2 0.9.9", "tempfile", @@ -3700,27 +3696,6 @@ dependencies = [ "rustc-hex", ] -[[package]] -name = "rust_decimal" -version = "1.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee9164faf726e4f3ece4978b25ca877ddc6802fa77f38cdccb32c7f805ecd70c" -dependencies = [ - "arrayvec 0.7.2", - "num-traits", - "serde", -] - -[[package]] -name = "rust_decimal_macros" -version = "1.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4903d8db81d2321699ca8318035d6ff805c548868df435813968795a802171b2" -dependencies = [ - "quote", - "rust_decimal", -] - [[package]] name = "rustc-demangle" version = "0.1.21" From c97a9e98af097e78f94595c1808bc7a49dfffe54 Mon Sep 17 00:00:00 2001 From: mariari Date: Wed, 10 May 2023 17:12:45 +0800 Subject: [PATCH 037/151] Fix Key from always parsing Previously the key would always parse, even if the string is empty, this causes an issue where if we wanted an Optional Key, then it would always be parsed as Some, causing issues for various parts of the system. --- core/src/types/storage.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/core/src/types/storage.rs b/core/src/types/storage.rs index 9ceddecf15..4f5aec48f3 100644 --- a/core/src/types/storage.rs +++ b/core/src/types/storage.rs @@ -474,11 +474,16 @@ impl Value for TreeBytes { impl Key { /// Parses string and returns a key pub fn parse(string: impl AsRef) -> Result { - let mut segments = Vec::new(); - for s in string.as_ref().split(KEY_SEGMENT_SEPARATOR) { - segments.push(DbKeySeg::parse(s.to_owned())?); + let string = string.as_ref(); + if string.is_empty() { + Err(Error::ParseKeySeg(string.to_string())) + } else { + let mut segments = Vec::new(); + for s in string.split(KEY_SEGMENT_SEPARATOR) { + segments.push(DbKeySeg::parse(s.to_owned())?); + } + Ok(Key { segments }) } - Ok(Key { segments }) } /// Returns a new key with segments of `Self` and the given segment From 26413eca80b5b9ba2386acdecd2cef3f887f68cf Mon Sep 17 00:00:00 2001 From: mariari Date: Mon, 1 May 2023 19:22:59 +0800 Subject: [PATCH 038/151] Fixing Masp Amounts to convert on the edges We move out the looping code from each part and move it to the edges of masp --- apps/src/lib/client/rpc.rs | 409 +++++++++++++---------------- apps/src/lib/client/tx.rs | 515 ++++++++++++++++++++++++++----------- core/src/types/token.rs | 60 ++++- core/src/types/uint.rs | 19 +- 4 files changed, 622 insertions(+), 381 deletions(-) diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index f574661cf1..7dcb7eb07b 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -45,7 +45,7 @@ use namada::types::storage::{ BlockHeight, BlockResults, Epoch, Key, KeySeg, PrefixValue, TxIndex, }; use namada::types::token::{ - balance_key, DenominatedAmount, MaspDenom, Transfer, + balance_key, Change, DenominatedAmount, MaspDenom, TokenAddress, Transfer, }; use namada::types::transaction::{ process_tx, AffineCurve, DecryptedTx, EllipticCurve, PairingEngine, TxType, @@ -58,7 +58,7 @@ use crate::cli::args::InputAmount; use crate::cli::{self, args, Context}; use crate::client::tendermint_rpc_types::TxResponse; use crate::client::tx::{ - Conversions, MaspDenominatedAmount, PinnedBalanceError, TransactionDelta, + Conversions, MaspAmount, MaspChange, PinnedBalanceError, TransactionDelta, TransferDelta, }; use crate::facade::tendermint::merkle::proof::Proof; @@ -251,30 +251,16 @@ pub async fn query_tx_deltas( } // Describe how a Transfer simply subtracts from one // account and adds the same to another - let mut delta = TransferDelta::default(); - for denom in MaspDenom::iter() { - let denominated = - denom.denominate(&transfer.amount); - if denominated != 0 { - let tfer_delta = Amount::from_nonnegative( - ( - transfer.token.clone(), - transfer.sub_prefix.clone(), - denom, - ), - denominated, - ) - .expect("invalid value for amount"); - delta.insert( - transfer.source.clone(), - Amount::zero() - &tfer_delta, - ); - delta.insert( - transfer.target.clone(), - tfer_delta, - ); - } - } + let delta = TransferDelta::from([( + transfer.source.clone(), + MaspChange { + asset: TokenAddress { + address: transfer.token.clone(), + sub_prefix: transfer.sub_prefix.clone(), + }, + change: -transfer.amount.amount.change(), + }, + )]); // No shielded accounts are affected by this Transfer transfers.insert( (height, idx), @@ -345,90 +331,72 @@ pub async fn query_transfers(mut ctx: Context, args: args::QueryTransfers) { shielded_accounts.insert(acc, dec); } // Check if this transfer pertains to the supplied token - relevant &= - match &query_token { - Some(token) => { - tfer_delta.values().zip(MaspDenom::iter()).any( - |(x, denom)| { - x[&(token.clone(), sub_prefix.clone(), denom)] != 0 - }, - ) || shielded_accounts.values().zip(MaspDenom::iter()).any( - |(x, denom)| { - x[&(token.clone(), sub_prefix.clone(), denom)] != 0 - }, - ) - } - None => true, - }; + relevant &= match &query_token { + Some(token) => { + let check = |(tok, amt): (&TokenAddress, &token::Amount)| { + tok.sub_prefix == sub_prefix + && &tok.address == token + && !amt.is_zero() + }; + tfer_delta.values().cloned().any( + |MaspChange { ref asset, change }| { + check((asset, &token::Amount::from(change))) + }, + ) || shielded_accounts + .values() + .cloned() + .any(|x| x.iter().any(check)) + } + None => true, + }; // Filter out those entries that do not satisfy user query if !relevant { continue; } println!("Height: {}, Index: {}, Transparent Transfer:", height, idx); // Display the transparent changes first - for (account, amt) in tfer_delta { + for (account, MaspChange { ref asset, change }) in tfer_delta { if account != masp() { print!(" {}:", account); - for ((addr, sub_prefix, denom), val) in amt.components() { - let token_alias = lookup_alias(&ctx, addr); - let sign = match val.cmp(&0) { - Ordering::Greater => "+", - Ordering::Less => "-", - Ordering::Equal => "", - }; - print!( - " {}{} {}{}", - sign, - format_denominated_amount( - &client, - addr, - sub_prefix, - token::Amount::from_masp_denominated( - val.unsigned_abs(), - *denom - ) - ) - .await, - token_alias, - sub_prefix - .as_ref() - .map(|k| format!("/{}", k)) - .unwrap_or_default(), - ); - } - println!(); + let token_alias = lookup_alias(&ctx, &asset.address); + let sign = match change.cmp(&Change::zero()) { + Ordering::Greater => "+", + Ordering::Less => "-", + Ordering::Equal => "", + }; + print!( + " {}{} {}", + sign, + format_denominated_amount( + &client, + asset, + token::Amount::from(change), + ) + .await, + asset.format_with_alias(&token_alias) + ); } + println!(); } // Then display the shielded changes afterwards // TODO: turn this to a display impl - for (account, amt) in shielded_accounts { + // (account, amt) + for (account, masp_change) in shielded_accounts { if fvk_map.contains_key(&account) { print!(" {}:", fvk_map[&account]); - for ((addr, sub_prefix, denom), val) in amt.components() { - let token_alias = lookup_alias(&ctx, addr); - let sign = match val.cmp(&0) { + for (token_addr, val) in masp_change { + let token_alias = lookup_alias(&ctx, &token_addr.address); + let sign = match val.cmp(&token::Amount::zero()) { Ordering::Greater => "+", Ordering::Less => "-", Ordering::Equal => "", }; print!( - " {}{} {}{}", + " {}{} {}", sign, - format_denominated_amount( - &client, - addr, - sub_prefix, - token::Amount::from_masp_denominated( - val.unsigned_abs(), - *denom - ) - ) - .await, - token_alias, - sub_prefix - .as_ref() - .map(|k| format!("/{}", k)) - .unwrap_or_default() + format_denominated_amount(&client, &token_addr, val,) + .await, + token_addr.format_with_alias(&token_alias), ); } println!(); @@ -552,8 +520,10 @@ pub async fn query_transparent_balance( Some(balance) => { let balance = format_denominated_amount( &client, - &token, - &sub_prefix, + &TokenAddress { + address: token, + sub_prefix, + }, balance, ) .await; @@ -721,23 +691,25 @@ pub async fn query_pinned_balance(ctx: &mut Context, args: args::QueryBalance) { .unwrap_or_default() ); } else { + let token_addr = TokenAddress { + address: token, + sub_prefix: sub_prefix + .as_ref() + .map(|k| Key::parse(k).unwrap()), + }; let formatted = format_denominated_amount( &client, - &token, - &sub_prefix.map(|k| Key::parse(k).unwrap()), + &token_addr, total_balance, ) .await; println!( "Payment address {} was consumed during epoch {}. \ - Received {} {}{}", + Received {} {}", owner, epoch, formatted, - token_alias, - sub_prefix - .map(|k| format!("/{}", k)) - .unwrap_or_default() + token_addr.format_with_alias(&token_alias), ); } } @@ -748,11 +720,7 @@ pub async fn query_pinned_balance(ctx: &mut Context, args: args::QueryBalance) { .shielded .decode_amount(client.clone(), balance, epoch) .await; - for ((addr, sub_prefix, denom), value) in balance.components() { - let asset_value = token::Amount::from_masp_denominated( - *value as u64, - *denom, - ); + for (token_addr, value) in balance.iter() { if !found_any { println!( "Payment address {} was consumed during epoch {}. \ @@ -761,23 +729,16 @@ pub async fn query_pinned_balance(ctx: &mut Context, args: args::QueryBalance) { ); found_any = true; } - let formatted = format_denominated_amount( - &client, - addr, - sub_prefix, - asset_value, - ) - .await; + let formatted = + format_denominated_amount(&client, token_addr, *value) + .await; + let token_alias = tokens + .get(&token_addr.address) + .map(|a| a.to_string()) + .unwrap_or_else(|| token_addr.address.to_string()); println!( - " {}{}: {}", - tokens - .get(addr) - .cloned() - .unwrap_or_else(|| addr.clone()), - sub_prefix - .as_ref() - .map(|k| format!("/{}", k)) - .unwrap_or_default(), + " {}: {}", + token_addr.format_with_alias(&token_alias), formatted, ); } @@ -814,8 +775,10 @@ async fn print_balances( sub_prefix.clone(), format_denominated_amount( client, - tok, - &Some(sub_prefix), + &TokenAddress { + address: tok.clone(), + sub_prefix: Some(sub_prefix) + }, balance ) .await, @@ -831,7 +794,12 @@ async fn print_balances( format!( ": {}, owned by {}", format_denominated_amount( - client, tok, &None, balance + client, + &TokenAddress { + address: tok.clone(), + sub_prefix: None + }, + balance ) .await, lookup_alias(ctx, owner) @@ -1002,7 +970,7 @@ pub fn value_by_address( epoch: Epoch, ) -> (AssetType, i64) { // Compute the unique asset identifier from the token address - let asset_type = make_asset_type(epoch, &token, &sub_prefix, denom); + let asset_type = make_asset_type(Some(epoch), &token, &sub_prefix, denom); (asset_type, amt[&asset_type]) } @@ -1065,8 +1033,12 @@ pub async fn query_shielded_balance( .expect("context should contain viewing key") }; // Compute the unique asset identifier from the token address - let asset_type = - make_asset_type(epoch, &token, &args.sub_prefix, denom); + let asset_type = make_asset_type( + Some(epoch), + &token, + &args.sub_prefix, + denom, + ); total_balance += token::Amount::from_masp_denominated( balance[&asset_type] as u64, denom, @@ -1084,17 +1056,16 @@ pub async fn query_shielded_balance( .unwrap_or_default(), ); } else { + let token_address = TokenAddress { + address: token, + sub_prefix: args.sub_prefix.map(|k| Key::parse(k).unwrap()), + }; println!( - "{}{}: {}", - token_alias, - args.sub_prefix - .as_ref() - .map(|k| format!("/{}", k)) - .unwrap_or_default(), + "{}: {}", + token_address.format_with_alias(&token_alias), format_denominated_amount( &client, - &token, - &args.sub_prefix.map(|k| Key::parse(k).unwrap()), + &token_address, total_balance ) .await @@ -1177,19 +1148,22 @@ pub async fn query_shielded_balance( .entry(addr.clone()) .and_modify(|addr_vec| addr_vec.push(sub_prefix.clone())) .or_insert_with(|| vec![sub_prefix.clone()]); + let token_address = TokenAddress { + address: addr, + sub_prefix, + }; // Only assets with the current timestamp count + let alias = tokens + .get(&token_address.address) + .map(|a| a.to_string()) + .unwrap_or_else(|| token_address.address.to_string()); println!( - "Shielded Token {}{}:", - tokens.get(&addr).cloned().unwrap_or_else(|| addr.clone()), - sub_prefix - .as_ref() - .map(|k| format!("/{}", k)) - .unwrap_or_default(), + "Shielded Token {}:", + token_address.format_with_alias(&alias), ); let formatted = format_denominated_amount( &client, - &addr, - &sub_prefix, + &token_address, token_balance, ) .await; @@ -1233,19 +1207,26 @@ pub async fn query_shielded_balance( let token = ctx.get(&token); let mut found_any = false; let token_alias = lookup_alias(ctx, &token); - println!( - "Shielded Token {}{}:", - token_alias, - args.sub_prefix + let token_address = TokenAddress { + address: token.clone(), + sub_prefix: args + .sub_prefix .as_ref() - .map(|k| format!("/{}", k)) - .unwrap_or_default() + .map(|k| Key::parse(k).unwrap()), + }; + println!( + "Shielded Token {}:", + token_address.format_with_alias(&token_alias), ); for fvk in viewing_keys { let mut balance = token::Amount::default(); for denom in MaspDenom::iter() { - let asset_type = - make_asset_type(epoch, &token, &args.sub_prefix, denom); + let asset_type = make_asset_type( + Some(epoch), + &token, + &args.sub_prefix, + denom, + ); // Query the multi-asset balance at the given spending key let viewing_key = ExtendedFullViewingKey::from(fvk).fvk.vk; let denom_balance = if no_conversions { @@ -1270,19 +1251,15 @@ pub async fn query_shielded_balance( found_any = true; } } - let formatted = format_denominated_amount( - &client, - &token, - &args.sub_prefix.as_ref().map(|k| Key::parse(k).unwrap()), - balance, - ) - .await; + let formatted = + format_denominated_amount(&client, &token_address, balance) + .await; println!(" {}, owned by {}", formatted, fvk); } if !found_any { println!( "No shielded {} balance found for any wallet key", - token_alias + token_address.format_with_alias(&token_alias), ); } } @@ -1298,10 +1275,8 @@ pub async fn query_shielded_balance( .compute_shielded_balance(&viewing_key) .expect("context should contain viewing key"); // Print balances by human-readable token names - let decoded_balance = ctx - .shielded - .decode_all_amounts(client.clone(), balance) - .await; + let decoded_balance = + ctx.shielded.decode_all_amounts(&client, balance).await; print_decoded_balance_with_epoch(ctx, &client, decoded_balance) .await; } else { @@ -1328,30 +1303,17 @@ pub async fn query_shielded_balance( pub async fn print_decoded_balance( ctx: &mut Context, client: &HttpClient, - decoded_balance: MaspDenominatedAmount, + decoded_balance: HashMap, ) { - let mut balances = HashMap::new(); - for ((addr, sub_prefix, denom), value) in decoded_balance.components() { - let asset_value = - token::Amount::from_masp_denominated(*value as u64, *denom); - balances - .entry((addr, sub_prefix)) - .and_modify(|val| *val += asset_value) - .or_insert(asset_value); - } - if balances.is_empty() { + if decoded_balance.is_empty() { println!("No shielded balance found for given key"); } else { - for ((addr, sub_prefix), amount) in balances { + for (token_addr, amount) in decoded_balance { println!( - "{}{} : {}", - lookup_alias(ctx, addr), - sub_prefix - .as_ref() - .map(|k| format!("/{}", k)) - .unwrap_or_default(), - format_denominated_amount(client, addr, sub_prefix, amount,) - .await, + "{} : {}", + token_addr + .format_with_alias(&lookup_alias(ctx, &token_addr.address)), + format_denominated_amount(client, &token_addr, amount).await, ); } } @@ -1360,36 +1322,24 @@ pub async fn print_decoded_balance( pub async fn print_decoded_balance_with_epoch( ctx: &mut Context, client: &HttpClient, - decoded_balance: Amount<(Address, Option, MaspDenom, Epoch)>, + decoded_balance: MaspAmount, ) { let tokens = ctx.tokens(); - let mut balances = HashMap::new(); - for ((addr, sub_prefix, denom, epoch), value) in - decoded_balance.components() - { - let asset_value = - token::Amount::from_masp_denominated(*value as u64, *denom); - balances - .entry((addr, sub_prefix, epoch)) - .and_modify(|val| *val += asset_value) - .or_insert(asset_value); - } - if balances.is_empty() { + if decoded_balance.is_empty() { println!("No shielded balance found for given key"); - } else { - for ((addr, sub_prefix, epoch), amount) in balances { - println!( - "{}{} | {} : {}", - tokens.get(addr).cloned().unwrap_or_else(|| addr.clone()), - sub_prefix - .as_ref() - .map(|k| format!("/{}", k)) - .unwrap_or_default(), - epoch, - format_denominated_amount(client, addr, sub_prefix, amount,) - .await, - ); - } + } + for ((epoch, token_addr), value) in decoded_balance.iter() { + let asset_value = token::Amount::from(*value); + let alias = tokens + .get(&token_addr.address) + .map(|a| a.to_string()) + .unwrap_or_else(|| token_addr.to_string()); + println!( + "{} | {} : {}", + token_addr.format_with_alias(&alias), + epoch, + format_denominated_amount(client, token_addr, asset_value).await, + ); } } @@ -2899,14 +2849,13 @@ pub(super) fn unwrap_client_response( /// correctly as a string. pub(super) async fn format_denominated_amount( client: &HttpClient, - token: &Address, - sub_prefix: &Option, + token: &TokenAddress, amount: token::Amount, ) -> String { let denom = unwrap_client_response( RPC.vp() .token() - .denomination(client, token, sub_prefix) + .denomination(client, &token.address, &token.sub_prefix) .await, ) .unwrap_or_else(|| { @@ -2921,23 +2870,35 @@ pub(super) async fn format_denominated_amount( /// Make asset type corresponding to given address and epoch pub fn make_asset_type( - epoch: Epoch, + epoch: Option, token: &Address, sub_prefix: &Option, denom: MaspDenom, ) -> AssetType { // Typestamp the chosen token with the current epoch - let token_bytes = ( - token, - sub_prefix - .as_ref() - .map(|k| k.to_string()) - .unwrap_or_default(), - denom, - epoch.0, - ) - .try_to_vec() - .expect("token should serialize"); + let token_bytes = match epoch { + None => ( + token, + sub_prefix + .as_ref() + .map(|k| k.to_string()) + .unwrap_or_default(), + denom, + ) + .try_to_vec() + .expect("token should serialize"), + Some(epoch) => ( + token, + sub_prefix + .as_ref() + .map(|k| k.to_string()) + .unwrap_or_default(), + denom, + epoch.0, + ) + .try_to_vec() + .expect("token should serialize"), + }; // Generate the unique asset identifier from the unique token address AssetType::new(token_bytes.as_ref()).expect("unable to create asset type") } diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 743bb4c600..712c0382e3 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -53,8 +53,8 @@ use namada::types::storage::{ }; use namada::types::time::DateTimeUtc; use namada::types::token::{ - DenominatedAmount, MaspDenom, Transfer, HEAD_TX_KEY, PIN_KEY_PREFIX, - TX_KEY_PREFIX, + DenominatedAmount, MaspDenom, TokenAddress, Transfer, HEAD_TX_KEY, + PIN_KEY_PREFIX, TX_KEY_PREFIX, }; use namada::types::transaction::governance::{ InitProposalData, ProposalType, VoteProposalData, @@ -507,19 +507,105 @@ pub enum PinnedBalanceError { InvalidViewingKey, } +// #[derive(BorshSerialize, BorshDeserialize, Debug, Clone)] +// pub struct MaspAmount { +// pub asset: TokenAddress, +// pub amount: token::Amount, +// } + +#[derive(BorshSerialize, BorshDeserialize, Debug, Clone)] +pub struct MaspChange { + pub asset: TokenAddress, + pub change: token::Change, +} + +#[derive(BorshSerialize, BorshDeserialize, Debug, Clone, Default)] +pub struct MaspAmount(HashMap<(Epoch, TokenAddress), token::Change>); + +impl std::ops::Deref for MaspAmount { + type Target = HashMap<(Epoch, TokenAddress), token::Change>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for MaspAmount { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl std::ops::Add for MaspAmount { + type Output = MaspAmount; + + fn add(mut self, mut rhs: MaspAmount) -> Self::Output { + for (key, value) in rhs.drain() { + self.entry(key) + .and_modify(|val| *val += value) + .or_insert(value); + } + self + } +} + +impl std::ops::AddAssign for MaspAmount { + fn add_assign(&mut self, amount: MaspAmount) { + *self = self.clone() + amount + } +} + +// please stop copying and pasting make a function +impl std::ops::Sub for MaspAmount { + type Output = MaspAmount; + + fn sub(mut self, mut rhs: MaspAmount) -> Self::Output { + for (key, value) in rhs.drain() { + self.entry(key) + .and_modify(|val| *val -= value) + .or_insert(value); + } + self + } +} + +impl std::ops::SubAssign for MaspAmount { + fn sub_assign(&mut self, amount: MaspAmount) { + *self = self.clone() - amount + } +} + +impl<'a> From<&'a MaspAmount> for Amount { + fn from(masp_amount: &'a MaspAmount) -> Amount { + let mut res = Amount::zero(); + for ((epoch, key), val) in masp_amount.iter() { + for denom in MaspDenom::iter() { + let asset = make_asset_type( + Some(*epoch), + &key.address, + &key.sub_prefix, + denom, + ); + res += Amount::from_pair(asset, denom.denominate_i64(val)) + .unwrap(); + } + } + res + } +} + /// Represents the amount used of different conversions pub type Conversions = HashMap, i64)>; /// Represents an amount that is -pub type MaspDenominatedAmount = Amount<(Address, Option, MaspDenom)>; +pub type MaspDenominatedAmount = Amount<(TokenAddress, MaspDenom)>; /// Represents the changes that were made to a list of transparent accounts -pub type TransferDelta = - HashMap, MaspDenom)>>; +pub type TransferDelta = HashMap; /// Represents the changes that were made to a list of shielded accounts -pub type TransactionDelta = HashMap; +pub type TransactionDelta = HashMap; /// Represents the current state of the shielded pool from the perspective of /// the chosen viewing keys. @@ -665,6 +751,7 @@ impl ShieldedContext { sks: &[ExtendedSpendingKey], fvks: &[ViewingKey], ) { + let client = HttpClient::new(ledger_address.clone()).unwrap(); // First determine which of the keys requested to be fetched are new. // Necessary because old transactions will need to be scanned for new // keys. @@ -696,7 +783,7 @@ impl ShieldedContext { // Update this unknown shielded context until it is level with self while tx_ctx.last_txidx != self.last_txidx { if let Some(((height, idx), (epoch, tx))) = tx_iter.next() { - tx_ctx.scan_tx(*height, *idx, *epoch, tx); + tx_ctx.scan_tx(&client, *height, *idx, *epoch, tx).await; } else { break; } @@ -714,7 +801,7 @@ impl ShieldedContext { // Now that we possess the unspent notes corresponding to both old and // new keys up until tx_pos, proceed to scan the new transactions. for ((height, idx), (epoch, tx)) in &mut tx_iter { - self.scan_tx(*height, *idx, *epoch, tx); + self.scan_tx(&client, *height, *idx, *epoch, tx).await; } } @@ -792,8 +879,9 @@ impl ShieldedContext { /// we have spent are updated. The witness map is maintained to make it /// easier to construct note merkle paths in other code. See /// - pub fn scan_tx( + pub async fn scan_tx( &mut self, + client: &HttpClient, height: BlockHeight, index: TxIndex, epoch: Epoch, @@ -826,7 +914,9 @@ impl ShieldedContext { self.witness_map.insert(note_pos, witness); // Let's try to see if any of our viewing keys can decrypt latest // note - for (vk, notes) in self.pos_map.iter_mut() { + let mut pos_map = HashMap::new(); + std::mem::swap(&mut pos_map, &mut self.pos_map); + for (vk, notes) in pos_map.iter_mut() { let decres = try_sapling_note_decryption::( 0, &vk.ivk().0, @@ -850,16 +940,24 @@ impl ShieldedContext { // Note the account changes let balance = transaction_delta .entry(*vk) - .or_insert_with(Amount::zero); - *balance += - Amount::from_nonnegative(note.asset_type, note.value) + .or_insert_with(MaspAmount::default); + *balance += self + .decode_all_amounts( + client, + Amount::from_nonnegative( + note.asset_type, + note.value, + ) .expect( "found note with invalid value or asset type", - ); + ), + ) + .await; self.vk_map.insert(note_pos, *vk); break; } } + std::mem::swap(&mut pos_map, &mut self.pos_map); } // Cancel out those of our notes that have been spent for ss in &shielded.shielded_spends { @@ -870,30 +968,35 @@ impl ShieldedContext { // Note the account changes let balance = transaction_delta .entry(self.vk_map[note_pos]) - .or_insert_with(Amount::zero); + .or_insert_with(MaspAmount::default); let note = self.note_map[note_pos]; - *balance -= - Amount::from_nonnegative(note.asset_type, note.value) - .expect("found note with invalid value or asset type"); + *balance -= self + .decode_all_amounts( + client, + Amount::from_nonnegative(note.asset_type, note.value) + .expect( + "found note with invalid value or asset type", + ), + ) + .await; } } // Record the changes to the transparent accounts let mut transfer_delta = TransferDelta::new(); - for denom in MaspDenom::iter() { - let transparent_delta = Amount::from_nonnegative( - (tx.token.clone(), tx.sub_prefix.clone(), denom), - denom.denominate(&tx.amount.amount), - ) - .expect("invalid value for amount"); - if transparent_delta == Amount::zero() { - continue; - } - transfer_delta - .insert(tx.source.clone(), Amount::zero() - &transparent_delta); - transfer_delta.insert(tx.target.clone(), transparent_delta); - self.last_txidx += 1; - } + let token_addr = TokenAddress { + address: tx.token.clone(), + sub_prefix: tx.sub_prefix.clone(), + }; + transfer_delta.insert( + tx.source.clone(), + MaspChange { + asset: token_addr, + change: -tx.amount.amount.change(), + }, + ); + self.last_txidx += 1; + self.delta_map.insert( (height, index), (epoch, transfer_delta, transaction_delta), @@ -1007,6 +1110,7 @@ impl ShieldedContext { ) -> Option { // First get the unexchanged balance if let Some(balance) = self.compute_shielded_balance(vk) { + let balance = self.decode_all_amounts(&client, balance).await; // And then exchange balance into current asset types Some( self.compute_exchanged_amount( @@ -1028,12 +1132,15 @@ impl ShieldedContext { /// conversion used, the conversions are applied to the given input, and /// the trace amount that could not be converted is moved from input to /// output. - fn apply_conversion( + #[allow(clippy::too_many_arguments)] + async fn apply_conversion( + &mut self, + client: &HttpClient, conv: AllowedConversion, asset_type: AssetType, value: i64, usage: &mut i64, - input: &mut Amount, + input: &mut MaspAmount, output: &mut Amount, ) { // If conversion if possible, accumulate the exchanged amount @@ -1056,7 +1163,9 @@ impl ShieldedContext { // Record how much more of the given conversion has been used *usage += required; // Apply the conversions to input and move the trace amount to output - *input += conv * required - &trace; + *input += self + .decode_all_amounts(client, conv * required - &trace) + .await; *output += trace; } @@ -1067,76 +1176,104 @@ impl ShieldedContext { pub async fn compute_exchanged_amount( &mut self, client: HttpClient, - mut input: Amount, + mut input: MaspAmount, target_epoch: Epoch, mut conversions: Conversions, ) -> (Amount, Conversions) { // Where we will store our exchanged value let mut output = Amount::zero(); // Repeatedly exchange assets until it is no longer possible - while let Some((asset_type, value)) = - input.components().next().map(cloned_pair) + while let Some(((asset_epoch, token_addr), value)) = + input.iter().next().map(cloned_pair) { - let target_asset_type = self - .decode_asset_type(client.clone(), asset_type) - .await - .map(|(addr, sub, denom, _epoch)| { - make_asset_type(target_epoch, &addr, &sub, denom) - }) - .unwrap_or(asset_type); - let at_target_asset_type = asset_type == target_asset_type; - if let (Some((conv, _wit, usage)), false) = ( - self.query_allowed_conversion( - client.clone(), - asset_type, - &mut conversions, - ) - .await, - at_target_asset_type, - ) { - println!( - "converting current asset type to latest asset type..." + for denom in MaspDenom::iter() { + let target_asset_type = make_asset_type( + Some(target_epoch), + &token_addr.address, + &token_addr.sub_prefix, + denom, ); - // Not at the target asset type, not at the latest asset type. - // Apply conversion to get from current asset type to the latest - // asset type. - Self::apply_conversion( - conv.clone(), - asset_type, - value, - usage, - &mut input, - &mut output, + let asset_type = make_asset_type( + Some(asset_epoch), + &token_addr.address, + &token_addr.sub_prefix, + denom, ); - } else if let (Some((conv, _wit, usage)), false) = ( - self.query_allowed_conversion( - client.clone(), - target_asset_type, - &mut conversions, - ) - .await, - at_target_asset_type, - ) { - println!( - "converting latest asset type to target asset type..." - ); - // Not at the target asset type, yes at the latest asset type. - // Apply inverse conversion to get from latest asset type to - // the target asset type. - Self::apply_conversion( - conv.clone(), - asset_type, - value, - usage, - &mut input, - &mut output, - ); - } else { - // At the target asset type. Then move component over to output. - let comp = input.project(asset_type); - output += ∁ - // Strike from input to avoid repeating computation - input -= comp; + let at_target_asset_type = target_epoch == asset_epoch; + + let denom_value = denom.denominate_i64(&value); + _ = self + .query_allowed_conversion( + client.clone(), + target_asset_type, + &mut conversions, + ) + .await; + + if let (Some((conv, _wit, usage)), false) = ( + conversions.get_mut(&target_asset_type), + at_target_asset_type, + ) { + println!( + "converting current asset type to latest asset type..." + ); + // Not at the target asset type, not at the latest asset + // type. Apply conversion to get from + // current asset type to the latest + // asset type. + self.apply_conversion( + &client, + conv.clone(), + target_asset_type, + denom_value, + usage, + &mut input, + &mut output, + ) + .await; + break; + } + _ = self + .query_allowed_conversion( + client.clone(), + asset_type, + &mut conversions, + ) + .await; + if let (Some((conv, _wit, usage)), false) = + (conversions.get_mut(&asset_type), at_target_asset_type) + { + println!( + "converting latest asset type to target asset type..." + ); + // Not at the target asset type, yes at the latest asset + // type. Apply inverse conversion to get + // from latest asset type to the target + // asset type. + self.apply_conversion( + &client, + conv.clone(), + target_asset_type, + denom_value, + usage, + &mut input, + &mut output, + ) + .await; + } else { + // At the target asset type. Then move component over to + // output. + + let mut comp = MaspAmount::default(); + for ((e, key), val) in input.iter() { + if *key == token_addr { + comp.insert((*e, key.clone()), *val); + } + } + output += Amount::from(&comp); + // Strike from input to avoid repeating computation + input -= comp; + } } } (output, conversions) @@ -1180,10 +1317,11 @@ impl ShieldedContext { // The amount contributed by this note before conversion let pre_contr = Amount::from_pair(note.asset_type, note.value) .expect("received note has invalid value or asset type"); + let input = self.decode_all_amounts(&client, pre_contr).await; let (contr, proposed_convs) = self .compute_exchanged_amount( client.clone(), - pre_contr, + input, target_epoch, conversions.clone(), ) @@ -1301,9 +1439,10 @@ impl ShieldedContext { .await?; // Establish connection with which to do exchange rate queries let client = HttpClient::new(ledger_address.clone()).unwrap(); + let amount = self.decode_all_amounts(&client, amt).await; // Finally, exchange the balance to the transaction's epoch Ok(( - self.compute_exchanged_amount(client, amt, ep, HashMap::new()) + self.compute_exchanged_amount(client, amount, ep, HashMap::new()) .await .0, ep, @@ -1318,16 +1457,26 @@ impl ShieldedContext { client: HttpClient, amt: Amount, target_epoch: Epoch, - ) -> MaspDenominatedAmount { - let mut res = Amount::zero(); + ) -> HashMap { + let mut res = HashMap::new(); for (asset_type, val) in amt.components() { // Decode the asset type let decoded = self.decode_asset_type(client.clone(), *asset_type).await; // Only assets with the target timestamp count match decoded { - Some((addr, sub, denom, epoch)) if epoch == target_epoch => { - res += &Amount::from_pair((addr, sub, denom), *val).unwrap() + Some(asset_type @ (_, _, _, epoch)) + if epoch == target_epoch => + { + decode_component( + asset_type, + *val, + &mut res, + |address, sub_prefix, _| TokenAddress { + address, + sub_prefix, + }, + ); } _ => {} } @@ -1335,25 +1484,37 @@ impl ShieldedContext { res } + // TODO :: Panics if we ever switch to an i128 in the masp crate /// Convert an amount whose units are AssetTypes to one whose units are /// Addresses that they decode to. pub async fn decode_all_amounts( &mut self, - client: HttpClient, + client: &HttpClient, amt: Amount, - ) -> Amount<(Address, Option, MaspDenom, Epoch)> { - let mut res = Amount::zero(); + ) -> MaspAmount { + let mut res = HashMap::default(); for (asset_type, val) in amt.components() { // Decode the asset type - let decoded = - self.decode_asset_type(client.clone(), *asset_type).await; - // Only assets with the target timestamp count - if let Some((addr, sub, denom, epoch)) = decoded { - res += - &Amount::from_pair((addr, sub, denom, epoch), *val).unwrap() + if let Some(decoded) = + self.decode_asset_type(client.clone(), *asset_type).await + { + decode_component( + decoded, + *val, + &mut res, + |address, sub_prefix, epoch| { + ( + epoch, + TokenAddress { + address, + sub_prefix, + }, + ) + }, + ) } } - res + MaspAmount(res.into_iter().map(|(k, v)| (k, v.change())).collect()) } } @@ -1367,7 +1528,8 @@ fn convert_amount( let mut amount = Amount::zero(); let asset_types: [AssetType; 4] = MaspDenom::iter() .map(|denom| { - let asset_type = make_asset_type(epoch, token, sub_prefix, denom); + let asset_type = + make_asset_type(Some(epoch), token, sub_prefix, denom); // Combine the value and unit into one amount amount += Amount::from_nonnegative(asset_type, denom.denominate(val)) @@ -1630,29 +1792,22 @@ pub async fn submit_transfer(mut ctx: Context, mut args: args::TxTransfer) { args.amount = InputAmount::Validated(validated_amount); args.tx.fee_amount = InputAmount::Validated(validate_fee); + let token_addr = TokenAddress { + address: token.clone(), + sub_prefix: sub_prefix.clone(), + }; match rpc::query_storage_value::(&client, &balance_key).await { Some(balance) => { if balance < validated_amount.amount { - let balance_amount = format_denominated_amount( - &client, - &token, - &sub_prefix, - balance, - ) - .await; + let balance_amount = + format_denominated_amount(&client, &token_addr, balance) + .await; eprintln!( - "The balance of the source {} of token {}{} is lower than \ + "The balance of the source {} of token {} is lower than \ the amount to be transferred. Amount to transfer is {} \ and the balance is {}.", - source, - token, - validated_amount, - args.sub_prefix - .as_ref() - .map(|s| format!("/{}", s)) - .unwrap_or_default(), - balance_amount + source, token_addr, validated_amount, balance_amount ); if !args.tx.force { safe_exit(1) @@ -1661,13 +1816,8 @@ pub async fn submit_transfer(mut ctx: Context, mut args: args::TxTransfer) { } None => { eprintln!( - "No balance found for the source {} of token {}{}", - source, - token, - args.sub_prefix - .as_ref() - .map(|s| format!("/{}", s)) - .unwrap_or_default(), + "No balance found for the source {} of token {}", + source, token_addr, ); if !args.tx.force { safe_exit(1) @@ -1854,32 +2004,24 @@ pub async fn submit_ibc_transfer(ctx: Context, args: args::TxIbcTransfer) { { Some(balance) => { if balance < args.amount { + let token_addr = TokenAddress { + address: token.clone(), + sub_prefix: sub_prefix.clone(), + }; let formatted_amount = format_denominated_amount( &client, - &token, - &sub_prefix, + &token_addr, args.amount, ) .await; - let formatted_balance = format_denominated_amount( - &client, - &token, - &sub_prefix, - balance, - ) - .await; + let formatted_balance = + format_denominated_amount(&client, &token_addr, balance) + .await; eprintln!( - "The balance of the source {} of token {}{} is lower than \ + "The balance of the source {} of token {} is lower than \ the amount to be transferred. Amount to transfer is {} \ and the balance is {}.", - source, - token, - args.sub_prefix - .as_ref() - .map(|s| format!("/{}", s)) - .unwrap_or_default(), - formatted_amount, - formatted_balance + source, token_addr, formatted_amount, formatted_balance ); if !args.tx.force { safe_exit(1) @@ -2134,7 +2276,7 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { "yay" => { if let Some(pgf) = args.proposal_pgf { let splits = pgf.trim().split_ascii_whitespace(); - let address_iter = splits.clone().into_iter().step_by(2); + let address_iter = splits.clone().step_by(2); let cap_iter = splits.into_iter().skip(1).step_by(2); let mut set = HashSet::new(); for (address, cap) in @@ -3250,3 +3392,66 @@ pub async fn submit_tx( parsed } + +fn decode_component( + (addr, sub, denom, epoch): (Address, Option, MaspDenom, Epoch), + val: i64, + res: &mut HashMap, + mk_key: F, +) where + F: FnOnce(Address, Option, Epoch) -> K, + K: Eq + std::hash::Hash, +{ + let decoded_amount = token::Amount::from_uint( + u64::try_from(val).expect("negative cash does not exist"), + denom as u8, + ) + .unwrap(); + res.entry(mk_key(addr, sub, epoch)) + .and_modify(|val| *val += decoded_amount) + .or_insert(decoded_amount); +} + +#[cfg(test)] +mod test_tx { + + use namada::types::address::testing::gen_established_address; + use namada::types::storage::DbKeySeg; + + use super::*; + + #[test] + fn test_masp_add_amount() { + let address_1 = gen_established_address(); + let prefix_1: Key = DbKeySeg::StringSeg("eth_seg".into()).into(); + let prefix_2: Key = DbKeySeg::StringSeg("crypto_kitty".into()).into(); + let denom_1 = MaspDenom::One; + let denom_2 = MaspDenom::Three; + let epoch = Epoch::default(); + let _masp_amount = MaspAmount::default(); + + let asset_base = make_asset_type( + Some(epoch), + &address_1, + &Some(prefix_1.clone()), + denom_1, + ); + let _asset_denom = + make_asset_type(Some(epoch), &address_1, &Some(prefix_1), denom_2); + let _asset_prefix = + make_asset_type(Some(epoch), &address_1, &Some(prefix_2), denom_1); + + let _amount_base = + Amount::from_pair(asset_base, 16).expect("Test failed"); + let _amount_denom = + Amount::from_pair(asset_base, 2).expect("Test failed"); + let _amount_prefix = + Amount::from_pair(asset_base, 4).expect("Test failed"); + + // masp_amount += amount_base; + // assert_eq!(masp_amount.get((epoch,)), Uint::zero()); + // Amount::from_pair(atype, amount) + // MaspDenom::One + // assert_eq!(zero.abs(), Uint::zero()); + } +} diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 7cc997684e..df675c6c53 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -1,6 +1,6 @@ //! A basic fungible token -use std::fmt::Display; +use std::fmt::{Display, Formatter}; use std::ops::{Add, AddAssign, Div, Mul, Sub, SubAssign}; use std::str::FromStr; @@ -689,6 +689,16 @@ impl MaspDenom { let amount = amount.into(); amount.raw.0[*self as usize] } + + /// Get the corresponding u64 word from the input uint256. + pub fn denominate_i64(&self, amount: &Change) -> i64 { + let val = amount.abs().0[*self as usize] as i64; + if Change::is_negative(amount) { + -val + } else { + val + } + } } /// Key segment for a balance key @@ -705,6 +715,54 @@ pub const CONVERSION_KEY_PREFIX: &str = "conv"; pub const PIN_KEY_PREFIX: &str = "pin-"; const TOTAL_SUPPLY_STORAGE_KEY: &str = "total_supply"; +/// A fully qualified (multi-) token address. +#[derive( + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Debug, + Hash, + BorshSerialize, + BorshDeserialize, +)] +pub struct TokenAddress { + /// The address of the (multi-) token + pub address: Address, + /// If it is a mutli-token, this indicates the sub-token. + pub sub_prefix: Option, +} + +impl TokenAddress { + /// A function for displaying a [`TokenAddress`]. Takes a + /// human readable name of the token as input. + pub fn format_with_alias(&self, alias: &str) -> String { + format!( + "{}{}", + alias, + self.sub_prefix + .as_ref() + .map(|k| format!("/{}", k)) + .unwrap_or_default() + ) + } +} + +impl Display for TokenAddress { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let formatted = format!( + "{}{}", + self.address, + self.sub_prefix + .as_ref() + .map(|k| format!("/{}", k)) + .unwrap_or_default() + ); + f.write_str(&formatted) + } +} + /// Obtain a storage key for user's balance. pub fn balance_key(token_addr: &Address, owner: &Address) -> Key { Key::from(token_addr.to_db_key()) diff --git a/core/src/types/uint.rs b/core/src/types/uint.rs index 39d0839c71..6698a2e8cf 100644 --- a/core/src/types/uint.rs +++ b/core/src/types/uint.rs @@ -2,7 +2,7 @@ //! An unsigned 256 integer type. Used for, among other things, //! the backing type of token amounts. use std::cmp::Ordering; -use std::ops::{Add, AddAssign, BitXor, Div, Mul, Neg, Sub}; +use std::ops::{Add, AddAssign, BitXor, Div, Mul, Neg, Sub, SubAssign}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use impl_num_traits::impl_uint_num_traits; @@ -111,6 +111,11 @@ impl I256 { self.0 == Uint::zero() } + /// Gives the zero value of an I256 + pub fn zero() -> I256 { + Self(Uint::zero()) + } + /// Get a string representation of `self` as a /// native token amount. pub fn to_string_native(&self) -> String { @@ -243,6 +248,12 @@ impl Sub for I256 { } } +impl SubAssign for I256 { + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } +} + impl Mul for I256 { type Output = Self; @@ -289,6 +300,12 @@ impl From for I256 { } } +impl std::iter::Sum for I256 { + fn sum>(iter: I) -> Self { + iter.fold(I256::zero(), |acc, amt| acc + amt) + } +} + impl TryFrom for i128 { type Error = std::io::Error; From e2189991f13197b21e9d1b2fa54fa1f7d1c78f6d Mon Sep 17 00:00:00 2001 From: mariari Date: Thu, 4 May 2023 20:23:31 +0800 Subject: [PATCH 039/151] Remove all references to Masp::Amount out of RPC With this, the amounts code compiles with RPC not referencing the base namada amounts --- apps/src/lib/client/rpc.rs | 328 +++++++++++--------------- apps/src/lib/client/tx.rs | 50 ++-- core/src/ledger/storage/wl_storage.rs | 6 +- core/src/types/token.rs | 2 +- shared/src/ledger/vp_host_fns.rs | 10 +- tests/src/e2e/ledger_tests.rs | 1 - 6 files changed, 173 insertions(+), 224 deletions(-) diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 7dcb7eb07b..49bcbd5fec 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -18,7 +18,6 @@ use masp_primitives::asset_type::AssetType; use masp_primitives::merkle_tree::MerklePath; use masp_primitives::primitives::ViewingKey; use masp_primitives::sapling::Node; -use masp_primitives::transaction::components::Amount; use masp_primitives::zip32::ExtendedFullViewingKey; #[cfg(not(feature = "mainnet"))] use namada::core::ledger::testnet_pow; @@ -340,7 +339,7 @@ pub async fn query_transfers(mut ctx: Context, args: args::QueryTransfers) { }; tfer_delta.values().cloned().any( |MaspChange { ref asset, change }| { - check((asset, &token::Amount::from(change))) + check((asset, &change.into())) }, ) || shielded_accounts .values() @@ -367,12 +366,8 @@ pub async fn query_transfers(mut ctx: Context, args: args::QueryTransfers) { print!( " {}{} {}", sign, - format_denominated_amount( - &client, - asset, - token::Amount::from(change), - ) - .await, + format_denominated_amount(&client, asset, change.into(),) + .await, asset.format_with_alias(&token_alias) ); } @@ -611,8 +606,9 @@ pub async fn query_pinned_balance(ctx: &mut Context, args: args::QueryBalance) { // Establish connection with which to do exchange rate queries let client = HttpClient::new(args.query.ledger_address.clone()).unwrap(); // Print the token balances by payment address + let pinned_error = Err(PinnedBalanceError::InvalidViewingKey); for owner in owners { - let mut balance = Err(PinnedBalanceError::InvalidViewingKey); + let mut balance = pinned_error.clone(); // Find the viewing key that can recognize payments the current payment // address for vk in &viewing_keys { @@ -624,12 +620,12 @@ pub async fn query_pinned_balance(ctx: &mut Context, args: args::QueryBalance) { vk, ) .await; - if balance != Err(PinnedBalanceError::InvalidViewingKey) { + if balance != pinned_error { break; } } // If a suitable viewing key was not found, then demand it from the user - if balance == Err(PinnedBalanceError::InvalidViewingKey) { + if balance == pinned_error { print!("Enter the viewing key for {}: ", owner); io::stdout().flush().unwrap(); let mut vk_str = String::new(); @@ -664,43 +660,28 @@ pub async fn query_pinned_balance(ctx: &mut Context, args: args::QueryBalance) { (Ok((balance, epoch)), Some(token), sub_prefix) => { let token = ctx.get(token); let token_alias = lookup_alias(ctx, &token); - let mut total_balance = token::Amount::default(); - for denom in MaspDenom::iter() { - // Extract and print only the specified token from the total - let (_asset_type, value) = value_by_address( - &balance, - token.clone(), - sub_prefix, - denom, - epoch, - ); - total_balance += token::Amount::from_masp_denominated( - value as u64, - denom, - ); - } + + let token_address = TokenAddress { + address: token.clone(), + sub_prefix: sub_prefix + .map(|string| Key::parse(string).unwrap()), + }; + + let total_balance = balance[&(epoch, token_address.clone())]; + if total_balance.is_zero() { println!( "Payment address {} was consumed during epoch {}. \ - Received no shielded {}{}", + Received no shielded {}", owner, epoch, - token_alias, - sub_prefix - .map(|k| format!("/{}", k)) - .unwrap_or_default() + token_address.format_with_alias(&token_alias) ); } else { - let token_addr = TokenAddress { - address: token, - sub_prefix: sub_prefix - .as_ref() - .map(|k| Key::parse(k).unwrap()), - }; let formatted = format_denominated_amount( &client, - &token_addr, - total_balance, + &token_address, + total_balance.into(), ) .await; println!( @@ -709,18 +690,17 @@ pub async fn query_pinned_balance(ctx: &mut Context, args: args::QueryBalance) { owner, epoch, formatted, - token_addr.format_with_alias(&token_alias), + token_address.format_with_alias(&token_alias), ); } } (Ok((balance, epoch)), None, _) => { let mut found_any = false; - // Print balances by human-readable token names - let balance = ctx - .shielded - .decode_amount(client.clone(), balance, epoch) - .await; - for (token_addr, value) in balance.iter() { + + for ((_, token_addr), value) in balance + .iter() + .filter(|((token_epoch, _), _)| *token_epoch == epoch) + { if !found_any { println!( "Payment address {} was consumed during epoch {}. \ @@ -729,9 +709,12 @@ pub async fn query_pinned_balance(ctx: &mut Context, args: args::QueryBalance) { ); found_any = true; } - let formatted = - format_denominated_amount(&client, token_addr, *value) - .await; + let formatted = format_denominated_amount( + &client, + token_addr, + (*value).into(), + ) + .await; let token_alias = tokens .get(&token_addr.address) .map(|a| a.to_string()) @@ -961,19 +944,6 @@ pub async fn query_proposal(_ctx: Context, args: args::QueryProposal) { } } -/// Get the component of the given amount corresponding to the given token -pub fn value_by_address( - amt: &masp_primitives::transaction::components::Amount, - token: Address, - sub_prefix: Option<&String>, - denom: MaspDenom, - epoch: Epoch, -) -> (AssetType, i64) { - // Compute the unique asset identifier from the token address - let asset_type = make_asset_type(Some(epoch), &token, &sub_prefix, denom); - (asset_type, amt[&asset_type]) -} - /// Query token shielded balance(s) pub async fn query_shielded_balance( ctx: &mut Context, @@ -1012,61 +982,48 @@ pub async fn query_shielded_balance( match (args.token, owner.is_some()) { // Here the user wants to know the balance for a specific token (Some(token), true) => { - let mut total_balance = token::Amount::zero(); let token = ctx.get(&token); - for denom in MaspDenom::iter() { - // Query the multi-asset balance at the given spending key - let viewing_key = - ExtendedFullViewingKey::from(viewing_keys[0]).fvk.vk; - let balance: Amount = if no_conversions { - ctx.shielded - .compute_shielded_balance(&viewing_key) - .expect("context should contain viewing key") - } else { - ctx.shielded - .compute_exchanged_balance( - client.clone(), - &viewing_key, - epoch, - ) - .await - .expect("context should contain viewing key") - }; - // Compute the unique asset identifier from the token address - let asset_type = make_asset_type( - Some(epoch), - &token, - &args.sub_prefix, - denom, - ); - total_balance += token::Amount::from_masp_denominated( - balance[&asset_type] as u64, - denom, - ); - } + + // Query the multi-asset balance at the given spending key + let viewing_key = + ExtendedFullViewingKey::from(viewing_keys[0]).fvk.vk; + let balance: MaspAmount = if no_conversions { + ctx.shielded + .compute_shielded_balance(&client, &viewing_key) + .await + .expect("context should contain viewing key") + } else { + ctx.shielded + .compute_exchanged_balance( + client.clone(), + &viewing_key, + epoch, + ) + .await + .expect("context should contain viewing key") + }; let token_alias = lookup_alias(ctx, &token); + + let token_address = TokenAddress { + address: token, + sub_prefix: args.sub_prefix.map(|k| Key::parse(k).unwrap()), + }; + + let total_balance = balance[&(epoch, token_address.clone())]; if total_balance.is_zero() { println!( - "No shielded {}{} balance found for given key", - token_alias, - args.sub_prefix - .as_ref() - .map(|k| format!("/{}", k)) - .unwrap_or_default(), + "No shielded {} balance found for given key", + token_address.format_with_alias(&token_alias) ); } else { - let token_address = TokenAddress { - address: token, - sub_prefix: args.sub_prefix.map(|k| Key::parse(k).unwrap()), - }; println!( "{}: {}", token_address.format_with_alias(&token_alias), format_denominated_amount( &client, &token_address, - total_balance + token::Amount::from(total_balance) ) .await ); @@ -1081,7 +1038,8 @@ pub async fn query_shielded_balance( let viewing_key = ExtendedFullViewingKey::from(fvk).fvk.vk; let balance = if no_conversions { ctx.shielded - .compute_shielded_balance(&viewing_key) + .compute_shielded_balance(&client, &viewing_key) + .await .expect("context should contain viewing key") } else { ctx.shielded @@ -1093,11 +1051,11 @@ pub async fn query_shielded_balance( .await .expect("context should contain viewing key") }; - for (asset_type, value) in balance.components() { - if !balances.contains_key(asset_type) { - balances.insert(*asset_type, Vec::new()); + for (key, value) in balance.iter() { + if !balances.contains_key(key) { + balances.insert(key.clone(), Vec::new()); } - balances.get_mut(asset_type).unwrap().push((fvk, *value)); + balances.get_mut(key).unwrap().push((fvk, *value)); } } @@ -1108,42 +1066,32 @@ pub async fn query_shielded_balance( // TODO Implement a function for this let mut balance_map = HashMap::new(); - for (asset_type, balances) in balances { - let decoded = ctx - .shielded - .decode_asset_type(client.clone(), asset_type) - .await; - - match decoded { - Some((addr, sub_prefix, denom, asset_epoch)) - if asset_epoch == epoch => - { - // remove this from here, should not be making the - // hashtable creation any uglier - if balances.is_empty() { - println!( - "No shielded {} balance found for any wallet \ - key", - asset_type - ); - } - let asset_type = (addr, sub_prefix); - for (fvk, value) in balances { - let token_value = - token::Amount::from_masp_denominated( - value as u64, - denom, - ); - balance_map - .entry((fvk, asset_type.clone())) - .and_modify(|value_2| *value_2 += token_value) - .or_insert(token_value); - } + for ((asset_epoch, token_addr), balances) in balances { + if asset_epoch == epoch { + // remove this from here, should not be making the + // hashtable creation any uglier + if balances.is_empty() { + println!( + "No shielded {} balance found for any wallet key", + &token_addr + ); } - _ => {} - }; + for (fvk, value) in balances { + balance_map.insert((fvk, token_addr.clone()), value); + } + } } - for ((fvk, (addr, sub_prefix)), token_balance) in balance_map { + for ( + ( + fvk, + TokenAddress { + address: addr, + sub_prefix, + }, + ), + token_balance, + ) in balance_map + { read_tokens .entry(addr.clone()) .and_modify(|addr_vec| addr_vec.push(sub_prefix.clone())) @@ -1164,7 +1112,7 @@ pub async fn query_shielded_balance( let formatted = format_denominated_amount( &client, &token_address, - token_balance, + token_balance.into(), ) .await; println!(" {}, owned by {}", formatted, fvk); @@ -1219,42 +1167,36 @@ pub async fn query_shielded_balance( token_address.format_with_alias(&token_alias), ); for fvk in viewing_keys { - let mut balance = token::Amount::default(); - for denom in MaspDenom::iter() { - let asset_type = make_asset_type( - Some(epoch), - &token, - &args.sub_prefix, - denom, - ); - // Query the multi-asset balance at the given spending key - let viewing_key = ExtendedFullViewingKey::from(fvk).fvk.vk; - let denom_balance = if no_conversions { - ctx.shielded - .compute_shielded_balance(&viewing_key) - .expect("context should contain viewing key") - } else { - ctx.shielded - .compute_exchanged_balance( - client.clone(), - &viewing_key, - epoch, - ) - .await - .expect("context should contain viewing key") - }; - if denom_balance[&asset_type] != 0 { - balance += token::Amount::from_masp_denominated( - denom_balance[&asset_type] as u64, - denom, - ); + // Query the multi-asset balance at the given spending key + let viewing_key = ExtendedFullViewingKey::from(fvk).fvk.vk; + let balance = if no_conversions { + ctx.shielded + .compute_shielded_balance(&client, &viewing_key) + .await + .expect("context should contain viewing key") + } else { + ctx.shielded + .compute_exchanged_balance( + client.clone(), + &viewing_key, + epoch, + ) + .await + .expect("context should contain viewing key") + }; + + for ((_, address), val) in balance.iter() { + if !val.is_zero() { found_any = true; } + let formatted = format_denominated_amount( + &client, + address, + (*val).into(), + ) + .await; + println!(" {}, owned by {}", formatted, fvk); } - let formatted = - format_denominated_amount(&client, &token_address, balance) - .await; - println!(" {}, owned by {}", formatted, fvk); } if !found_any { println!( @@ -1268,19 +1210,16 @@ pub async fn query_shielded_balance( // Query the multi-asset balance at the given spending key let viewing_key = ExtendedFullViewingKey::from(viewing_keys[0]).fvk.vk; - let balance; if no_conversions { - balance = ctx + let balance = ctx .shielded - .compute_shielded_balance(&viewing_key) + .compute_shielded_balance(&client, &viewing_key) + .await .expect("context should contain viewing key"); // Print balances by human-readable token names - let decoded_balance = - ctx.shielded.decode_all_amounts(&client, balance).await; - print_decoded_balance_with_epoch(ctx, &client, decoded_balance) - .await; + print_decoded_balance_with_epoch(ctx, &client, balance).await; } else { - balance = ctx + let balance = ctx .shielded .compute_exchanged_balance( client.clone(), @@ -1290,11 +1229,7 @@ pub async fn query_shielded_balance( .await .expect("context should contain viewing key"); // Print balances by human-readable token names - let decoded_balance = ctx - .shielded - .decode_amount(client.clone(), balance, epoch) - .await; - print_decoded_balance(ctx, &client, decoded_balance).await; + print_decoded_balance(ctx, &client, balance, epoch).await; } } } @@ -1303,17 +1238,22 @@ pub async fn query_shielded_balance( pub async fn print_decoded_balance( ctx: &mut Context, client: &HttpClient, - decoded_balance: HashMap, + decoded_balance: MaspAmount, + epoch: Epoch, ) { if decoded_balance.is_empty() { println!("No shielded balance found for given key"); } else { - for (token_addr, amount) in decoded_balance { + for ((_, token_addr), amount) in decoded_balance + .iter() + .filter(|((token_epoch, _), _)| *token_epoch == epoch) + { println!( "{} : {}", token_addr .format_with_alias(&lookup_alias(ctx, &token_addr.address)), - format_denominated_amount(client, &token_addr, amount).await, + format_denominated_amount(client, token_addr, (*amount).into()) + .await, ); } } @@ -1329,7 +1269,7 @@ pub async fn print_decoded_balance_with_epoch( println!("No shielded balance found for given key"); } for ((epoch, token_addr), value) in decoded_balance.iter() { - let asset_value = token::Amount::from(*value); + let asset_value = (*value).into(); let alias = tokens .get(&token_addr.address) .map(|a| a.to_string()) diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 712c0382e3..9c2b6d3b7c 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -499,7 +499,7 @@ fn cloned_pair((a, b): (&T, &U)) -> (T, U) { } /// Errors that can occur when trying to retrieve pinned transaction -#[derive(PartialEq, Eq)] +#[derive(PartialEq, Eq, Clone, Copy)] pub enum PinnedBalanceError { /// No transaction has yet been pinned to the given payment address NoTransactionPinned, @@ -519,7 +519,9 @@ pub struct MaspChange { pub change: token::Change, } -#[derive(BorshSerialize, BorshDeserialize, Debug, Clone, Default)] +#[derive( + BorshSerialize, BorshDeserialize, Debug, Clone, Default, PartialEq, Eq, +)] pub struct MaspAmount(HashMap<(Epoch, TokenAddress), token::Change>); impl std::ops::Deref for MaspAmount { @@ -1017,7 +1019,11 @@ impl ShieldedContext { /// Compute the total unspent notes associated with the viewing key in the /// context. If the key is not in the context, then we do not know the /// balance and hence we return None. - pub fn compute_shielded_balance(&self, vk: &ViewingKey) -> Option { + pub async fn compute_shielded_balance( + &mut self, + client: &HttpClient, + vk: &ViewingKey, + ) -> Option { // Cannot query the balance of a key that's not in the map if !self.pos_map.contains_key(vk) { return None; @@ -1038,7 +1044,7 @@ impl ShieldedContext { .expect("found note with invalid value or asset type"); } } - Some(val_acc) + Some(self.decode_all_amounts(client, val_acc).await) } /// Query the ledger for the decoding of the given asset type and cache it @@ -1107,21 +1113,21 @@ impl ShieldedContext { client: HttpClient, vk: &ViewingKey, target_epoch: Epoch, - ) -> Option { + ) -> Option { // First get the unexchanged balance - if let Some(balance) = self.compute_shielded_balance(vk) { - let balance = self.decode_all_amounts(&client, balance).await; - // And then exchange balance into current asset types - Some( - self.compute_exchanged_amount( - client, + if let Some(balance) = self.compute_shielded_balance(&client, vk).await + { + let exchanged_amount = self + .compute_exchanged_amount( + client.clone(), balance, target_epoch, HashMap::new(), ) .await - .0, - ) + .0; + // And then exchange balance into current asset types + Some(self.decode_all_amounts(&client, exchanged_amount).await) } else { None } @@ -1432,7 +1438,7 @@ impl ShieldedContext { ledger_address: &TendermintAddress, owner: PaymentAddress, viewing_key: &ViewingKey, - ) -> Result<(Amount, Epoch), PinnedBalanceError> { + ) -> Result<(MaspAmount, Epoch), PinnedBalanceError> { // Obtain the balance that will be exchanged let (amt, ep) = Self::compute_pinned_balance(ledger_address, owner, viewing_key) @@ -1441,12 +1447,16 @@ impl ShieldedContext { let client = HttpClient::new(ledger_address.clone()).unwrap(); let amount = self.decode_all_amounts(&client, amt).await; // Finally, exchange the balance to the transaction's epoch - Ok(( - self.compute_exchanged_amount(client, amount, ep, HashMap::new()) - .await - .0, - ep, - )) + let computed_amount = self + .compute_exchanged_amount( + client.clone(), + amount, + ep, + HashMap::new(), + ) + .await + .0; + Ok((self.decode_all_amounts(&client, computed_amount).await, ep)) } /// Convert an amount whose units are AssetTypes to one whose units are diff --git a/core/src/ledger/storage/wl_storage.rs b/core/src/ledger/storage/wl_storage.rs index 7e6eaf7584..88d44bf871 100644 --- a/core/src/ledger/storage/wl_storage.rs +++ b/core/src/ledger/storage/wl_storage.rs @@ -360,14 +360,14 @@ where // try to read from the write log first let (log_val, _gas) = self.write_log().read(key); match log_val { - Some(&write_log::StorageModification::Write { ref value }) => { + Some(write_log::StorageModification::Write { ref value }) => { Ok(Some(value.clone())) } Some(&write_log::StorageModification::Delete) => Ok(None), - Some(&write_log::StorageModification::InitAccount { + Some(write_log::StorageModification::InitAccount { ref vp_code_hash, }) => Ok(Some(vp_code_hash.to_vec())), - Some(&write_log::StorageModification::Temp { ref value }) => { + Some(write_log::StorageModification::Temp { ref value }) => { Ok(Some(value.clone())) } None => { diff --git a/core/src/types/token.rs b/core/src/types/token.rs index df675c6c53..8b1d3e3101 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -681,7 +681,7 @@ impl From for MaspDenom { impl MaspDenom { /// Iterator over the possible denominations pub fn iter() -> impl Iterator { - (0u8..3).into_iter().map(Self::from) + (0u8..3).map(Self::from) } /// Get the corresponding u64 word from the input uint256. diff --git a/shared/src/ledger/vp_host_fns.rs b/shared/src/ledger/vp_host_fns.rs index b0e4ce7118..37f0118bcb 100644 --- a/shared/src/ledger/vp_host_fns.rs +++ b/shared/src/ledger/vp_host_fns.rs @@ -67,14 +67,14 @@ where let (log_val, gas) = write_log.read_pre(key); add_gas(gas_meter, gas)?; match log_val { - Some(&write_log::StorageModification::Write { ref value }) => { + Some(write_log::StorageModification::Write { ref value }) => { Ok(Some(value.clone())) } Some(&write_log::StorageModification::Delete) => { // Given key has been deleted Ok(None) } - Some(&write_log::StorageModification::InitAccount { + Some(write_log::StorageModification::InitAccount { ref vp_code_hash, }) => { // Read the VP of a new account @@ -109,14 +109,14 @@ where let (log_val, gas) = write_log.read(key); add_gas(gas_meter, gas)?; match log_val { - Some(&write_log::StorageModification::Write { ref value }) => { + Some(write_log::StorageModification::Write { ref value }) => { Ok(Some(value.clone())) } Some(&write_log::StorageModification::Delete) => { // Given key has been deleted Ok(None) } - Some(&write_log::StorageModification::InitAccount { + Some(write_log::StorageModification::InitAccount { ref vp_code_hash, }) => { // Read the VP code hash of a new account @@ -146,7 +146,7 @@ pub fn read_temp( let (log_val, gas) = write_log.read(key); add_gas(gas_meter, gas)?; match log_val { - Some(&write_log::StorageModification::Temp { ref value }) => { + Some(write_log::StorageModification::Temp { ref value }) => { Ok(Some(value.clone())) } None => Ok(None), diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 2418deb884..848bb38a88 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -2599,7 +2599,6 @@ fn ledger_many_txs_in_a_block() -> Result<()> { // We collect to run the threads in parallel. #[allow(clippy::needless_collect)] let tasks: Vec> = (0..4) - .into_iter() .map(|_| { let test = Arc::clone(&test); let validator_one_rpc = Arc::clone(&validator_one_rpc); From f6d0f6285eda2af543d63d4cbba3117fca31be96 Mon Sep 17 00:00:00 2001 From: mariari Date: Fri, 5 May 2023 17:03:16 +0800 Subject: [PATCH 040/151] Gensis Parsing config issue solved We made sure that all parameters in the config file have a DENOM, there is NO DEFAULT!!!! Furhter we renamed Dot to DOT, please write configs correctly We also derived serialization for dec, meaning that all decimals that were unquoted now have to be quoted, making configuration a bit more annoying --- core/Cargo.toml | 2 ++ core/src/types/dec.rs | 49 ++++++++++++++++++++++++++++-- genesis/dev.toml | 25 +++++++++------ genesis/e2e-tests-single-node.toml | 35 ++++++++++++--------- 4 files changed, 86 insertions(+), 25 deletions(-) diff --git a/core/Cargo.toml b/core/Cargo.toml index a074390bcc..806ac757f4 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -113,6 +113,8 @@ rand = {version = "0.8"} rand_core = {version = "0.6"} test-log = {version = "0.2.7", default-features = false, features = ["trace"]} tracing-subscriber = {version = "0.3.7", default-features = false, features = ["env-filter", "fmt"]} +toml = {version = "=0.5.9"} + [build-dependencies] tonic-build = "0.6.0" diff --git a/core/src/types/dec.rs b/core/src/types/dec.rs index e725f2f3a5..bbdf431009 100644 --- a/core/src/types/dec.rs +++ b/core/src/types/dec.rs @@ -36,16 +36,18 @@ pub type Result = std::result::Result; Default, BorshSerialize, BorshDeserialize, - Serialize, - Deserialize, BorshSchema, PartialEq, + Serialize, + Deserialize, Eq, PartialOrd, Ord, Debug, Hash, )] +#[serde(try_from = "String")] +#[serde(into = "String")] pub struct Dec(pub Uint); impl std::ops::Deref for Dec { @@ -161,6 +163,14 @@ impl FromStr for Dec { } } +impl TryFrom for Dec { + type Error = Error; + + fn try_from(value: String) -> Result { + Self::from_str(&value) + } +} + impl From for Dec { fn from(amt: Amount) -> Self { Self( @@ -186,6 +196,12 @@ impl From for Dec { } } +impl From for String { + fn from(value: Dec) -> String { + value.to_string() + } +} + impl Add for Dec { type Output = Self; @@ -306,6 +322,19 @@ mod test_dec { use super::*; use crate::types::token::{Amount, Change}; + #[derive(Debug, Serialize, Deserialize)] + struct SerializerTest { + dec: Dec, + } + + #[test] + fn dump_toml() { + let serializer = SerializerTest { + dec: Dec::new(3, 0).unwrap(), + }; + println!("{:?}", toml::to_string(&serializer)); + } + /// Fill in tests later #[test] fn test_dec_basics() { @@ -426,4 +455,20 @@ mod test_dec { } assert!(Dec::from_str(&yuge).is_err()); } + + /// Test that parsing from string is correct. + #[test] + fn test_dec_from_serde() { + assert_eq!( + serde_json::from_str::(r#""0.667""#).expect("all good"), + Dec::from_str("0.667").expect("should work") + ); + + let dec = Dec::from_str("0.667").unwrap(); + assert_eq!( + dec, + serde_json::from_str::(&serde_json::to_string(&dec).unwrap()) + .unwrap() + ); + } } diff --git a/genesis/dev.toml b/genesis/dev.toml index 0aff92206f..620298db3d 100644 --- a/genesis/dev.toml +++ b/genesis/dev.toml @@ -17,9 +17,9 @@ non_staked_balance = 100000 # VP for the validator account validator_vp = "vp_validator" # Commission rate for rewards -commission_rate = 0.05 +commission_rate = "0.05" # Maximum change per epoch in the commission rate -max_commission_rate_change = 0.01 +max_commission_rate_change = "0.01" # Public IP:port address net_address = "127.0.0.1:26656" @@ -27,6 +27,7 @@ net_address = "127.0.0.1:26656" [token.NAM] address = "atest1v4ehgw36x3prswzxggunzv6pxqmnvdj9xvcyzvpsggeyvs3cg9qnywf589qnwvfsg5erg3fkl09rg5" +denom = 8 vp = "vp_token" [token.NAM.balances] # In token balances, we can use: @@ -43,6 +44,7 @@ bertha = 1000000 [token.BTC] address = "atest1v4ehgw36xdzryve5gsc52veeg5cnsv2yx5eygvp38qcrvd29xy6rys6p8yc5xvp4xfpy2v694wgwcp" +denom = 8 vp = "vp_token" [token.BTC.balances] atest1v4ehgw368ycryv2z8qcnxv3cxgmrgvjpxs6yg333gym5vv2zxepnj334g4rryvj9xucrgve4x3xvr4 = 1000000 @@ -52,6 +54,7 @@ a1qyqzsqqqqqcyvvf5xcu5vd6rg4z5233hg9pn23pjgdryzdjy8pz52wzxxscnvvjxx3rryvzz8y5p6m [token.ETH] address = "atest1v4ehgw36xqmr2d3nx3ryvd2xxgmrq33j8qcns33sxezrgv6zxdzrydjrxveygd2yxumrsdpsf9jc2p" +denom = 18 vp = "vp_token" [token.ETH.balances] atest1v4ehgw368ycryv2z8qcnxv3cxgmrgvjpxs6yg333gym5vv2zxepnj334g4rryvj9xucrgve4x3xvr4 = 1000000 @@ -61,6 +64,7 @@ a1qyqzsqqqqqcyvvf5xcu5vd6rg4z5233hg9pn23pjgdryzdjy8pz52wzxxscnvvjxx3rryvzz8y5p6m [token.DOT] address = "atest1v4ehgw36gg6nvs2zgfpyxsfjgc65yv6pxy6nwwfsxgungdzrggeyzv35gveyxsjyxymyz335hur2jn" +denom = 10 vp = "vp_token" [token.DOT.balances] atest1v4ehgw368ycryv2z8qcnxv3cxgmrgvjpxs6yg333gym5vv2zxepnj334g4rryvj9xucrgve4x3xvr4 = 1000000 @@ -70,6 +74,7 @@ a1qyqzsqqqqqcyvvf5xcu5vd6rg4z5233hg9pn23pjgdryzdjy8pz52wzxxscnvvjxx3rryvzz8y5p6m [token.schnitzel] address = "atest1v4ehgw36xue5xvf5xvuyzvpjx5un2v3k8qeyvd3cxdqns32p89rrxd6xx9zngvpegccnzs699rdnnt" +denom = 6 vp = "vp_token" [token.schnitzel.balances] atest1v4ehgw368ycryv2z8qcnxv3cxgmrgvjpxs6yg333gym5vv2zxepnj334g4rryvj9xucrgve4x3xvr4 = 1000000 @@ -79,6 +84,7 @@ a1qyqzsqqqqqcyvvf5xcu5vd6rg4z5233hg9pn23pjgdryzdjy8pz52wzxxscnvvjxx3rryvzz8y5p6m [token.apfel] address = "atest1v4ehgw36gfryydj9g3p5zv3kg9znyd358ycnzsfcggc5gvecgc6ygs2rxv6ry3zpg4zrwdfeumqcz9" +denom = 6 vp = "vp_token" [token.apfel.balances] atest1v4ehgw368ycryv2z8qcnxv3cxgmrgvjpxs6yg333gym5vv2zxepnj334g4rryvj9xucrgve4x3xvr4 = 1000000 @@ -88,6 +94,7 @@ a1qyqzsqqqqqcyvvf5xcu5vd6rg4z5233hg9pn23pjgdryzdjy8pz52wzxxscnvvjxx3rryvzz8y5p6m [token.kartoffel] address = "atest1v4ehgw36gep5ysecxq6nyv3jg3zygv3e89qn2vp48pryxsf4xpznvve5gvmy23fs89pryvf5a6ht90" +denom = 6 public_key = "" vp = "vp_token" [token.kartoffel.balances] @@ -160,21 +167,21 @@ pipeline_len = 2 # for a fault in epoch 'n' up through epoch 'n + unbonding_len'. unbonding_len = 21 # Votes per fundamental staking token (namnam) -tm_votes_per_token = 1 +tm_votes_per_token = "1" # Reward for proposing a block. -block_proposer_reward = 0.125 +block_proposer_reward = "0.125" # Reward for voting on a block. -block_vote_reward = 0.1 +block_vote_reward = "0.1" # Maximum inflation rate per annum (10%) -max_inflation_rate = 0.1 +max_inflation_rate = "0.1" # Targeted ratio of staked tokens to total tokens in the supply -target_staked_ratio = 0.6667 +target_staked_ratio = "0.6667" # Portion of a validator's stake that should be slashed on a duplicate # vote. -duplicate_vote_min_slash_rate = 0.001 +duplicate_vote_min_slash_rate = "0.001" # Portion of a validator's stake that should be slashed on a light # client attack. -light_client_attack_min_slash_rate = 0.001 +light_client_attack_min_slash_rate = "0.001" # Governance parameters. [gov_params] diff --git a/genesis/e2e-tests-single-node.toml b/genesis/e2e-tests-single-node.toml index 6a1cc634f3..5fc2d4f231 100644 --- a/genesis/e2e-tests-single-node.toml +++ b/genesis/e2e-tests-single-node.toml @@ -5,7 +5,7 @@ genesis_time = "2021-09-30T10:00:00Z" native_token = "NAM" faucet_pow_difficulty = 1 -faucet_withdrawal_limit = "1_000" +faucet_withdrawal_limit = "1000" [validator.validator-0] # Validator's staked NAM at genesis. @@ -15,11 +15,11 @@ non_staked_balance = 1000000000000 # VP for the validator account validator_vp = "vp_validator" # Commission rate for rewards -commission_rate = 0.05 +commission_rate = "0.05" # Maximum change per epoch in the commission rate -max_commission_rate_change = 0.01 +max_commission_rate_change = "0.01" # Public IP:port address. -# We set the port to be the default+1000, so that if a local node was running at +# We set the port to be the default+1000, so that if a local node was running at # the same time as the E2E tests, it wouldn't affect them. net_address = "127.0.0.1:27656" @@ -27,6 +27,7 @@ net_address = "127.0.0.1:27656" [token.NAM] address = "atest1v4ehgw36x3prswzxggunzv6pxqmnvdj9xvcyzvpsggeyvs3cg9qnywf589qnwvfsg5erg3fkl09rg5" +denom = 6 vp = "vp_token" [token.NAM.balances] Albert = 1000000 @@ -42,6 +43,7 @@ faucet = 9223372036 [token.BTC] address = "atest1v4ehgw36xdzryve5gsc52veeg5cnsv2yx5eygvp38qcrvd29xy6rys6p8yc5xvp4xfpy2v694wgwcp" +denom = 8 vp = "vp_token" [token.BTC.balances] Albert = 1000000 @@ -52,6 +54,7 @@ faucet = 9223372036854 [token.ETH] address = "atest1v4ehgw36xqmr2d3nx3ryvd2xxgmrq33j8qcns33sxezrgv6zxdzrydjrxveygd2yxumrsdpsf9jc2p" +denom = 18 vp = "vp_token" [token.ETH.balances] Albert = 1000000 @@ -62,8 +65,9 @@ faucet = 9223372036854 [token.DOT] address = "atest1v4ehgw36gg6nvs2zgfpyxsfjgc65yv6pxy6nwwfsxgungdzrggeyzv35gveyxsjyxymyz335hur2jn" +denom = 10 vp = "vp_token" -[token.Dot.balances] +[token.DOT.balances] Albert = 1000000 Bertha = 1000000 Christel = 1000000 @@ -72,6 +76,7 @@ faucet = 9223372036854 [token.Schnitzel] address = "atest1v4ehgw36xue5xvf5xvuyzvpjx5un2v3k8qeyvd3cxdqns32p89rrxd6xx9zngvpegccnzs699rdnnt" +denom = 6 vp = "vp_token" [token.Schnitzel.balances] Albert = 1000000 @@ -82,6 +87,7 @@ faucet = 9223372036854 [token.Apfel] address = "atest1v4ehgw36gfryydj9g3p5zv3kg9znyd358ycnzsfcggc5gvecgc6ygs2rxv6ry3zpg4zrwdfeumqcz9" +denom = 6 vp = "vp_token" [token.Apfel.balances] Albert = 1000000 @@ -93,6 +99,7 @@ faucet = 9223372036854 [token.Kartoffel] address = "atest1v4ehgw36gep5ysecxq6nyv3jg3zygv3e89qn2vp48pryxsf4xpznvve5gvmy23fs89pryvf5a6ht90" public_key = "" +denom = 6 vp = "vp_token" [token.Kartoffel.balances] Albert = 1000000 @@ -164,9 +171,9 @@ implicit_vp = "vp_implicit" # Expected number of epochs per year (also sets the min duration of an epoch in seconds) epochs_per_year = 31_536_000 # The P gain factor in the Proof of Stake rewards controller -pos_gain_p = 0.1 +pos_gain_p = "0.1" # The D gain factor in the Proof of Stake rewards controller -pos_gain_d = 0.1 +pos_gain_d = "0.1" # Proof of stake parameters. [pos_params] @@ -179,21 +186,21 @@ pipeline_len = 2 # for a fault in epoch 'n' up through epoch 'n + unbonding_len'. unbonding_len = 3 # Votes per fundamental staking token (namnam) -tm_votes_per_token = 0.1 +tm_votes_per_token = "0.1" # Reward for proposing a block. -block_proposer_reward = 0.125 +block_proposer_reward = "0.125" # Reward for voting on a block. -block_vote_reward = 0.1 +block_vote_reward = "0.1" # Maximum inflation rate per annum (10%) -max_inflation_rate = 0.1 +max_inflation_rate = "0.1" # Targeted ratio of staked tokens to total tokens in the supply -target_staked_ratio = 0.6667 +target_staked_ratio = "0.6667" # Portion of a validator's stake that should be slashed on a duplicate # vote. -duplicate_vote_min_slash_rate = 0.001 +duplicate_vote_min_slash_rate = "0.001" # Portion of a validator's stake that should be slashed on a light # client attack. -light_client_attack_min_slash_rate = 0.001 +light_client_attack_min_slash_rate = "0.001" # Governance parameters. [gov_params] From 997ff52d993dd7a6167e7d6b699cd2c36d291e1d Mon Sep 17 00:00:00 2001 From: mariari Date: Fri, 5 May 2023 19:37:20 +0800 Subject: [PATCH 041/151] Improve dec display method Instead of showing all trailing zeros, we truncate the trailing 0's from being displayed --- core/src/types/dec.rs | 7 ++++++- core/src/types/token.rs | 11 ++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/core/src/types/dec.rs b/core/src/types/dec.rs index bbdf431009..3c99c8610c 100644 --- a/core/src/types/dec.rs +++ b/core/src/types/dec.rs @@ -313,7 +313,12 @@ impl Display for Dec { str_pre.push_str(string.as_str()); string = str_pre; }; - f.write_str(&string) + let stripped_string = string.trim_end_matches(['.', '0']); + if stripped_string.is_empty() { + f.write_str("0") + } else { + f.write_str(stripped_string) + } } } diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 8b1d3e3101..59413f84f3 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -324,12 +324,9 @@ impl DenominatedAmount { impl Display for DenominatedAmount { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let string = self.to_string_precise(); - let string = string.trim_end_matches(&['0', '.']); - if string.is_empty() { - f.write_str("0") - } else { - f.write_str(string) - } + let string = string.trim_end_matches(&['0']); + let string = string.trim_end_matches(&['.']); + f.write_str(string) } } @@ -704,7 +701,7 @@ impl MaspDenom { /// Key segment for a balance key pub const BALANCE_STORAGE_KEY: &str = "balance"; /// Key segment for a denomination key -pub const DENOM_STORAGE_KEY: &str = "balance"; +pub const DENOM_STORAGE_KEY: &str = "denom"; /// Key segment for head shielded transaction pointer keys pub const HEAD_TX_KEY: &str = "head-tx"; /// Key segment prefix for shielded transaction key From 8f7270a8793f6343a680d0864215410648fa14bd Mon Sep 17 00:00:00 2001 From: mariari Date: Fri, 5 May 2023 21:04:17 +0800 Subject: [PATCH 042/151] Fix u64 going to Dec from overflowing We simply convert to Dec before doing any shifting math --- core/src/types/dec.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/core/src/types/dec.rs b/core/src/types/dec.rs index 3c99c8610c..ae29e36702 100644 --- a/core/src/types/dec.rs +++ b/core/src/types/dec.rs @@ -185,7 +185,7 @@ impl From for Dec { impl From for Dec { fn from(num: u64) -> Self { - Self(Uint::from(num * 10u64.pow(POS_DECIMAL_PRECISION as u32))) + Self(Uint::from(num) * Uint::exp10(POS_DECIMAL_PRECISION as usize)) } } @@ -410,6 +410,15 @@ mod test_dec { debug_assert_eq!(dec * chg, Change::from(-2809i64)); } + #[test] + fn test_into() { + assert_eq!( + Dec::from(u64::MAX), + Dec::from_str("18446744073709551615.000000000000") + .expect("only 104 bits") + ) + } + /// Test that parsing from string is correct. #[test] fn test_dec_from_string() { From 85a82b4e109c866cf2404fd3704ac4119f3899b2 Mon Sep 17 00:00:00 2001 From: mariari Date: Tue, 9 May 2023 13:01:31 +0800 Subject: [PATCH 043/151] Temporary remove check for chain id verification Further temporarly change the denomination to be ignored. --- Cargo.lock | 1 + apps/src/lib/node/ledger/shell/init_chain.rs | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 21e402a594..8168c7f218 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3960,6 +3960,7 @@ dependencies = [ "tendermint-proto 0.23.6", "test-log", "thiserror", + "toml", "tonic-build", "tracing 0.1.37", "tracing-subscriber 0.3.16", diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index 5bc3c4cfb4..7f44315706 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -52,11 +52,11 @@ where let errors = self.wl_storage.storage.chain_id.validate(genesis_bytes); use itertools::Itertools; - assert!( - errors.is_empty(), - "Chain ID validation failed: {}", - errors.into_iter().format(". ") - ); + // assert!( + // errors.is_empty(), + // "Chain ID validation failed: {}", + // errors.into_iter().format(". ") + // ); } #[cfg(feature = "dev")] let genesis = genesis::genesis(num_validators); From bfb81af754399f05b035b429c8a169837317f517 Mon Sep 17 00:00:00 2001 From: mariari Date: Tue, 9 May 2023 23:07:53 +0800 Subject: [PATCH 044/151] Remove signed conversion errors Instead of using Amounts, we use Change instead. This prevents vp_token from overflowing --- core/src/types/uint.rs | 5 +++++ wasm/wasm_source/src/vp_token.rs | 34 ++++++++++++++++++++------------ 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/core/src/types/uint.rs b/core/src/types/uint.rs index 6698a2e8cf..8e4bc2ec0d 100644 --- a/core/src/types/uint.rs +++ b/core/src/types/uint.rs @@ -152,6 +152,11 @@ impl I256 { fn canonical(self) -> Self { Self(self.0.canonical()) } + + /// the maximum I256 value + pub fn maximum() -> Self { + Self(MAX_SIGNED_VALUE) + } } impl From for I256 { diff --git a/wasm/wasm_source/src/vp_token.rs b/wasm/wasm_source/src/vp_token.rs index 5b958e1222..0fd227fb3a 100644 --- a/wasm/wasm_source/src/vp_token.rs +++ b/wasm/wasm_source/src/vp_token.rs @@ -77,25 +77,33 @@ fn token_checks( } Some(owner) => { // accumulate the change - let pre: token::Amount = match owner { + let pre: token::Change = match owner { Address::Internal(InternalAddress::IbcMint) => { - token::Amount::max() + token::Change::maximum() } Address::Internal(InternalAddress::IbcBurn) => { - token::Amount::default() + token::Change::default() } - _ => ctx.read_pre(key)?.unwrap_or_default(), + _ => ctx + .read_pre::(key)? + .unwrap_or_default() + .change(), }; - let post: token::Amount = match owner { - Address::Internal(InternalAddress::IbcMint) => { - ctx.read_temp(key)?.unwrap_or_else(token::Amount::max) - } - Address::Internal(InternalAddress::IbcBurn) => { - ctx.read_temp(key)?.unwrap_or_default() - } - _ => ctx.read_post(key)?.unwrap_or_default(), + let post: token::Change = match owner { + Address::Internal(InternalAddress::IbcMint) => ctx + .read_temp::(key)? + .map(|x| x.change()) + .unwrap_or_else(token::Change::maximum), + Address::Internal(InternalAddress::IbcBurn) => ctx + .read_temp::(key)? + .unwrap_or_default() + .change(), + _ => ctx + .read_post::(key)? + .unwrap_or_default() + .change(), }; - let this_change = post.change() - pre.change(); + let this_change = post - pre; change += this_change; // make sure that the spender approved the transaction if !(this_change.non_negative() From 79133e4f2a164b9279d0d155a89127cdf3987def Mon Sep 17 00:00:00 2001 From: mariari Date: Wed, 10 May 2023 12:09:38 +0800 Subject: [PATCH 045/151] Fix underflow issue in commisions --- core/src/types/dec.rs | 10 ++++++++++ wasm/wasm_source/src/tx_change_validator_commission.rs | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/core/src/types/dec.rs b/core/src/types/dec.rs index ae29e36702..da9e0d2d8a 100644 --- a/core/src/types/dec.rs +++ b/core/src/types/dec.rs @@ -114,6 +114,16 @@ impl Dec { pub fn to_uint(&self) -> Uint { self.0 / Uint::exp10(POS_DECIMAL_PRECISION as usize) } + + /// Do subtraction of two [`Dec`]s If and only if the value is + /// greater + pub fn checked_sub(&self, other: &Self) -> Option { + if self > other { + Some(*self - *other) + } else { + None + } + } } impl FromStr for Dec { diff --git a/wasm/wasm_source/src/tx_change_validator_commission.rs b/wasm/wasm_source/src/tx_change_validator_commission.rs index e2a41bc69e..208a836cb0 100644 --- a/wasm/wasm_source/src/tx_change_validator_commission.rs +++ b/wasm/wasm_source/src/tx_change_validator_commission.rs @@ -175,7 +175,7 @@ mod tests { rate_pre: Dec, max_change: Dec, ) -> impl Strategy { - let min = cmp::max(rate_pre - max_change, Dec::zero()); + let min = rate_pre.checked_sub(&max_change).unwrap_or_default(); let max = cmp::min(rate_pre + max_change, Dec::one()); (arb_established_address(), arb_new_rate(min, max, rate_pre)).prop_map( |(validator, new_rate)| transaction::pos::CommissionChange { From ecb135c7d3302a5a3cbb3bfb2e11003ff8611c46 Mon Sep 17 00:00:00 2001 From: mariari Date: Wed, 10 May 2023 18:47:39 +0800 Subject: [PATCH 046/151] Add a new destination for ibc rpc endpoint --- shared/src/ledger/queries/vp/token.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/shared/src/ledger/queries/vp/token.rs b/shared/src/ledger/queries/vp/token.rs index 71ef3a52b4..cbad27005f 100644 --- a/shared/src/ledger/queries/vp/token.rs +++ b/shared/src/ledger/queries/vp/token.rs @@ -9,6 +9,7 @@ use crate::ledger::queries::RequestCtx; router! {TOKEN, ( "denomination" / [addr: Address] / [sub_prefix: opt Key] ) -> Option = denomination, + ( "denomination" / [addr: Address] / "ibc" / [_ibc_junk: String] ) -> Option = denomination_ibc, } /// Get the number of decimal places (in base 10) for a @@ -24,3 +25,19 @@ where { read_denom(ctx.wl_storage, &addr, sub_prefix.as_ref()) } + +// TODO Please fix this + +/// Get the number of decimal places (in base 10) for a +/// token specified by `addr`. +fn denomination_ibc( + ctx: RequestCtx<'_, D, H>, + addr: Address, + _ibc_junk: String, +) -> storage_api::Result> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + read_denom(ctx.wl_storage, &addr, None) +} From c09dd7e1e882e2633424b25f5acc5ca8038eed71 Mon Sep 17 00:00:00 2001 From: mariari Date: Wed, 10 May 2023 18:50:54 +0800 Subject: [PATCH 047/151] Removed denominated from being called from user contracts --- wasm/wasm_source/src/vp_implicit.rs | 7 ++----- wasm/wasm_source/src/vp_user.rs | 7 ++----- wasm/wasm_source/src/vp_validator.rs | 7 ++----- 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/wasm/wasm_source/src/vp_implicit.rs b/wasm/wasm_source/src/vp_implicit.rs index 3f1ea331f2..fd76f1bd97 100644 --- a/wasm/wasm_source/src/vp_implicit.rs +++ b/wasm/wasm_source/src/vp_implicit.rs @@ -143,15 +143,12 @@ fn validate_tx( // debit has to signed, credit doesn't let valid = change.non_negative() || *valid_sig; let sign = if change.non_negative() { "" } else { "-" }; - let denom_amount = token::Amount::from_change(change) - .denominated(token, sub_prefix.as_ref(), &ctx.pre()) - .unwrap(); debug_log!( - "token key: {}, change: {}{}, valid_sig: {}, valid \ + "token key: {}, change: {}{:?}, valid_sig: {}, valid \ modification: {}", key, sign, - denom_amount, + change, *valid_sig, valid ); diff --git a/wasm/wasm_source/src/vp_user.rs b/wasm/wasm_source/src/vp_user.rs index 1cbb2b4d94..63ae898099 100644 --- a/wasm/wasm_source/src/vp_user.rs +++ b/wasm/wasm_source/src/vp_user.rs @@ -116,14 +116,11 @@ fn validate_tx( // debit has to signed, credit doesn't let valid = change.non_negative() || addr == masp() || *valid_sig; - let denom_amount = token::Amount::from(change) - .denominated(token, sub_prefix.as_ref(), &ctx.pre()) - .unwrap(); debug_log!( - "token key: {}, change: {}, valid_sig: {}, valid \ + "token key: {}, change: {:?}, valid_sig: {}, valid \ modification: {}", key, - denom_amount, + change, *valid_sig, valid ); diff --git a/wasm/wasm_source/src/vp_validator.rs b/wasm/wasm_source/src/vp_validator.rs index 24169fb17d..0328fdd066 100644 --- a/wasm/wasm_source/src/vp_validator.rs +++ b/wasm/wasm_source/src/vp_validator.rs @@ -115,14 +115,11 @@ fn validate_tx( let change = post.change() - pre.change(); // debit has to signed, credit doesn't let valid = change.non_negative() || *valid_sig; - let amount = token::Amount::from(change) - .denominated(token, sub_prefix.as_ref(), &ctx.pre()) - .unwrap(); debug_log!( - "token key: {}, change: {}, valid_sig: {}, valid \ + "token key: {}, change: {:?}, valid_sig: {}, valid \ modification: {}", key, - amount, + change, *valid_sig, valid ); From 34e5be79944208b1b484bc7b0187247b3f46523a Mon Sep 17 00:00:00 2001 From: brentstone Date: Thu, 11 May 2023 00:37:15 -0400 Subject: [PATCH 048/151] WIP fix `pos_bonds` --- tests/src/e2e/ledger_tests.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 848bb38a88..eba045f775 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -1965,7 +1965,8 @@ fn pos_bonds() -> Result<()> { ]; let mut client = run_as!(test, Who::Validator(0), Bin::Client, tx_args, Some(40))?; - client.exp_string("Amount 5100 withdrawable starting from epoch ")?; + client + .exp_string("Amount 5100.000000 withdrawable starting from epoch ")?; client.assert_success(); // 5. Submit an unbond of the delegation @@ -1987,7 +1988,7 @@ fn pos_bonds() -> Result<()> { &validator_one_rpc, ]; let mut client = run!(test, Bin::Client, tx_args, Some(40))?; - let expected = "Amount 3200 withdrawable starting from epoch "; + let expected = "Amount 3200.000000 withdrawable starting from epoch "; let (_unread, matched) = client.exp_regex(&format!("{expected}.*\n"))?; let epoch_raw = matched.trim().split_once(expected).unwrap().1; let delegation_withdrawable_epoch = Epoch::from_str(epoch_raw).unwrap(); From fcaa11ac4682815c0f69376aae057e26b9571a1c Mon Sep 17 00:00:00 2001 From: brentstone Date: Fri, 12 May 2023 01:30:02 -0400 Subject: [PATCH 049/151] refactor `Dec` to allow negative values --- core/src/types/dec.rs | 182 ++++++++++++++---- core/src/types/token.rs | 11 +- core/src/types/uint.rs | 26 ++- proof_of_stake/src/parameters.rs | 13 +- proof_of_stake/src/rewards.rs | 5 +- proof_of_stake/src/types.rs | 3 +- shared/src/ledger/inflation.rs | 6 +- .../src/tx_change_validator_commission.rs | 8 +- 8 files changed, 195 insertions(+), 59 deletions(-) diff --git a/core/src/types/dec.rs b/core/src/types/dec.rs index da9e0d2d8a..4681c9fbd5 100644 --- a/core/src/types/dec.rs +++ b/core/src/types/dec.rs @@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize}; use super::token::NATIVE_MAX_DECIMAL_PLACES; use crate::types::token::{Amount, Change}; -use crate::types::uint::Uint; +use crate::types::uint::{Uint, I256}; /// The number of Dec places for PoS rational calculations pub const POS_DECIMAL_PRECISION: u8 = 12; @@ -48,10 +48,10 @@ pub type Result = std::result::Result; )] #[serde(try_from = "String")] #[serde(into = "String")] -pub struct Dec(pub Uint); +pub struct Dec(pub I256); impl std::ops::Deref for Dec { - type Target = Uint; + type Target = I256; fn deref(&self) -> &Self::Target { &self.0 @@ -67,37 +67,59 @@ impl std::ops::DerefMut for Dec { impl Dec { /// Division with truncation (TODO: better description) pub fn trunc_div(&self, rhs: &Self) -> Option { - self.0 - .fixed_precision_div(rhs, POS_DECIMAL_PRECISION) - .map(Self) + let is_neg = self.0.is_negative() ^ rhs.0.is_negative(); + let inner_uint = self.0.abs(); + let inner_rhs_uint = rhs.0.abs(); + match inner_uint + .fixed_precision_div(&inner_rhs_uint, POS_DECIMAL_PRECISION) + { + Some(res) => { + let res = I256::try_from(res).ok()?; + if is_neg { + Some(Self(-res)) + } else { + Some(Self(res)) + } + } + None => None, + } } /// The representation of 0 pub fn zero() -> Self { - Self(Uint::zero()) + Self(I256::zero()) } /// The representation of 1 pub fn one() -> Self { - Self(Uint::one() * Uint::exp10(POS_DECIMAL_PRECISION as usize)) + Self(I256( + Uint::one() * Uint::exp10(POS_DECIMAL_PRECISION as usize), + )) } /// The representation of 2 pub fn two() -> Self { - Self( - (Uint::one() + Uint::one()) - * Uint::exp10(POS_DECIMAL_PRECISION as usize), - ) + Self::one() + Self::one() } /// Create a new [`Dec`] using a mantissa and a scale. - pub fn new(mantissa: u64, scale: u8) -> Option { + pub fn new(mantissa: i128, scale: u8) -> Option { if scale > POS_DECIMAL_PRECISION { None } else { - Uint::exp10((POS_DECIMAL_PRECISION - scale) as usize) - .checked_mul(Uint::from(mantissa)) - .map(Self) + let abs = u64::try_from(mantissa.abs()).ok()?; + match Uint::exp10((POS_DECIMAL_PRECISION - scale) as usize) + .checked_mul(Uint::from(abs)) + { + Some(res) => { + if mantissa.is_negative() { + Some(Self(-I256(res))) + } else { + Some(Self(I256(res))) + } + } + None => None, + } } } @@ -110,11 +132,20 @@ impl Dec { } } - /// Convert the Dec type into a Uint with truncation - pub fn to_uint(&self) -> Uint { + /// Convert the Dec type into a I256 with truncation + pub fn to_i256(&self) -> I256 { self.0 / Uint::exp10(POS_DECIMAL_PRECISION as usize) } + /// Convert the Dec type into a Uint with truncation + pub fn to_uint(&self) -> Option { + if self.is_negative() { + None + } else { + Some(self.0.abs() / Uint::exp10(POS_DECIMAL_PRECISION as usize)) + } + } + /// Do subtraction of two [`Dec`]s If and only if the value is /// greater pub fn checked_sub(&self, other: &Self) -> Option { @@ -124,17 +155,24 @@ impl Dec { None } } + + /// Return if the [`Dec`] is negative + pub fn is_negative(&self) -> bool { + self.0.is_negative() + } } impl FromStr for Dec { type Err = Error; fn from_str(s: &str) -> Result { - if s.starts_with('-') { - return Err(eyre!("Dec cannot be negative").into()); - } + let ((large, small), is_neg) = if let Some(strip) = s.strip_prefix('-') + { + (strip.split_once('.').unwrap_or((s, "0")), true) + } else { + (s.split_once('.').unwrap_or((s, "0")), false) + }; - let (large, small) = s.split_once('.').unwrap_or((s, "0")); let num_large = Uint::from_str_radix(large, 10).map_err(|e| { eyre!("Could not parse {} as an integer: {}", large, e) })?; @@ -169,7 +207,13 @@ impl FromStr for Dec { num_large ) })?; - Ok(Dec(int_part + decimal_part)) + let inner = I256::try_from(int_part + decimal_part) + .map_err(|e| eyre!("Could not convert Uint to I256: {}", e))?; + if is_neg { + Ok(Dec(-inner)) + } else { + Ok(Dec(inner)) + } } } @@ -183,25 +227,43 @@ impl TryFrom for Dec { impl From for Dec { fn from(amt: Amount) -> Self { - Self( - amt.raw_amount() - * Uint::exp10( + match I256::try_from(amt.raw_amount()).ok() { + Some(raw) => Self( + raw * Uint::exp10( (POS_DECIMAL_PRECISION - NATIVE_MAX_DECIMAL_PLACES) as usize, ), - ) + ), + None => Self::zero(), + } + } +} + +impl TryFrom for Dec { + type Error = Error; + + fn try_from(value: Uint) -> std::result::Result { + let i256 = I256::try_from(value) + .map_err(|e| eyre!("Could not convert Uint to I256: {}", e))?; + Ok(Self(i256 * Uint::exp10(POS_DECIMAL_PRECISION as usize))) } } impl From for Dec { fn from(num: u64) -> Self { - Self(Uint::from(num) * Uint::exp10(POS_DECIMAL_PRECISION as usize)) + Self(I256::from(num) * Uint::exp10(POS_DECIMAL_PRECISION as usize)) + } +} + +impl From for Dec { + fn from(num: i128) -> Self { + Self(I256::from(num) * Uint::exp10(POS_DECIMAL_PRECISION as usize)) } } // Is error handling needed for this? -impl From for Dec { - fn from(num: Uint) -> Self { +impl From for Dec { + fn from(num: I256) -> Self { Self(num * Uint::exp10(POS_DECIMAL_PRECISION as usize)) } } @@ -224,7 +286,7 @@ impl Add for Dec { type Output = Self; fn add(self, rhs: u64) -> Self::Output { - Self(self.0 + Uint::from(rhs)) + Self(self.0 + I256::from(rhs)) } } @@ -242,11 +304,19 @@ impl Sub for Dec { } } -impl Mul for Dec { - type Output = Uint; +// impl Mul for Dec { +// type Output = Uint; + +// fn mul(self, rhs: Uint) -> Self::Output { +// self.0 * rhs / Uint::exp10(POS_DECIMAL_PRECISION as usize) +// } +// } - fn mul(self, rhs: Uint) -> Self::Output { - self.0 * rhs / Uint::exp10(POS_DECIMAL_PRECISION as usize) +impl Mul for Dec { + type Output = Dec; + + fn mul(self, rhs: u64) -> Self::Output { + Self(self.0 * Uint::from(rhs)) } } @@ -262,7 +332,11 @@ impl Mul for Dec { type Output = Amount; fn mul(self, rhs: Amount) -> Self::Output { - (rhs * self.0) / 10u64.pow(POS_DECIMAL_PRECISION as u32) + if !self.is_negative() { + (rhs * self.0.abs()) / 10u64.pow(POS_DECIMAL_PRECISION as u32) + } else { + panic!("aaa"); + } } } @@ -311,7 +385,8 @@ impl Div for Dec { impl Display for Dec { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let mut string = self.0.to_string(); + let is_neg = self.is_negative(); + let mut string = self.0.abs().to_string(); if string.len() > POS_DECIMAL_PRECISION as usize { let idx = string.len() - POS_DECIMAL_PRECISION as usize; string.insert(idx, '.'); @@ -326,6 +401,9 @@ impl Display for Dec { let stripped_string = string.trim_end_matches(['.', '0']); if stripped_string.is_empty() { f.write_str("0") + } else if is_neg { + let stripped_string = format!("-{}", stripped_string); + f.write_str(stripped_string.as_str()) } else { f.write_str(stripped_string) } @@ -360,21 +438,43 @@ mod test_dec { assert_eq!(Dec::new(1, 0).expect("Test failed"), Dec::one()); assert_eq!(Dec::new(2, 0).expect("Test failed"), Dec::two()); assert_eq!( - Dec(Uint::from(1653)), + Dec(I256(Uint::from(1653))), Dec::new(1653, POS_DECIMAL_PRECISION).expect("Test failed") ); assert_eq!( - Dec::new(123456789, 4).expect("Test failed").to_uint(), + Dec(I256::from(-48756)), + Dec::new(-48756, POS_DECIMAL_PRECISION).expect("Test failed") + ); + assert_eq!( + Dec::new(123456789, 4) + .expect("Test failed") + .to_uint() + .unwrap(), Uint::from(12345) ); assert_eq!( - Dec::new(123, 4).expect("Test failed").to_uint(), + Dec::new(-123456789, 4).expect("Test failed").to_i256(), + I256::from(-12345) + ); + assert_eq!( + Dec::new(123, 4).expect("Test failed").to_uint().unwrap(), Uint::zero() ); assert_eq!( - Dec::from_str("4876.3855").expect("Test failed").to_uint(), + Dec::new(123, 4).expect("Test failed").to_i256(), + I256::zero() + ); + assert_eq!( + Dec::from_str("4876.3855") + .expect("Test failed") + .to_uint() + .unwrap(), Uint::from(4876) ); + assert_eq!( + Dec::from_str("4876.3855").expect("Test failed").to_i256(), + I256::from(4876) + ); // Fixed precision division is more thoroughly tested for the `Uint` // type. These are sanity checks that the precision is correct. diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 59413f84f3..bd01e1917e 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -449,8 +449,15 @@ impl From for Amount { impl From for Amount { fn from(dec: Dec) -> Amount { - Amount { - raw: dec.0 / Uint::exp10(POS_DECIMAL_PRECISION as usize), + if !dec.is_negative() { + Amount { + raw: dec.0.abs() / Uint::exp10(POS_DECIMAL_PRECISION as usize), + } + } else { + panic!( + "The Dec value is negative and cannot be multiplied by an \ + Amount" + ) } } } diff --git a/core/src/types/uint.rs b/core/src/types/uint.rs index 8e4bc2ec0d..1749af7b51 100644 --- a/core/src/types/uint.rs +++ b/core/src/types/uint.rs @@ -81,9 +81,18 @@ const MINUS_ZERO: Uint = Uint([0u64, 0u64, 0u64, 9223372036854775808]); /// A signed 256 big integer. #[derive( - Copy, Clone, Debug, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize, + Copy, + Clone, + Debug, + Default, + PartialEq, + Eq, + Hash, + BorshSerialize, + BorshDeserialize, + BorshSchema, )] -pub struct I256(Uint); +pub struct I256(pub Uint); impl I256 { /// Check if the amount is not negative (greater @@ -259,6 +268,7 @@ impl SubAssign for I256 { } } +// NOTE: watch the overflow impl Mul for I256 { type Output = Self; @@ -269,6 +279,18 @@ impl Mul for I256 { } } +impl Mul for I256 { + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output { + if rhs.is_negative() { + -self * rhs.abs() + } else { + self * rhs.abs() + } + } +} + impl Div for I256 { type Output = Self; diff --git a/proof_of_stake/src/parameters.rs b/proof_of_stake/src/parameters.rs index a67878b8d1..c3bcd146a3 100644 --- a/proof_of_stake/src/parameters.rs +++ b/proof_of_stake/src/parameters.rs @@ -113,9 +113,11 @@ impl PosParams { // Check maximum total voting power cannot get larger than what // Tendermint allows - let max_total_voting_power = self.tm_votes_per_token - * Uint::from(TOKEN_MAX_AMOUNT) - * Uint::from(self.max_validator_slots); + let max_total_voting_power = (self.tm_votes_per_token + * TOKEN_MAX_AMOUNT + * self.max_validator_slots) + .to_uint() + .expect("Cannot fail"); match i64::try_from(max_total_voting_power) { Ok(max_total_voting_power_i64) => { if max_total_voting_power_i64 > MAX_TOTAL_VOTING_POWER { @@ -178,7 +180,7 @@ pub mod testing { // `unbonding_len` > `pipeline_len` unbonding_len in pipeline_len + 1..pipeline_len + 8, pipeline_len in Just(pipeline_len), - tm_votes_per_token in 1..10_001_u64) + tm_votes_per_token in 1..10_001_i128) -> PosParams { PosParams { max_validator_slots, @@ -195,6 +197,7 @@ pub mod testing { /// Get an arbitrary rate - a Dec value between 0 and 1 inclusive, with /// some fixed precision pub fn arb_rate() -> impl Strategy { - (0..=100_000_u64).prop_map(|num| Dec::new(num, 5).expect("Test failed")) + (0..=100_000_i128) + .prop_map(|num| Dec::new(num, 5).expect("Test failed")) } } diff --git a/proof_of_stake/src/rewards.rs b/proof_of_stake/src/rewards.rs index acfca50673..26d1b91442 100644 --- a/proof_of_stake/src/rewards.rs +++ b/proof_of_stake/src/rewards.rs @@ -2,11 +2,12 @@ use namada_core::types::dec::Dec; use namada_core::types::token::Amount; -use namada_core::types::uint::Uint; +use namada_core::types::uint::{Uint, I256}; use thiserror::Error; /// This is equal to 0.01. -const MIN_PROPOSER_REWARD: Dec = Dec(Uint([10000u64, 0u64, 0u64, 0u64])); +const MIN_PROPOSER_REWARD: Dec = + Dec(I256(Uint([10000000000u64, 0u64, 0u64, 0u64]))); /// Errors during rewards calculation #[derive(Debug, Error)] diff --git a/proof_of_stake/src/types.rs b/proof_of_stake/src/types.rs index eb87cb22f1..da7c3f2914 100644 --- a/proof_of_stake/src/types.rs +++ b/proof_of_stake/src/types.rs @@ -509,7 +509,8 @@ impl Display for SlashType { pub fn into_tm_voting_power(votes_per_token: Dec, tokens: Amount) -> i64 { let pow = votes_per_token * u128::try_from(tokens).expect("Voting power out of bounds"); - i64::try_from(pow.to_uint()).expect("Invalid voting power") + i64::try_from(pow.to_uint().expect("Cant fail")) + .expect("Invalid voting power") } #[cfg(test)] diff --git a/shared/src/ledger/inflation.rs b/shared/src/ledger/inflation.rs index b5b1de321a..1d869dcff9 100644 --- a/shared/src/ledger/inflation.rs +++ b/shared/src/ledger/inflation.rs @@ -63,8 +63,10 @@ impl RewardsController { // Token amounts must be expressed in terms of the raw amount (namnam) // to properly run the PD controller - let locked = Dec::from(locked_tokens.raw_amount()); - let total = Dec::from(total_tokens.raw_amount()); + let locked = + Dec::try_from(locked_tokens.raw_amount()).expect("Should not fail"); + let total = + Dec::try_from(total_tokens.raw_amount()).expect("Should not fail"); let epochs_py: Dec = epochs_per_year.into(); let locked_ratio = locked / total; diff --git a/wasm/wasm_source/src/tx_change_validator_commission.rs b/wasm/wasm_source/src/tx_change_validator_commission.rs index 208a836cb0..3869dbafe2 100644 --- a/wasm/wasm_source/src/tx_change_validator_commission.rs +++ b/wasm/wasm_source/src/tx_change_validator_commission.rs @@ -153,10 +153,10 @@ mod tests { fn arb_rate(min: Dec, max: Dec) -> impl Strategy { let scale = Dec::new(100_000, 0).expect("Test failed"); - let int_min = (min * scale).to_uint(); - let int_min = u64::try_from(int_min).unwrap(); - let int_max = (max * scale).to_uint(); - let int_max = u64::try_from(int_max).unwrap(); + let int_min = (min * scale).to_i256(); + let int_min = i128::try_from(int_min).unwrap(); + let int_max = (max * scale).to_i256(); + let int_max = i128::try_from(int_max).unwrap(); (int_min..=int_max).prop_map(move |num| Dec::from(num) / scale) } From f855fd15434bcd09420901352f52657faeb5b76d Mon Sep 17 00:00:00 2001 From: "Raymond E. Pasco" Date: Sun, 21 May 2023 10:07:28 -0400 Subject: [PATCH 050/151] fixup! Merge remote-tracking branch 'origin/base' into HEAD --- shared/src/ledger/args.rs | 2 +- shared/src/ledger/ibc/vp/mod.rs | 11 ++++++----- shared/src/ledger/masp.rs | 21 ++++++++++++++------- shared/src/ledger/tx.rs | 20 ++++++++++---------- 4 files changed, 31 insertions(+), 23 deletions(-) diff --git a/shared/src/ledger/args.rs b/shared/src/ledger/args.rs index 0e372d15fd..ef61dad92c 100644 --- a/shared/src/ledger/args.rs +++ b/shared/src/ledger/args.rs @@ -1,7 +1,7 @@ //! Structures encapsulating SDK arguments use namada_core::types::chain::ChainId; +use namada_core::types::dec::Dec; use namada_core::types::time::DateTimeUtc; -use rust_decimal::Decimal; use crate::ibc::core::ics24_host::identifier::{ChannelId, PortId}; use crate::types::address::Address; diff --git a/shared/src/ledger/ibc/vp/mod.rs b/shared/src/ledger/ibc/vp/mod.rs index 5cb1a61129..d491f63a03 100644 --- a/shared/src/ledger/ibc/vp/mod.rs +++ b/shared/src/ledger/ibc/vp/mod.rs @@ -22,6 +22,7 @@ use namada_core::types::address::{Address, InternalAddress}; use namada_core::types::storage::Key; use namada_proof_of_stake::read_pos_params; use thiserror::Error; +use namada_core::types::dec::Dec; pub use token::{Error as IbcTokenError, IbcToken}; use crate::ledger::native_vp::{self, Ctx, NativeVp, VpEnv}; @@ -236,8 +237,8 @@ pub fn get_dummy_genesis_validator() let consensus_sk = common_sk_from_simple_seed(0); let consensus_key = consensus_sk.to_public(); - let commission_rate = Decimal::new(1, 1); - let max_commission_rate_change = Decimal::new(1, 1); + let commission_rate = Dec::new(1, -1); + let max_commission_rate_change = Dec::new(1, -1); namada_proof_of_stake::types::GenesisValidator { address, tokens, @@ -1987,7 +1988,7 @@ mod tests { // init balance let sender = established_address_1(); let balance_key = balance_key(&nam(), &sender); - let amount = Amount::whole(100); + let amount = Amount::native_whole(100); wl_storage .write_log .write(&balance_key, amount.try_to_vec().unwrap()) @@ -2449,7 +2450,7 @@ mod tests { // init the escrow balance let balance_key = balance_key(&nam(), &Address::Internal(InternalAddress::IbcEscrow)); - let amount = Amount::whole(100); + let amount = Amount::native_whole(100); wl_storage .write_log .write(&balance_key, amount.try_to_vec().unwrap()) @@ -2601,7 +2602,7 @@ mod tests { // init the escrow balance let balance_key = balance_key(&nam(), &Address::Internal(InternalAddress::IbcEscrow)); - let amount = Amount::whole(100); + let amount = Amount::native_whole(100); wl_storage .write_log .write(&balance_key, amount.try_to_vec().unwrap()) diff --git a/shared/src/ledger/masp.rs b/shared/src/ledger/masp.rs index 1d2cf92a64..89b751d1ab 100644 --- a/shared/src/ledger/masp.rs +++ b/shared/src/ledger/masp.rs @@ -54,6 +54,8 @@ use sha2::Digest; use crate::ledger::queries::Client; use crate::ledger::{args, rpc}; +use crate::ledger::args::InputAmount; +use crate::ledger::rpc::query_conversion; use crate::proto::{SignedTxData, Tx}; use crate::tendermint_rpc::query::Query; use crate::tendermint_rpc::Order; @@ -890,6 +892,7 @@ impl ShieldedContext { /// the trace amount that could not be converted is moved from input to /// output. async fn apply_conversion( + &mut self, conv: AllowedConversion, client: &U::C, asset_type: AssetType, @@ -1187,15 +1190,16 @@ impl ShieldedContext { ) -> Result<(Amount, Epoch), PinnedBalanceError> { // Obtain the balance that will be exchanged let (amt, ep) = - Self::compute_pinned_balance(ledger_address, owner, viewing_key) + Self::compute_pinned_balance(client, owner, viewing_key) .await?; // Establish connection with which to do exchange rate queries - let client = HttpClient::new(ledger_address.clone()).unwrap(); let amount = self.decode_all_amounts(&client, amt).await; + let token = token_exists_or_err(args.token.clone(), args.tx.force, client).await?; + let validated_amount = validate_amount(client, amount, &token, &None); // Finally, exchange the balance to the transaction's epoch let computed_amount = self .compute_exchanged_amount( - client.clone(), + client, amount, ep, HashMap::new(), @@ -1322,8 +1326,8 @@ impl ShieldedContext { let (asset_types, amount) = convert_amount( epoch, &args.token, - &args.sub_prefix, - &amt.amount + &args.sub_prefix.as_ref(), + amt.amount.clone() ); // Transactions with transparent input and shielded output @@ -1471,13 +1475,16 @@ impl ShieldedContext { )?; } + let token = token_exists_or_err(args.token.clone(), args.tx.force, client).await?; + let validated_amount = validate_amount(client, args.amount, &token, &None); + let (new_asset_type, _) = - convert_amount(new_epoch, &args.token, args.amount); + convert_amount(new_epoch, &args.token, &None, validated_amount); replay_builder.add_sapling_output( ovk_opt, payment_address.unwrap().into(), new_asset_type, - amt, + validated_amount, memo, )?; diff --git a/shared/src/ledger/tx.rs b/shared/src/ledger/tx.rs index 774fe5b405..0e4403b2fb 100644 --- a/shared/src/ledger/tx.rs +++ b/shared/src/ledger/tx.rs @@ -61,7 +61,7 @@ pub enum Error { TxBroadcast(RpcError), /// Invalid comission rate set #[error("Invalid new commission rate, received {0}")] - InvalidCommisionRate(Decimal), + InvalidCommisionRate(Dec), /// Invalid validator address #[error("The address {0} doesn't belong to any known validator account.")] InvalidValidatorAddress(Address), @@ -70,7 +70,7 @@ pub enum Error { "New rate, {0}, is too large of a change with respect to the \ predecessor epoch in which the rate will take effect." )] - TooLargeOfChange(Decimal), + TooLargeOfChange(Dec), /// Error retrieving from storage #[error("Error retrieving from storage")] Retrival, @@ -89,15 +89,15 @@ pub enum Error { /// Lower bond amount than the unbond #[error( "The total bonds of the source {0} is lower than the amount to be \ - unbonded. Amount to unbond is {1.to_string_native()} and the total bonds is {2.to_string_native()}." + unbonded. Amount to unbond is {1} and the total bonds is {2}." )] - LowerBondThanUnbond(Address, token::Amount, token::Amount), + LowerBondThanUnbond(Address, String, String), /// Balance is too low #[error( "The balance of the source {0} of token {1} is lower than the amount \ - to be transferred. Amount to transfer is {2.to_string_native()} and the balance is {3.to_string_native()}." + to be transferred. Amount to transfer is {2} and the balance is {3}." )] - BalanceTooLow(Address, Address, token::Amount, token::Amount), + BalanceTooLow(Address, Address, String, String), /// Token Address does not exist on chain #[error("The token address {0} doesn't exist on chain.")] TokenDoesNotExist(Address), @@ -116,14 +116,14 @@ pub enum Error { /// Negative balance after transfer #[error( "The balance of the source {0} is lower than the amount to be \ - transferred and fees. Amount to transfer is {1.to_string_native()} {2} and fees are {3.to_string_native()} \ + transferred and fees. Amount to transfer is {1} {2} and fees are {3} \ {4}." )] NegativeBalanceAfterTransfer( Address, - token::Amount, + String, Address, - token::Amount, + String, Address, ), /// No Balance found for token @@ -554,7 +554,7 @@ pub async fn submit_validator_commission_change< commission_rate, max_commission_change_per_epoch, }) => { - if args.rate.abs_diff(&comission_rate) + if args.rate.abs_diff(&commission_rate) > max_commission_change_per_epoch { eprintln!( From bcfcd32c6fb3353c264b57c46ff2dd9fc4ff8c38 Mon Sep 17 00:00:00 2001 From: "Raymond E. Pasco" Date: Sun, 21 May 2023 10:46:14 -0400 Subject: [PATCH 051/151] fixup! Merge remote-tracking branch 'origin/base' into HEAD --- apps/src/lib/cli.rs | 26 +++-- apps/src/lib/client/rpc.rs | 13 +-- core/src/ledger/ibc/context/transfer_mod.rs | 12 ++- core/src/types/token.rs | 2 +- proof_of_stake/src/tests.rs | 2 +- shared/src/ledger/args.rs | 1 - shared/src/ledger/ibc/vp/context.rs | 4 +- shared/src/ledger/ibc/vp/mod.rs | 3 +- shared/src/ledger/masp.rs | 114 ++++++++++---------- shared/src/ledger/rpc.rs | 20 ++-- shared/src/ledger/tx.rs | 41 +++---- 11 files changed, 128 insertions(+), 110 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 3ec1b275d1..4a77ee2dee 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -1726,16 +1726,20 @@ pub mod args { pub const EXPIRATION_OPT: ArgOpt = arg_opt("expiration"); pub const FORCE: ArgFlag = flag("force"); pub const DONT_PREFETCH_WASM: ArgFlag = flag("dont-prefetch-wasm"); - pub const GAS_AMOUNT: ArgDefault = - arg_default("gas-amount", DefaultFn(|| token::DenominatedAmount { + pub const GAS_AMOUNT: ArgDefault = arg_default( + "gas-amount", + DefaultFn(|| token::DenominatedAmount { amount: token::Amount::default(), denom: NATIVE_MAX_DECIMAL_PLACES.into(), - })); - pub const GAS_LIMIT: ArgDefault = - arg_default("gas-limit", DefaultFn(|| token::DenominatedAmount { + }), + ); + pub const GAS_LIMIT: ArgDefault = arg_default( + "gas-limit", + DefaultFn(|| token::DenominatedAmount { amount: token::Amount::default(), denom: NATIVE_MAX_DECIMAL_PLACES.into(), - })); + }), + ); pub const GAS_TOKEN: ArgDefaultFromCtx = arg_default_from_ctx("gas-token", DefaultFn(|| "NAM".parse().unwrap())); pub const GENESIS_PATH: Arg = arg("genesis-path"); @@ -2056,10 +2060,12 @@ pub mod args { fn to_sdk(self, ctx: &mut Context) -> TxTransfer { TxTransfer:: { tx: match self.tx { - InputAmount::Validated(tx) => - InputAmount::Validated(tx.to_sdk(ctx)), - InputAmount::Unvalidated(tx) => - InputAmount::Unvalidated(tx.to_sdk(ctx)), + InputAmount::Validated(tx) => { + InputAmount::Validated(tx.to_sdk(ctx)) + } + InputAmount::Unvalidated(tx) => { + InputAmount::Unvalidated(tx.to_sdk(ctx)) + } }, source: ctx.get_cached(&self.source), target: ctx.get(&self.target), diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index e4d813130b..62109e033b 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -305,9 +305,7 @@ pub async fn query_transparent_balance< ), }; let token_alias = lookup_alias(wallet, &token); - match query_storage_value::(&client, &key) - .await - { + match query_storage_value::(&client, &key).await { Some(balance) => { let balance = format_denominated_amount( &client, @@ -365,7 +363,8 @@ pub async fn query_transparent_balance< query_storage_prefix::(client, &key) .await; if let Some(balances) = balances { - print_balances(wallet, &client, balances, &token, None).await; + print_balances(wallet, &client, balances, &token, None) + .await; } } } @@ -522,11 +521,7 @@ pub async fn query_pinned_balance< } } - - -async fn print_balances< - C: namada::ledger::queries::Client + Sync, ->( +async fn print_balances( client: &C, wallet: &Wallet, balances: impl Iterator, diff --git a/core/src/ledger/ibc/context/transfer_mod.rs b/core/src/ledger/ibc/context/transfer_mod.rs index f0d6025569..ad0aa75800 100644 --- a/core/src/ledger/ibc/context/transfer_mod.rs +++ b/core/src/ledger/ibc/context/transfer_mod.rs @@ -480,7 +480,9 @@ where ChannelError::Other { description: format!( "Sending a coin failed: from {}, to {}, amount {}", - src, dest, amount.to_string_native() + src, + dest, + amount.to_string_native() ), }, )) @@ -517,7 +519,9 @@ where ChannelError::Other { description: format!( "Sending a coin failed: from {}, to {}, amount {}", - src, dest, amount.to_string_native() + src, + dest, + amount.to_string_native() ), }, )) @@ -554,7 +558,9 @@ where ChannelError::Other { description: format!( "Sending a coin failed: from {}, to {}, amount {}", - src, dest, amount.to_string_native() + src, + dest, + amount.to_string_native() ), }, )) diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 5bbdcebf6e..fe03df0eff 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -11,9 +11,9 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use super::dec::POS_DECIMAL_PRECISION; +use crate::ibc::applications::transfer::Amount as IbcAmount; use crate::ledger::storage_api::token::read_denom; use crate::ledger::storage_api::StorageRead; -use crate::ibc::applications::transfer::Amount as IbcAmount; use crate::types::address::{masp, Address, DecodeError as AddressError}; use crate::types::dec::Dec; use crate::types::storage; diff --git a/proof_of_stake/src/tests.rs b/proof_of_stake/src/tests.rs index d1532a772b..13ceab1cf5 100644 --- a/proof_of_stake/src/tests.rs +++ b/proof_of_stake/src/tests.rs @@ -453,7 +453,7 @@ fn test_bonds_aux(params: PosParams, validators: Vec) { // Unbond the self-bond with an amount that will remove all of the self-bond // executed after genesis and some of the genesis bond let amount_self_unbond: token::Amount = - amount_self_bond + (u64::from(validator.tokens) / 2); + amount_self_bond + (validator.tokens / 2); // When the difference is 0, only the non-genesis self-bond is unbonded let unbonded_genesis_self_bond = amount_self_unbond - amount_self_bond != token::Amount::default(); diff --git a/shared/src/ledger/args.rs b/shared/src/ledger/args.rs index ef61dad92c..0467befae4 100644 --- a/shared/src/ledger/args.rs +++ b/shared/src/ledger/args.rs @@ -111,7 +111,6 @@ pub enum InputAmount { Unvalidated(token::DenominatedAmount), } - /// IBC transfer transaction arguments #[derive(Clone, Debug)] pub struct TxIbcTransfer { diff --git a/shared/src/ledger/ibc/vp/context.rs b/shared/src/ledger/ibc/vp/context.rs index 4421f21e75..9f6b32c1b7 100644 --- a/shared/src/ledger/ibc/vp/context.rs +++ b/shared/src/ledger/ibc/vp/context.rs @@ -121,7 +121,9 @@ where ) -> Result<(), Self::Error> { let src_owner = is_any_token_balance_key(src); let mut src_bal = match src_owner { - Some([_, Address::Internal(InternalAddress::IbcMint)]) => Amount::max(), + Some([_, Address::Internal(InternalAddress::IbcMint)]) => { + Amount::max() + } Some([_, Address::Internal(InternalAddress::IbcBurn)]) => { unreachable!("Invalid transfer from IBC burn address") } diff --git a/shared/src/ledger/ibc/vp/mod.rs b/shared/src/ledger/ibc/vp/mod.rs index d491f63a03..8db8a5bd1f 100644 --- a/shared/src/ledger/ibc/vp/mod.rs +++ b/shared/src/ledger/ibc/vp/mod.rs @@ -19,10 +19,10 @@ use namada_core::ledger::storage::write_log::StorageModification; use namada_core::ledger::storage::{self as ledger_storage, StorageHasher}; use namada_core::proto::SignedTxData; use namada_core::types::address::{Address, InternalAddress}; +use namada_core::types::dec::Dec; use namada_core::types::storage::Key; use namada_proof_of_stake::read_pos_params; use thiserror::Error; -use namada_core::types::dec::Dec; pub use token::{Error as IbcTokenError, IbcToken}; use crate::ledger::native_vp::{self, Ctx, NativeVp, VpEnv}; @@ -227,7 +227,6 @@ pub fn get_dummy_header() -> crate::types::storage::Header { #[cfg(any(feature = "test", feature = "testing"))] pub fn get_dummy_genesis_validator() -> namada_proof_of_stake::types::GenesisValidator { - use crate::core::types::address::testing::established_address_1; use crate::types::key::testing::common_sk_from_simple_seed; use crate::types::token::Amount; diff --git a/shared/src/ledger/masp.rs b/shared/src/ledger/masp.rs index 89b751d1ab..8cfecdf8eb 100644 --- a/shared/src/ledger/masp.rs +++ b/shared/src/ledger/masp.rs @@ -52,10 +52,11 @@ use rand_core::{CryptoRng, OsRng, RngCore}; #[cfg(feature = "masp-tx-gen")] use sha2::Digest; -use crate::ledger::queries::Client; -use crate::ledger::{args, rpc}; use crate::ledger::args::InputAmount; +use crate::ledger::queries::Client; use crate::ledger::rpc::query_conversion; +use crate::ledger::tx::token_exists_or_err; +use crate::ledger::{args, rpc}; use crate::proto::{SignedTxData, Tx}; use crate::tendermint_rpc::query::Query; use crate::tendermint_rpc::Order; @@ -423,7 +424,7 @@ pub type Conversions = pub type TransferDelta = HashMap; /// Represents the changes that were made to a list of shielded accounts -pub type TransactionDelta = HashMap; +pub type TransactionDelta = HashMap; /// Represents the current state of the shielded pool from the perspective of /// the chosen viewing keys. @@ -456,7 +457,8 @@ 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, MaspDenom, Epoch)>, + pub asset_types: + HashMap, MaspDenom, Epoch)>, /// Maps note positions to their corresponding viewing keys pub vk_map: HashMap, } @@ -776,7 +778,11 @@ impl ShieldedContext { /// Compute the total unspent notes associated with the viewing key in the /// context. If the key is not in the context, then we do not know the /// balance and hence we return None. - pub async fn compute_shielded_balance(&self, client: &U::C, vk: &ViewingKey) -> Option { + pub async fn compute_shielded_balance( + &self, + client: &U::C, + vk: &ViewingKey, + ) -> Option { // Cannot query the balance of a key that's not in the map if !self.pos_map.contains_key(vk) { return None; @@ -934,7 +940,7 @@ impl ShieldedContext { pub async fn compute_exchanged_amount( &mut self, client: &U::C, - mut input: Amount, + mut input: MaspAmount, target_epoch: Epoch, mut conversions: Conversions, ) -> (Amount, Conversions) { @@ -1187,23 +1193,15 @@ impl ShieldedContext { client: &U::C, owner: PaymentAddress, viewing_key: &ViewingKey, - ) -> Result<(Amount, Epoch), PinnedBalanceError> { + ) -> Result<(MaspAmount, Epoch), PinnedBalanceError> { // Obtain the balance that will be exchanged let (amt, ep) = - Self::compute_pinned_balance(client, owner, viewing_key) - .await?; + Self::compute_pinned_balance(client, owner, viewing_key).await?; // Establish connection with which to do exchange rate queries let amount = self.decode_all_amounts(&client, amt).await; - let token = token_exists_or_err(args.token.clone(), args.tx.force, client).await?; - let validated_amount = validate_amount(client, amount, &token, &None); // Finally, exchange the balance to the transaction's epoch let computed_amount = self - .compute_exchanged_amount( - client, - amount, - ep, - HashMap::new(), - ) + .compute_exchanged_amount(client, amount, ep, HashMap::new()) .await .0; Ok((self.decode_all_amounts(&client, computed_amount).await, ep)) @@ -1327,7 +1325,7 @@ impl ShieldedContext { epoch, &args.token, &args.sub_prefix.as_ref(), - amt.amount.clone() + amt.amount.clone(), ); // Transactions with transparent input and shielded output @@ -1399,7 +1397,8 @@ impl ShieldedContext { let script = TransparentAddress::PublicKey(hash.into()).script(); epoch_sensitive = true; - for (denom, asset_type) in MaspDenom::iter().zip(asset_types.iter()) { + for (denom, asset_type) in MaspDenom::iter().zip(asset_types.iter()) + { builder.add_transparent_input( secp_sk, OutPoint::new([0u8; 32], 0), @@ -1412,12 +1411,12 @@ impl ShieldedContext { } } - // Now handle the outputs of this transaction // If there is a shielded output if let Some(pa) = payment_address { let ovk_opt = spending_key.map(|x| x.expsk.ovk); - for (denom, asset_type) in MaspDenom::iter().zip(asset_types.iter()) { + for (denom, asset_type) in MaspDenom::iter().zip(asset_types.iter()) + { builder.add_sapling_output( ovk_opt, pa.into(), @@ -1439,7 +1438,8 @@ impl ShieldedContext { let hash = ripemd160::Ripemd160::digest( sha2::Sha256::digest(target_enc.as_ref()).as_slice(), ); - for (denom, asset_type) in MaspDenom::iter().zip(asset_types.iter()) { + for (denom, asset_type) in MaspDenom::iter().zip(asset_types.iter()) + { builder.add_transparent_output( &TransparentAddress::PublicKey(hash.into()), *asset_type, @@ -1463,30 +1463,32 @@ impl ShieldedContext { replay_builder.set_fee(Amount::zero())?; let ovk_opt = spending_key.map(|x| x.expsk.ovk); - for (denom, asset_type) in MaspDenom::iter().zip(asset_types.iter()) { - builder.add_transparent_input( - secp_sk, - OutPoint::new([0u8; 32], 0), - TxOut { - asset_type: *asset_type, - value: denom.denominate(&amt), - script_pubkey: script.clone(), - }, - )?; - } - - let token = token_exists_or_err(args.token.clone(), args.tx.force, client).await?; - let validated_amount = validate_amount(client, args.amount, &token, &None); + let token = token_exists_or_err( + args.token.clone(), + args.tx.force, + client, + ) + .await?; + let validated_amount = + validate_amount(client, args.amount, &token, &None); - let (new_asset_type, _) = - convert_amount(new_epoch, &args.token, &None, validated_amount); - replay_builder.add_sapling_output( - ovk_opt, - payment_address.unwrap().into(), - new_asset_type, + let (new_asset_type, _) = convert_amount( + new_epoch, + &args.token, + &None, validated_amount, - memo, - )?; + ); + for (denom, asset_type) in + MaspDenom::iter().zip(new_asset_type.iter()) + { + replay_builder.add_sapling_output( + ovk_opt, + payment_address.unwrap().into(), + *asset_type, + validated_amount, + memo.clone(), + )?; + } let secp_sk = secp256k1::SecretKey::from_slice(&[0xcd; 32]) .expect("secret key"); @@ -1500,15 +1502,19 @@ impl ShieldedContext { ); let script = TransparentAddress::PublicKey(hash.into()).script(); - replay_builder.add_transparent_input( - secp_sk, - OutPoint::new([0u8; 32], 0), - TxOut { - asset_type: new_asset_type, - value: amt, - script_pubkey: script, - }, - )?; + for (denom, asset_type) in + MaspDenom::iter().zip(asset_types.iter()) + { + replay_builder.add_transparent_input( + secp_sk, + OutPoint::new([0u8; 32], 0), + TxOut { + asset_type: *asset_type, + value: denom.denominate(amt), + script_pubkey: script.clone(), + }, + )?; + } let (replay_tx, _) = replay_builder.build(consensus_branch_id, &prover)?; @@ -1627,7 +1633,6 @@ impl ShieldedContext { }, )]); - // No shielded accounts are affected by this // Transfer transfers.insert( @@ -1721,7 +1726,6 @@ pub fn make_asset_type( AssetType::new(token_bytes.as_ref()).expect("unable to create asset type") } - /// Convert Anoma amount and token type to MASP equivalents fn convert_amount( epoch: Epoch, diff --git a/shared/src/ledger/rpc.rs b/shared/src/ledger/rpc.rs index 51ff0bda80..8a1534e083 100644 --- a/shared/src/ledger/rpc.rs +++ b/shared/src/ledger/rpc.rs @@ -88,14 +88,15 @@ pub async fn query_epoch( /// Query the epoch of the given block height, if it exists. /// Will return none if the input block height is greater than /// the latest committed block height. -pub async fn query_epoch_at_height( +pub async fn query_epoch_at_height( client: &C, height: BlockHeight, ) -> Option { - unwrap_client_response::(RPC.shell().epoch_at_height(client, &height).await) + unwrap_client_response::( + RPC.shell().epoch_at_height(client, &height).await, + ) } - /// Query the last committed block pub async fn query_block( client: &C, @@ -656,8 +657,8 @@ pub async fn get_proposal_votes( let amount: VotePower = get_validator_stake(client, epoch, &voter_address) .await - .try_into() - .expect("Amount of bonds"); + .try_into() + .expect("Amount of bonds"); yay_validators.insert(voter_address, (amount, vote)); } else if !validators.contains(&voter_address) { let validator_address = @@ -792,15 +793,18 @@ pub async fn query_and_print_unbonds< } } if total_withdrawable != token::Amount::default() { - println!("Total withdrawable now: {}.", total_withdrawable.to_string_native()); + println!( + "Total withdrawable now: {}.", + total_withdrawable.to_string_native() + ); } if !not_yet_withdrawable.is_empty() { println!("Current epoch: {current_epoch}.") } for (withdraw_epoch, amount) in not_yet_withdrawable { println!( - "Amount {} withdrawable starting from epoch \ - {withdraw_epoch}.", amount.to_string_native() + "Amount {} withdrawable starting from epoch {withdraw_epoch}.", + amount.to_string_native() ); } } diff --git a/shared/src/ledger/tx.rs b/shared/src/ledger/tx.rs index 0e4403b2fb..c7527d1fbb 100644 --- a/shared/src/ledger/tx.rs +++ b/shared/src/ledger/tx.rs @@ -7,6 +7,7 @@ use borsh::BorshSerialize; use itertools::Either::*; use masp_primitives::transaction::builder; use namada_core::types::address::{masp, masp_tx_key, Address}; +use namada_core::types::dec::Dec; use namada_proof_of_stake::parameters::PosParams; use namada_proof_of_stake::types::CommissionPair; use prost::EncodeError; @@ -32,7 +33,6 @@ use crate::tendermint_rpc::endpoint::broadcast::tx_sync::Response; use crate::tendermint_rpc::error::Error as RpcError; use crate::types::key::*; use crate::types::masp::TransferTarget; -use namada_core::types::dec::Dec; use crate::types::storage::{Epoch, RESERVED_ADDRESS_PREFIX}; use crate::types::time::DateTimeUtc; use crate::types::transaction::{pos, InitAccount, UpdateVp}; @@ -119,13 +119,7 @@ pub enum Error { transferred and fees. Amount to transfer is {1} {2} and fees are {3} \ {4}." )] - NegativeBalanceAfterTransfer( - Address, - String, - Address, - String, - Address, - ), + NegativeBalanceAfterTransfer(Address, String, Address, String, Address), /// No Balance found for token #[error("{0}")] MaspError(builder::Error), @@ -690,13 +684,18 @@ pub async fn submit_unbond< let bond_source = source.clone().unwrap_or_else(|| validator.clone()); let bond_amount = rpc::query_bond(client, &bond_source, &validator, None).await; - println!("Bond amount available for unbonding: {} NAM", bond_amount.to_string_native()); + println!( + "Bond amount available for unbonding: {} NAM", + bond_amount.to_string_native() + ); if args.amount > bond_amount { eprintln!( "The total bonds of the source {} is lower than the amount to be \ unbonded. Amount to unbond is {} and the total bonds is {}.", - bond_source, args.amount.to_string_native(), bond_amount.to_string_native() + bond_source, + args.amount.to_string_native(), + bond_amount.to_string_native() ); if !args.tx.force { return Err(Error::LowerBondThanUnbond( @@ -768,21 +767,24 @@ pub async fn submit_unbond< std::cmp::Ordering::Equal => { println!( "Amount {} withdrawable starting from epoch {}", - (latest_withdraw_amount_post - latest_withdraw_amount_pre).to_string_native(), + (latest_withdraw_amount_post - latest_withdraw_amount_pre) + .to_string_native(), latest_withdraw_epoch_post ); } std::cmp::Ordering::Greater => { println!( "Amount {} withdrawable starting from epoch {}", - latest_withdraw_amount_post.to_string_native(), latest_withdraw_epoch_post.to_string() + latest_withdraw_amount_post.to_string_native(), + latest_withdraw_epoch_post.to_string() ); } } } else { println!( "Amount {} withdrawable starting from epoch {}", - latest_withdraw_amount_post.to_string_native(), latest_withdraw_epoch_post.to_string() + latest_withdraw_amount_post.to_string_native(), + latest_withdraw_epoch_post.to_string() ); } @@ -994,7 +996,7 @@ pub async fn submit_transfer< client: &C, wallet: &mut Wallet, shielded: &mut ShieldedContext, - args: args::TxTransfer, + mut args: args::TxTransfer, ) -> Result<(), Error> { // Check that the source address exists on chain let force = args.tx.force; @@ -1041,7 +1043,7 @@ pub async fn submit_transfer< // TODO: Currently multi-tokens cannot be used to pay fees &None, ) - .await; + .await; args.amount = InputAmount::Validated(validated_amount); args.tx.fee_amount = InputAmount::Validated(validate_fee); @@ -1051,7 +1053,6 @@ pub async fn submit_transfer< sub_prefix: sub_prefix.clone(), }; - check_balance_too_low_err( token, &source, @@ -1119,7 +1120,6 @@ pub async fn submit_transfer< Err(err) => Err(Error::MaspError(err)), }?; - let transfer = token::Transfer { source: source.clone(), target, @@ -1381,7 +1381,7 @@ where /// Returns the given token if the given address exists on chain /// otherwise returns an error, force forces the address through even /// if it isn't on chain -async fn token_exists_or_err( +pub async fn token_exists_or_err( token: Address, force: bool, client: &C, @@ -1459,7 +1459,10 @@ async fn check_balance_too_low_err( "The balance of the source {} of token {} is lower \ than the amount to be transferred. Amount to \ transfer is {} and the balance is {}.", - source, token, amount.to_string_native(), balance.to_string_native() + source, + token, + amount.to_string_native(), + balance.to_string_native() ); Ok(()) } else { From d9d26547769bd7ed85566c9232b0d0cafd6a86e2 Mon Sep 17 00:00:00 2001 From: "Raymond E. Pasco" Date: Mon, 22 May 2023 04:36:50 -0400 Subject: [PATCH 052/151] fixup! Merge remote-tracking branch 'origin/base' into HEAD --- apps/src/lib/client/tx.rs | 19 ----------- shared/src/ledger/ibc/vp/mod.rs | 4 +-- shared/src/ledger/masp.rs | 57 ++++++++++++++++++--------------- shared/src/ledger/rpc.rs | 28 +++++++++++++++- shared/src/ledger/tx.rs | 45 +++++++++++++++++++------- 5 files changed, 95 insertions(+), 58 deletions(-) diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index ec554fb519..d28cb5b0c4 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -1061,25 +1061,6 @@ pub async fn submit_tx( tx::submit_tx(client, to_broadcast).await } -fn decode_component( - (addr, sub, denom, epoch): (Address, Option, MaspDenom, Epoch), - val: i64, - res: &mut HashMap, - mk_key: F, -) where - F: FnOnce(Address, Option, Epoch) -> K, - K: Eq + std::hash::Hash, -{ - let decoded_amount = token::Amount::from_uint( - u64::try_from(val).expect("negative cash does not exist"), - denom as u8, - ) - .unwrap(); - res.entry(mk_key(addr, sub, epoch)) - .and_modify(|val| *val += decoded_amount) - .or_insert(decoded_amount); -} - #[cfg(test)] mod test_tx { diff --git a/shared/src/ledger/ibc/vp/mod.rs b/shared/src/ledger/ibc/vp/mod.rs index 8db8a5bd1f..be38a33c3c 100644 --- a/shared/src/ledger/ibc/vp/mod.rs +++ b/shared/src/ledger/ibc/vp/mod.rs @@ -236,8 +236,8 @@ pub fn get_dummy_genesis_validator() let consensus_sk = common_sk_from_simple_seed(0); let consensus_key = consensus_sk.to_public(); - let commission_rate = Dec::new(1, -1); - let max_commission_rate_change = Dec::new(1, -1); + let commission_rate = Dec::new(1, 1).expect("expected 0.1 to be a valid decimal"); + let max_commission_rate_change = Dec::new(1, 1).expect("expected 0.1 to be a valid decimal"); namada_proof_of_stake::types::GenesisValidator { address, tokens, diff --git a/shared/src/ledger/masp.rs b/shared/src/ledger/masp.rs index 8cfecdf8eb..15e6b5d4a8 100644 --- a/shared/src/ledger/masp.rs +++ b/shared/src/ledger/masp.rs @@ -54,8 +54,8 @@ use sha2::Digest; use crate::ledger::args::InputAmount; use crate::ledger::queries::Client; -use crate::ledger::rpc::query_conversion; -use crate::ledger::tx::token_exists_or_err; +use crate::ledger::rpc::{query_conversion, validate_amount}; +use crate::ledger::tx::{decode_component, token_exists_or_err}; use crate::ledger::{args, rpc}; use crate::proto::{SignedTxData, Tx}; use crate::tendermint_rpc::query::Query; @@ -677,7 +677,7 @@ impl ShieldedContext { let mut pos_map = HashMap::new(); std::mem::swap(&mut pos_map, &mut self.pos_map); - for (vk, notes) in self.pos_map.iter_mut() { + for (vk, notes) in pos_map.iter_mut() { let decres = try_sapling_note_decryption::( 0, &vk.ivk().0, @@ -779,10 +779,10 @@ impl ShieldedContext { /// context. If the key is not in the context, then we do not know the /// balance and hence we return None. pub async fn compute_shielded_balance( - &self, + &mut self, client: &U::C, vk: &ViewingKey, - ) -> Option { + ) -> Option { // Cannot query the balance of a key that's not in the map if !self.pos_map.contains_key(vk) { return None; @@ -904,7 +904,7 @@ impl ShieldedContext { asset_type: AssetType, value: i64, usage: &mut i64, - input: &mut Amount, + input: &mut MaspAmount, output: &mut Amount, ) { // If conversion if possible, accumulate the exchanged amount @@ -986,8 +986,8 @@ impl ShieldedContext { // current asset type to the latest // asset type. self.apply_conversion( - &client, conv.clone(), + &client, target_asset_type, denom_value, usage, @@ -1015,8 +1015,8 @@ impl ShieldedContext { // from latest asset type to the target // asset type. self.apply_conversion( - &client, conv.clone(), + &client, target_asset_type, denom_value, usage, @@ -1080,10 +1080,11 @@ impl ShieldedContext { // The amount contributed by this note before conversion let pre_contr = Amount::from_pair(note.asset_type, note.value) .expect("received note has invalid value or asset type"); + let input = self.decode_all_amounts(client, pre_contr).await; let (contr, proposed_convs) = self .compute_exchanged_amount( client, - pre_contr, + input, target_epoch, conversions.clone(), ) @@ -1249,7 +1250,8 @@ impl ShieldedContext { client: &U::C, amt: Amount, ) -> MaspAmount { - let mut res = HashMap::default(); + let mut res: HashMap<(Epoch, TokenAddress), token::Amount> = + HashMap::default(); for (asset_type, val) in amt.components() { // Decode the asset type if let Some(decoded) = @@ -1340,7 +1342,7 @@ impl ShieldedContext { // Transaction fees need to match the amount in the wrapper Transfer // when MASP source is used let (_, shielded_fee) = - convert_amount(epoch, &args.tx.fee_token, &None, &fee.amount); + convert_amount(epoch, &args.tx.fee_token, &None, fee.amount); builder.set_fee(shielded_fee.clone())?; let required_amt = if shielded_gas { @@ -1468,15 +1470,18 @@ impl ShieldedContext { args.tx.force, client, ) - .await?; + .await.expect("expected token to exist"); + let validated_amount = - validate_amount(client, args.amount, &token, &None); + validate_amount(client, args.amount, &token, &None) + .await + .expect("expected to be able to validate amount"); let (new_asset_type, _) = convert_amount( new_epoch, &args.token, &None, - validated_amount, + validated_amount.amount, ); for (denom, asset_type) in MaspDenom::iter().zip(new_asset_type.iter()) @@ -1485,7 +1490,7 @@ impl ShieldedContext { ovk_opt, payment_address.unwrap().into(), *asset_type, - validated_amount, + denom.denominate(&amt), memo.clone(), )?; } @@ -1510,21 +1515,23 @@ impl ShieldedContext { OutPoint::new([0u8; 32], 0), TxOut { asset_type: *asset_type, - value: denom.denominate(amt), + value: denom.denominate(&amt), script_pubkey: script.clone(), }, )?; + + let (replay_tx, _) = + replay_builder.build(consensus_branch_id, &prover)?; + tx = tx.map(|(t, tm)| { + let mut temp = t.deref().clone(); + temp.shielded_outputs = replay_tx.shielded_outputs.clone(); + temp.value_balance = temp.value_balance.reject(*asset_type) + - Amount::from_pair(new_asset_type, denom.denominate(&amt)).unwrap(); + (temp.freeze().unwrap(), tm) + }); } - let (replay_tx, _) = - replay_builder.build(consensus_branch_id, &prover)?; - tx = tx.map(|(t, tm)| { - let mut temp = t.deref().clone(); - temp.shielded_outputs = replay_tx.shielded_outputs.clone(); - temp.value_balance = temp.value_balance.reject(asset_type) - - Amount::from_pair(new_asset_type, amt).unwrap(); - (temp.freeze().unwrap(), tm) - }); + } } diff --git a/shared/src/ledger/rpc.rs b/shared/src/ledger/rpc.rs index 8a1534e083..a1f76e3af9 100644 --- a/shared/src/ledger/rpc.rs +++ b/shared/src/ledger/rpc.rs @@ -8,11 +8,12 @@ use masp_primitives::sapling::Node; use namada_core::ledger::testnet_pow; use namada_core::types::address::Address; use namada_core::types::storage::Key; -use namada_core::types::token::{Amount, MaspDenom}; +use namada_core::types::token::{Amount, Denomination, MaspDenom}; use namada_proof_of_stake::types::CommissionPair; use serde::Serialize; use tokio::time::Duration; +use crate::ledger::args::InputAmount; use crate::ledger::events::Event; use crate::ledger::governance::parameters::GovParams; use crate::ledger::governance::storage as gov_storage; @@ -904,3 +905,28 @@ pub async fn get_bond_amount_at( ); Some(total_active) } + +/// Get the correct representation of the amount given the token type. +pub async fn validate_amount( + client: &C, + amount: InputAmount, + token: &Address, + sub_prefix: &Option, +) -> Option { + let input_amount = match amount { + InputAmount::Unvalidated(amt) => amt.canonical(), + InputAmount::Validated(amt) => return Some(amt), + }; + let denom = unwrap_client_response::>( + RPC.vp() + .token() + .denomination(client, token, sub_prefix) + .await, + ) + .unwrap(); + if denom < input_amount.denom { + None + } else { + Some(input_amount.increase_precision(denom).unwrap()) + } +} diff --git a/shared/src/ledger/tx.rs b/shared/src/ledger/tx.rs index c7527d1fbb..4b929041ea 100644 --- a/shared/src/ledger/tx.rs +++ b/shared/src/ledger/tx.rs @@ -1,6 +1,6 @@ //! SDK functions to construct different types of transactions use std::borrow::Cow; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashMap}; use std::str::FromStr; use borsh::BorshSerialize; @@ -8,6 +8,8 @@ use itertools::Either::*; use masp_primitives::transaction::builder; use namada_core::types::address::{masp, masp_tx_key, Address}; use namada_core::types::dec::Dec; +use namada_core::types::storage::Key; +use namada_core::types::token::{MaspDenom, TokenAddress}; use namada_proof_of_stake::parameters::PosParams; use namada_proof_of_stake::types::CommissionPair; use prost::EncodeError; @@ -25,7 +27,7 @@ use crate::ibc_proto::cosmos::base::v1beta1::Coin; use crate::ledger::args::{self, InputAmount}; use crate::ledger::governance::storage as gov_storage; use crate::ledger::masp::{ShieldedContext, ShieldedUtils}; -use crate::ledger::rpc::{self, TxBroadcastData, TxResponse}; +use crate::ledger::rpc::{self, validate_amount, TxBroadcastData, TxResponse}; use crate::ledger::signing::{find_keypair, sign_tx, tx_signer, TxSigningKey}; use crate::ledger::wallet::{Wallet, WalletUtils}; use crate::proto::Tx; @@ -449,6 +451,25 @@ pub async fn submit_tx( parsed } +pub fn decode_component( + (addr, sub, denom, epoch): (Address, Option, MaspDenom, Epoch), + val: i64, + res: &mut HashMap, + mk_key: F, +) where + F: FnOnce(Address, Option, Epoch) -> K, + K: Eq + std::hash::Hash, +{ + let decoded_amount = token::Amount::from_uint( + u64::try_from(val).expect("negative cash does not exist"), + denom as u8, + ) + .unwrap(); + res.entry(mk_key(addr, sub, epoch)) + .and_modify(|val| *val += decoded_amount) + .or_insert(decoded_amount); +} + /// Save accounts initialized from a tx into the wallet, if any. pub async fn save_initialized_accounts( wallet: &mut Wallet, @@ -700,8 +721,8 @@ pub async fn submit_unbond< if !args.tx.force { return Err(Error::LowerBondThanUnbond( bond_source, - args.amount, - bond_amount, + args.amount.to_string_native(), + bond_amount.to_string_native(), )); } } @@ -1035,15 +1056,17 @@ pub async fn submit_transfer< // validate the amount given let validated_amount = - validate_amount(&client, args.amount, &token, &sub_prefix).await; + validate_amount(client, args.amount, &token, &sub_prefix) + .await + .expect("expected to validate amount"); let validate_fee = validate_amount( - &client, + client, args.tx.fee_amount, &fee_token, // TODO: Currently multi-tokens cannot be used to pay fees &None, ) - .await; + .await.expect("expected to be able to validate fee"); args.amount = InputAmount::Validated(validated_amount); args.tx.fee_amount = InputAmount::Validated(validate_fee); @@ -1111,9 +1134,9 @@ pub async fn submit_transfer< Err(builder::Error::ChangeIsNegative(_)) => { Err(Error::NegativeBalanceAfterTransfer( source.clone(), - validated_amount.amount, + validated_amount.amount.to_string_native(), token.clone(), - validate_fee.amount, + validate_fee.amount.to_string_native(), args.tx.fee_token.clone(), )) } @@ -1469,8 +1492,8 @@ async fn check_balance_too_low_err( Err(Error::BalanceTooLow( source.clone(), token.clone(), - amount, - balance, + amount.to_string_native(), + balance.to_string_native(), )) } } else { From eb81893e062a75ee59b83d2bd4714e1f04fc9115 Mon Sep 17 00:00:00 2001 From: "Raymond E. Pasco" Date: Mon, 22 May 2023 05:00:10 -0400 Subject: [PATCH 053/151] fixup! Merge remote-tracking branch 'origin/base' into HEAD --- shared/src/ledger/ibc/vp/mod.rs | 6 ++++-- shared/src/ledger/masp.rs | 33 +++++++++++++++++++++++---------- shared/src/ledger/tx.rs | 15 ++++++--------- tests/src/vm_host_env/ibc.rs | 2 +- 4 files changed, 34 insertions(+), 22 deletions(-) diff --git a/shared/src/ledger/ibc/vp/mod.rs b/shared/src/ledger/ibc/vp/mod.rs index be38a33c3c..5057b19763 100644 --- a/shared/src/ledger/ibc/vp/mod.rs +++ b/shared/src/ledger/ibc/vp/mod.rs @@ -236,8 +236,10 @@ pub fn get_dummy_genesis_validator() let consensus_sk = common_sk_from_simple_seed(0); let consensus_key = consensus_sk.to_public(); - let commission_rate = Dec::new(1, 1).expect("expected 0.1 to be a valid decimal"); - let max_commission_rate_change = Dec::new(1, 1).expect("expected 0.1 to be a valid decimal"); + let commission_rate = + Dec::new(1, 1).expect("expected 0.1 to be a valid decimal"); + let max_commission_rate_change = + Dec::new(1, 1).expect("expected 0.1 to be a valid decimal"); namada_proof_of_stake::types::GenesisValidator { address, tokens, diff --git a/shared/src/ledger/masp.rs b/shared/src/ledger/masp.rs index 15e6b5d4a8..b40c909274 100644 --- a/shared/src/ledger/masp.rs +++ b/shared/src/ledger/masp.rs @@ -333,12 +333,16 @@ pub enum PinnedBalanceError { // pub amount: token::Amount, // } +/// a masp change #[derive(BorshSerialize, BorshDeserialize, Debug, Clone)] pub struct MaspChange { + /// the token address pub asset: TokenAddress, + /// the change in the token pub change: token::Change, } +/// a masp amount #[derive( BorshSerialize, BorshDeserialize, Debug, Clone, Default, PartialEq, Eq, )] @@ -1315,7 +1319,6 @@ impl ShieldedContext { // Now we build up the transaction within this object let mut builder = Builder::::new(0u32); - let prover = self.utils.local_tx_prover(); // break up a transfer into a number of transfers with suitable // denominations @@ -1470,7 +1473,8 @@ impl ShieldedContext { args.tx.force, client, ) - .await.expect("expected token to exist"); + .await + .expect("expected token to exist"); let validated_amount = validate_amount(client, args.amount, &token, &None) @@ -1508,7 +1512,7 @@ impl ShieldedContext { let script = TransparentAddress::PublicKey(hash.into()).script(); for (denom, asset_type) in - MaspDenom::iter().zip(asset_types.iter()) + MaspDenom::iter().zip(new_asset_type.iter()) { replay_builder.add_transparent_input( secp_sk, @@ -1519,19 +1523,28 @@ impl ShieldedContext { script_pubkey: script.clone(), }, )?; + } + + let (replay_tx, _) = + replay_builder.build(consensus_branch_id, &prover)?; - let (replay_tx, _) = - replay_builder.build(consensus_branch_id, &prover)?; + for (denom, asset_type) in + MaspDenom::iter().zip(new_asset_type.iter()) + { tx = tx.map(|(t, tm)| { let mut temp = t.deref().clone(); - temp.shielded_outputs = replay_tx.shielded_outputs.clone(); - temp.value_balance = temp.value_balance.reject(*asset_type) - - Amount::from_pair(new_asset_type, denom.denominate(&amt)).unwrap(); + temp.shielded_outputs = + replay_tx.shielded_outputs.clone(); + temp.value_balance = + temp.value_balance.reject(*asset_type) + - Amount::from_pair( + *asset_type, + denom.denominate(&amt), + ) + .unwrap(); (temp.freeze().unwrap(), tm) }); } - - } } diff --git a/shared/src/ledger/tx.rs b/shared/src/ledger/tx.rs index 4b929041ea..8206a835cd 100644 --- a/shared/src/ledger/tx.rs +++ b/shared/src/ledger/tx.rs @@ -9,7 +9,7 @@ use masp_primitives::transaction::builder; use namada_core::types::address::{masp, masp_tx_key, Address}; use namada_core::types::dec::Dec; use namada_core::types::storage::Key; -use namada_core::types::token::{MaspDenom, TokenAddress}; +use namada_core::types::token::MaspDenom; use namada_proof_of_stake::parameters::PosParams; use namada_proof_of_stake::types::CommissionPair; use prost::EncodeError; @@ -451,6 +451,7 @@ pub async fn submit_tx( parsed } +/// decode components of a masp note pub fn decode_component( (addr, sub, denom, epoch): (Address, Option, MaspDenom, Epoch), val: i64, @@ -1062,20 +1063,16 @@ pub async fn submit_transfer< let validate_fee = validate_amount( client, args.tx.fee_amount, - &fee_token, + &args.tx.fee_token, // TODO: Currently multi-tokens cannot be used to pay fees &None, ) - .await.expect("expected to be able to validate fee"); + .await + .expect("expected to be able to validate fee"); args.amount = InputAmount::Validated(validated_amount); args.tx.fee_amount = InputAmount::Validated(validate_fee); - let token_addr = TokenAddress { - address: token.clone(), - sub_prefix: sub_prefix.clone(), - }; - check_balance_too_low_err( token, &source, @@ -1092,7 +1089,7 @@ pub async fn submit_transfer< // signer. Also, if the transaction is shielded, redact the amount and token // types by setting the transparent value to 0 and token type to a constant. // This has no side-effect because transaction is to self. - let (default_signer, amount, token) = + let (default_signer, _amount, token) = if source == masp_addr && target == masp_addr { // TODO Refactor me, we shouldn't rely on any specific token here. ( diff --git a/tests/src/vm_host_env/ibc.rs b/tests/src/vm_host_env/ibc.rs index 47ff3808fd..5efd407ed0 100644 --- a/tests/src/vm_host_env/ibc.rs +++ b/tests/src/vm_host_env/ibc.rs @@ -238,7 +238,7 @@ pub fn init_storage() -> (Address, Address) { // initialize an account let account = tx::ctx().init_account(code_hash).unwrap(); let key = token::balance_key(&token, &account); - let init_bal = Amount::whole(100); + let init_bal = Amount::native_whole(100); let bytes = init_bal.try_to_vec().expect("encoding failed"); tx_host_env::with(|env| { env.wl_storage.storage.write(&key, &bytes).unwrap(); From d481cfa65624976e88d8cff7663ca5f2c176602d Mon Sep 17 00:00:00 2001 From: "Raymond E. Pasco" Date: Mon, 22 May 2023 05:37:39 -0400 Subject: [PATCH 054/151] compiles --- Cargo.lock | 1 + apps/Cargo.toml | 1 + apps/src/lib/cli.rs | 10 +--- apps/src/lib/client/rpc.rs | 76 ++++++++++++++-------------- apps/src/lib/client/tx.rs | 10 ++-- shared/src/ledger/masp.rs | 2 +- tests/src/storage_api/testnet_pow.rs | 2 +- tests/src/vm_host_env/mod.rs | 24 ++++----- 8 files changed, 61 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ce77e5d893..40a99a5094 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4070,6 +4070,7 @@ dependencies = [ "config", "data-encoding", "derivative", + "directories", "ed25519-consensus", "eyre", "ferveo", diff --git a/apps/Cargo.toml b/apps/Cargo.toml index 9acb00b424..450e55d59c 100644 --- a/apps/Cargo.toml +++ b/apps/Cargo.toml @@ -90,6 +90,7 @@ color-eyre = "0.5.10" config = "0.11.0" data-encoding = "2.3.2" derivative = "2.2.0" +directories = "4.0.1" ed25519-consensus = "1.2.0" ferveo = {git = "https://github.com/anoma/ferveo", rev = "e5abd0acc938da90140351a65a26472eb495ce4d"} ferveo-common = {git = "https://github.com/anoma/ferveo", rev = "e5abd0acc938da90140351a65a26472eb495ce4d"} diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 4a77ee2dee..e782d24dd8 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -1663,7 +1663,6 @@ pub mod args { use namada::types::time::DateTimeUtc; use namada::types::token; use namada::types::token::NATIVE_MAX_DECIMAL_PLACES; - use namada::types::transaction::GasLimit; use super::context::*; use super::utils::*; @@ -2059,14 +2058,7 @@ pub mod args { impl CliToSdk> for TxTransfer { fn to_sdk(self, ctx: &mut Context) -> TxTransfer { TxTransfer:: { - tx: match self.tx { - InputAmount::Validated(tx) => { - InputAmount::Validated(tx.to_sdk(ctx)) - } - InputAmount::Unvalidated(tx) => { - InputAmount::Unvalidated(tx.to_sdk(ctx)) - } - }, + tx: self.tx.to_sdk(ctx), source: ctx.get_cached(&self.source), target: ctx.get(&self.target), token: ctx.get(&self.token), diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 62109e033b..871ba01305 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -20,12 +20,11 @@ use masp_primitives::primitives::ViewingKey; use masp_primitives::sapling::Node; use masp_primitives::zip32::ExtendedFullViewingKey; use namada::core::types::transaction::governance::ProposalType; +use namada::ledger::args::InputAmount; use namada::ledger::events::Event; use namada::ledger::governance::parameters::GovParams; use namada::ledger::governance::storage as gov_storage; -use namada::ledger::masp::{ - Conversions, PinnedBalanceError, ShieldedContext, ShieldedUtils, -}; +use namada::ledger::masp::{Conversions, MaspAmount, MaspChange, PinnedBalanceError, ShieldedContext, ShieldedUtils}; use namada::ledger::native_vp::governance::utils::{self, Votes}; use namada::ledger::parameters::{storage as param_storage, EpochDuration}; use namada::ledger::pos::{ @@ -45,6 +44,7 @@ use namada::types::key::*; use namada::types::masp::{BalanceOwner, ExtendedViewingKey, PaymentAddress}; use namada::types::storage::{BlockHeight, BlockResults, Epoch, Key, KeySeg}; use namada::types::{storage, token}; +use namada::types::token::{Change, DenominatedAmount, Denomination, MaspDenom, TokenAddress}; use crate::cli::{self, args}; use crate::facade::tendermint::merkle::proof::Proof; @@ -102,7 +102,7 @@ pub async fn query_transfers< args: args::QueryTransfers, ) { let query_token = args.token; - let sub_prefix = args.sub_prefix; + let sub_prefix = args.sub_prefix.map(|s| Key::parse(s).unwrap()); let query_owner = args.owner.map_or_else( || Either::Right(wallet.get_addresses().into_values().collect()), Either::Left, @@ -181,7 +181,7 @@ pub async fn query_transfers< for (account, MaspChange { ref asset, change }) in tfer_delta { if account != masp() { print!(" {}:", account); - let token_alias = lookup_alias(&ctx, &asset.address); + let token_alias = lookup_alias(wallet, &asset.address); let sign = match change.cmp(&Change::zero()) { Ordering::Greater => "+", Ordering::Less => "-", @@ -190,7 +190,7 @@ pub async fn query_transfers< print!( " {}{} {}", sign, - format_denominated_amount(&client, asset, change.into(),) + format_denominated_amount(client, asset, change.into(),) .await, asset.format_with_alias(&token_alias) ); @@ -213,7 +213,7 @@ pub async fn query_transfers< print!( " {}{} {}", sign, - format_denominated_amount(&client, &token_addr, val,) + format_denominated_amount(client, &token_addr, val,) .await, token_addr.format_with_alias(&token_alias), ); @@ -286,7 +286,7 @@ pub async fn query_transparent_balance< let tokens = wallet.get_addresses_with_vp_type(AddressVpType::Token); match (args.token, args.owner) { (Some(token), Some(owner)) => { - let (balance_key, sub_prefix) = match &args.sub_prefix { + let (_balance_key, sub_prefix) = match &args.sub_prefix { Some(sub_prefix) => { let sub_prefix = Key::parse(sub_prefix).unwrap(); let prefix = @@ -305,10 +305,11 @@ pub async fn query_transparent_balance< ), }; let token_alias = lookup_alias(wallet, &token); + let key = token::balance_key(&token, &owner.address().unwrap()); match query_storage_value::(&client, &key).await { Some(balance) => { let balance = format_denominated_amount( - &client, + client, &TokenAddress { address: token, sub_prefix, @@ -339,6 +340,7 @@ pub async fn query_transparent_balance< .await; if let Some(balances) = balances { print_balances( + client, wallet, balances, &token, @@ -353,7 +355,7 @@ pub async fn query_transparent_balance< let balances = query_storage_prefix::(client, &prefix).await; if let Some(balances) = balances { - print_balances(wallet, &client, balances, &token, None).await; + print_balances(client, wallet, balances, &token, None).await; } } (None, None) => { @@ -363,7 +365,7 @@ pub async fn query_transparent_balance< query_storage_prefix::(client, &key) .await; if let Some(balances) = balances { - print_balances(wallet, &client, balances, &token, None) + print_balances(client, wallet, balances, &token, None) .await; } } @@ -443,7 +445,7 @@ pub async fn query_pinned_balance< println!("Payment address {} has not yet been consumed.", owner) } (Ok((balance, epoch)), Some(token), sub_prefix) => { - let token_alias = lookup_alias(client, &token); + let token_alias = lookup_alias(wallet, &token); let token_address = TokenAddress { address: token.clone(), @@ -463,7 +465,7 @@ pub async fn query_pinned_balance< ); } else { let formatted = format_denominated_amount( - &client, + client, &token_address, total_balance.into(), ) @@ -494,7 +496,7 @@ pub async fn query_pinned_balance< found_any = true; } let formatted = format_denominated_amount( - &client, + client, token_addr, (*value).into(), ) @@ -550,7 +552,7 @@ async fn print_balances( balance ) .await, - lookup_alias(ctx, owner) + lookup_alias(wallet, owner) ), ), None => { @@ -768,8 +770,6 @@ pub async fn query_shielded_balance< match (args.token, owner.is_some()) { // Here the user wants to know the balance for a specific token (Some(token), true) => { - let token = ctx.get(&token); - // Query the multi-asset balance at the given spending key let viewing_key = ExtendedFullViewingKey::from(viewing_keys[0]).fvk.vk; @@ -785,7 +785,7 @@ pub async fn query_shielded_balance< .expect("context should contain viewing key") }; - let token_alias = lookup_alias(client, &token); + let token_alias = lookup_alias(wallet, &token); let token_address = TokenAddress { address: token, @@ -803,7 +803,7 @@ pub async fn query_shielded_balance< "{}: {}", token_address.format_with_alias(&token_alias), format_denominated_amount( - &client, + client, &token_address, token::Amount::from(total_balance) ) @@ -888,7 +888,7 @@ pub async fn query_shielded_balance< token_address.format_with_alias(&alias), ); let formatted = format_denominated_amount( - &client, + client, &token_address, token_balance.into(), ) @@ -898,7 +898,7 @@ pub async fn query_shielded_balance< // Print zero balances for remaining assets for token in tokens { if let Some(sub_addrs) = read_tokens.get(&token) { - let token_alias = lookup_alias(client, &token); + let token_alias = lookup_alias(wallet, &token); for sub_addr in sub_addrs { match sub_addr { // abstract out these prints @@ -931,7 +931,7 @@ pub async fn query_shielded_balance< (Some(token), false) => { // Compute the unique asset identifier from the token address let token = token; - let asset_type = AssetType::new( + let _asset_type = AssetType::new( (token.clone(), epoch.0) .try_to_vec() .expect("token addresses should serialize") @@ -941,7 +941,7 @@ pub async fn query_shielded_balance< let token_alias = lookup_alias(wallet, &token); println!("Shielded Token {}:", token_alias); let mut found_any = false; - let token_alias = lookup_alias(ctx, &token); + let token_alias = lookup_alias(wallet, &token); let token_address = TokenAddress { address: token.clone(), sub_prefix: args @@ -973,7 +973,7 @@ pub async fn query_shielded_balance< found_any = true; } let formatted = format_denominated_amount( - &client, + client, address, (*val).into(), ) @@ -999,7 +999,7 @@ pub async fn query_shielded_balance< .await .expect("context should contain viewing key"); // Print balances by human-readable token names - print_decoded_balance_with_epoch(wallet, balance, epoch).await; + print_decoded_balance_with_epoch(client, wallet, balance).await; } else { let balance = shielded .compute_exchanged_balance( @@ -1010,13 +1010,14 @@ pub async fn query_shielded_balance< .await .expect("context should contain viewing key"); // Print balances by human-readable token names - print_decoded_balance(wallet, balance, epoch).await; + print_decoded_balance(client, wallet, balance, epoch).await; } } } } -pub fn print_decoded_balance( +pub async fn print_decoded_balance( + client: &C, wallet: &mut Wallet, decoded_balance: MaspAmount, epoch: Epoch, @@ -1031,7 +1032,7 @@ pub fn print_decoded_balance( println!( "{} : {}", token_addr - .format_with_alias(&lookup_alias(ctx, &token_addr.address)), + .format_with_alias(&lookup_alias(wallet, &token_addr.address)), format_denominated_amount(client, token_addr, (*amount).into()) .await, ); @@ -1039,7 +1040,8 @@ pub fn print_decoded_balance( } } -pub fn print_decoded_balance_with_epoch( +pub async fn print_decoded_balance_with_epoch( + client: &C, wallet: &mut Wallet, decoded_balance: MaspAmount, ) { @@ -1584,7 +1586,7 @@ pub async fn query_bonded_stake( } } - let total_staked_tokens = get_total_staked_tokens(&client, epoch).await; + let total_staked_tokens = get_total_staked_tokens(client, epoch).await; println!( "Total bonded stake: {}", total_staked_tokens.to_string_native() @@ -2254,19 +2256,19 @@ fn unwrap_client_response( response: Result, ) -> T { response.unwrap_or_else(|_err| { - eprintln!("Error in the query {}", err); + eprintln!("Error in the query"); cli::safe_exit(1) }) } /// Look up the denomination of a token in order to format it /// correctly as a string. -pub(super) async fn format_denominated_amount( - client: &HttpClient, +pub(super) async fn format_denominated_amount( + client: &C, token: &TokenAddress, amount: token::Amount, ) -> String { - let denom = unwrap_client_response( + let denom = unwrap_client_response::>( RPC.vp() .token() .denomination(client, &token.address, &token.sub_prefix) @@ -2283,8 +2285,8 @@ pub(super) async fn format_denominated_amount( } /// Get the correct representation of the amount given the token type. -pub async fn validate_amount( - client: &HttpClient, +pub async fn validate_amount( + client: &C, amount: InputAmount, token: &Address, sub_prefix: &Option, @@ -2293,7 +2295,7 @@ pub async fn validate_amount( InputAmount::Unvalidated(amt) => amt.canonical(), InputAmount::Validated(amt) => return amt, }; - let denom = unwrap_client_response( + let denom = unwrap_client_response::>( RPC.vp() .token() .denomination(client, token, sub_prefix) diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index d28cb5b0c4..24c49afc72 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -10,7 +10,6 @@ use async_std::io::prelude::WriteExt; use borsh::{BorshDeserialize, BorshSerialize}; use data_encoding::HEXLOWER_PERMISSIVE; use masp_proofs::prover::LocalTxProver; -use namada::core::types::uint::Uint; use namada::ledger::governance::storage as gov_storage; use namada::ledger::rpc::{TxBroadcastData, TxResponse}; use namada::ledger::signing::TxSigningKey; @@ -32,7 +31,6 @@ use namada::types::transaction::InitValidator; use tendermint_rpc::HttpClient; use super::rpc; -use crate::cli::args::InputAmount; use crate::cli::context::WalletAddress; use crate::cli::{args, safe_exit, Context}; use crate::client::rpc::query_wasm_code_hash; @@ -1063,17 +1061,19 @@ pub async fn submit_tx( #[cfg(test)] mod test_tx { - + use masp_primitives::transaction::components::Amount; + use namada::ledger::masp::{make_asset_type, MaspAmount}; use namada::types::address::testing::gen_established_address; use namada::types::storage::DbKeySeg; + use namada::types::token::MaspDenom; use super::*; #[test] fn test_masp_add_amount() { let address_1 = gen_established_address(); - let prefix_1: Key = DbKeySeg::StringSeg("eth_seg".into()).into(); - let prefix_2: Key = DbKeySeg::StringSeg("crypto_kitty".into()).into(); + let prefix_1: Key = DbKeySeg::StringSeg("eth_seg".parse().unwrap()).into(); + let prefix_2: Key = DbKeySeg::StringSeg("crypto_kitty".parse().unwrap()).into(); let denom_1 = MaspDenom::One; let denom_2 = MaspDenom::Three; let epoch = Epoch::default(); diff --git a/shared/src/ledger/masp.rs b/shared/src/ledger/masp.rs index b40c909274..a3fb7e9349 100644 --- a/shared/src/ledger/masp.rs +++ b/shared/src/ledger/masp.rs @@ -319,7 +319,7 @@ fn cloned_pair((a, b): (&T, &U)) -> (T, U) { } /// Errors that can occur when trying to retrieve pinned transaction -#[derive(PartialEq, Eq)] +#[derive(PartialEq, Eq, Copy, Clone)] pub enum PinnedBalanceError { /// No transaction has yet been pinned to the given payment address NoTransactionPinned, diff --git a/tests/src/storage_api/testnet_pow.rs b/tests/src/storage_api/testnet_pow.rs index cd61331858..f6a42e64a1 100644 --- a/tests/src/storage_api/testnet_pow.rs +++ b/tests/src/storage_api/testnet_pow.rs @@ -11,7 +11,7 @@ use crate::vp; fn test_challenge_and_solution() -> storage_api::Result<()> { let faucet_address = address::testing::established_address_1(); let difficulty = Difficulty::try_new(1).unwrap(); - let withdrawal_limit = token::Amount::whole(1_000); + let withdrawal_limit = token::Amount::native_whole(1_000); let mut tx_env = TestTxEnv::default(); diff --git a/tests/src/vm_host_env/mod.rs b/tests/src/vm_host_env/mod.rs index c19b47629a..746d4f4921 100644 --- a/tests/src/vm_host_env/mod.rs +++ b/tests/src/vm_host_env/mod.rs @@ -1143,7 +1143,7 @@ mod tests { let balance: Option = tx_host_env::with(|env| { env.wl_storage.read(&balance_key).expect("read error") }); - assert_eq!(balance, Some(Amount::whole(0))); + assert_eq!(balance, Some(Amount::native_whole(0))); let escrow_key = token::balance_key( &token, &address::Address::Internal(address::InternalAddress::IbcEscrow), @@ -1151,7 +1151,7 @@ mod tests { let escrow: Option = tx_host_env::with(|env| { env.wl_storage.read(&escrow_key).expect("read error") }); - assert_eq!(escrow, Some(Amount::whole(100))); + assert_eq!(escrow, Some(Amount::native_whole(100))); } #[test] @@ -1171,7 +1171,7 @@ mod tests { let denom = format!("{}/{}/{}", port_id, channel_id, token); let key_prefix = ibc_storage::ibc_token_prefix(&denom).unwrap(); let balance_key = token::multitoken_balance_key(&key_prefix, &sender); - let init_bal = Amount::whole(100); + let init_bal = Amount::native_whole(100); writes.insert(balance_key.clone(), init_bal.try_to_vec().unwrap()); // original denom let hash = ibc_storage::calc_hash(&denom); @@ -1225,7 +1225,7 @@ mod tests { let balance: Option = tx_host_env::with(|env| { env.wl_storage.read(&balance_key).expect("read error") }); - assert_eq!(balance, Some(Amount::whole(0))); + assert_eq!(balance, Some(Amount::native_whole(0))); let burn_key = token::balance_key( &token, &address::Address::Internal(address::InternalAddress::IbcBurn), @@ -1233,7 +1233,7 @@ mod tests { let burn: Option = tx_host_env::with(|env| { env.wl_storage.read(&burn_key).expect("read error") }); - assert_eq!(burn, Some(Amount::whole(100))); + assert_eq!(burn, Some(Amount::native_whole(100))); } #[test] @@ -1309,7 +1309,7 @@ mod tests { let balance: Option = tx_host_env::with(|env| { env.wl_storage.read(&key).expect("read error") }); - assert_eq!(balance, Some(Amount::whole(100))); + assert_eq!(balance, Some(Amount::native_whole(100))); } #[test] @@ -1338,7 +1338,7 @@ mod tests { &token, &address::Address::Internal(address::InternalAddress::IbcEscrow), ); - let val = Amount::whole(100).try_to_vec().unwrap(); + let val = Amount::native_whole(100).try_to_vec().unwrap(); tx_host_env::with(|env| { env.wl_storage .storage @@ -1393,11 +1393,11 @@ mod tests { let balance: Option = tx_host_env::with(|env| { env.wl_storage.read(&key).expect("read error") }); - assert_eq!(balance, Some(Amount::whole(200))); + assert_eq!(balance, Some(Amount::native_whole(200))); let escrow: Option = tx_host_env::with(|env| { env.wl_storage.read(&escrow_key).expect("read error") }); - assert_eq!(escrow, Some(Amount::whole(0))); + assert_eq!(escrow, Some(Amount::native_whole(0))); } #[test] @@ -1426,7 +1426,7 @@ mod tests { &token, &address::Address::Internal(address::InternalAddress::IbcEscrow), ); - let val = Amount::whole(100).try_to_vec().unwrap(); + let val = Amount::native_whole(100).try_to_vec().unwrap(); tx_host_env::with(|env| { env.wl_storage .storage @@ -1488,11 +1488,11 @@ mod tests { let balance: Option = tx_host_env::with(|env| { env.wl_storage.read(&key).expect("read error") }); - assert_eq!(balance, Some(Amount::whole(100))); + assert_eq!(balance, Some(Amount::native_whole(100))); let escrow: Option = tx_host_env::with(|env| { env.wl_storage.read(&escrow_key).expect("read error") }); - assert_eq!(escrow, Some(Amount::whole(0))); + assert_eq!(escrow, Some(Amount::native_whole(0))); } #[test] From 42de0403f48198d390937f6ba4d9835a39fecc3c Mon Sep 17 00:00:00 2001 From: "Raymond E. Pasco" Date: Mon, 22 May 2023 10:56:01 -0400 Subject: [PATCH 055/151] some test fixes --- apps/src/lib/client/rpc.rs | 27 +++-- apps/src/lib/client/tx.rs | 6 +- .../lib/node/ledger/shell/finalize_block.rs | 3 +- apps/src/lib/node/ledger/shell/init_chain.rs | 3 +- apps/src/lib/node/ledger/storage/rocksdb.rs | 12 +- core/src/ledger/storage/mockdb.rs | 16 ++- core/src/ledger/storage/mod.rs | 17 ++- wasm/Cargo.lock | 108 ++++++++++++++---- 8 files changed, 152 insertions(+), 40 deletions(-) diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 871ba01305..ae6c423b4d 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -24,7 +24,10 @@ use namada::ledger::args::InputAmount; use namada::ledger::events::Event; use namada::ledger::governance::parameters::GovParams; use namada::ledger::governance::storage as gov_storage; -use namada::ledger::masp::{Conversions, MaspAmount, MaspChange, PinnedBalanceError, ShieldedContext, ShieldedUtils}; +use namada::ledger::masp::{ + Conversions, MaspAmount, MaspChange, PinnedBalanceError, ShieldedContext, + ShieldedUtils, +}; use namada::ledger::native_vp::governance::utils::{self, Votes}; use namada::ledger::parameters::{storage as param_storage, EpochDuration}; use namada::ledger::pos::{ @@ -43,8 +46,10 @@ use namada::types::hash::Hash; use namada::types::key::*; use namada::types::masp::{BalanceOwner, ExtendedViewingKey, PaymentAddress}; use namada::types::storage::{BlockHeight, BlockResults, Epoch, Key, KeySeg}; +use namada::types::token::{ + Change, DenominatedAmount, Denomination, MaspDenom, TokenAddress, +}; use namada::types::{storage, token}; -use namada::types::token::{Change, DenominatedAmount, Denomination, MaspDenom, TokenAddress}; use crate::cli::{self, args}; use crate::facade::tendermint::merkle::proof::Proof; @@ -1016,7 +1021,9 @@ pub async fn query_shielded_balance< } } -pub async fn print_decoded_balance( +pub async fn print_decoded_balance< + C: namada::ledger::queries::Client + Sync, +>( client: &C, wallet: &mut Wallet, decoded_balance: MaspAmount, @@ -1031,8 +1038,10 @@ pub async fn print_decoded_balance( { println!( "{} : {}", - token_addr - .format_with_alias(&lookup_alias(wallet, &token_addr.address)), + token_addr.format_with_alias(&lookup_alias( + wallet, + &token_addr.address + )), format_denominated_amount(client, token_addr, (*amount).into()) .await, ); @@ -1040,7 +1049,9 @@ pub async fn print_decoded_balance( } } -pub async fn print_decoded_balance_with_epoch( +pub async fn print_decoded_balance_with_epoch< + C: namada::ledger::queries::Client + Sync, +>( client: &C, wallet: &mut Wallet, decoded_balance: MaspAmount, @@ -2263,7 +2274,9 @@ fn unwrap_client_response( /// Look up the denomination of a token in order to format it /// correctly as a string. -pub(super) async fn format_denominated_amount( +pub(super) async fn format_denominated_amount< + C: namada::ledger::queries::Client + Sync, +>( client: &C, token: &TokenAddress, amount: token::Amount, diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 24c49afc72..425400ed8e 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -1072,8 +1072,10 @@ mod test_tx { #[test] fn test_masp_add_amount() { let address_1 = gen_established_address(); - let prefix_1: Key = DbKeySeg::StringSeg("eth_seg".parse().unwrap()).into(); - let prefix_2: Key = DbKeySeg::StringSeg("crypto_kitty".parse().unwrap()).into(); + let prefix_1: Key = + DbKeySeg::StringSeg("eth_seg".parse().unwrap()).into(); + let prefix_2: Key = + DbKeySeg::StringSeg("crypto_kitty".parse().unwrap()).into(); let denom_1 = MaspDenom::One; let denom_2 = MaspDenom::Three; let epoch = Epoch::default(); diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index a4836d8f37..1d6a5d1e7f 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -1341,12 +1341,11 @@ mod test_finalize_block { // Collect all storage key-vals into a sorted map let store_block_state = |shell: &TestShell| -> BTreeMap<_, _> { - let prefix: Key = FromStr::from_str("").unwrap(); shell .wl_storage .storage .db - .iter_prefix(&prefix) + .iter_optional_prefix(None) .map(|(key, val, _gas)| (key, val)) .collect() }; diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index 7f44315706..5312fba457 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -493,12 +493,11 @@ mod test { // Collect all storage key-vals into a sorted map let store_block_state = |shell: &TestShell| -> BTreeMap<_, _> { - let prefix: storage::Key = FromStr::from_str("").unwrap(); shell .wl_storage .storage .db - .iter_prefix(&prefix) + .iter_optional_prefix(None) .map(|(key, val, _gas)| (key, val)) .collect() }; diff --git a/apps/src/lib/node/ledger/storage/rocksdb.rs b/apps/src/lib/node/ledger/storage/rocksdb.rs index 9d54bc6de3..dd7c534c3a 100644 --- a/apps/src/lib/node/ledger/storage/rocksdb.rs +++ b/apps/src/lib/node/ledger/storage/rocksdb.rs @@ -1188,9 +1188,9 @@ impl DB for RocksDB { impl<'iter> DBIter<'iter> for RocksDB { type PrefixIter = PersistentPrefixIterator<'iter>; - fn iter_prefix( + fn iter_optional_prefix( &'iter self, - prefix: &Key, + prefix: Option<&Key>, ) -> PersistentPrefixIterator<'iter> { iter_subspace_prefix(self, prefix) } @@ -1228,13 +1228,17 @@ impl<'iter> DBIter<'iter> for RocksDB { fn iter_subspace_prefix<'iter>( db: &'iter RocksDB, - prefix: &Key, + prefix: Option<&Key>, ) -> PersistentPrefixIterator<'iter> { let subspace_cf = db .get_column_family(SUBSPACE_CF) .expect("{SUBSPACE_CF} column family should exist"); let db_prefix = "".to_owned(); - iter_prefix(db, subspace_cf, db_prefix, prefix.to_string()) + let prefix_string = match prefix { + Some(prefix) => prefix.to_string(), + None => "".to_string(), + }; + iter_prefix(db, subspace_cf, db_prefix, prefix_string) } fn iter_diffs_prefix( diff --git a/core/src/ledger/storage/mockdb.rs b/core/src/ledger/storage/mockdb.rs index 16e28d2759..a5db20582c 100644 --- a/core/src/ledger/storage/mockdb.rs +++ b/core/src/ledger/storage/mockdb.rs @@ -480,9 +480,21 @@ impl DB for MockDB { impl<'iter> DBIter<'iter> for MockDB { type PrefixIter = MockPrefixIterator; - fn iter_prefix(&'iter self, prefix: &Key) -> MockPrefixIterator { + fn iter_optional_prefix( + &'iter self, + prefix: Option<&Key>, + ) -> MockPrefixIterator { let db_prefix = "subspace/".to_owned(); - let prefix = format!("{}{}", db_prefix, prefix); + let prefix = format!( + "{}{}", + db_prefix, + match prefix { + None => "".to_string(), + Some(prefix) => { + prefix.to_string() + } + } + ); let iter = self.0.borrow().clone().into_iter(); MockPrefixIterator::new(MockIterator { prefix, iter }, db_prefix) } diff --git a/core/src/ledger/storage/mod.rs b/core/src/ledger/storage/mod.rs index 1ad537234c..f85cb3979b 100644 --- a/core/src/ledger/storage/mod.rs +++ b/core/src/ledger/storage/mod.rs @@ -12,6 +12,7 @@ pub mod write_log; use core::fmt::Debug; use std::cmp::Ordering; +use std::format; pub use merkle_tree::{ MembershipProof, MerkleTree, MerkleTreeStoresRead, MerkleTreeStoresWrite, @@ -31,6 +32,7 @@ use crate::ledger::parameters::{self, EpochDuration, Parameters}; use crate::ledger::storage::merkle_tree::{ Error as MerkleTreeError, MerkleRoot, }; +use crate::ledger::storage::mockdb::{MockIterator, MockPrefixIterator}; #[cfg(any(feature = "tendermint", feature = "tendermint-abcipp"))] use crate::tendermint::merkle::proof::Proof; use crate::types::address::{ @@ -321,7 +323,20 @@ pub trait DBIter<'iter> { /// /// Read account subspace key value pairs with the given prefix from the DB, /// ordered by the storage keys. - fn iter_prefix(&'iter self, prefix: &Key) -> Self::PrefixIter; + fn iter_prefix(&'iter self, prefix: &Key) -> Self::PrefixIter { + self.iter_optional_prefix(Some(prefix)) + } + + /// Iterate over all keys + fn iter_all(&'iter self) -> Self::PrefixIter { + self.iter_optional_prefix(None) + } + + /// Iterate over subspace keys, with optional prefix + fn iter_optional_prefix( + &'iter self, + prefix: Option<&Key>, + ) -> Self::PrefixIter; /// Read results subspace key value pairs from the DB fn iter_results(&'iter self) -> Self::PrefixIter; diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index 5d8409ef24..ada51b05e0 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -1626,6 +1626,50 @@ dependencies = [ "version_check", ] +[[package]] +name = "ethabi" +version = "18.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7413c5f74cc903ea37386a8965a936cbeb334bd270862fdece542c1b2dcbc898" +dependencies = [ + "ethereum-types", + "hex", + "once_cell", + "regex", + "serde", + "serde_json", + "sha3", + "thiserror", + "uint", +] + +[[package]] +name = "ethbloom" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c22d4b5885b6aa2fe5e8b9329fb8d232bf739e434e6b87347c63bdd00c120f60" +dependencies = [ + "crunchy 0.2.2", + "fixed-hash", + "impl-rlp", + "impl-serde", + "tiny-keccak", +] + +[[package]] +name = "ethereum-types" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d215cbf040552efcbe99a38372fe80ab9d00268e20012b79fcd0f073edd8ee" +dependencies = [ + "ethbloom", + "fixed-hash", + "impl-rlp", + "impl-serde", + "primitive-types", + "uint", +] + [[package]] name = "event-listener" version = "2.5.3" @@ -2532,6 +2576,26 @@ dependencies = [ "parity-scale-codec", ] +[[package]] +name = "impl-num-traits" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951641f13f873bff03d4bf19ae8bec531935ac0ac2cc775f84d7edfdcfed3f17" +dependencies = [ + "integer-sqrt", + "num-traits", + "uint", +] + +[[package]] +name = "impl-rlp" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28220f89297a075ddc7245cd538076ee98b01f2a9c23a53a4f1105d5a322808" +dependencies = [ + "rlp", +] + [[package]] name = "impl-serde" version = "0.4.0" @@ -2599,6 +2663,15 @@ dependencies = [ "web-sys", ] +[[package]] +name = "integer-sqrt" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770" +dependencies = [ + "num-traits", +] + [[package]] name = "io-lifetimes" version = "1.0.10" @@ -3056,8 +3129,6 @@ dependencies = [ "rand 0.8.5", "rand_core 0.6.4", "rayon", - "rust_decimal", - "rust_decimal_macros", "serde", "serde_json", "sha2 0.9.9", @@ -3093,12 +3164,15 @@ dependencies = [ "data-encoding", "derivative", "ed25519-consensus", + "ethabi", + "eyre", "ferveo", "ferveo-common", "group-threshold-cryptography", "ibc", "ibc-proto 0.26.0", "ics23", + "impl-num-traits", "index-set", "itertools", "libsecp256k1", @@ -3110,8 +3184,6 @@ dependencies = [ "rand 0.8.5", "rand_core 0.6.4", "rayon", - "rust_decimal", - "rust_decimal_macros", "serde", "serde_json", "sha2 0.9.9", @@ -3121,6 +3193,7 @@ dependencies = [ "thiserror", "tonic-build", "tracing", + "uint", "zeroize", ] @@ -3144,8 +3217,6 @@ dependencies = [ "namada_core", "once_cell", "proptest", - "rust_decimal", - "rust_decimal_macros", "thiserror", "tracing", ] @@ -3175,8 +3246,6 @@ dependencies = [ "namada_vp_prelude", "prost", "regex", - "rust_decimal", - "rust_decimal_macros", "serde_json", "sha2 0.9.9", "tempfile", @@ -3200,7 +3269,6 @@ dependencies = [ "namada_macros", "namada_proof_of_stake", "namada_vm_env", - "rust_decimal", "sha2 0.10.6", "thiserror", ] @@ -3692,6 +3760,7 @@ checksum = "9f3486ccba82358b11a77516035647c34ba167dfa53312630de83b12bd4f3d66" dependencies = [ "fixed-hash", "impl-codec", + "impl-rlp", "impl-serde", "uint", ] @@ -4207,25 +4276,24 @@ dependencies = [ ] [[package]] -name = "rust_decimal" -version = "1.26.1" +name = "rlp" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee9164faf726e4f3ece4978b25ca877ddc6802fa77f38cdccb32c7f805ecd70c" +checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" dependencies = [ - "arrayvec 0.7.2", - "borsh", - "num-traits", - "serde", + "bytes", + "rustc-hex", ] [[package]] -name = "rust_decimal_macros" +name = "rust_decimal" version = "1.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4903d8db81d2321699ca8318035d6ff805c548868df435813968795a802171b2" +checksum = "ee9164faf726e4f3ece4978b25ca877ddc6802fa77f38cdccb32c7f805ecd70c" dependencies = [ - "quote", - "rust_decimal", + "arrayvec 0.7.2", + "num-traits", + "serde", ] [[package]] From 5801ef522708a6c78bc4e1ddcf7a7c29d2a2b444 Mon Sep 17 00:00:00 2001 From: "Raymond E. Pasco" Date: Mon, 22 May 2023 12:02:56 -0400 Subject: [PATCH 056/151] minor stuff again --- core/src/ledger/storage/mod.rs | 1 - core/src/types/token.rs | 9 +++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/src/ledger/storage/mod.rs b/core/src/ledger/storage/mod.rs index f85cb3979b..9ef84014c7 100644 --- a/core/src/ledger/storage/mod.rs +++ b/core/src/ledger/storage/mod.rs @@ -32,7 +32,6 @@ use crate::ledger::parameters::{self, EpochDuration, Parameters}; use crate::ledger::storage::merkle_tree::{ Error as MerkleTreeError, MerkleRoot, }; -use crate::ledger::storage::mockdb::{MockIterator, MockPrefixIterator}; #[cfg(any(feature = "tendermint", feature = "tendermint-abcipp"))] use crate::tendermint::merkle::proof::Proof; use crate::types::address::{ diff --git a/core/src/types/token.rs b/core/src/types/token.rs index fe03df0eff..37390aa320 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -711,10 +711,11 @@ impl TryFrom for Amount { fn try_from(amount: IbcAmount) -> Result { // TODO: https://github.com/anoma/namada/issues/1089 - if amount > u64::MAX.into() { - return Err(AmountParseError::InvalidRange); - } - Self::from_str(&amount.to_string(), 6) + // TODO: OVERFLOW CHECK PLEASE (PATCH IBC TO ALLOW GETTING IBCAMOUNT::MAX OR SIMILAR) + //if amount > u64::MAX.into() { + // return Err(AmountParseError::InvalidRange); + //} + DenominatedAmount::from_str(&amount.to_string()).map(|a| a.amount) } } From 48a27dc7f047effaa8d623641ed54f78b0eca194 Mon Sep 17 00:00:00 2001 From: "Raymond E. Pasco" Date: Tue, 23 May 2023 05:26:28 -0400 Subject: [PATCH 057/151] dubious e2e fixes --- core/src/ledger/ibc/context/transfer_mod.rs | 79 ++++++++++++--------- core/src/types/token.rs | 12 +++- shared/src/ledger/ibc/vp/context.rs | 6 +- shared/src/ledger/ibc/vp/token.rs | 7 +- tests/src/vm_host_env/mod.rs | 4 +- tx_prelude/src/token.rs | 2 +- 6 files changed, 67 insertions(+), 43 deletions(-) diff --git a/core/src/ledger/ibc/context/transfer_mod.rs b/core/src/ledger/ibc/context/transfer_mod.rs index ad0aa75800..de11d6e20e 100644 --- a/core/src/ledger/ibc/context/transfer_mod.rs +++ b/core/src/ledger/ibc/context/transfer_mod.rs @@ -5,6 +5,10 @@ use std::fmt::Debug; use std::rc::Rc; use std::str::FromStr; +use ethabi::Token; +use ethabi::Token::Int; +use masp_primitives::zcash_primitives::zip32::AccountId; + use super::common::IbcCommonContext; use crate::ibc::applications::transfer::coin::PrefixedCoin; use crate::ibc::applications::transfer::context::{ @@ -49,6 +53,7 @@ use crate::ibc::events::IbcEvent; use crate::ibc::signer::Signer; use crate::ledger::ibc::storage; use crate::types::address::{Address, InternalAddress}; +use crate::types::storage::Key; use crate::types::token; /// IBC module wrapper for getting the reference of the module @@ -83,6 +88,34 @@ where pub fn module_id(&self) -> ModuleId { ModuleId::from_str(MODULE_ID_STR).expect("should be parsable") } + + // returns source and dest for burn, dest and source for mint + fn burn_and_mint_coins_addresses( + &mut self, + account: &Address, + coin: &PrefixedCoin, + token: &Address, + mode: InternalAddress, + ) -> Result<[Key; 2], TokenTransferError> { + if coin.denom.trace_path.is_empty() { + Ok([ + token::balance_key(token, account), + token::balance_key(token, &Address::Internal(mode)), + ]) + } else { + let prefix = storage::ibc_token_prefix(coin.denom.to_string()) + .map_err(|_| TokenTransferError::InvalidCoin { + coin: coin.to_string(), + })?; + Ok([ + token::multitoken_balance_key(&prefix, account), + token::multitoken_balance_key( + &prefix, + &Address::Internal(mode), + ), + ]) + } + } } impl ModuleWrapper for TransferModule @@ -446,10 +479,7 @@ where // has no prefix let (token, amount) = get_token_amount(coin)?; - let src = if coin.denom.trace_path.is_empty() - || *from == Address::Internal(InternalAddress::IbcEscrow) - || *from == Address::Internal(InternalAddress::IbcMint) - { + let src = if coin.denom.trace_path.is_empty() { token::balance_key(&token, from) } else { let prefix = storage::ibc_token_prefix(coin.denom.to_string()) @@ -459,10 +489,7 @@ where token::multitoken_balance_key(&prefix, from) }; - let dest = if coin.denom.trace_path.is_empty() - || *to == Address::Internal(InternalAddress::IbcEscrow) - || *to == Address::Internal(InternalAddress::IbcBurn) - { + let dest = if coin.denom.trace_path.is_empty() { token::balance_key(&token, to) } else { let prefix = storage::ibc_token_prefix(coin.denom.to_string()) @@ -496,20 +523,12 @@ where ) -> Result<(), TokenTransferError> { let (token, amount) = get_token_amount(coin)?; - let src = token::balance_key( + let [dest, src] = self.burn_and_mint_coins_addresses( + account, + coin, &token, - &Address::Internal(InternalAddress::IbcMint), - ); - - let dest = if coin.denom.trace_path.is_empty() { - token::balance_key(&token, account) - } else { - let prefix = storage::ibc_token_prefix(coin.denom.to_string()) - .map_err(|_| TokenTransferError::InvalidCoin { - coin: coin.to_string(), - })?; - token::multitoken_balance_key(&prefix, account) - }; + InternalAddress::IbcMint, + )?; self.ctx .borrow_mut() @@ -535,20 +554,12 @@ where ) -> Result<(), TokenTransferError> { let (token, amount) = get_token_amount(coin)?; - let src = if coin.denom.trace_path.is_empty() { - token::balance_key(&token, account) - } else { - let prefix = storage::ibc_token_prefix(coin.denom.to_string()) - .map_err(|_| TokenTransferError::InvalidCoin { - coin: coin.to_string(), - })?; - token::multitoken_balance_key(&prefix, account) - }; - - let dest = token::balance_key( + let [src, dest] = self.burn_and_mint_coins_addresses( + account, + coin, &token, - &Address::Internal(InternalAddress::IbcBurn), - ); + InternalAddress::IbcBurn, + )?; self.ctx .borrow_mut() diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 37390aa320..5b83a31ee8 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -711,8 +711,8 @@ impl TryFrom for Amount { fn try_from(amount: IbcAmount) -> Result { // TODO: https://github.com/anoma/namada/issues/1089 - // TODO: OVERFLOW CHECK PLEASE (PATCH IBC TO ALLOW GETTING IBCAMOUNT::MAX OR SIMILAR) - //if amount > u64::MAX.into() { + // TODO: OVERFLOW CHECK PLEASE (PATCH IBC TO ALLOW GETTING + // IBCAMOUNT::MAX OR SIMILAR) if amount > u64::MAX.into() { // return Err(AmountParseError::InvalidRange); //} DenominatedAmount::from_str(&amount.to_string()).map(|a| a.amount) @@ -914,6 +914,14 @@ pub fn is_any_multitoken_balance_key( } } +pub fn is_any_token_or_multitoken_balance_key( + key: &Key, +) -> Option<[&Address; 2]> { + is_any_multitoken_balance_key(key) + .map(|a| a.1) + .or_else(|| is_any_token_balance_key(key)) +} + fn multitoken_balance_owner(key: &Key) -> Option<(Key, &Address)> { let len = key.segments.len(); if len < 4 { diff --git a/shared/src/ledger/ibc/vp/context.rs b/shared/src/ledger/ibc/vp/context.rs index 9f6b32c1b7..0da78bba53 100644 --- a/shared/src/ledger/ibc/vp/context.rs +++ b/shared/src/ledger/ibc/vp/context.rs @@ -11,7 +11,9 @@ use namada_core::ledger::storage_api::StorageRead; use namada_core::types::address::{Address, InternalAddress}; use namada_core::types::ibc::IbcEvent; use namada_core::types::storage::{BlockHeight, Header, Key}; -use namada_core::types::token::{is_any_token_balance_key, Amount}; +use namada_core::types::token::{ + is_any_token_balance_key, is_any_token_or_multitoken_balance_key, Amount, +}; use super::Error; use crate::ledger::native_vp::CtxPreStorageRead; @@ -119,7 +121,7 @@ where dest: &Key, amount: Amount, ) -> Result<(), Self::Error> { - let src_owner = is_any_token_balance_key(src); + let src_owner = is_any_token_or_multitoken_balance_key(src); let mut src_bal = match src_owner { Some([_, Address::Internal(InternalAddress::IbcMint)]) => { Amount::max() diff --git a/shared/src/ledger/ibc/vp/token.rs b/shared/src/ledger/ibc/vp/token.rs index 0e04159150..cd193a4a81 100644 --- a/shared/src/ledger/ibc/vp/token.rs +++ b/shared/src/ledger/ibc/vp/token.rs @@ -3,6 +3,7 @@ use std::collections::{BTreeSet, HashMap, HashSet}; use borsh::BorshDeserialize; +use namada_core::ledger::ibc::storage; use prost::Message; use thiserror::Error; @@ -267,6 +268,8 @@ where .map_err(|e| Error::Denom(e.to_string()))?; let amount = Amount::try_from(data.token.amount).map_err(Error::Amount)?; + let prefix = storage::ibc_token_prefix(&data.token.denom.to_string()) + .map_err(|e| Error::Denom(e.to_string()))?; let change = if is_receiver_chain_source( packet.port_id_on_a.clone(), @@ -290,8 +293,8 @@ where } else { // the sender is the source // check the amount of the token has been minted - let source_key = token::balance_key( - &token, + let source_key = token::multitoken_balance_key( + &prefix, &Address::Internal(InternalAddress::IbcMint), ); let post = try_decode_token_amount( diff --git a/tests/src/vm_host_env/mod.rs b/tests/src/vm_host_env/mod.rs index 746d4f4921..9cc390d799 100644 --- a/tests/src/vm_host_env/mod.rs +++ b/tests/src/vm_host_env/mod.rs @@ -1296,8 +1296,8 @@ mod tests { let result = ibc::validate_ibc_vp_from_tx(&env, &tx); assert!(result.expect("validation failed unexpectedly")); // Check if the token was minted - let mint = token::balance_key( - &token, + let mint = token::multitoken_balance_key( + &key_prefix, &address::Address::Internal(address::InternalAddress::IbcMint), ); let result = ibc::validate_token_vp_from_tx(&env, &tx, &mint); diff --git a/tx_prelude/src/token.rs b/tx_prelude/src/token.rs index d49b8faeb3..4aa3dd04e1 100644 --- a/tx_prelude/src/token.rs +++ b/tx_prelude/src/token.rs @@ -140,7 +140,7 @@ pub fn transfer_with_keys( dest_key: &storage::Key, amount: Amount, ) -> TxResult { - let src_owner = is_any_token_balance_key(src_key); + let src_owner = is_any_token_or_multitoken_balance_key(src_key); let src_bal: Option = match src_owner { Some([_, Address::Internal(InternalAddress::IbcMint)]) => { Some(Amount::max_signed()) From aebb5f5076fafc6dac5623f0344e1bed74c4074c Mon Sep 17 00:00:00 2001 From: "Raymond E. Pasco" Date: Wed, 24 May 2023 13:09:33 -0400 Subject: [PATCH 058/151] we don't take separators anymore whoops --- tests/src/e2e/ledger_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index eba045f775..63dbdc2539 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -2548,7 +2548,7 @@ fn pos_init_validator() -> Result<()> { find_bonded_stake(&test, new_validator, &validator_one_rpc)?; assert_eq!( bonded_stake, - token::Amount::from_str("11_000.5", NATIVE_MAX_DECIMAL_PLACES).unwrap() + token::Amount::from_str("11000.5", NATIVE_MAX_DECIMAL_PLACES).unwrap() ); Ok(()) From f09312e49b0af6e02aed0cd794d22de3572e6033 Mon Sep 17 00:00:00 2001 From: "Raymond E. Pasco" Date: Wed, 24 May 2023 15:49:47 -0400 Subject: [PATCH 059/151] disambiguate token denom and ibc denom --- core/src/ledger/ibc/storage.rs | 4 ++-- core/src/types/token.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/ledger/ibc/storage.rs b/core/src/ledger/ibc/storage.rs index 478a9e10f3..785e309fa3 100644 --- a/core/src/ledger/ibc/storage.rs +++ b/core/src/ledger/ibc/storage.rs @@ -25,7 +25,7 @@ const CONNECTIONS_COUNTER: &str = "connections/counter"; const CHANNELS_COUNTER: &str = "channelEnds/counter"; const CAPABILITIES_INDEX: &str = "capabilities/index"; const CAPABILITIES: &str = "capabilities"; -const DENOM: &str = "denom"; +const DENOM: &str = "ibc_denom"; /// Key segment for a multitoken related to IBC pub const MULTITOKEN_STORAGE_KEY: &str = "ibc"; @@ -83,7 +83,7 @@ pub fn ibc_prefix(key: &Key) -> Option { "receipts" => IbcPrefix::Receipt, "acks" => IbcPrefix::Ack, "event" => IbcPrefix::Event, - "denom" => IbcPrefix::Denom, + "ibc_denom" => IbcPrefix::Denom, _ => IbcPrefix::Unknown, }) } diff --git a/core/src/types/token.rs b/core/src/types/token.rs index bd01e1917e..ed5e58f5d2 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -708,7 +708,7 @@ impl MaspDenom { /// Key segment for a balance key pub const BALANCE_STORAGE_KEY: &str = "balance"; /// Key segment for a denomination key -pub const DENOM_STORAGE_KEY: &str = "denom"; +pub const DENOM_STORAGE_KEY: &str = "denomination"; /// Key segment for head shielded transaction pointer keys pub const HEAD_TX_KEY: &str = "head-tx"; /// Key segment prefix for shielded transaction key From 55591d57e78b4c4631a8309c70403297e3922370 Mon Sep 17 00:00:00 2001 From: "Raymond E. Pasco" Date: Wed, 24 May 2023 15:51:20 -0400 Subject: [PATCH 060/151] force 0 ibc denoms --- core/src/ledger/storage_api/token.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/src/ledger/storage_api/token.rs b/core/src/ledger/storage_api/token.rs index 24494dbf1d..b2abbb8a97 100644 --- a/core/src/ledger/storage_api/token.rs +++ b/core/src/ledger/storage_api/token.rs @@ -3,12 +3,14 @@ use super::{StorageRead, StorageWrite}; use crate::ledger::storage_api; use crate::types::address::Address; +use crate::types::storage::DbKeySeg::StringSeg; use crate::types::storage::Key; use crate::types::token; pub use crate::types::token::{ balance_key, is_balance_key, is_total_supply_key, total_supply_key, Amount, Change, }; +use crate::types::token::Denomination; /// Read the balance of a given token and owner. pub fn read_balance( @@ -48,6 +50,11 @@ pub fn read_denom( where S: StorageRead, { + if let Some(sub_prefix) = sub_prefix { + if sub_prefix.segments.contains(&StringSeg("ibc".to_string())) { + return Ok(Some(Denomination(0))) + } + } let key = token::denom_key(token, sub_prefix); storage.read(&key) } From 9be7ef86f13b582560f33b9daded57e7d03be85a Mon Sep 17 00:00:00 2001 From: "Raymond E. Pasco" Date: Wed, 24 May 2023 15:51:52 -0400 Subject: [PATCH 061/151] stupid test fix: add six zeroes --- tests/src/e2e/ibc_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/src/e2e/ibc_tests.rs b/tests/src/e2e/ibc_tests.rs index 7ded2c703a..e1b5e4d15e 100644 --- a/tests/src/e2e/ibc_tests.rs +++ b/tests/src/e2e/ibc_tests.rs @@ -784,7 +784,7 @@ fn transfer_received_token( "--sub-prefix", &sub_prefix, "--amount", - "50000", + "50000000000", "--gas-amount", "0", "--gas-limit", From f558899b1b08b22796f90742dbfeade8aab999c3 Mon Sep 17 00:00:00 2001 From: yito88 Date: Thu, 25 May 2023 19:08:20 +0900 Subject: [PATCH 062/151] fix IBC stuff, IBC e2e test still failed --- core/src/ledger/ibc/context/transfer_mod.rs | 79 +++++++++------------ core/src/types/token.rs | 3 +- shared/src/ledger/ibc/vp/token.rs | 30 ++++---- shared/src/ledger/tx.rs | 12 ++-- tests/src/e2e/ibc_tests.rs | 3 +- tests/src/vm_host_env/mod.rs | 10 +-- 6 files changed, 60 insertions(+), 77 deletions(-) diff --git a/core/src/ledger/ibc/context/transfer_mod.rs b/core/src/ledger/ibc/context/transfer_mod.rs index de11d6e20e..ad0aa75800 100644 --- a/core/src/ledger/ibc/context/transfer_mod.rs +++ b/core/src/ledger/ibc/context/transfer_mod.rs @@ -5,10 +5,6 @@ use std::fmt::Debug; use std::rc::Rc; use std::str::FromStr; -use ethabi::Token; -use ethabi::Token::Int; -use masp_primitives::zcash_primitives::zip32::AccountId; - use super::common::IbcCommonContext; use crate::ibc::applications::transfer::coin::PrefixedCoin; use crate::ibc::applications::transfer::context::{ @@ -53,7 +49,6 @@ use crate::ibc::events::IbcEvent; use crate::ibc::signer::Signer; use crate::ledger::ibc::storage; use crate::types::address::{Address, InternalAddress}; -use crate::types::storage::Key; use crate::types::token; /// IBC module wrapper for getting the reference of the module @@ -88,34 +83,6 @@ where pub fn module_id(&self) -> ModuleId { ModuleId::from_str(MODULE_ID_STR).expect("should be parsable") } - - // returns source and dest for burn, dest and source for mint - fn burn_and_mint_coins_addresses( - &mut self, - account: &Address, - coin: &PrefixedCoin, - token: &Address, - mode: InternalAddress, - ) -> Result<[Key; 2], TokenTransferError> { - if coin.denom.trace_path.is_empty() { - Ok([ - token::balance_key(token, account), - token::balance_key(token, &Address::Internal(mode)), - ]) - } else { - let prefix = storage::ibc_token_prefix(coin.denom.to_string()) - .map_err(|_| TokenTransferError::InvalidCoin { - coin: coin.to_string(), - })?; - Ok([ - token::multitoken_balance_key(&prefix, account), - token::multitoken_balance_key( - &prefix, - &Address::Internal(mode), - ), - ]) - } - } } impl ModuleWrapper for TransferModule @@ -479,7 +446,10 @@ where // has no prefix let (token, amount) = get_token_amount(coin)?; - let src = if coin.denom.trace_path.is_empty() { + let src = if coin.denom.trace_path.is_empty() + || *from == Address::Internal(InternalAddress::IbcEscrow) + || *from == Address::Internal(InternalAddress::IbcMint) + { token::balance_key(&token, from) } else { let prefix = storage::ibc_token_prefix(coin.denom.to_string()) @@ -489,7 +459,10 @@ where token::multitoken_balance_key(&prefix, from) }; - let dest = if coin.denom.trace_path.is_empty() { + let dest = if coin.denom.trace_path.is_empty() + || *to == Address::Internal(InternalAddress::IbcEscrow) + || *to == Address::Internal(InternalAddress::IbcBurn) + { token::balance_key(&token, to) } else { let prefix = storage::ibc_token_prefix(coin.denom.to_string()) @@ -523,12 +496,20 @@ where ) -> Result<(), TokenTransferError> { let (token, amount) = get_token_amount(coin)?; - let [dest, src] = self.burn_and_mint_coins_addresses( - account, - coin, + let src = token::balance_key( &token, - InternalAddress::IbcMint, - )?; + &Address::Internal(InternalAddress::IbcMint), + ); + + let dest = if coin.denom.trace_path.is_empty() { + token::balance_key(&token, account) + } else { + let prefix = storage::ibc_token_prefix(coin.denom.to_string()) + .map_err(|_| TokenTransferError::InvalidCoin { + coin: coin.to_string(), + })?; + token::multitoken_balance_key(&prefix, account) + }; self.ctx .borrow_mut() @@ -554,12 +535,20 @@ where ) -> Result<(), TokenTransferError> { let (token, amount) = get_token_amount(coin)?; - let [src, dest] = self.burn_and_mint_coins_addresses( - account, - coin, + let src = if coin.denom.trace_path.is_empty() { + token::balance_key(&token, account) + } else { + let prefix = storage::ibc_token_prefix(coin.denom.to_string()) + .map_err(|_| TokenTransferError::InvalidCoin { + coin: coin.to_string(), + })?; + token::multitoken_balance_key(&prefix, account) + }; + + let dest = token::balance_key( &token, - InternalAddress::IbcBurn, - )?; + &Address::Internal(InternalAddress::IbcBurn), + ); self.ctx .borrow_mut() diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 5b83a31ee8..90b05348b2 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -715,7 +715,8 @@ impl TryFrom for Amount { // IBCAMOUNT::MAX OR SIMILAR) if amount > u64::MAX.into() { // return Err(AmountParseError::InvalidRange); //} - DenominatedAmount::from_str(&amount.to_string()).map(|a| a.amount) + DenominatedAmount::from_str(&amount.to_string()) + .map(|a| a.amount * NATIVE_SCALE) } } diff --git a/shared/src/ledger/ibc/vp/token.rs b/shared/src/ledger/ibc/vp/token.rs index cd193a4a81..fa39b19141 100644 --- a/shared/src/ledger/ibc/vp/token.rs +++ b/shared/src/ledger/ibc/vp/token.rs @@ -3,7 +3,6 @@ use std::collections::{BTreeSet, HashMap, HashSet}; use borsh::BorshDeserialize; -use namada_core::ledger::ibc::storage; use prost::Message; use thiserror::Error; @@ -93,23 +92,20 @@ where SignedTxData::try_from_slice(tx_data).map_err(Error::Decoding)?; let tx_data = &signed.data.ok_or(Error::NoTxData)?; - // Check the non-owner balance updates + // Check the non-onwer balance updates let ibc_keys_changed: HashSet = keys_changed .iter() .filter(|k| { matches!( - token::is_any_multitoken_balance_key(k), - Some(( + token::is_any_token_balance_key(k), + Some([ _, - [ - _, - Address::Internal( - InternalAddress::IbcEscrow - | InternalAddress::IbcBurn - | InternalAddress::IbcMint - ) - ] - )) + Address::Internal( + InternalAddress::IbcEscrow + | InternalAddress::IbcBurn + | InternalAddress::IbcMint + ) + ]) ) }) .cloned() @@ -268,8 +264,6 @@ where .map_err(|e| Error::Denom(e.to_string()))?; let amount = Amount::try_from(data.token.amount).map_err(Error::Amount)?; - let prefix = storage::ibc_token_prefix(&data.token.denom.to_string()) - .map_err(|e| Error::Denom(e.to_string()))?; let change = if is_receiver_chain_source( packet.port_id_on_a.clone(), @@ -293,8 +287,8 @@ where } else { // the sender is the source // check the amount of the token has been minted - let source_key = token::multitoken_balance_key( - &prefix, + let source_key = token::balance_key( + &token, &Address::Internal(InternalAddress::IbcMint), ); let post = try_decode_token_amount( @@ -353,7 +347,7 @@ where )? .unwrap_or_default(); // the previous balance of the mint address should be the maximum - Amount::max().change() - post.change() + Amount::max_signed().change() - post.change() }; if change == amount.change() { diff --git a/shared/src/ledger/tx.rs b/shared/src/ledger/tx.rs index 8206a835cd..6b21bb2351 100644 --- a/shared/src/ledger/tx.rs +++ b/shared/src/ledger/tx.rs @@ -953,10 +953,14 @@ pub async fn submit_ibc_transfer< Some(sp) => sp.to_string().replace(RESERVED_ADDRESS_PREFIX, ""), None => token.to_string(), }; - let token = Coin { - denom, - amount: args.amount.to_string_native(), - }; + let amount = args + .amount + .to_string_native() + .split('.') + .next() + .expect("invalid amount") + .to_string(); + let token = Coin { denom, amount }; // this height should be that of the destination chain, not this chain let timeout_height = match args.timeout_height { diff --git a/tests/src/e2e/ibc_tests.rs b/tests/src/e2e/ibc_tests.rs index b9b1c79c35..e3b5ee9101 100644 --- a/tests/src/e2e/ibc_tests.rs +++ b/tests/src/e2e/ibc_tests.rs @@ -755,6 +755,7 @@ fn transfer_received_token( .to_string(); let rpc = get_actor_rpc(test, &Who::Validator(0)); + let amount = Amount::native_whole(50000).to_string_native(); let tx_args = [ "transfer", "--source", @@ -766,7 +767,7 @@ fn transfer_received_token( "--sub-prefix", &sub_prefix, "--amount", - "50000", + &amount, "--gas-amount", "0", "--gas-limit", diff --git a/tests/src/vm_host_env/mod.rs b/tests/src/vm_host_env/mod.rs index 9cc390d799..708217bbae 100644 --- a/tests/src/vm_host_env/mod.rs +++ b/tests/src/vm_host_env/mod.rs @@ -1249,12 +1249,6 @@ mod tests { let (port_id, channel_id, channel_writes) = ibc::prepare_opened_channel(&conn_id, false); writes.extend(channel_writes); - // the origin-specific token - let denom = format!("{}/{}/{}", port_id, channel_id, token); - let key_prefix = ibc_storage::ibc_token_prefix(denom).unwrap(); - let key = token::multitoken_balance_key(&key_prefix, &receiver); - let init_bal = Amount::from_uint(1_000_000_000u64, 0).unwrap(); - writes.insert(key, init_bal.try_to_vec().unwrap()); writes.into_iter().for_each(|(key, val)| { tx_host_env::with(|env| { @@ -1296,8 +1290,8 @@ mod tests { let result = ibc::validate_ibc_vp_from_tx(&env, &tx); assert!(result.expect("validation failed unexpectedly")); // Check if the token was minted - let mint = token::multitoken_balance_key( - &key_prefix, + let mint = token::balance_key( + &token, &address::Address::Internal(address::InternalAddress::IbcMint), ); let result = ibc::validate_token_vp_from_tx(&env, &tx, &mint); From 43ce2af0640604f55f518c184b12b6a7c7f86be5 Mon Sep 17 00:00:00 2001 From: "Raymond E. Pasco" Date: Thu, 25 May 2023 09:23:19 -0400 Subject: [PATCH 063/151] fix ledger e2e tests --- apps/src/lib/client/rpc.rs | 25 +++++++++++++++++-------- apps/src/lib/client/tx.rs | 11 +++++++++-- core/src/ledger/storage_api/token.rs | 4 ++-- core/src/types/uint.rs | 14 +++++++++++++- genesis/e2e-tests-single-node.toml | 2 +- tests/src/e2e/ledger_tests.rs | 2 +- 6 files changed, 43 insertions(+), 15 deletions(-) diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 49bcbd5fec..0db6ea5508 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -44,7 +44,8 @@ use namada::types::storage::{ BlockHeight, BlockResults, Epoch, Key, KeySeg, PrefixValue, TxIndex, }; use namada::types::token::{ - balance_key, Change, DenominatedAmount, MaspDenom, TokenAddress, Transfer, + balance_key, Change, DenominatedAmount, Denomination, MaspDenom, + TokenAddress, Transfer, }; use namada::types::transaction::{ process_tx, AffineCurve, DecryptedTx, EllipticCurve, PairingEngine, TxType, @@ -2849,6 +2850,7 @@ pub async fn validate_amount( amount: InputAmount, token: &Address, sub_prefix: &Option, + force: bool, ) -> token::DenominatedAmount { let input_amount = match amount { InputAmount::Unvalidated(amt) => amt.canonical(), @@ -2861,14 +2863,21 @@ pub async fn validate_amount( .await, ) .unwrap_or_else(|| { - println!( - "No denomination found for token: {token}, the input arguments \ - could - not be parsed." - ); - cli::safe_exit(1); + if force { + println!( + "No denomination found for token: {token}, but --force was \ + passed. Defaulting to the provided denomination." + ); + input_amount.denom + } else { + println!( + "No denomination found for token: {token}, the input \ + arguments could not be parsed." + ); + cli::safe_exit(1); + } }); - if denom < input_amount.denom { + if denom < input_amount.denom && !force { println!( "The input amount contained a higher precision than allowed by \ {token}." diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 9c2b6d3b7c..8807259bbf 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -1788,14 +1788,21 @@ pub async fn submit_transfer(mut ctx: Context, mut args: args::TxTransfer) { None => (None, token::balance_key(&token, &source)), }; // validate the amount given - let validated_amount = - validate_amount(&client, args.amount, &token, &sub_prefix).await; + let validated_amount = validate_amount( + &client, + args.amount, + &token, + &sub_prefix, + args.tx.force, + ) + .await; let validate_fee = validate_amount( &client, args.tx.fee_amount, &fee_token, // TODO: Currently multi-tokens cannot be used to pay fees &None, + args.tx.force, ) .await; diff --git a/core/src/ledger/storage_api/token.rs b/core/src/ledger/storage_api/token.rs index b2abbb8a97..1964a69a98 100644 --- a/core/src/ledger/storage_api/token.rs +++ b/core/src/ledger/storage_api/token.rs @@ -6,11 +6,11 @@ use crate::types::address::Address; use crate::types::storage::DbKeySeg::StringSeg; use crate::types::storage::Key; use crate::types::token; +use crate::types::token::Denomination; pub use crate::types::token::{ balance_key, is_balance_key, is_total_supply_key, total_supply_key, Amount, Change, }; -use crate::types::token::Denomination; /// Read the balance of a given token and owner. pub fn read_balance( @@ -52,7 +52,7 @@ where { if let Some(sub_prefix) = sub_prefix { if sub_prefix.segments.contains(&StringSeg("ibc".to_string())) { - return Ok(Some(Denomination(0))) + return Ok(Some(Denomination(0))); } } let key = token::denom_key(token, sub_prefix); diff --git a/core/src/types/uint.rs b/core/src/types/uint.rs index 1749af7b51..8cc7537733 100644 --- a/core/src/types/uint.rs +++ b/core/src/types/uint.rs @@ -2,6 +2,7 @@ //! An unsigned 256 integer type. Used for, among other things, //! the backing type of token amounts. use std::cmp::Ordering; +use std::fmt::{Debug, Display, Formatter}; use std::ops::{Add, AddAssign, BitXor, Div, Mul, Neg, Sub, SubAssign}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; @@ -83,7 +84,6 @@ const MINUS_ZERO: Uint = Uint([0u64, 0u64, 0u64, 9223372036854775808]); #[derive( Copy, Clone, - Debug, Default, PartialEq, Eq, @@ -333,6 +333,18 @@ impl std::iter::Sum for I256 { } } +impl Display for I256 { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(self.to_string_native().as_str()) + } +} + +impl Debug for I256 { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt(self, f) + } +} + impl TryFrom for i128 { type Error = std::io::Error; diff --git a/genesis/e2e-tests-single-node.toml b/genesis/e2e-tests-single-node.toml index 5fc2d4f231..eb771e2341 100644 --- a/genesis/e2e-tests-single-node.toml +++ b/genesis/e2e-tests-single-node.toml @@ -5,7 +5,7 @@ genesis_time = "2021-09-30T10:00:00Z" native_token = "NAM" faucet_pow_difficulty = 1 -faucet_withdrawal_limit = "1000" +faucet_withdrawal_limit = "1000000000" [validator.validator-0] # Validator's staked NAM at genesis. diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 63dbdc2539..ffd062a793 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -1832,7 +1832,7 @@ fn invalid_transactions() -> Result<()> { "--token", BERTHA, "--amount", - "1_000_000.1", + "1000000.1", "--gas-amount", "0", "--gas-limit", From ae4eb8172e364ec31e192e3bfab4582ab7c52d4a Mon Sep 17 00:00:00 2001 From: yito88 Date: Thu, 25 May 2023 23:44:51 +0900 Subject: [PATCH 064/151] set NATIVE_MAX_DECIMAL_PLACES if denom isn't stored --- apps/src/lib/client/rpc.rs | 7 ++++--- core/src/ledger/storage_api/token.rs | 4 +++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index ae6c423b4d..1155fcd843 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -291,7 +291,7 @@ pub async fn query_transparent_balance< let tokens = wallet.get_addresses_with_vp_type(AddressVpType::Token); match (args.token, args.owner) { (Some(token), Some(owner)) => { - let (_balance_key, sub_prefix) = match &args.sub_prefix { + let (balance_key, sub_prefix) = match &args.sub_prefix { Some(sub_prefix) => { let sub_prefix = Key::parse(sub_prefix).unwrap(); let prefix = @@ -310,8 +310,9 @@ pub async fn query_transparent_balance< ), }; let token_alias = lookup_alias(wallet, &token); - let key = token::balance_key(&token, &owner.address().unwrap()); - match query_storage_value::(&client, &key).await { + match query_storage_value::(&client, &balance_key) + .await + { Some(balance) => { let balance = format_denominated_amount( client, diff --git a/core/src/ledger/storage_api/token.rs b/core/src/ledger/storage_api/token.rs index 24494dbf1d..6bb249ba24 100644 --- a/core/src/ledger/storage_api/token.rs +++ b/core/src/ledger/storage_api/token.rs @@ -49,7 +49,9 @@ where S: StorageRead, { let key = token::denom_key(token, sub_prefix); - storage.read(&key) + storage.read(&key).map(|opt_denom| { + Some(opt_denom.unwrap_or(token::NATIVE_MAX_DECIMAL_PLACES.into())) + }) } /// Write the denomination of a given token. From 41b626700f5e9d6c27c7f53a24539bda60be9aad Mon Sep 17 00:00:00 2001 From: "Raymond E. Pasco" Date: Mon, 29 May 2023 14:30:26 -0400 Subject: [PATCH 065/151] unsorted wips on e2e --- apps/src/lib/client/rpc.rs | 23 +++--- apps/src/lib/client/tx.rs | 30 ++++---- apps/src/lib/config/genesis.rs | 6 +- core/src/ledger/storage/masp_conversions.rs | 8 +-- core/src/types/token.rs | 3 +- core/src/types/uint.rs | 25 ++++++- genesis/dev.toml | 2 +- genesis/e2e-tests-single-node.toml | 80 ++++++++++----------- tests/src/e2e/ledger_tests.rs | 52 +++++++------- wasm/wasm_source/src/vp_masp.rs | 31 +++++++- 10 files changed, 160 insertions(+), 100 deletions(-) diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 0db6ea5508..1b663174d0 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -333,15 +333,13 @@ pub async fn query_transfers(mut ctx: Context, args: args::QueryTransfers) { // Check if this transfer pertains to the supplied token relevant &= match &query_token { Some(token) => { - let check = |(tok, amt): (&TokenAddress, &token::Amount)| { + let check = |(tok, chg): (&TokenAddress, &Change)| { tok.sub_prefix == sub_prefix && &tok.address == token - && !amt.is_zero() + && !chg.is_zero() }; tfer_delta.values().cloned().any( - |MaspChange { ref asset, change }| { - check((asset, &change.into())) - }, + |MaspChange { ref asset, change }| check((asset, &change)), ) || shielded_accounts .values() .cloned() @@ -382,7 +380,7 @@ pub async fn query_transfers(mut ctx: Context, args: args::QueryTransfers) { print!(" {}:", fvk_map[&account]); for (token_addr, val) in masp_change { let token_alias = lookup_alias(&ctx, &token_addr.address); - let sign = match val.cmp(&token::Amount::zero()) { + let sign = match val.cmp(&Change::zero()) { Ordering::Greater => "+", Ordering::Less => "-", Ordering::Equal => "", @@ -390,8 +388,12 @@ pub async fn query_transfers(mut ctx: Context, args: args::QueryTransfers) { print!( " {}{} {}", sign, - format_denominated_amount(&client, &token_addr, val,) - .await, + format_denominated_amount( + &client, + &token_addr, + val.into(), + ) + .await, token_addr.format_with_alias(&token_alias), ); } @@ -1011,7 +1013,10 @@ pub async fn query_shielded_balance( sub_prefix: args.sub_prefix.map(|k| Key::parse(k).unwrap()), }; - let total_balance = balance[&(epoch, token_address.clone())]; + let total_balance = balance + .get(&(epoch, token_address.clone())) + .cloned() + .unwrap_or_default(); if total_balance.is_zero() { println!( "No shielded {} balance found for given key", diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 8807259bbf..5ac3f90637 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -524,6 +524,14 @@ pub struct MaspChange { )] pub struct MaspAmount(HashMap<(Epoch, TokenAddress), token::Change>); +impl MaspAmount { + pub fn pop(&mut self) -> Option<((Epoch, TokenAddress), token::Change)>{ + let key = self.keys().next()?.clone(); + let value = self.remove(&key).unwrap(); + Some((key, value)) + } +} + impl std::ops::Deref for MaspAmount { type Target = HashMap<(Epoch, TokenAddress), token::Change>; @@ -1189,9 +1197,8 @@ impl ShieldedContext { // Where we will store our exchanged value let mut output = Amount::zero(); // Repeatedly exchange assets until it is no longer possible - while let Some(((asset_epoch, token_addr), value)) = - input.iter().next().map(cloned_pair) - { + loop { + let Some(((asset_epoch, token_addr), value)) = input.pop() else { break }; for denom in MaspDenom::iter() { let target_asset_type = make_asset_type( Some(target_epoch), @@ -1467,7 +1474,7 @@ impl ShieldedContext { client: HttpClient, amt: Amount, target_epoch: Epoch, - ) -> HashMap { + ) -> HashMap { let mut res = HashMap::new(); for (asset_type, val) in amt.components() { // Decode the asset type @@ -1524,7 +1531,7 @@ impl ShieldedContext { ) } } - MaspAmount(res.into_iter().map(|(k, v)| (k, v.change())).collect()) + MaspAmount(res) } } @@ -3413,20 +3420,17 @@ pub async fn submit_tx( fn decode_component( (addr, sub, denom, epoch): (Address, Option, MaspDenom, Epoch), val: i64, - res: &mut HashMap, + res: &mut HashMap, mk_key: F, ) where F: FnOnce(Address, Option, Epoch) -> K, K: Eq + std::hash::Hash, { - let decoded_amount = token::Amount::from_uint( - u64::try_from(val).expect("negative cash does not exist"), - denom as u8, - ) - .unwrap(); + let decoded_change = token::Change::from_masp_denominated(val, denom) + .expect("expected this to fit"); res.entry(mk_key(addr, sub, epoch)) - .and_modify(|val| *val += decoded_amount) - .or_insert(decoded_amount); + .and_modify(|val| *val += decoded_change) + .or_insert(decoded_change); } #[cfg(test)] diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index 4136f5c665..3bb6bb0db7 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -213,7 +213,7 @@ pub mod genesis_config { pub vp: Option, // Initial balances held by accounts defined elsewhere. // XXX: u64 doesn't work with toml-rs! - pub balances: Option>, + pub balances: Option>, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -460,7 +460,9 @@ pub mod genesis_config { } } }, - token::Amount::native_whole(*amount), + token::Amount::from_uint(*amount, config.denom).expect( + "expected a balance that fits into 256 bits", + ), ) }) .collect(), diff --git a/core/src/ledger/storage/masp_conversions.rs b/core/src/ledger/storage/masp_conversions.rs index 37ce49892f..4dec02d4f5 100644 --- a/core/src/ledger/storage/masp_conversions.rs +++ b/core/src/ledger/storage/masp_conversions.rs @@ -65,12 +65,8 @@ where // with the zeroth epoch to minimize the number of convert notes clients // have to use. This trick works under the assumption that reward tokens // from different epochs are exactly equivalent. - let reward_asset_bytes = - (address::nam(), None::, MaspDenom::Zero, 0u64) - .try_to_vec() - .expect("unable to serialize address and epoch"); - let reward_asset = AssetType::new(reward_asset_bytes.as_ref()) - .expect("unable to derive asset identifier"); + let reward_asset = + encode_asset_type(address::nam(), &None, MaspDenom::Zero, Epoch(0)); // Conversions from the previous to current asset for each address let mut current_convs = BTreeMap::<(Address, Option, MaspDenom), AllowedConversion>::new(); diff --git a/core/src/types/token.rs b/core/src/types/token.rs index ed5e58f5d2..dccef5683d 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -685,7 +685,8 @@ impl From for MaspDenom { impl MaspDenom { /// Iterator over the possible denominations pub fn iter() -> impl Iterator { - (0u8..3).map(Self::from) + // 0, 1, 2, 3 + (0u8..4).map(Self::from) } /// Get the corresponding u64 word from the input uint256. diff --git a/core/src/types/uint.rs b/core/src/types/uint.rs index 8cc7537733..ff20e28fd5 100644 --- a/core/src/types/uint.rs +++ b/core/src/types/uint.rs @@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize}; use uint::construct_uint; use crate::types::token; -use crate::types::token::Amount; +use crate::types::token::{Amount, AmountParseError, MaspDenom}; construct_uint! { /// Namada native type to replace for unsigned 256 bit @@ -166,6 +166,29 @@ impl I256 { pub fn maximum() -> Self { Self(MAX_SIGNED_VALUE) } + + /// Attempt to convert a MASP-denominated integer to an I256 + /// using the given denomination. + pub fn from_masp_denominated( + value: impl Into, + denom: MaspDenom, + ) -> Result { + let value = value.into(); + let is_negative = value < 0; + let value = value.unsigned_abs(); + let mut result = [0u64; 4]; + result[denom as usize] = value; + let result = Uint(result); + if result <= MAX_SIGNED_VALUE { + if is_negative { + Ok(Self(result.negate()).canonical()) + } else { + Ok(Self(result).canonical()) + } + } else { + Err(AmountParseError::InvalidRange) + } + } } impl From for I256 { diff --git a/genesis/dev.toml b/genesis/dev.toml index 620298db3d..e2d5c1e39f 100644 --- a/genesis/dev.toml +++ b/genesis/dev.toml @@ -36,7 +36,7 @@ a1qyqzsqqqqqcyvvf5xcu5vd6rg4z5233hg9pn23pjgdryzdjy8pz52wzxxscnvvjxx3rryvzz8y5p6m atest1v4ehgw36x3qng3jzggu5yvpsxgcngv2xgguy2dpkgvu5x33kx3pr2w2zgep5xwfkxscrxs2pj8075p = 1000000 atest1v4ehgw36xvcyyvejgvenxs34g3zygv3jxqunjd6rxyeyys3sxy6rwvfkx4qnj33hg9qnvse4lsfctw = 1000000 # 2. An alias of any account -bertha = 1000000 +Bertha = "1000000" # 3. A public key of a validator or an established account from which the # address of the implicit account is derived) "bertha.public_key" = 100 diff --git a/genesis/e2e-tests-single-node.toml b/genesis/e2e-tests-single-node.toml index eb771e2341..33114e3640 100644 --- a/genesis/e2e-tests-single-node.toml +++ b/genesis/e2e-tests-single-node.toml @@ -30,71 +30,71 @@ address = "atest1v4ehgw36x3prswzxggunzv6pxqmnvdj9xvcyzvpsggeyvs3cg9qnywf589qnwvf denom = 6 vp = "vp_token" [token.NAM.balances] -Albert = 1000000 -"Albert.public_key" = 100 -Bertha = 1000000 -"Bertha.public_key" = 2000 -Christel = 1000000 -"Christel.public_key" = 100 -Daewon = 1000000 -faucet = 9223372036 -"faucet.public_key" = 100 -"validator-0.public_key" = 100 +Albert = "1000000" +"Albert.public_key" = "100" +Bertha = "1000000" +"Bertha.public_key" = "2000" +Christel = "1000000" +"Christel.public_key" = "100" +Daewon = "1000000" +faucet = "9223372036854" +"faucet.public_key" = "100" +"validator-0.public_key" = "100" [token.BTC] address = "atest1v4ehgw36xdzryve5gsc52veeg5cnsv2yx5eygvp38qcrvd29xy6rys6p8yc5xvp4xfpy2v694wgwcp" denom = 8 vp = "vp_token" [token.BTC.balances] -Albert = 1000000 -Bertha = 1000000 -Christel = 1000000 -Daewon = 1000000 -faucet = 9223372036854 +Albert = "1000000" +Bertha = "1000000" +Christel = "1000000" +Daewon = "1000000" +faucet = "9223372036854" [token.ETH] address = "atest1v4ehgw36xqmr2d3nx3ryvd2xxgmrq33j8qcns33sxezrgv6zxdzrydjrxveygd2yxumrsdpsf9jc2p" denom = 18 vp = "vp_token" [token.ETH.balances] -Albert = 1000000 -Bertha = 1000000 -Christel = 1000000 -Daewon = 1000000 -faucet = 9223372036854 +Albert = "1000000" +Bertha = "1000000" +Christel = "1000000" +Daewon = "1000000" +faucet = "9223372036854" [token.DOT] address = "atest1v4ehgw36gg6nvs2zgfpyxsfjgc65yv6pxy6nwwfsxgungdzrggeyzv35gveyxsjyxymyz335hur2jn" denom = 10 vp = "vp_token" [token.DOT.balances] -Albert = 1000000 -Bertha = 1000000 -Christel = 1000000 -Daewon = 1000000 -faucet = 9223372036854 +Albert = "1000000" +Bertha = "1000000" +Christel = "1000000" +Daewon = "1000000" +faucet = "9223372036854" [token.Schnitzel] address = "atest1v4ehgw36xue5xvf5xvuyzvpjx5un2v3k8qeyvd3cxdqns32p89rrxd6xx9zngvpegccnzs699rdnnt" denom = 6 vp = "vp_token" [token.Schnitzel.balances] -Albert = 1000000 -Bertha = 1000000 -Christel = 1000000 -Daewon = 1000000 -faucet = 9223372036854 +Albert = "1000000" +Bertha = "1000000" +Christel = "1000000" +Daewon = "1000000" +faucet = "9223372036854" [token.Apfel] address = "atest1v4ehgw36gfryydj9g3p5zv3kg9znyd358ycnzsfcggc5gvecgc6ygs2rxv6ry3zpg4zrwdfeumqcz9" denom = 6 vp = "vp_token" [token.Apfel.balances] -Albert = 1000000 -Bertha = 1000000 -Christel = 1000000 -Daewon = 1000000 -faucet = 9223372036854 +Albert = "1000000" +Bertha = "1000000" +Christel = "1000000" +Daewon = "1000000" +faucet = "9223372036854" [token.Kartoffel] address = "atest1v4ehgw36gep5ysecxq6nyv3jg3zygv3e89qn2vp48pryxsf4xpznvve5gvmy23fs89pryvf5a6ht90" @@ -102,11 +102,11 @@ public_key = "" denom = 6 vp = "vp_token" [token.Kartoffel.balances] -Albert = 1000000 -Bertha = 1000000 -Christel = 1000000 -Daewon = 1000000 -faucet = 9223372036854 +Albert = "1000000" +Bertha = "1000000" +Christel = "1000000" +Daewon = "1000000" +faucet = "9223372036854" # Some established accounts present at genesis. [established.faucet] diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index ffd062a793..3a78294e41 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -1069,6 +1069,8 @@ fn masp_incentives() -> Result<()> { ], Some(300) )?; + client.exp_string("Transaction accepted")?; + client.exp_string("Transaction applied")?; client.exp_string("Transaction is valid")?; client.assert_success(); @@ -1085,7 +1087,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string("btc: 20")?; client.assert_success(); @@ -1103,7 +1105,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string("No shielded nam balance found")?; client.assert_success(); @@ -1126,7 +1128,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string("btc: 20")?; client.assert_success(); @@ -1149,7 +1151,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string(&format!( "nam: {}", @@ -1171,7 +1173,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string(&format!( "nam: {}", @@ -1196,7 +1198,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string("btc: 20")?; client.assert_success(); @@ -1214,7 +1216,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string(&format!( "nam: {}", @@ -1236,7 +1238,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string(&format!( "nam: {}", @@ -1283,7 +1285,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string("eth: 30")?; client.assert_success(); @@ -1301,7 +1303,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string("No shielded nam balance found")?; client.assert_success(); @@ -1322,7 +1324,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string("eth: 30")?; client.assert_success(); @@ -1340,7 +1342,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string(&format!( "nam: {}", @@ -1363,7 +1365,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string(&format!( "nam: {}", @@ -1413,7 +1415,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string("No shielded eth balance found")?; client.assert_success(); @@ -1433,7 +1435,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string(&format!( "nam: {}", @@ -1457,7 +1459,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string(&format!( "nam: {}", @@ -1507,7 +1509,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string("No shielded btc balance found")?; client.assert_success(); @@ -1525,7 +1527,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string(&format!( "nam: {}", @@ -1548,7 +1550,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string(&format!( "nam: {}", @@ -1574,7 +1576,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string(&format!( "nam: {}", @@ -1596,7 +1598,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string(&format!( "nam: {}", @@ -1619,7 +1621,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string(&format!( "nam: {}", @@ -1699,7 +1701,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string("No shielded nam balance found")?; client.assert_success(); @@ -1717,7 +1719,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string("No shielded nam balance found")?; client.assert_success(); @@ -1735,7 +1737,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string("nam: 0")?; client.assert_success(); diff --git a/wasm/wasm_source/src/vp_masp.rs b/wasm/wasm_source/src/vp_masp.rs index 5dd02a95b5..7fee57705b 100644 --- a/wasm/wasm_source/src/vp_masp.rs +++ b/wasm/wasm_source/src/vp_masp.rs @@ -14,10 +14,11 @@ use ripemd::{Digest, Ripemd160}; fn asset_type_from_epoched_address( epoch: Epoch, token: &Address, + sub_prefix: String, denom: token::MaspDenom, ) -> AssetType { // Timestamp the chosen token with the current epoch - let token_bytes = (token, denom, epoch.0) + let token_bytes = (token, sub_prefix, denom, epoch.0) .try_to_vec() .expect("token should serialize"); // Generate the unique asset identifier from the unique token address @@ -63,10 +64,19 @@ fn valid_transfer_amount( fn convert_amount( epoch: Epoch, token: &Address, + sub_prefix: &Option, val: token::Amount, denom: token::MaspDenom, ) -> (AssetType, Amount) { - let asset_type = asset_type_from_epoched_address(epoch, token, denom); + let asset_type = asset_type_from_epoched_address( + epoch, + token, + sub_prefix + .as_ref() + .map(|k| k.to_string()) + .unwrap_or_default(), + denom, + ); // Combine the value and unit into one amount let amount = Amount::from_nonnegative(asset_type, denom.denominate(&val)) .expect("invalid value or asset type for amount"); @@ -102,6 +112,7 @@ fn validate_tx( transparent_tx_pool += shielded_tx.value_balance.clone(); if transfer.source != masp() { + log_string("transparent input"); // Handle transparent input // Note that the asset type is timestamped so shields // where the shielded value has an incorrect timestamp @@ -110,10 +121,12 @@ fn validate_tx( let (_transp_asset, transp_amt) = convert_amount( ctx.get_block_epoch().unwrap(), &transfer.token, + &transfer.sub_prefix, transfer.amount.into(), denom, ); + log_string(format!("transparent amount: {:?}", transp_amt)); // Non-masp sources add to transparent tx pool transparent_tx_pool += transp_amt; } @@ -135,6 +148,7 @@ fn validate_tx( } if transfer.target != masp() { + log_string("transparent output"); // Handle transparent output // The following boundary conditions must be satisfied // 1. One to 4 transparent outputs @@ -166,6 +180,11 @@ fn validate_tx( asset_type_from_epoched_address( ctx.get_block_epoch().unwrap(), &transfer.token, + transfer + .sub_prefix + .as_ref() + .map(|k| k.to_string()) + .unwrap_or_default(), denom, ); @@ -185,6 +204,7 @@ fn validate_tx( let (_transp_asset, transp_amt) = convert_amount( ctx.get_block_epoch().unwrap(), &transfer.token, + &transfer.sub_prefix, transfer.amount.amount, denom, ); @@ -227,6 +247,7 @@ fn validate_tx( // Satisfies 1. if !shielded_tx.vout.is_empty() { + log_string(format!("transparent vout {:?}", shielded_tx.vout)); debug_log!( "Transparent output to a transaction from the masp must \ be 0 but is {}", @@ -245,6 +266,11 @@ fn validate_tx( ); // Section 3.4: The remaining value in the transparent // transaction value pool MUST be nonnegative. + log_string(format!( + "would give the masp a negative balance; transparent tx \ + {:?}", + transparent_tx_pool + )); return reject(); } _ => {} @@ -252,5 +278,6 @@ fn validate_tx( } // Do the expensive proof verification in the VM at the end. + log_string("reached proof verification"); ctx.verify_masp(data) } From 6e5782bdb8a95bc11a60530bfdd65cfa412d5b9c Mon Sep 17 00:00:00 2001 From: satan Date: Tue, 30 May 2023 17:00:17 +0200 Subject: [PATCH 066/151] WIP fixing masp amounts --- Cargo.lock | 4 +- Makefile | 2 +- apps/Cargo.toml | 4 +- apps/src/lib/client/rpc.rs | 2 +- apps/src/lib/client/tx.rs | 37 ++++++----- core/Cargo.toml | 2 +- core/src/types/uint.rs | 4 +- shared/Cargo.toml | 4 +- shared/src/ledger/masp.rs | 2 +- tests/src/e2e/ledger_tests.rs | 96 +++++++++++++++------------- tx_prelude/Cargo.toml | 2 +- vm_env/Cargo.toml | 4 +- wasm/Cargo.lock | 4 +- wasm/checksums.json | 36 +++++------ wasm/wasm_source/Cargo.toml | 4 +- wasm/wasm_source/src/vp_implicit.rs | 3 +- wasm/wasm_source/src/vp_user.rs | 3 +- wasm/wasm_source/src/vp_validator.rs | 3 +- 18 files changed, 108 insertions(+), 108 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8168c7f218..2beee040c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3484,7 +3484,7 @@ dependencies = [ [[package]] name = "masp_primitives" version = "0.5.0" -source = "git+https://github.com/anoma/masp?rev=bee40fc465f6afbd10558d12fe96eb1742eee45c#bee40fc465f6afbd10558d12fe96eb1742eee45c" +source = "git+https://github.com/anoma/masp?rev=402b08accff6cf656b7f63823d074931cdcf7773#402b08accff6cf656b7f63823d074931cdcf7773" dependencies = [ "aes", "bip0039", @@ -3517,7 +3517,7 @@ dependencies = [ [[package]] name = "masp_proofs" version = "0.5.0" -source = "git+https://github.com/anoma/masp?rev=bee40fc465f6afbd10558d12fe96eb1742eee45c#bee40fc465f6afbd10558d12fe96eb1742eee45c" +source = "git+https://github.com/anoma/masp?rev=402b08accff6cf656b7f63823d074931cdcf7773#402b08accff6cf656b7f63823d074931cdcf7773" dependencies = [ "bellman", "blake2b_simd 1.0.0", diff --git a/Makefile b/Makefile index b65e8b698d..756fc222b5 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ endif audit-ignores += RUSTSEC-2021-0076 build: - $(cargo) $(jobs) build + $(cargo) build $(jobs) build-test: $(cargo) +$(nightly) build --tests $(jobs) -Z unstable-options diff --git a/apps/Cargo.toml b/apps/Cargo.toml index ee1e99ea4b..5e8b337f0e 100644 --- a/apps/Cargo.toml +++ b/apps/Cargo.toml @@ -148,8 +148,8 @@ tracing-subscriber = {version = "0.3.7", features = ["env-filter", "json"]} websocket = "0.26.2" winapi = "0.3.9" #libmasp = { git = "https://github.com/anoma/masp", branch = "murisi/masp-incentive" } -masp_primitives = { git = "https://github.com/anoma/masp", rev = "bee40fc465f6afbd10558d12fe96eb1742eee45c", features = ["transparent-inputs"] } -masp_proofs = { git = "https://github.com/anoma/masp", rev = "bee40fc465f6afbd10558d12fe96eb1742eee45c", features = ["bundled-prover", "download-params"] } +masp_primitives = { git = "https://github.com/anoma/masp", rev = "402b08accff6cf656b7f63823d074931cdcf7773", features = ["transparent-inputs"] } +masp_proofs = { git = "https://github.com/anoma/masp", rev = "402b08accff6cf656b7f63823d074931cdcf7773", features = ["bundled-prover", "download-params"] } bimap = {version = "0.6.2", features = ["serde"]} [dev-dependencies] diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 1b663174d0..d90283aa9f 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -44,7 +44,7 @@ use namada::types::storage::{ BlockHeight, BlockResults, Epoch, Key, KeySeg, PrefixValue, TxIndex, }; use namada::types::token::{ - balance_key, Change, DenominatedAmount, Denomination, MaspDenom, + balance_key, Change, DenominatedAmount, MaspDenom, TokenAddress, Transfer, }; use namada::types::transaction::{ diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 5ac3f90637..973580a653 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -493,11 +493,6 @@ pub fn is_amount_required(src: Amount, dest: Amount, delta: Amount) -> bool { false } -/// An extension of Option's cloned method for pair types -fn cloned_pair((a, b): (&T, &U)) -> (T, U) { - (a.clone(), b.clone()) -} - /// Errors that can occur when trying to retrieve pinned transaction #[derive(PartialEq, Eq, Clone, Copy)] pub enum PinnedBalanceError { @@ -555,6 +550,7 @@ impl std::ops::Add for MaspAmount { .and_modify(|val| *val += value) .or_insert(value); } + self.retain(|_, v| !v.is_zero()); self } } @@ -606,7 +602,7 @@ impl<'a> From<&'a MaspAmount> for Amount { /// Represents the amount used of different conversions pub type Conversions = - HashMap, i64)>; + HashMap, i128)>; /// Represents an amount that is pub type MaspDenominatedAmount = Amount<(TokenAddress, MaspDenom)>; @@ -1087,7 +1083,7 @@ impl ShieldedContext { client: HttpClient, asset_type: AssetType, conversions: &'a mut Conversions, - ) -> Option<&'a mut (AllowedConversion, MerklePath, i64)> { + ) -> Option<&'a mut (AllowedConversion, MerklePath, i128)> { match conversions.entry(asset_type) { Entry::Occupied(conv_entry) => Some(conv_entry.into_mut()), Entry::Vacant(conv_entry) => { @@ -1152,8 +1148,8 @@ impl ShieldedContext { client: &HttpClient, conv: AllowedConversion, asset_type: AssetType, - value: i64, - usage: &mut i64, + value: i128, + usage: &mut i128, input: &mut MaspAmount, output: &mut Amount, ) { @@ -1170,6 +1166,9 @@ impl ShieldedContext { } // We should use an amount of the AllowedConversion that almost // cancels the original amount + if threshold > value { + return + } let required = value / threshold; // Forget about the trace amount left over because we cannot // realize its value @@ -1178,7 +1177,7 @@ impl ShieldedContext { *usage += required; // Apply the conversions to input and move the trace amount to output *input += self - .decode_all_amounts(client, conv * required - &trace) + .decode_all_amounts(client, conv.clone() * required - &trace) .await; *output += trace; } @@ -1214,7 +1213,7 @@ impl ShieldedContext { ); let at_target_asset_type = target_epoch == asset_epoch; - let denom_value = denom.denominate_i64(&value); + let denom_value = denom.denominate(&token::Amount::from(value)) as i128; _ = self .query_allowed_conversion( client.clone(), @@ -1237,7 +1236,7 @@ impl ShieldedContext { self.apply_conversion( &client, conv.clone(), - target_asset_type, + asset_type, denom_value, usage, &mut input, @@ -1266,26 +1265,24 @@ impl ShieldedContext { self.apply_conversion( &client, conv.clone(), - target_asset_type, + asset_type, denom_value, usage, &mut input, &mut output, ) - .await; + .await; } else { // At the target asset type. Then move component over to // output. - let mut comp = MaspAmount::default(); + comp.insert((asset_epoch, token_addr.clone()), denom_value.into()); for ((e, key), val) in input.iter() { if *key == token_addr { comp.insert((*e, key.clone()), *val); } } output += Amount::from(&comp); - // Strike from input to avoid repeating computation - input -= comp; } } } @@ -1501,7 +1498,6 @@ impl ShieldedContext { res } - // TODO :: Panics if we ever switch to an i128 in the masp crate /// Convert an amount whose units are AssetTypes to one whose units are /// Addresses that they decode to. pub async fn decode_all_amounts( @@ -1542,11 +1538,14 @@ fn convert_amount( sub_prefix: &Option<&String>, val: &token::Amount, ) -> ([AssetType; 4], Amount) { + println!("{}", DenominatedAmount { amount: val.clone(), denom: 18.into()}); let mut amount = Amount::zero(); let asset_types: [AssetType; 4] = MaspDenom::iter() .map(|denom| { let asset_type = make_asset_type(Some(epoch), token, sub_prefix, denom); + let inner = denom.denominate(val); + println!("{}", inner); // Combine the value and unit into one amount amount += Amount::from_nonnegative(asset_type, denom.denominate(val)) @@ -3419,7 +3418,7 @@ pub async fn submit_tx( fn decode_component( (addr, sub, denom, epoch): (Address, Option, MaspDenom, Epoch), - val: i64, + val: i128, res: &mut HashMap, mk_key: F, ) where diff --git a/core/Cargo.toml b/core/Cargo.toml index 806ac757f4..3a68ca3abc 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -83,7 +83,7 @@ impl-num-traits = "0.1.2" index-set = {git = "https://github.com/heliaxdev/index-set", tag = "v0.7.1", features = ["serialize-borsh", "serialize-serde"]} itertools = "0.10.0" libsecp256k1 = {git = "https://github.com/heliaxdev/libsecp256k1", rev = "bbb3bd44a49db361f21d9db80f9a087c194c0ae9", default-features = false, features = ["std", "static-context"]} -masp_primitives = { git = "https://github.com/anoma/masp", rev = "bee40fc465f6afbd10558d12fe96eb1742eee45c" } +masp_primitives = { git = "https://github.com/anoma/masp", rev = "402b08accff6cf656b7f63823d074931cdcf7773" } proptest = {git = "https://github.com/heliaxdev/proptest", branch = "tomas/sm", optional = true} prost = "0.9.0" prost-types = "0.9.0" diff --git a/core/src/types/uint.rs b/core/src/types/uint.rs index ff20e28fd5..640f79ee6c 100644 --- a/core/src/types/uint.rs +++ b/core/src/types/uint.rs @@ -170,14 +170,14 @@ impl I256 { /// Attempt to convert a MASP-denominated integer to an I256 /// using the given denomination. pub fn from_masp_denominated( - value: impl Into, + value: impl Into, denom: MaspDenom, ) -> Result { let value = value.into(); let is_negative = value < 0; let value = value.unsigned_abs(); let mut result = [0u64; 4]; - result[denom as usize] = value; + result[denom as usize] = value as u64; let result = Uint(result); if result <= MAX_SIGNED_VALUE { if is_negative { diff --git a/shared/Cargo.toml b/shared/Cargo.toml index d9dc8ad02a..0f059bc663 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -98,6 +98,8 @@ ibc = {version = "0.14.0", default-features = false, optional = true} ibc-proto = {version = "0.17.1", default-features = false, optional = true} itertools = "0.10.0" loupe = {version = "0.1.3", optional = true} +masp_primitives = { git = "https://github.com/anoma/masp", rev = "402b08accff6cf656b7f63823d074931cdcf7773" } +masp_proofs = { git = "https://github.com/anoma/masp", rev = "402b08accff6cf656b7f63823d074931cdcf7773" } parity-wasm = {version = "0.45.0", features = ["sign_ext"], optional = true} paste = "1.0.9" # A fork with state machine testing @@ -125,8 +127,6 @@ wasmer-engine-universal = {version = "=2.2.0", optional = true} wasmer-vm = {version = "2.2.0", optional = true} wasmparser = "0.83.0" #libmasp = { git = "https://github.com/anoma/masp", branch = "murisi/masp-incentive" } -masp_primitives = { git = "https://github.com/anoma/masp", rev = "bee40fc465f6afbd10558d12fe96eb1742eee45c" } -masp_proofs = { git = "https://github.com/anoma/masp", rev = "bee40fc465f6afbd10558d12fe96eb1742eee45c" } zeroize = "1.5.5" [dev-dependencies] diff --git a/shared/src/ledger/masp.rs b/shared/src/ledger/masp.rs index 10c63db4cb..8c51803622 100644 --- a/shared/src/ledger/masp.rs +++ b/shared/src/ledger/masp.rs @@ -176,7 +176,7 @@ pub fn verify_shielded_tx(transaction: &Transaction) -> bool { tracing::info!("passed spend/output verification"); - let assets_and_values: Vec<(AssetType, i64)> = + let assets_and_values: Vec<(AssetType, i128)> = tx_data.value_balance.clone().into_components().collect(); tracing::info!("accumulated {} assets/values", assets_and_values.len()); diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 3a78294e41..26582aa20e 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -27,7 +27,7 @@ use namada_apps::client::tx::ShieldedContext; use namada_apps::config::genesis::genesis_config::{ GenesisConfig, ParametersConfig, PosParamsConfig, }; -use namada_core::types::token::NATIVE_MAX_DECIMAL_PLACES; +use namada_core::types::token::{DenominatedAmount, Denomination, NATIVE_MAX_DECIMAL_PLACES}; use namada_test_utils::TestWasms; use serde_json::json; use setup::constants::*; @@ -1014,6 +1014,10 @@ fn masp_pinned_txs() -> Result<()> { #[test] fn masp_incentives() -> Result<()> { + // The number of decimal places used by BTC amounts. + const BTC_DENOMINATION: u8 = 8; + // The number of decimal places used by ETH amounts. + const ETH_DENOMINATION: u8 = 18; // Download the shielded pool parameters before starting node let _ = ShieldedContext::new(PathBuf::new()); // Lengthen epoch to ensure that a transaction can be constructed and @@ -1134,9 +1138,9 @@ fn masp_incentives() -> Result<()> { client.assert_success(); let amt20 = - token::Amount::from_uint(20, NATIVE_MAX_DECIMAL_PLACES).unwrap(); + token::Amount::from_uint(20, BTC_DENOMINATION).unwrap(); let amt30 = - token::Amount::from_uint(30, NATIVE_MAX_DECIMAL_PLACES).unwrap(); + token::Amount::from_uint(30, ETH_DENOMINATION).unwrap(); // Assert NAM balance at VK(A) is 20*BTC_reward*(epoch_1-epoch_0) let mut client = run!( @@ -1153,10 +1157,10 @@ fn masp_incentives() -> Result<()> { ], Some(60) )?; + let amt = (amt20 * masp_rewards[&(btc(), None)]).0 * (ep1.0 - ep0.0); + let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; client.exp_string(&format!( - "nam: {}", - ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep1.0 - ep0.0)) - .to_string_native() + "nam: {}", denominated, ))?; client.assert_success(); @@ -1175,10 +1179,10 @@ fn masp_incentives() -> Result<()> { ], Some(60) )?; + let amt = (amt20 * masp_rewards[&(btc(), None)]).0 * (ep1.0 - ep0.0); + let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into() }; client.exp_string(&format!( - "nam: {}", - ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep1.0 - ep0.0)) - .to_string_native() + "nam: {}", denominated, ))?; client.assert_success(); @@ -1218,10 +1222,10 @@ fn masp_incentives() -> Result<()> { ], Some(60) )?; + let amt = (amt20 * masp_rewards[&(btc(), None)]).0 * (ep2.0 - ep0.0); + let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; client.exp_string(&format!( - "nam: {}", - ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep2.0 - ep0.0)) - .to_string_native() + "nam: {}", denominated, ))?; client.assert_success(); @@ -1240,10 +1244,10 @@ fn masp_incentives() -> Result<()> { ], Some(60) )?; + let amt = (amt20 * masp_rewards[&(btc(), None)]).0 * (ep2.0 - ep0.0); + let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; client.exp_string(&format!( - "nam: {}", - ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep2.0 - ep0.0)) - .to_string_native(), + "nam: {}", denominated, ))?; client.assert_success(); @@ -1344,10 +1348,10 @@ fn masp_incentives() -> Result<()> { ], Some(60) )?; + let amt = (amt30 * masp_rewards[&(eth(), None)]).0 * (ep4.0 - ep3.0); + let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; client.exp_string(&format!( - "nam: {}", - ((amt30 * masp_rewards[&(eth(), None)]).0 * (ep4.0 - ep3.0)) - .to_string_native(), + "nam: {}", denominated, ))?; client.assert_success(); @@ -1367,11 +1371,11 @@ fn masp_incentives() -> Result<()> { ], Some(60) )?; + let amt = ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep4.0 - ep0.0)) + + ((amt30 * masp_rewards[&(eth(), None)]).0 * (ep4.0 - ep3.0)); + let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; client.exp_string(&format!( - "nam: {}", - (((amt20 * masp_rewards[&(btc(), None)]).0 * (ep4.0 - ep0.0)) - + ((amt30 * masp_rewards[&(eth(), None)]).0 * (ep4.0 - ep3.0))) - .to_string_native() + "nam: {}", denominated ))?; client.assert_success(); @@ -1437,10 +1441,10 @@ fn masp_incentives() -> Result<()> { ], Some(60) )?; + let amt = (amt30 * masp_rewards[&(eth(), None)]).0 * (ep.0 - ep3.0); + let denominated = DenominatedAmount{ amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; client.exp_string(&format!( - "nam: {}", - ((amt30 * masp_rewards[&(eth(), None)]).0 * (ep.0 - ep3.0)) - .to_string_native() + "nam: {}", denominated, ))?; client.assert_success(); @@ -1461,11 +1465,11 @@ fn masp_incentives() -> Result<()> { ], Some(60) )?; + let amt = ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep.0 - ep0.0)) + + ((amt30 * masp_rewards[&(eth(), None)]).0 * (ep.0 - ep3.0)); + let denominated = DenominatedAmount{ amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; client.exp_string(&format!( - "nam: {}", - (((amt20 * masp_rewards[&(btc(), None)]).0 * (ep.0 - ep0.0)) - + ((amt30 * masp_rewards[&(eth(), None)]).0 * (ep.0 - ep3.0))) - .to_string_native() + "nam: {}", denominated ))?; client.assert_success(); @@ -1529,10 +1533,10 @@ fn masp_incentives() -> Result<()> { ], Some(60) )?; + let amt = (amt20 * masp_rewards[&(btc(), None)]).0 * (ep6.0 - ep0.0); + let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; client.exp_string(&format!( - "nam: {}", - ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep6.0 - ep0.0)) - .to_string_native() + "nam: {}", denominated, ))?; client.assert_success(); @@ -1552,11 +1556,11 @@ fn masp_incentives() -> Result<()> { ], Some(60) )?; + let amt = ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep6.0 - ep0.0)) + + ((amt30 * masp_rewards[&(eth(), None)]).0 * (ep5.0 - ep3.0)); + let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into() }; client.exp_string(&format!( - "nam: {}", - (((amt20 * masp_rewards[&(btc(), None)]).0 * (ep6.0 - ep0.0)) - + ((amt30 * masp_rewards[&(eth(), None)]).0 * (ep5.0 - ep3.0))) - .to_string_native() + "nam: {}", denominated, ))?; client.assert_success(); @@ -1578,10 +1582,10 @@ fn masp_incentives() -> Result<()> { ], Some(60) )?; + let amt = (amt20 * masp_rewards[&(btc(), None)]).0 * (ep6.0 - ep0.0); + let denominated = DenominatedAmount {amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; client.exp_string(&format!( - "nam: {}", - ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep6.0 - ep0.0)) - .to_string_native() + "nam: {}", denominated ))?; client.assert_success(); @@ -1600,10 +1604,10 @@ fn masp_incentives() -> Result<()> { ], Some(60) )?; + let amt = (amt30 * masp_rewards[&(eth(), None)]).0 * (ep5.0 - ep3.0); + let denominated = DenominatedAmount {amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; client.exp_string(&format!( - "nam: {}", - ((amt30 * masp_rewards[&(eth(), None)]).0 * (ep5.0 - ep3.0)) - .to_string_native() + "nam: {}", denominated, ))?; client.assert_success(); @@ -1623,11 +1627,11 @@ fn masp_incentives() -> Result<()> { ], Some(60) )?; + let amt = ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep6.0 - ep0.0)) + + ((amt30 * masp_rewards[&(eth(), None)]).0 * (ep5.0 - ep3.0)); + let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; client.exp_string(&format!( - "nam: {}", - (((amt20 * masp_rewards[&(btc(), None)]).0 * (ep6.0 - ep0.0)) - + ((amt30 * masp_rewards[&(eth(), None)]).0 * (ep5.0 - ep3.0))) - .to_string_native() + "nam: {}", denominated, ))?; client.assert_success(); diff --git a/tx_prelude/Cargo.toml b/tx_prelude/Cargo.toml index e669fdefee..6de40aed20 100644 --- a/tx_prelude/Cargo.toml +++ b/tx_prelude/Cargo.toml @@ -15,7 +15,7 @@ abciplus = [ ] [dependencies] -masp_primitives = { git = "https://github.com/anoma/masp", rev = "bee40fc465f6afbd10558d12fe96eb1742eee45c" } +masp_primitives = { git = "https://github.com/anoma/masp", rev = "402b08accff6cf656b7f63823d074931cdcf7773" } namada_core = {path = "../core", default-features = false} namada_macros = {path = "../macros"} namada_proof_of_stake = {path = "../proof_of_stake", default-features = false} diff --git a/vm_env/Cargo.toml b/vm_env/Cargo.toml index b851f716b9..4e9bd48a47 100644 --- a/vm_env/Cargo.toml +++ b/vm_env/Cargo.toml @@ -16,6 +16,6 @@ abciplus = [ namada_core = {path = "../core", default-features = false} borsh = "0.9.0" #libmasp = { git = "https://github.com/anoma/masp", branch = "murisi/masp-incentive" } -masp_primitives = { git = "https://github.com/anoma/masp", rev = "bee40fc465f6afbd10558d12fe96eb1742eee45c" } -masp_proofs = { git = "https://github.com/anoma/masp", rev = "bee40fc465f6afbd10558d12fe96eb1742eee45c" } +masp_primitives = { git = "https://github.com/anoma/masp", rev = "402b08accff6cf656b7f63823d074931cdcf7773" } +masp_proofs = { git = "https://github.com/anoma/masp", rev = "402b08accff6cf656b7f63823d074931cdcf7773" } hex = "0.4.3" diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index c7edff1984..b4d4e87839 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -2425,7 +2425,7 @@ dependencies = [ [[package]] name = "masp_primitives" version = "0.5.0" -source = "git+https://github.com/anoma/masp?rev=bee40fc465f6afbd10558d12fe96eb1742eee45c#bee40fc465f6afbd10558d12fe96eb1742eee45c" +source = "git+https://github.com/anoma/masp?rev=402b08accff6cf656b7f63823d074931cdcf7773#402b08accff6cf656b7f63823d074931cdcf7773" dependencies = [ "aes", "bip0039", @@ -2456,7 +2456,7 @@ dependencies = [ [[package]] name = "masp_proofs" version = "0.5.0" -source = "git+https://github.com/anoma/masp?rev=bee40fc465f6afbd10558d12fe96eb1742eee45c#bee40fc465f6afbd10558d12fe96eb1742eee45c" +source = "git+https://github.com/anoma/masp?rev=402b08accff6cf656b7f63823d074931cdcf7773#402b08accff6cf656b7f63823d074931cdcf7773" dependencies = [ "bellman", "blake2b_simd 1.0.0", diff --git a/wasm/checksums.json b/wasm/checksums.json index 32215c5ba0..ebd6ccc2b1 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,20 +1,20 @@ { - "tx_bond.wasm": "tx_bond.91275d24c59a883c629c5b26ff5f7b57cf0f18009cbed190050d752a2e96a0a8.wasm", - "tx_change_validator_commission.wasm": "tx_change_validator_commission.73aa36169a1366c89fdd7f24b6d25f2931b96ed2bf8f2cdb2f690a4132c99788.wasm", - "tx_ibc.wasm": "tx_ibc.bc180e0716d5e28f0395abe8c21ab05c98d85011c377633f91e1d0291e969ef8.wasm", - "tx_init_account.wasm": "tx_init_account.bad8964f803f6e369ccc911027deb78e48a7896131296172147c92666d27227d.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.c70f106d3c26f0f2c5c2ac6a9b5ff851b67ecfb19f76fc57d0563aa4a9e012f5.wasm", - "tx_init_validator.wasm": "tx_init_validator.0538743e4433d9657670307828c5df727bc936bad9f751653f9a914fbd29c983.wasm", - "tx_reveal_pk.wasm": "tx_reveal_pk.e4806260ff21c1a441ddbdba4f8640a18db03a5687f0ab5dc2e08a72423b808b.wasm", - "tx_transfer.wasm": "tx_transfer.c564498c1d419e566e03a1b5f39927ceda4b3efc1e78128c574d823d1f7cfaeb.wasm", - "tx_unbond.wasm": "tx_unbond.e800fac2d595b7ac3892b1e1efced00ee1f4b5fec22c08a90ee9838725104b8f.wasm", - "tx_update_vp.wasm": "tx_update_vp.6ff4bd7fbf11449abef4f3632243d6e8cf93949e76be206be6eaf8d88e15ccd2.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.4defe4f78e6aee8b6c6faabaf4a8cf6c2939abd9edcc2a772a02f0f6bcc4b8cf.wasm", - "tx_withdraw.wasm": "tx_withdraw.4a31fad26cf1e34d0b7ac5b47d78ccdc79de4666840c81b441592a1b5fe9d4b5.wasm", - "vp_implicit.wasm": "vp_implicit.d014edfd0b81d679c918d033962c347d0f833cd86642b91205e1705e33caddb5.wasm", - "vp_masp.wasm": "vp_masp.8e725e075108905e316a1f86fc696ee9f1f2ba4b0e7646505b3046443bb22d0d.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.99f470dbb03b51f2082b8ba31c6cd1580ff5504752b04b8243ffbfc1b60b5c99.wasm", - "vp_token.wasm": "vp_token.d642c37273de047a4d3bd126ad0bbd4f7b16b2efb1a766fcec0104d7ca231a64.wasm", - "vp_user.wasm": "vp_user.113f37df167b59d278b24f9c1a4c64f2e1b03641cb3d55942e02fe8ba8e461f2.wasm", - "vp_validator.wasm": "vp_validator.f02cf20e2cfda256bc3deb5458de8aa6cb614c5cf2524c4be33495b30a8ef91d.wasm" + "tx_bond.wasm": "tx_bond.a63917a5f9971fd9e0c38039327c477f6a5ac70850a6714d6606c7d8c625ec5d.wasm", + "tx_change_validator_commission.wasm": "tx_change_validator_commission.494320a5fd32eb09f8d5ec4e0950f57ab4da68aae5940e0e5d27030cca4334bf.wasm", + "tx_ibc.wasm": "tx_ibc.bed0f324d800aa2da8492767d7addd607d85918159407735120c8b8d0cdff882.wasm", + "tx_init_account.wasm": "tx_init_account.025fef966350ee18b643b1d2b83d99d264b82e4f247be3d3d9709cd68db1e99d.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.02388a86b695c410830ce7eb03a948dcc1cb74b37cf2ff04488485a84a0fe4c6.wasm", + "tx_init_validator.wasm": "tx_init_validator.a2f7e28fc73d67b17d2dec7cda589d54c988ce34267d4a08d3b31c831973ac53.wasm", + "tx_reveal_pk.wasm": "tx_reveal_pk.da894ee43081312e9f0dc7a1ebc010ea087348d0e17588a4f9ed876b5a64466c.wasm", + "tx_transfer.wasm": "tx_transfer.569847cc57c5872b6be5485d8e51ae7fdbb2c147b442f3dced2d7a47dad997d3.wasm", + "tx_unbond.wasm": "tx_unbond.02932b2c5b5ddc5c5fa23955014eb7e40551c62e065d135d3666003c7f53c206.wasm", + "tx_update_vp.wasm": "tx_update_vp.1910af6c9d86bb241b7e8d2aede17aa131e77ea888f37ab125bc3f8163bc45ec.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.99f64828e6e8571b7f4ae0050c1ed3f36184f984ed595f3b588a9c639943aede.wasm", + "tx_withdraw.wasm": "tx_withdraw.6ccc14bcbbaaa4fe02cbc10b16301dee9c306cef6fad0b16091295cc105d6e79.wasm", + "vp_implicit.wasm": "vp_implicit.6434963d9844da7dc382fcc09808583bd57740f0ab0a6f13ad940e35ebe4dec3.wasm", + "vp_masp.wasm": "vp_masp.7e29cd91a45bf0e2f8976e9db739dcdb7ae6cbb38f8f5f6d82eedbe24857474b.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.b581d376d9b4454800bb82c32a4377cfe4dc67ac2a08cf8d7ad9a7a80bf6a16b.wasm", + "vp_token.wasm": "vp_token.246329086c51c170ff890a3d361db42f2f34af9781a8b7b8f12c81764fcad223.wasm", + "vp_user.wasm": "vp_user.74814ed9d67905454fdda739d2f4959f08bca96095c0b6a30abcdb60b7474af2.wasm", + "vp_validator.wasm": "vp_validator.fe9672f182cdbece544d9bf2c5bd86ed984b07add82ecabc47380ae6685f90c7.wasm" } \ No newline at end of file diff --git a/wasm/wasm_source/Cargo.toml b/wasm/wasm_source/Cargo.toml index 2e334d2caa..61d6e9f4b3 100644 --- a/wasm/wasm_source/Cargo.toml +++ b/wasm/wasm_source/Cargo.toml @@ -40,8 +40,8 @@ once_cell = {version = "1.8.0", optional = true} rust_decimal = {version = "1.26.1", optional = true} wee_alloc = "0.4.5" getrandom = { version = "0.2", features = ["custom"] } -masp_proofs = { git = "https://github.com/anoma/masp", rev = "bee40fc465f6afbd10558d12fe96eb1742eee45c", optional = true } -masp_primitives = { git = "https://github.com/anoma/masp", rev = "bee40fc465f6afbd10558d12fe96eb1742eee45c", optional = true } +masp_proofs = { git = "https://github.com/anoma/masp", rev = "402b08accff6cf656b7f63823d074931cdcf7773", optional = true } +masp_primitives = { git = "https://github.com/anoma/masp", rev = "402b08accff6cf656b7f63823d074931cdcf7773", optional = true } ripemd = "0.1.3" [dev-dependencies] diff --git a/wasm/wasm_source/src/vp_implicit.rs b/wasm/wasm_source/src/vp_implicit.rs index fd76f1bd97..f8906a5f15 100644 --- a/wasm/wasm_source/src/vp_implicit.rs +++ b/wasm/wasm_source/src/vp_implicit.rs @@ -130,9 +130,8 @@ fn validate_tx( true } KeyType::Token { - token, owner, - sub_prefix, + .. } => { if owner == &addr { let pre: token::Amount = diff --git a/wasm/wasm_source/src/vp_user.rs b/wasm/wasm_source/src/vp_user.rs index 63ae898099..c3f6424336 100644 --- a/wasm/wasm_source/src/vp_user.rs +++ b/wasm/wasm_source/src/vp_user.rs @@ -103,9 +103,8 @@ fn validate_tx( let key_type: KeyType = key.into(); let is_valid = match key_type { KeyType::Token { - token, owner, - sub_prefix, + .. } => { if owner == &addr { let pre: token::Amount = diff --git a/wasm/wasm_source/src/vp_validator.rs b/wasm/wasm_source/src/vp_validator.rs index 0328fdd066..3309b75e26 100644 --- a/wasm/wasm_source/src/vp_validator.rs +++ b/wasm/wasm_source/src/vp_validator.rs @@ -103,9 +103,8 @@ fn validate_tx( let key_type: KeyType = key.into(); let is_valid = match key_type { KeyType::Token { - token, owner, - sub_prefix, + .. } => { if owner == &addr { let pre: token::Amount = From 5053330ad06b11cb64a2f089a3fa9e398ace5da7 Mon Sep 17 00:00:00 2001 From: satan Date: Thu, 1 Jun 2023 16:58:36 +0200 Subject: [PATCH 067/151] WIP fixing masp incentives --- Cargo.lock | 4 +- apps/Cargo.toml | 4 +- apps/src/lib/client/rpc.rs | 3 +- apps/src/lib/client/tx.rs | 328 +++++++++++++++++---------- core/Cargo.toml | 2 +- core/src/types/token.rs | 19 +- core/src/types/uint.rs | 30 ++- shared/Cargo.toml | 4 +- tests/src/e2e/ledger_tests.rs | 127 ++++++----- tx_prelude/Cargo.toml | 2 +- vm_env/Cargo.toml | 4 +- wasm/Cargo.lock | 4 +- wasm/checksums.json | 36 +-- wasm/wasm_source/Cargo.toml | 4 +- wasm/wasm_source/src/vp_implicit.rs | 5 +- wasm/wasm_source/src/vp_user.rs | 5 +- wasm/wasm_source/src/vp_validator.rs | 5 +- 17 files changed, 358 insertions(+), 228 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2beee040c5..ab6910b899 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3484,7 +3484,7 @@ dependencies = [ [[package]] name = "masp_primitives" version = "0.5.0" -source = "git+https://github.com/anoma/masp?rev=402b08accff6cf656b7f63823d074931cdcf7773#402b08accff6cf656b7f63823d074931cdcf7773" +source = "git+https://github.com/anoma/masp?rev=ea4fcc68d9412b94042c1c2f0ea0212394e1be37#ea4fcc68d9412b94042c1c2f0ea0212394e1be37" dependencies = [ "aes", "bip0039", @@ -3517,7 +3517,7 @@ dependencies = [ [[package]] name = "masp_proofs" version = "0.5.0" -source = "git+https://github.com/anoma/masp?rev=402b08accff6cf656b7f63823d074931cdcf7773#402b08accff6cf656b7f63823d074931cdcf7773" +source = "git+https://github.com/anoma/masp?rev=ea4fcc68d9412b94042c1c2f0ea0212394e1be37#ea4fcc68d9412b94042c1c2f0ea0212394e1be37" dependencies = [ "bellman", "blake2b_simd 1.0.0", diff --git a/apps/Cargo.toml b/apps/Cargo.toml index 5e8b337f0e..cc319c8287 100644 --- a/apps/Cargo.toml +++ b/apps/Cargo.toml @@ -148,8 +148,8 @@ tracing-subscriber = {version = "0.3.7", features = ["env-filter", "json"]} websocket = "0.26.2" winapi = "0.3.9" #libmasp = { git = "https://github.com/anoma/masp", branch = "murisi/masp-incentive" } -masp_primitives = { git = "https://github.com/anoma/masp", rev = "402b08accff6cf656b7f63823d074931cdcf7773", features = ["transparent-inputs"] } -masp_proofs = { git = "https://github.com/anoma/masp", rev = "402b08accff6cf656b7f63823d074931cdcf7773", features = ["bundled-prover", "download-params"] } +masp_primitives = { git = "https://github.com/anoma/masp", rev = "ea4fcc68d9412b94042c1c2f0ea0212394e1be37", features = ["transparent-inputs"] } +masp_proofs = { git = "https://github.com/anoma/masp", rev = "ea4fcc68d9412b94042c1c2f0ea0212394e1be37", features = ["bundled-prover", "download-params"] } bimap = {version = "0.6.2", features = ["serde"]} [dev-dependencies] diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index d90283aa9f..317882f9a7 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -44,8 +44,7 @@ use namada::types::storage::{ BlockHeight, BlockResults, Epoch, Key, KeySeg, PrefixValue, TxIndex, }; use namada::types::token::{ - balance_key, Change, DenominatedAmount, MaspDenom, - TokenAddress, Transfer, + balance_key, Change, DenominatedAmount, MaspDenom, TokenAddress, Transfer, }; use namada::types::transaction::{ process_tx, AffineCurve, DecryptedTx, EllipticCurve, PairingEngine, TxType, diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 973580a653..8f95c34f84 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -53,7 +53,7 @@ use namada::types::storage::{ }; use namada::types::time::DateTimeUtc; use namada::types::token::{ - DenominatedAmount, MaspDenom, TokenAddress, Transfer, HEAD_TX_KEY, + Change, DenominatedAmount, MaspDenom, TokenAddress, Transfer, HEAD_TX_KEY, PIN_KEY_PREFIX, TX_KEY_PREFIX, }; use namada::types::transaction::governance::{ @@ -520,8 +520,8 @@ pub struct MaspChange { pub struct MaspAmount(HashMap<(Epoch, TokenAddress), token::Change>); impl MaspAmount { - pub fn pop(&mut self) -> Option<((Epoch, TokenAddress), token::Change)>{ - let key = self.keys().next()?.clone(); + pub fn pop(&mut self) -> Option<((Epoch, TokenAddress), token::Change)> { + let key = self.keys().find(|(e, _)| e.0 != 0)?.clone(); let value = self.remove(&key).unwrap(); Some((key, value)) } @@ -581,6 +581,17 @@ impl std::ops::SubAssign for MaspAmount { } } +impl std::ops::Mul for MaspAmount { + type Output = Self; + + fn mul(mut self, rhs: Change) -> Self::Output { + for (_, value) in self.iter_mut() { + *value = *value * rhs + } + self + } +} + impl<'a> From<&'a MaspAmount> for Amount { fn from(masp_amount: &'a MaspAmount) -> Amount { let mut res = Amount::zero(); @@ -592,7 +603,7 @@ impl<'a> From<&'a MaspAmount> for Amount { &key.sub_prefix, denom, ); - res += Amount::from_pair(asset, denom.denominate_i64(val)) + res += Amount::from_pair(asset, denom.denominate_i128(val)) .unwrap(); } } @@ -600,9 +611,15 @@ impl<'a> From<&'a MaspAmount> for Amount { } } +impl From for Amount { + fn from(amt: MaspAmount) -> Self { + Self::from(&amt) + } +} + /// Represents the amount used of different conversions pub type Conversions = - HashMap, i128)>; + HashMap, Change)>; /// Represents an amount that is pub type MaspDenominatedAmount = Amount<(TokenAddress, MaspDenom)>; @@ -1083,26 +1100,17 @@ impl ShieldedContext { client: HttpClient, asset_type: AssetType, conversions: &'a mut Conversions, - ) -> Option<&'a mut (AllowedConversion, MerklePath, i128)> { - match conversions.entry(asset_type) { - Entry::Occupied(conv_entry) => Some(conv_entry.into_mut()), - Entry::Vacant(conv_entry) => { - // Query for the ID of the last accepted transaction - let (addr, sub_prefix, denom, ep, conv, path): ( - Address, - _, - _, - _, - _, - _, - ) = query_conversion(client, asset_type).await?; + ) { + if let Entry::Vacant(conv_entry) = conversions.entry(asset_type) { + // Query for the ID of the last accepted transaction + if let Some((addr, sub_prefix, denom, ep, conv, path)) = + query_conversion(client, asset_type).await + { self.asset_types .insert(asset_type, (addr, sub_prefix, denom, ep)); // If the conversion is 0, then we just have a pure decoding - if conv == Amount::zero() { - None - } else { - Some(conv_entry.insert((Amount::into(conv), path, 0))) + if conv != Amount::zero() { + conv_entry.insert((conv.into(), path, Change::zero())); } } } @@ -1146,40 +1154,46 @@ impl ShieldedContext { async fn apply_conversion( &mut self, client: &HttpClient, - conv: AllowedConversion, - asset_type: AssetType, - value: i128, - usage: &mut i128, + conv: &MaspAmount, + asset_type: (Epoch, TokenAddress), + value: Change, input: &mut MaspAmount, - output: &mut Amount, - ) { + output: &mut MaspAmount, + ) -> Option { + if !value.non_negative() { + return None; + } // If conversion if possible, accumulate the exchanged amount - let conv: Amount = conv.into(); + let conv = self.decode_all_amounts(client, conv.into()).await; + // println!("conv {:?}", conv); // The amount required of current asset to qualify for conversion - let threshold = -conv[&asset_type]; - if threshold == 0 { + let threshold = token::Change::from(-conv[&asset_type]); + // println!("theshold: {}, value: {}", threshold, value); + if threshold.is_zero() { eprintln!( - "Asset threshold of selected conversion for asset type {} is \ - 0, this is a bug, please report it.", + "Asset threshold of selected conversion for asset type {:?} \ + is 0, this is a bug, please report it.", asset_type ); } // We should use an amount of the AllowedConversion that almost // cancels the original amount if threshold > value { - return + return None; } let required = value / threshold; + // Forget about the trace amount left over because we cannot // realize its value - let trace = Amount::from_pair(asset_type, value % threshold).unwrap(); - // Record how much more of the given conversion has been used - *usage += required; + let trace = + MaspAmount(HashMap::from([(asset_type, value % threshold)])); + // println!("Trace: {:?}", trace); + // println!("required {:?}", required); // Apply the conversions to input and move the trace amount to output - *input += self - .decode_all_amounts(client, conv.clone() * required - &trace) - .await; + *input += conv.clone() * required - trace.clone(); + // println!("Input {:?}", input); *output += trace; + Some(required) } /// Convert the given amount into the latest asset types whilst making a @@ -1194,99 +1208,106 @@ impl ShieldedContext { mut conversions: Conversions, ) -> (Amount, Conversions) { // Where we will store our exchanged value - let mut output = Amount::zero(); + let mut output = MaspAmount::default(); // Repeatedly exchange assets until it is no longer possible loop { + println!("Input {:?}", input); + let Some(((asset_epoch, token_addr), value)) = input.pop() else { break }; - for denom in MaspDenom::iter() { - let target_asset_type = make_asset_type( - Some(target_epoch), - &token_addr.address, - &token_addr.sub_prefix, - denom, - ); - let asset_type = make_asset_type( - Some(asset_epoch), - &token_addr.address, - &token_addr.sub_prefix, - denom, + let at_target_asset_type = target_epoch == asset_epoch; + let conv = self + .aggregate_conversions( + &client, + asset_epoch, + target_epoch, + token_addr.clone(), + &mut conversions, + ) + .await; + println!("conversions {:?}", conv); + if let (Some(conv), false) = ( + conv.get(&(target_epoch, token_addr.clone())), + at_target_asset_type, + ) { + println!( + "converting current asset type to latest asset type..." ); - let at_target_asset_type = target_epoch == asset_epoch; - - let denom_value = denom.denominate(&token::Amount::from(value)) as i128; - _ = self - .query_allowed_conversion( - client.clone(), - target_asset_type, - &mut conversions, - ) - .await; - - if let (Some((conv, _wit, usage)), false) = ( - conversions.get_mut(&target_asset_type), - at_target_asset_type, - ) { - println!( - "converting current asset type to latest asset type..." - ); - // Not at the target asset type, not at the latest asset - // type. Apply conversion to get from - // current asset type to the latest - // asset type. - self.apply_conversion( + if let Some(used) = self + .apply_conversion( &client, - conv.clone(), - asset_type, - denom_value, - usage, + conv, + (asset_epoch, token_addr.clone()), + value, &mut input, &mut output, ) - .await; - break; - } - _ = self - .query_allowed_conversion( - client.clone(), - asset_type, - &mut conversions, - ) - .await; - if let (Some((conv, _wit, usage)), false) = - (conversions.get_mut(&asset_type), at_target_asset_type) + .await { - println!( - "converting latest asset type to target asset type..." - ); - // Not at the target asset type, yes at the latest asset - // type. Apply inverse conversion to get - // from latest asset type to the target - // asset type. - self.apply_conversion( + for denom in MaspDenom::iter() { + let target_asset_type = make_asset_type( + Some(target_epoch), + &token_addr.address, + &token_addr.sub_prefix, + denom, + ); + if let Some((_, _, usage)) = + conversions.get_mut(&target_asset_type) + { + *usage += used; + } + } + }; + break; + } + if let (Some(conv), false) = + (conv.get(&(asset_epoch, token_addr.clone())), at_target_asset_type) + { + if let Some(used) = self + .apply_conversion( &client, - conv.clone(), - asset_type, - denom_value, - usage, + conv, + (asset_epoch, token_addr.clone()), + value, &mut input, &mut output, ) - .await; - } else { - // At the target asset type. Then move component over to - // output. - let mut comp = MaspAmount::default(); - comp.insert((asset_epoch, token_addr.clone()), denom_value.into()); - for ((e, key), val) in input.iter() { - if *key == token_addr { - comp.insert((*e, key.clone()), *val); + .await + { + for denom in MaspDenom::iter() { + let asset_type = make_asset_type( + Some(asset_epoch), + &token_addr.address, + &token_addr.sub_prefix, + denom, + ); + if let Some((_, _, usage)) = + conversions.get_mut(&asset_type) + { + *usage += used; } } - output += Amount::from(&comp); } + } else { + // At the target asset type. Then move component over to + // output. + let mut comp = MaspAmount::default(); + comp.insert((asset_epoch, token_addr.clone()), value); + for ((e, key), val) in input.iter() { + if *key == token_addr { + comp.insert((*e, key.clone()), *val); + } + } + output += comp; } } - (output, conversions) + // finally convert the rewards in epoch 0. + let mut comp = MaspAmount::default(); + for ((_, key), val) in input.drain() { + comp.insert((target_epoch, key), val); + } + output += comp; + println!("{:?}", output); + (output.into(), conversions) } /// Collect enough unspent notes in this context to exceed the given amount @@ -1356,6 +1377,9 @@ impl ShieldedContext { } } } + println!("val_acc: {:?}", val_acc); + println!("notes: {:?}", notes); + println!("conversions: {:?}", conversions); (val_acc, notes, conversions) } @@ -1529,6 +1553,58 @@ impl ShieldedContext { } MaspAmount(res) } + + /// Change conversions to use 256 bit numbers + async fn aggregate_conversions( + &mut self, + client: &HttpClient, + asset_epoch: Epoch, + target_epoch: Epoch, + token_addr: TokenAddress, + conversions: &mut Conversions, + ) -> HashMap<(Epoch, TokenAddress), MaspAmount> { + let mut conv = HashMap::new(); + for denom in MaspDenom::iter() { + let target_asset_type = make_asset_type( + Some(target_epoch), + &token_addr.address, + &token_addr.sub_prefix, + denom, + ); + let asset_type = make_asset_type( + Some(asset_epoch), + &token_addr.address, + &token_addr.sub_prefix, + denom, + ); + + self.query_allowed_conversion( + client.clone(), + target_asset_type, + conversions, + ) + .await; + self.query_allowed_conversion( + client.clone(), + asset_type, + conversions, + ) + .await; + if let Some((a, _, _)) = conversions.get(&target_asset_type) { + conv.insert( + (target_epoch, token_addr.clone()), + self.decode_all_amounts(&client, a.clone().into()).await, + ); + } + if let Some((a, _, _)) = conversions.get(&asset_type) { + conv.insert( + (asset_epoch, token_addr.clone()), + self.decode_all_amounts(&client, a.clone().into()).await, + ); + } + } + conv + } } /// Convert Namada amount and token type to MASP equivalents @@ -1538,7 +1614,13 @@ fn convert_amount( sub_prefix: &Option<&String>, val: &token::Amount, ) -> ([AssetType; 4], Amount) { - println!("{}", DenominatedAmount { amount: val.clone(), denom: 18.into()}); + println!( + "{}", + DenominatedAmount { + amount: val.clone(), + denom: 18.into() + } + ); let mut amount = Amount::zero(); let asset_types: [AssetType; 4] = MaspDenom::iter() .map(|denom| { @@ -1662,12 +1744,14 @@ async fn gen_shielded_transfer( } // Commit the conversion notes used during summation for (conv, wit, value) in used_convs.values() { - if *value > 0 { - builder.add_convert( - conv.clone(), - *value as u64, - wit.clone(), - )?; + if value.is_positive() { + for denom in MaspDenom::iter() { + builder.add_convert( + conv.clone(), + denom.denominate(&token::Amount::from(*value)), + wit.clone(), + )?; + } } } } else { diff --git a/core/Cargo.toml b/core/Cargo.toml index 3a68ca3abc..ba3beeb5e6 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -83,7 +83,7 @@ impl-num-traits = "0.1.2" index-set = {git = "https://github.com/heliaxdev/index-set", tag = "v0.7.1", features = ["serialize-borsh", "serialize-serde"]} itertools = "0.10.0" libsecp256k1 = {git = "https://github.com/heliaxdev/libsecp256k1", rev = "bbb3bd44a49db361f21d9db80f9a087c194c0ae9", default-features = false, features = ["std", "static-context"]} -masp_primitives = { git = "https://github.com/anoma/masp", rev = "402b08accff6cf656b7f63823d074931cdcf7773" } +masp_primitives = { git = "https://github.com/anoma/masp", rev = "ea4fcc68d9412b94042c1c2f0ea0212394e1be37" } proptest = {git = "https://github.com/heliaxdev/proptest", branch = "tomas/sm", optional = true} prost = "0.9.0" prost-types = "0.9.0" diff --git a/core/src/types/token.rs b/core/src/types/token.rs index dccef5683d..910e51b180 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -696,8 +696,8 @@ impl MaspDenom { } /// Get the corresponding u64 word from the input uint256. - pub fn denominate_i64(&self, amount: &Change) -> i64 { - let val = amount.abs().0[*self as usize] as i64; + pub fn denominate_i128(&self, amount: &Change) -> i128 { + let val = amount.abs().0[*self as usize] as i128; if Change::is_negative(amount) { -val } else { @@ -1150,6 +1150,21 @@ mod tests { let non_zero = Amount::from_uint(1, 0).expect("Test failed"); assert!(!non_zero.is_zero()); } + + #[test] + fn testy_poo() { + let change = Change::from(30000000000000000000i128); + let output = Change::from(6893488147419103231i128); + + let amt = DenominatedAmount { + amount: Amount::from(change), + denom: 18.into(), + }; + println!("{}", amt); + println!("{:?}", change.0.0); + println!("{:?}", output.0.0); + assert!(false); + } } /// Helpers for testing with addresses. diff --git a/core/src/types/uint.rs b/core/src/types/uint.rs index 640f79ee6c..0f5f617d8c 100644 --- a/core/src/types/uint.rs +++ b/core/src/types/uint.rs @@ -3,7 +3,7 @@ //! the backing type of token amounts. use std::cmp::Ordering; use std::fmt::{Debug, Display, Formatter}; -use std::ops::{Add, AddAssign, BitXor, Div, Mul, Neg, Sub, SubAssign}; +use std::ops::{Add, AddAssign, BitXor, Div, Mul, Neg, Rem, Sub, SubAssign}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use impl_num_traits::impl_uint_num_traits; @@ -106,6 +106,11 @@ impl I256 { !self.non_negative() } + /// Check if the amount is positive (greater than zero) + pub fn is_positive(&self) -> bool { + self.non_negative() && !self.is_zero() + } + /// Get the absolute value pub fn abs(&self) -> Uint { if self.non_negative() { @@ -327,6 +332,29 @@ impl Div for I256 { } } +impl Div for I256 { + type Output = Self; + + fn div(self, rhs: I256) -> Self::Output { + if rhs.is_negative() { + -(self / rhs.abs()) + } else { + self / rhs.abs() + } + } +} + +impl Rem for I256 { + type Output = Self; + + fn rem(self, rhs: Self) -> Self::Output { + if self.is_negative() { + -(Self(self.abs() % rhs.abs())) + } else { + Self(self.abs() % rhs.abs()) + } + } +} impl From for I256 { fn from(val: i128) -> Self { if val < 0 { diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 0f059bc663..4bc5900da0 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -98,8 +98,8 @@ ibc = {version = "0.14.0", default-features = false, optional = true} ibc-proto = {version = "0.17.1", default-features = false, optional = true} itertools = "0.10.0" loupe = {version = "0.1.3", optional = true} -masp_primitives = { git = "https://github.com/anoma/masp", rev = "402b08accff6cf656b7f63823d074931cdcf7773" } -masp_proofs = { git = "https://github.com/anoma/masp", rev = "402b08accff6cf656b7f63823d074931cdcf7773" } +masp_primitives = { git = "https://github.com/anoma/masp", rev = "ea4fcc68d9412b94042c1c2f0ea0212394e1be37" } +masp_proofs = { git = "https://github.com/anoma/masp", rev = "ea4fcc68d9412b94042c1c2f0ea0212394e1be37" } parity-wasm = {version = "0.45.0", features = ["sign_ext"], optional = true} paste = "1.0.9" # A fork with state machine testing diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 26582aa20e..453a2dd1c7 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -27,7 +27,9 @@ use namada_apps::client::tx::ShieldedContext; use namada_apps::config::genesis::genesis_config::{ GenesisConfig, ParametersConfig, PosParamsConfig, }; -use namada_core::types::token::{DenominatedAmount, Denomination, NATIVE_MAX_DECIMAL_PLACES}; +use namada_core::types::token::{ + DenominatedAmount, Denomination, NATIVE_MAX_DECIMAL_PLACES, +}; use namada_test_utils::TestWasms; use serde_json::json; use setup::constants::*; @@ -1137,10 +1139,8 @@ fn masp_incentives() -> Result<()> { client.exp_string("btc: 20")?; client.assert_success(); - let amt20 = - token::Amount::from_uint(20, BTC_DENOMINATION).unwrap(); - let amt30 = - token::Amount::from_uint(30, ETH_DENOMINATION).unwrap(); + let amt20 = token::Amount::from_uint(20, BTC_DENOMINATION).unwrap(); + let amt30 = token::Amount::from_uint(30, ETH_DENOMINATION).unwrap(); // Assert NAM balance at VK(A) is 20*BTC_reward*(epoch_1-epoch_0) let mut client = run!( @@ -1158,10 +1158,11 @@ fn masp_incentives() -> Result<()> { Some(60) )?; let amt = (amt20 * masp_rewards[&(btc(), None)]).0 * (ep1.0 - ep0.0); - let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; - client.exp_string(&format!( - "nam: {}", denominated, - ))?; + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated,))?; client.assert_success(); // Assert NAM balance at MASP pool is 20*BTC_reward*(epoch_1-epoch_0) @@ -1180,10 +1181,11 @@ fn masp_incentives() -> Result<()> { Some(60) )?; let amt = (amt20 * masp_rewards[&(btc(), None)]).0 * (ep1.0 - ep0.0); - let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into() }; - client.exp_string(&format!( - "nam: {}", denominated, - ))?; + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated,))?; client.assert_success(); // Wait till epoch boundary @@ -1223,10 +1225,11 @@ fn masp_incentives() -> Result<()> { Some(60) )?; let amt = (amt20 * masp_rewards[&(btc(), None)]).0 * (ep2.0 - ep0.0); - let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; - client.exp_string(&format!( - "nam: {}", denominated, - ))?; + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated,))?; client.assert_success(); // Assert NAM balance at MASP pool is 20*BTC_reward*(epoch_2-epoch_0) @@ -1245,10 +1248,11 @@ fn masp_incentives() -> Result<()> { Some(60) )?; let amt = (amt20 * masp_rewards[&(btc(), None)]).0 * (ep2.0 - ep0.0); - let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; - client.exp_string(&format!( - "nam: {}", denominated, - ))?; + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated,))?; client.assert_success(); // Wait till epoch boundary @@ -1349,10 +1353,11 @@ fn masp_incentives() -> Result<()> { Some(60) )?; let amt = (amt30 * masp_rewards[&(eth(), None)]).0 * (ep4.0 - ep3.0); - let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; - client.exp_string(&format!( - "nam: {}", denominated, - ))?; + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated,))?; client.assert_success(); // Assert NAM balance at MASP pool is @@ -1373,10 +1378,11 @@ fn masp_incentives() -> Result<()> { )?; let amt = ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep4.0 - ep0.0)) + ((amt30 * masp_rewards[&(eth(), None)]).0 * (ep4.0 - ep3.0)); - let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; - client.exp_string(&format!( - "nam: {}", denominated - ))?; + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated))?; client.assert_success(); // Wait till epoch boundary @@ -1442,10 +1448,11 @@ fn masp_incentives() -> Result<()> { Some(60) )?; let amt = (amt30 * masp_rewards[&(eth(), None)]).0 * (ep.0 - ep3.0); - let denominated = DenominatedAmount{ amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; - client.exp_string(&format!( - "nam: {}", denominated, - ))?; + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated,))?; client.assert_success(); ep = get_epoch(&test, &validator_one_rpc)?; @@ -1467,10 +1474,11 @@ fn masp_incentives() -> Result<()> { )?; let amt = ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep.0 - ep0.0)) + ((amt30 * masp_rewards[&(eth(), None)]).0 * (ep.0 - ep3.0)); - let denominated = DenominatedAmount{ amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; - client.exp_string(&format!( - "nam: {}", denominated - ))?; + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated))?; client.assert_success(); // Wait till epoch boundary @@ -1534,10 +1542,11 @@ fn masp_incentives() -> Result<()> { Some(60) )?; let amt = (amt20 * masp_rewards[&(btc(), None)]).0 * (ep6.0 - ep0.0); - let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; - client.exp_string(&format!( - "nam: {}", denominated, - ))?; + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated,))?; client.assert_success(); // Assert NAM balance at MASP pool is @@ -1558,10 +1567,11 @@ fn masp_incentives() -> Result<()> { )?; let amt = ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep6.0 - ep0.0)) + ((amt30 * masp_rewards[&(eth(), None)]).0 * (ep5.0 - ep3.0)); - let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into() }; - client.exp_string(&format!( - "nam: {}", denominated, - ))?; + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated,))?; client.assert_success(); // Wait till epoch boundary @@ -1583,10 +1593,11 @@ fn masp_incentives() -> Result<()> { Some(60) )?; let amt = (amt20 * masp_rewards[&(btc(), None)]).0 * (ep6.0 - ep0.0); - let denominated = DenominatedAmount {amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; - client.exp_string(&format!( - "nam: {}", denominated - ))?; + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated))?; client.assert_success(); // Assert NAM balance at VK(B) is 30*ETH_reward*(epoch_5-epoch_3) @@ -1605,10 +1616,11 @@ fn masp_incentives() -> Result<()> { Some(60) )?; let amt = (amt30 * masp_rewards[&(eth(), None)]).0 * (ep5.0 - ep3.0); - let denominated = DenominatedAmount {amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; - client.exp_string(&format!( - "nam: {}", denominated, - ))?; + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated,))?; client.assert_success(); // Assert NAM balance at MASP pool is @@ -1629,10 +1641,11 @@ fn masp_incentives() -> Result<()> { )?; let amt = ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep6.0 - ep0.0)) + ((amt30 * masp_rewards[&(eth(), None)]).0 * (ep5.0 - ep3.0)); - let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; - client.exp_string(&format!( - "nam: {}", denominated, - ))?; + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated,))?; client.assert_success(); // Wait till epoch boundary to prevent conversion expiry during transaction diff --git a/tx_prelude/Cargo.toml b/tx_prelude/Cargo.toml index 6de40aed20..6d5f9c1433 100644 --- a/tx_prelude/Cargo.toml +++ b/tx_prelude/Cargo.toml @@ -15,7 +15,7 @@ abciplus = [ ] [dependencies] -masp_primitives = { git = "https://github.com/anoma/masp", rev = "402b08accff6cf656b7f63823d074931cdcf7773" } +masp_primitives = { git = "https://github.com/anoma/masp", rev = "ea4fcc68d9412b94042c1c2f0ea0212394e1be37" } namada_core = {path = "../core", default-features = false} namada_macros = {path = "../macros"} namada_proof_of_stake = {path = "../proof_of_stake", default-features = false} diff --git a/vm_env/Cargo.toml b/vm_env/Cargo.toml index 4e9bd48a47..f359161765 100644 --- a/vm_env/Cargo.toml +++ b/vm_env/Cargo.toml @@ -16,6 +16,6 @@ abciplus = [ namada_core = {path = "../core", default-features = false} borsh = "0.9.0" #libmasp = { git = "https://github.com/anoma/masp", branch = "murisi/masp-incentive" } -masp_primitives = { git = "https://github.com/anoma/masp", rev = "402b08accff6cf656b7f63823d074931cdcf7773" } -masp_proofs = { git = "https://github.com/anoma/masp", rev = "402b08accff6cf656b7f63823d074931cdcf7773" } +masp_primitives = { git = "https://github.com/anoma/masp", rev = "ea4fcc68d9412b94042c1c2f0ea0212394e1be37" } +masp_proofs = { git = "https://github.com/anoma/masp", rev = "ea4fcc68d9412b94042c1c2f0ea0212394e1be37" } hex = "0.4.3" diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index b4d4e87839..604abb3686 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -2425,7 +2425,7 @@ dependencies = [ [[package]] name = "masp_primitives" version = "0.5.0" -source = "git+https://github.com/anoma/masp?rev=402b08accff6cf656b7f63823d074931cdcf7773#402b08accff6cf656b7f63823d074931cdcf7773" +source = "git+https://github.com/anoma/masp?rev=ea4fcc68d9412b94042c1c2f0ea0212394e1be37#ea4fcc68d9412b94042c1c2f0ea0212394e1be37" dependencies = [ "aes", "bip0039", @@ -2456,7 +2456,7 @@ dependencies = [ [[package]] name = "masp_proofs" version = "0.5.0" -source = "git+https://github.com/anoma/masp?rev=402b08accff6cf656b7f63823d074931cdcf7773#402b08accff6cf656b7f63823d074931cdcf7773" +source = "git+https://github.com/anoma/masp?rev=ea4fcc68d9412b94042c1c2f0ea0212394e1be37#ea4fcc68d9412b94042c1c2f0ea0212394e1be37" dependencies = [ "bellman", "blake2b_simd 1.0.0", diff --git a/wasm/checksums.json b/wasm/checksums.json index ebd6ccc2b1..d40a0ba7b4 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,20 +1,20 @@ { - "tx_bond.wasm": "tx_bond.a63917a5f9971fd9e0c38039327c477f6a5ac70850a6714d6606c7d8c625ec5d.wasm", - "tx_change_validator_commission.wasm": "tx_change_validator_commission.494320a5fd32eb09f8d5ec4e0950f57ab4da68aae5940e0e5d27030cca4334bf.wasm", - "tx_ibc.wasm": "tx_ibc.bed0f324d800aa2da8492767d7addd607d85918159407735120c8b8d0cdff882.wasm", - "tx_init_account.wasm": "tx_init_account.025fef966350ee18b643b1d2b83d99d264b82e4f247be3d3d9709cd68db1e99d.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.02388a86b695c410830ce7eb03a948dcc1cb74b37cf2ff04488485a84a0fe4c6.wasm", - "tx_init_validator.wasm": "tx_init_validator.a2f7e28fc73d67b17d2dec7cda589d54c988ce34267d4a08d3b31c831973ac53.wasm", - "tx_reveal_pk.wasm": "tx_reveal_pk.da894ee43081312e9f0dc7a1ebc010ea087348d0e17588a4f9ed876b5a64466c.wasm", - "tx_transfer.wasm": "tx_transfer.569847cc57c5872b6be5485d8e51ae7fdbb2c147b442f3dced2d7a47dad997d3.wasm", - "tx_unbond.wasm": "tx_unbond.02932b2c5b5ddc5c5fa23955014eb7e40551c62e065d135d3666003c7f53c206.wasm", - "tx_update_vp.wasm": "tx_update_vp.1910af6c9d86bb241b7e8d2aede17aa131e77ea888f37ab125bc3f8163bc45ec.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.99f64828e6e8571b7f4ae0050c1ed3f36184f984ed595f3b588a9c639943aede.wasm", - "tx_withdraw.wasm": "tx_withdraw.6ccc14bcbbaaa4fe02cbc10b16301dee9c306cef6fad0b16091295cc105d6e79.wasm", - "vp_implicit.wasm": "vp_implicit.6434963d9844da7dc382fcc09808583bd57740f0ab0a6f13ad940e35ebe4dec3.wasm", - "vp_masp.wasm": "vp_masp.7e29cd91a45bf0e2f8976e9db739dcdb7ae6cbb38f8f5f6d82eedbe24857474b.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.b581d376d9b4454800bb82c32a4377cfe4dc67ac2a08cf8d7ad9a7a80bf6a16b.wasm", - "vp_token.wasm": "vp_token.246329086c51c170ff890a3d361db42f2f34af9781a8b7b8f12c81764fcad223.wasm", - "vp_user.wasm": "vp_user.74814ed9d67905454fdda739d2f4959f08bca96095c0b6a30abcdb60b7474af2.wasm", - "vp_validator.wasm": "vp_validator.fe9672f182cdbece544d9bf2c5bd86ed984b07add82ecabc47380ae6685f90c7.wasm" + "tx_bond.wasm": "tx_bond.9259e3386b68f7ee0e35a3dfe7e96b1543e4ee8a6215010eae76de0104c9c9c2.wasm", + "tx_change_validator_commission.wasm": "tx_change_validator_commission.fcc26c4678517d7097f41257c84c294b8c48eb4a0250b0ec74ee634d7935cf1e.wasm", + "tx_ibc.wasm": "tx_ibc.7ca7ee70182913d4167aeec2703b21712af82785856bab59a25f1fe732095dcc.wasm", + "tx_init_account.wasm": "tx_init_account.709c1a6b0de57f8cfb4bf84676f263370bf47256fcdbdf0cb91a8ef5fccb7cea.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.e2568a89aed19218367d64f63a14a74973d7768577f2dcfd8c3b0e0558df6890.wasm", + "tx_init_validator.wasm": "tx_init_validator.63709c3297d1752efb9b7a943c82edb27aa61c82a95857739273080bee9d42cf.wasm", + "tx_reveal_pk.wasm": "tx_reveal_pk.c78cb00453c5f2b0eaec49b36e895ac8d363261eb2af5e719dd641424847383f.wasm", + "tx_transfer.wasm": "tx_transfer.6a656bcb2a13ff3c4402ff7e275c080f4b1cf1de8f6112bbfbc03bf07167b297.wasm", + "tx_unbond.wasm": "tx_unbond.96ca5b536a02093d3f35a0e631670f10960c5bad220082e992003eb02c42f5f8.wasm", + "tx_update_vp.wasm": "tx_update_vp.8fc0dde790bf9304ce80fdbb85c85f6671dac6eedeb19cde17e825e6b477c23b.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.c5a856595b0a76b7191aec1bf1912d69e9d29e94e412ce7ebd7c96467900bfd8.wasm", + "tx_withdraw.wasm": "tx_withdraw.43cf91dc3fec04ffe740da865e55b00b6e6d71a6da9dcfde378506aedd715f24.wasm", + "vp_implicit.wasm": "vp_implicit.377e14b90713516ed12fd10975a13c9d74bd3a4de8f40be17e9aa4315953c282.wasm", + "vp_masp.wasm": "vp_masp.eb53123cad15bf82a09501dfe9da8dbfa0070468f6c8376dab4a62fd8ac58548.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.9059ec6cd09ce0a7bde581b487d1e53921a9c26f29c76071d3a8d68fdba9bdab.wasm", + "vp_token.wasm": "vp_token.cd4d62be7dbb08a1f983857a9a59689b8ac12856ce15480173e26d94647adf86.wasm", + "vp_user.wasm": "vp_user.79e5cd7e4cb39fbb2b54c8a59b2a142f53050b030ec79acab37a36fd24402ece.wasm", + "vp_validator.wasm": "vp_validator.257b596e176e2db8057b2a6489f978b77c30dbd77282dd5e57809d75dd6d7f0d.wasm" } \ No newline at end of file diff --git a/wasm/wasm_source/Cargo.toml b/wasm/wasm_source/Cargo.toml index 61d6e9f4b3..9d56f416e4 100644 --- a/wasm/wasm_source/Cargo.toml +++ b/wasm/wasm_source/Cargo.toml @@ -40,8 +40,8 @@ once_cell = {version = "1.8.0", optional = true} rust_decimal = {version = "1.26.1", optional = true} wee_alloc = "0.4.5" getrandom = { version = "0.2", features = ["custom"] } -masp_proofs = { git = "https://github.com/anoma/masp", rev = "402b08accff6cf656b7f63823d074931cdcf7773", optional = true } -masp_primitives = { git = "https://github.com/anoma/masp", rev = "402b08accff6cf656b7f63823d074931cdcf7773", optional = true } +masp_proofs = { git = "https://github.com/anoma/masp", rev = "ea4fcc68d9412b94042c1c2f0ea0212394e1be37", optional = true } +masp_primitives = { git = "https://github.com/anoma/masp", rev = "ea4fcc68d9412b94042c1c2f0ea0212394e1be37", optional = true } ripemd = "0.1.3" [dev-dependencies] diff --git a/wasm/wasm_source/src/vp_implicit.rs b/wasm/wasm_source/src/vp_implicit.rs index f8906a5f15..272d613bd4 100644 --- a/wasm/wasm_source/src/vp_implicit.rs +++ b/wasm/wasm_source/src/vp_implicit.rs @@ -129,10 +129,7 @@ fn validate_tx( } true } - KeyType::Token { - owner, - .. - } => { + KeyType::Token { owner, .. } => { if owner == &addr { let pre: token::Amount = ctx.read_pre(key)?.unwrap_or_default(); diff --git a/wasm/wasm_source/src/vp_user.rs b/wasm/wasm_source/src/vp_user.rs index c3f6424336..429008f660 100644 --- a/wasm/wasm_source/src/vp_user.rs +++ b/wasm/wasm_source/src/vp_user.rs @@ -102,10 +102,7 @@ fn validate_tx( for key in keys_changed.iter() { let key_type: KeyType = key.into(); let is_valid = match key_type { - KeyType::Token { - owner, - .. - } => { + KeyType::Token { owner, .. } => { if owner == &addr { let pre: token::Amount = ctx.read_pre(key)?.unwrap_or_default(); diff --git a/wasm/wasm_source/src/vp_validator.rs b/wasm/wasm_source/src/vp_validator.rs index 3309b75e26..f3cad12e93 100644 --- a/wasm/wasm_source/src/vp_validator.rs +++ b/wasm/wasm_source/src/vp_validator.rs @@ -102,10 +102,7 @@ fn validate_tx( for key in keys_changed.iter() { let key_type: KeyType = key.into(); let is_valid = match key_type { - KeyType::Token { - owner, - .. - } => { + KeyType::Token { owner, .. } => { if owner == &addr { let pre: token::Amount = ctx.read_pre(key)?.unwrap_or_default(); From cdde306a10f31b367f7e92d72e4528bb05c53aa6 Mon Sep 17 00:00:00 2001 From: satan Date: Thu, 1 Jun 2023 17:35:55 +0200 Subject: [PATCH 068/151] WIP fixed bug in collecting unspent notes --- apps/src/lib/client/tx.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 8f95c34f84..30369842ad 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -485,7 +485,7 @@ pub fn is_amount_required(src: Amount, dest: Amount, delta: Amount) -> bool { if delta > Amount::zero() { let gap = dest - src; for (asset_type, value) in gap.components() { - if *value > 0 && delta[asset_type] > 0 { + if *value >= 0 && delta[asset_type] >= 0 { return true; } } @@ -1211,7 +1211,7 @@ impl ShieldedContext { let mut output = MaspAmount::default(); // Repeatedly exchange assets until it is no longer possible loop { - println!("Input {:?}", input); + println!("\n\nInput {:?}\n\n", input); let Some(((asset_epoch, token_addr), value)) = input.pop() else { break }; let at_target_asset_type = target_epoch == asset_epoch; @@ -1224,7 +1224,7 @@ impl ShieldedContext { &mut conversions, ) .await; - println!("conversions {:?}", conv); + println!("\n\nconversions {:?}\n\n", conv); if let (Some(conv), false) = ( conv.get(&(target_epoch, token_addr.clone())), at_target_asset_type, @@ -1306,7 +1306,7 @@ impl ShieldedContext { comp.insert((target_epoch, key), val); } output += comp; - println!("{:?}", output); + println!("\n\noutput {:?}\n\n", output); (output.into(), conversions) } From 16b17cce544676f6223638460bcdfa622fd85202 Mon Sep 17 00:00:00 2001 From: satan Date: Fri, 2 Jun 2023 13:39:24 +0200 Subject: [PATCH 069/151] WIP --- apps/src/bin/namada-client/cli.rs | 36 ++++++++++++++++++- apps/src/lib/client/tx.rs | 58 +++++++++++++++---------------- 2 files changed, 64 insertions(+), 30 deletions(-) diff --git a/apps/src/bin/namada-client/cli.rs b/apps/src/bin/namada-client/cli.rs index 1a60d3f01b..cb86945d6c 100644 --- a/apps/src/bin/namada-client/cli.rs +++ b/apps/src/bin/namada-client/cli.rs @@ -4,18 +4,52 @@ use std::time::Duration; use color_eyre::eyre::Result; use namada_apps::cli::cmds::*; -use namada_apps::cli::{self, safe_exit}; +use namada_apps::cli::{self, args, Context, safe_exit}; use namada_apps::client::{rpc, tx, utils}; use namada_apps::facade::tendermint::block::Height; use namada_apps::facade::tendermint_config::net::Address as TendermintAddress; use namada_apps::facade::tendermint_rpc::{Client, HttpClient}; use tokio::time::sleep; +use namada::types::token::{Amount, DenominatedAmount, Denomination}; +use namada_apps::cli::args::{InputAmount, Tx}; +use namada_apps::cli::context::FromContext; +use namada_apps::config::TendermintMode; pub async fn main() -> Result<()> { + let test_cmd = NamadaClientWithContext::TxTransfer(TxTransfer(args::TxTransfer{ + tx: Tx { + dry_run: false, + dump_tx: false, + force: false, + broadcast_only: false, + ledger_address: TendermintAddress::Tcp {peer_id: None, host: "127.0.0.1".to_string(), port: 27657}, + initialized_account_alias: None, + fee_amount: InputAmount::Unvalidated(DenominatedAmount { amount: Amount::zero(), denom: Denomination(6) }), + fee_token: FromContext::new("NAM".into()), + gas_limit: Default::default(), + expiration: None, + signing_key: None, + signer: Some(FromContext::new("Bertha".into())), + }, + source: FromContext::new("xsktest1qqqqqqqqqqqqqqpagte43rsza46v55dlz8cffahv0fnr6eqacvnrkyuf9lmndgal7c2k4r7f7zu2yr5rjwr374unjjeuzrh6mquzy6grfdcnnu5clzaq2llqhr70a8yyx0p62aajqvrqjxrht3myuyypsvm725uyt5vm0fqzrzuuedtf6fala4r4nnazm9y9hq5yu6pq24arjskmpv4mdgfn3spffxxv8ugvym36kmnj45jcvvmm227vqjm5fq8882yhjsq97p7xrwqqd82s0".into()), + target: FromContext::new("Christel".into()), + token: FromContext::new("ETH".into()), + sub_prefix: None, + amount: InputAmount::Unvalidated(DenominatedAmount { amount: Amount::from_uint(30, 0).unwrap(), denom: Denomination(0) }), + })); match cli::namada_client_cli()? { cli::NamadaClient::WithContext(cmd_box) => { let (cmd, ctx) = *cmd_box; use NamadaClientWithContext as Sub; + /*let global_args = args::Global { + chain_id: None, + base_dir: " /tmp/.tmpmalzmo".into(), + wasm_dir: None, + mode: Some(TendermintMode::Full) + }; + + let ctx = Context::new(global_args).unwrap(); + println!("{:?}", test_cmd);*/ match cmd { // Ledger cmds Sub::TxCustom(TxCustom(args)) => { diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 30369842ad..085525b193 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -619,7 +619,7 @@ impl From for Amount { /// Represents the amount used of different conversions pub type Conversions = - HashMap, Change)>; + HashMap, i128)>; /// Represents an amount that is pub type MaspDenominatedAmount = Amount<(TokenAddress, MaspDenom)>; @@ -1110,7 +1110,7 @@ impl ShieldedContext { .insert(asset_type, (addr, sub_prefix, denom, ep)); // If the conversion is 0, then we just have a pure decoding if conv != Amount::zero() { - conv_entry.insert((conv.into(), path, Change::zero())); + conv_entry.insert((conv.into(), path, 0)); } } } @@ -1153,7 +1153,6 @@ impl ShieldedContext { #[allow(clippy::too_many_arguments)] async fn apply_conversion( &mut self, - client: &HttpClient, conv: &MaspAmount, asset_type: (Epoch, TokenAddress), value: Change, @@ -1164,10 +1163,9 @@ impl ShieldedContext { return None; } // If conversion if possible, accumulate the exchanged amount - let conv = self.decode_all_amounts(client, conv.into()).await; // println!("conv {:?}", conv); // The amount required of current asset to qualify for conversion - let threshold = token::Change::from(-conv[&asset_type]); + let threshold = -conv[&asset_type]; // println!("theshold: {}, value: {}", threshold, value); if threshold.is_zero() { eprintln!( @@ -1224,7 +1222,6 @@ impl ShieldedContext { &mut conversions, ) .await; - println!("\n\nconversions {:?}\n\n", conv); if let (Some(conv), false) = ( conv.get(&(target_epoch, token_addr.clone())), at_target_asset_type, @@ -1234,7 +1231,6 @@ impl ShieldedContext { ); if let Some(used) = self .apply_conversion( - &client, conv, (asset_epoch, token_addr.clone()), value, @@ -1253,7 +1249,9 @@ impl ShieldedContext { if let Some((_, _, usage)) = conversions.get_mut(&target_asset_type) { - *usage += used; + println!("used: {:?}", used); + *usage += denom.denominate_i128(&used); + println!("usage: {:?}", usage); } } }; @@ -1264,7 +1262,6 @@ impl ShieldedContext { { if let Some(used) = self .apply_conversion( - &client, conv, (asset_epoch, token_addr.clone()), value, @@ -1283,7 +1280,9 @@ impl ShieldedContext { if let Some((_, _, usage)) = conversions.get_mut(&asset_type) { - *usage += used; + println!("used: {:?}", used); + *usage += denom.denominate_i128(&used); + println!("usage: {:?}", usage); } } } @@ -1306,7 +1305,7 @@ impl ShieldedContext { comp.insert((target_epoch, key), val); } output += comp; - println!("\n\noutput {:?}\n\n", output); + println!("\n\nconversions {:?}\n\n", output); (output.into(), conversions) } @@ -1377,9 +1376,6 @@ impl ShieldedContext { } } } - println!("val_acc: {:?}", val_acc); - println!("notes: {:?}", notes); - println!("conversions: {:?}", conversions); (val_acc, notes, conversions) } @@ -1591,16 +1587,16 @@ impl ShieldedContext { ) .await; if let Some((a, _, _)) = conversions.get(&target_asset_type) { - conv.insert( - (target_epoch, token_addr.clone()), - self.decode_all_amounts(&client, a.clone().into()).await, - ); + let amt = self.decode_all_amounts(&client, a.clone().into()).await; + conv.entry((target_epoch, token_addr.clone())) + .and_modify(|e| *e += amt.clone()) + .or_insert(amt); } if let Some((a, _, _)) = conversions.get(&asset_type) { - conv.insert( - (asset_epoch, token_addr.clone()), - self.decode_all_amounts(&client, a.clone().into()).await, - ); + let amt = self.decode_all_amounts(&client, a.clone().into()).await; + conv.entry((asset_epoch, token_addr.clone())) + .and_modify(|e| *e += amt.clone()) + .or_insert(amt); } } conv @@ -1738,20 +1734,22 @@ async fn gen_shielded_transfer( epoch, ) .await; + println!("used convs: {:?}", used_convs); // Commit the notes found to our transaction for (diversifier, note, merkle_path) in unspent_notes { + let decoded = ctx.shielded.decode_asset_type(client.clone(), note.asset_type.clone()).await.unwrap(); + println!("Adding note value: {:?}: {:?}", decoded, note.value); builder.add_sapling_spend(sk, diversifier, note, merkle_path)?; } // Commit the conversion notes used during summation for (conv, wit, value) in used_convs.values() { if value.is_positive() { - for denom in MaspDenom::iter() { - builder.add_convert( - conv.clone(), - denom.denominate(&token::Amount::from(*value)), - wit.clone(), - )?; - } + println!("adding conversion {:?} -> {}", conv.assets, *value as u64); + builder.add_convert( + conv.clone(), + *value as u64, + wit.clone(), + )?; } } } else { @@ -1809,6 +1807,8 @@ async fn gen_shielded_transfer( target_enc.as_ref(), )); for (denom, asset_type) in MaspDenom::iter().zip(asset_types.iter()) { + let decoded = ctx.shielded.decode_asset_type(client.clone(), asset_type.clone()).await.unwrap(); + println!("Adding transparent outpt: {:?}: {}", decoded, denom.denominate(&amt)); builder.add_transparent_output( &TransparentAddress::PublicKey(hash.into()), *asset_type, From 087341d65a546983b830cf9f147aa008db2c0188 Mon Sep 17 00:00:00 2001 From: satan Date: Thu, 1 Jun 2023 16:58:36 +0200 Subject: [PATCH 070/151] WIP fixing masp incentives --- Cargo.lock | 4 +- apps/Cargo.toml | 4 +- apps/src/lib/client/rpc.rs | 3 +- apps/src/lib/client/tx.rs | 77 ++++++++++------ core/Cargo.toml | 2 +- core/src/types/token.rs | 19 +++- core/src/types/uint.rs | 30 ++++++- shared/Cargo.toml | 4 +- tests/src/e2e/ledger_tests.rs | 127 +++++++++++++++------------ tx_prelude/Cargo.toml | 2 +- vm_env/Cargo.toml | 4 +- wasm/Cargo.lock | 4 +- wasm/checksums.json | 36 ++++---- wasm/wasm_source/Cargo.toml | 4 +- wasm/wasm_source/src/vp_implicit.rs | 5 +- wasm/wasm_source/src/vp_user.rs | 5 +- wasm/wasm_source/src/vp_validator.rs | 5 +- 17 files changed, 200 insertions(+), 135 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2beee040c5..ab6910b899 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3484,7 +3484,7 @@ dependencies = [ [[package]] name = "masp_primitives" version = "0.5.0" -source = "git+https://github.com/anoma/masp?rev=402b08accff6cf656b7f63823d074931cdcf7773#402b08accff6cf656b7f63823d074931cdcf7773" +source = "git+https://github.com/anoma/masp?rev=ea4fcc68d9412b94042c1c2f0ea0212394e1be37#ea4fcc68d9412b94042c1c2f0ea0212394e1be37" dependencies = [ "aes", "bip0039", @@ -3517,7 +3517,7 @@ dependencies = [ [[package]] name = "masp_proofs" version = "0.5.0" -source = "git+https://github.com/anoma/masp?rev=402b08accff6cf656b7f63823d074931cdcf7773#402b08accff6cf656b7f63823d074931cdcf7773" +source = "git+https://github.com/anoma/masp?rev=ea4fcc68d9412b94042c1c2f0ea0212394e1be37#ea4fcc68d9412b94042c1c2f0ea0212394e1be37" dependencies = [ "bellman", "blake2b_simd 1.0.0", diff --git a/apps/Cargo.toml b/apps/Cargo.toml index 5e8b337f0e..cc319c8287 100644 --- a/apps/Cargo.toml +++ b/apps/Cargo.toml @@ -148,8 +148,8 @@ tracing-subscriber = {version = "0.3.7", features = ["env-filter", "json"]} websocket = "0.26.2" winapi = "0.3.9" #libmasp = { git = "https://github.com/anoma/masp", branch = "murisi/masp-incentive" } -masp_primitives = { git = "https://github.com/anoma/masp", rev = "402b08accff6cf656b7f63823d074931cdcf7773", features = ["transparent-inputs"] } -masp_proofs = { git = "https://github.com/anoma/masp", rev = "402b08accff6cf656b7f63823d074931cdcf7773", features = ["bundled-prover", "download-params"] } +masp_primitives = { git = "https://github.com/anoma/masp", rev = "ea4fcc68d9412b94042c1c2f0ea0212394e1be37", features = ["transparent-inputs"] } +masp_proofs = { git = "https://github.com/anoma/masp", rev = "ea4fcc68d9412b94042c1c2f0ea0212394e1be37", features = ["bundled-prover", "download-params"] } bimap = {version = "0.6.2", features = ["serde"]} [dev-dependencies] diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index d90283aa9f..317882f9a7 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -44,8 +44,7 @@ use namada::types::storage::{ BlockHeight, BlockResults, Epoch, Key, KeySeg, PrefixValue, TxIndex, }; use namada::types::token::{ - balance_key, Change, DenominatedAmount, MaspDenom, - TokenAddress, Transfer, + balance_key, Change, DenominatedAmount, MaspDenom, TokenAddress, Transfer, }; use namada::types::transaction::{ process_tx, AffineCurve, DecryptedTx, EllipticCurve, PairingEngine, TxType, diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 973580a653..01fa0391ad 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -53,7 +53,7 @@ use namada::types::storage::{ }; use namada::types::time::DateTimeUtc; use namada::types::token::{ - DenominatedAmount, MaspDenom, TokenAddress, Transfer, HEAD_TX_KEY, + Change, DenominatedAmount, MaspDenom, TokenAddress, Transfer, HEAD_TX_KEY, PIN_KEY_PREFIX, TX_KEY_PREFIX, }; use namada::types::transaction::governance::{ @@ -520,8 +520,8 @@ pub struct MaspChange { pub struct MaspAmount(HashMap<(Epoch, TokenAddress), token::Change>); impl MaspAmount { - pub fn pop(&mut self) -> Option<((Epoch, TokenAddress), token::Change)>{ - let key = self.keys().next()?.clone(); + pub fn pop(&mut self) -> Option<((Epoch, TokenAddress), token::Change)> { + let key = self.keys().find(|(e, _)| e.0 != 0)?.clone(); let value = self.remove(&key).unwrap(); Some((key, value)) } @@ -581,6 +581,17 @@ impl std::ops::SubAssign for MaspAmount { } } +impl std::ops::Mul for MaspAmount { + type Output = Self; + + fn mul(mut self, rhs: Change) -> Self::Output { + for (_, value) in self.iter_mut() { + *value = *value * rhs + } + self + } +} + impl<'a> From<&'a MaspAmount> for Amount { fn from(masp_amount: &'a MaspAmount) -> Amount { let mut res = Amount::zero(); @@ -592,7 +603,7 @@ impl<'a> From<&'a MaspAmount> for Amount { &key.sub_prefix, denom, ); - res += Amount::from_pair(asset, denom.denominate_i64(val)) + res += Amount::from_pair(asset, denom.denominate_i128(val)) .unwrap(); } } @@ -600,9 +611,15 @@ impl<'a> From<&'a MaspAmount> for Amount { } } +impl From for Amount { + fn from(amt: MaspAmount) -> Self { + Self::from(&amt) + } +} + /// Represents the amount used of different conversions pub type Conversions = - HashMap, i128)>; + HashMap, Change)>; /// Represents an amount that is pub type MaspDenominatedAmount = Amount<(TokenAddress, MaspDenom)>; @@ -1083,26 +1100,17 @@ impl ShieldedContext { client: HttpClient, asset_type: AssetType, conversions: &'a mut Conversions, - ) -> Option<&'a mut (AllowedConversion, MerklePath, i128)> { - match conversions.entry(asset_type) { - Entry::Occupied(conv_entry) => Some(conv_entry.into_mut()), - Entry::Vacant(conv_entry) => { - // Query for the ID of the last accepted transaction - let (addr, sub_prefix, denom, ep, conv, path): ( - Address, - _, - _, - _, - _, - _, - ) = query_conversion(client, asset_type).await?; + ) { + if let Entry::Vacant(conv_entry) = conversions.entry(asset_type) { + // Query for the ID of the last accepted transaction + if let Some((addr, sub_prefix, denom, ep, conv, path)) = + query_conversion(client, asset_type).await + { self.asset_types .insert(asset_type, (addr, sub_prefix, denom, ep)); // If the conversion is 0, then we just have a pure decoding - if conv == Amount::zero() { - None - } else { - Some(conv_entry.insert((Amount::into(conv), path, 0))) + if conv != Amount::zero() { + conv_entry.insert((conv.into(), path, Change::zero())); } } } @@ -1356,6 +1364,9 @@ impl ShieldedContext { } } } + println!("val_acc: {:?}", val_acc); + println!("notes: {:?}", notes); + println!("conversions: {:?}", conversions); (val_acc, notes, conversions) } @@ -1538,7 +1549,13 @@ fn convert_amount( sub_prefix: &Option<&String>, val: &token::Amount, ) -> ([AssetType; 4], Amount) { - println!("{}", DenominatedAmount { amount: val.clone(), denom: 18.into()}); + println!( + "{}", + DenominatedAmount { + amount: val.clone(), + denom: 18.into() + } + ); let mut amount = Amount::zero(); let asset_types: [AssetType; 4] = MaspDenom::iter() .map(|denom| { @@ -1662,12 +1679,14 @@ async fn gen_shielded_transfer( } // Commit the conversion notes used during summation for (conv, wit, value) in used_convs.values() { - if *value > 0 { - builder.add_convert( - conv.clone(), - *value as u64, - wit.clone(), - )?; + if value.is_positive() { + for denom in MaspDenom::iter() { + builder.add_convert( + conv.clone(), + denom.denominate(&token::Amount::from(*value)), + wit.clone(), + )?; + } } } } else { diff --git a/core/Cargo.toml b/core/Cargo.toml index 3a68ca3abc..ba3beeb5e6 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -83,7 +83,7 @@ impl-num-traits = "0.1.2" index-set = {git = "https://github.com/heliaxdev/index-set", tag = "v0.7.1", features = ["serialize-borsh", "serialize-serde"]} itertools = "0.10.0" libsecp256k1 = {git = "https://github.com/heliaxdev/libsecp256k1", rev = "bbb3bd44a49db361f21d9db80f9a087c194c0ae9", default-features = false, features = ["std", "static-context"]} -masp_primitives = { git = "https://github.com/anoma/masp", rev = "402b08accff6cf656b7f63823d074931cdcf7773" } +masp_primitives = { git = "https://github.com/anoma/masp", rev = "ea4fcc68d9412b94042c1c2f0ea0212394e1be37" } proptest = {git = "https://github.com/heliaxdev/proptest", branch = "tomas/sm", optional = true} prost = "0.9.0" prost-types = "0.9.0" diff --git a/core/src/types/token.rs b/core/src/types/token.rs index dccef5683d..910e51b180 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -696,8 +696,8 @@ impl MaspDenom { } /// Get the corresponding u64 word from the input uint256. - pub fn denominate_i64(&self, amount: &Change) -> i64 { - let val = amount.abs().0[*self as usize] as i64; + pub fn denominate_i128(&self, amount: &Change) -> i128 { + let val = amount.abs().0[*self as usize] as i128; if Change::is_negative(amount) { -val } else { @@ -1150,6 +1150,21 @@ mod tests { let non_zero = Amount::from_uint(1, 0).expect("Test failed"); assert!(!non_zero.is_zero()); } + + #[test] + fn testy_poo() { + let change = Change::from(30000000000000000000i128); + let output = Change::from(6893488147419103231i128); + + let amt = DenominatedAmount { + amount: Amount::from(change), + denom: 18.into(), + }; + println!("{}", amt); + println!("{:?}", change.0.0); + println!("{:?}", output.0.0); + assert!(false); + } } /// Helpers for testing with addresses. diff --git a/core/src/types/uint.rs b/core/src/types/uint.rs index 640f79ee6c..0f5f617d8c 100644 --- a/core/src/types/uint.rs +++ b/core/src/types/uint.rs @@ -3,7 +3,7 @@ //! the backing type of token amounts. use std::cmp::Ordering; use std::fmt::{Debug, Display, Formatter}; -use std::ops::{Add, AddAssign, BitXor, Div, Mul, Neg, Sub, SubAssign}; +use std::ops::{Add, AddAssign, BitXor, Div, Mul, Neg, Rem, Sub, SubAssign}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use impl_num_traits::impl_uint_num_traits; @@ -106,6 +106,11 @@ impl I256 { !self.non_negative() } + /// Check if the amount is positive (greater than zero) + pub fn is_positive(&self) -> bool { + self.non_negative() && !self.is_zero() + } + /// Get the absolute value pub fn abs(&self) -> Uint { if self.non_negative() { @@ -327,6 +332,29 @@ impl Div for I256 { } } +impl Div for I256 { + type Output = Self; + + fn div(self, rhs: I256) -> Self::Output { + if rhs.is_negative() { + -(self / rhs.abs()) + } else { + self / rhs.abs() + } + } +} + +impl Rem for I256 { + type Output = Self; + + fn rem(self, rhs: Self) -> Self::Output { + if self.is_negative() { + -(Self(self.abs() % rhs.abs())) + } else { + Self(self.abs() % rhs.abs()) + } + } +} impl From for I256 { fn from(val: i128) -> Self { if val < 0 { diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 0f059bc663..4bc5900da0 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -98,8 +98,8 @@ ibc = {version = "0.14.0", default-features = false, optional = true} ibc-proto = {version = "0.17.1", default-features = false, optional = true} itertools = "0.10.0" loupe = {version = "0.1.3", optional = true} -masp_primitives = { git = "https://github.com/anoma/masp", rev = "402b08accff6cf656b7f63823d074931cdcf7773" } -masp_proofs = { git = "https://github.com/anoma/masp", rev = "402b08accff6cf656b7f63823d074931cdcf7773" } +masp_primitives = { git = "https://github.com/anoma/masp", rev = "ea4fcc68d9412b94042c1c2f0ea0212394e1be37" } +masp_proofs = { git = "https://github.com/anoma/masp", rev = "ea4fcc68d9412b94042c1c2f0ea0212394e1be37" } parity-wasm = {version = "0.45.0", features = ["sign_ext"], optional = true} paste = "1.0.9" # A fork with state machine testing diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 26582aa20e..453a2dd1c7 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -27,7 +27,9 @@ use namada_apps::client::tx::ShieldedContext; use namada_apps::config::genesis::genesis_config::{ GenesisConfig, ParametersConfig, PosParamsConfig, }; -use namada_core::types::token::{DenominatedAmount, Denomination, NATIVE_MAX_DECIMAL_PLACES}; +use namada_core::types::token::{ + DenominatedAmount, Denomination, NATIVE_MAX_DECIMAL_PLACES, +}; use namada_test_utils::TestWasms; use serde_json::json; use setup::constants::*; @@ -1137,10 +1139,8 @@ fn masp_incentives() -> Result<()> { client.exp_string("btc: 20")?; client.assert_success(); - let amt20 = - token::Amount::from_uint(20, BTC_DENOMINATION).unwrap(); - let amt30 = - token::Amount::from_uint(30, ETH_DENOMINATION).unwrap(); + let amt20 = token::Amount::from_uint(20, BTC_DENOMINATION).unwrap(); + let amt30 = token::Amount::from_uint(30, ETH_DENOMINATION).unwrap(); // Assert NAM balance at VK(A) is 20*BTC_reward*(epoch_1-epoch_0) let mut client = run!( @@ -1158,10 +1158,11 @@ fn masp_incentives() -> Result<()> { Some(60) )?; let amt = (amt20 * masp_rewards[&(btc(), None)]).0 * (ep1.0 - ep0.0); - let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; - client.exp_string(&format!( - "nam: {}", denominated, - ))?; + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated,))?; client.assert_success(); // Assert NAM balance at MASP pool is 20*BTC_reward*(epoch_1-epoch_0) @@ -1180,10 +1181,11 @@ fn masp_incentives() -> Result<()> { Some(60) )?; let amt = (amt20 * masp_rewards[&(btc(), None)]).0 * (ep1.0 - ep0.0); - let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into() }; - client.exp_string(&format!( - "nam: {}", denominated, - ))?; + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated,))?; client.assert_success(); // Wait till epoch boundary @@ -1223,10 +1225,11 @@ fn masp_incentives() -> Result<()> { Some(60) )?; let amt = (amt20 * masp_rewards[&(btc(), None)]).0 * (ep2.0 - ep0.0); - let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; - client.exp_string(&format!( - "nam: {}", denominated, - ))?; + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated,))?; client.assert_success(); // Assert NAM balance at MASP pool is 20*BTC_reward*(epoch_2-epoch_0) @@ -1245,10 +1248,11 @@ fn masp_incentives() -> Result<()> { Some(60) )?; let amt = (amt20 * masp_rewards[&(btc(), None)]).0 * (ep2.0 - ep0.0); - let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; - client.exp_string(&format!( - "nam: {}", denominated, - ))?; + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated,))?; client.assert_success(); // Wait till epoch boundary @@ -1349,10 +1353,11 @@ fn masp_incentives() -> Result<()> { Some(60) )?; let amt = (amt30 * masp_rewards[&(eth(), None)]).0 * (ep4.0 - ep3.0); - let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; - client.exp_string(&format!( - "nam: {}", denominated, - ))?; + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated,))?; client.assert_success(); // Assert NAM balance at MASP pool is @@ -1373,10 +1378,11 @@ fn masp_incentives() -> Result<()> { )?; let amt = ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep4.0 - ep0.0)) + ((amt30 * masp_rewards[&(eth(), None)]).0 * (ep4.0 - ep3.0)); - let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; - client.exp_string(&format!( - "nam: {}", denominated - ))?; + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated))?; client.assert_success(); // Wait till epoch boundary @@ -1442,10 +1448,11 @@ fn masp_incentives() -> Result<()> { Some(60) )?; let amt = (amt30 * masp_rewards[&(eth(), None)]).0 * (ep.0 - ep3.0); - let denominated = DenominatedAmount{ amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; - client.exp_string(&format!( - "nam: {}", denominated, - ))?; + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated,))?; client.assert_success(); ep = get_epoch(&test, &validator_one_rpc)?; @@ -1467,10 +1474,11 @@ fn masp_incentives() -> Result<()> { )?; let amt = ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep.0 - ep0.0)) + ((amt30 * masp_rewards[&(eth(), None)]).0 * (ep.0 - ep3.0)); - let denominated = DenominatedAmount{ amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; - client.exp_string(&format!( - "nam: {}", denominated - ))?; + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated))?; client.assert_success(); // Wait till epoch boundary @@ -1534,10 +1542,11 @@ fn masp_incentives() -> Result<()> { Some(60) )?; let amt = (amt20 * masp_rewards[&(btc(), None)]).0 * (ep6.0 - ep0.0); - let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; - client.exp_string(&format!( - "nam: {}", denominated, - ))?; + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated,))?; client.assert_success(); // Assert NAM balance at MASP pool is @@ -1558,10 +1567,11 @@ fn masp_incentives() -> Result<()> { )?; let amt = ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep6.0 - ep0.0)) + ((amt30 * masp_rewards[&(eth(), None)]).0 * (ep5.0 - ep3.0)); - let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into() }; - client.exp_string(&format!( - "nam: {}", denominated, - ))?; + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated,))?; client.assert_success(); // Wait till epoch boundary @@ -1583,10 +1593,11 @@ fn masp_incentives() -> Result<()> { Some(60) )?; let amt = (amt20 * masp_rewards[&(btc(), None)]).0 * (ep6.0 - ep0.0); - let denominated = DenominatedAmount {amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; - client.exp_string(&format!( - "nam: {}", denominated - ))?; + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated))?; client.assert_success(); // Assert NAM balance at VK(B) is 30*ETH_reward*(epoch_5-epoch_3) @@ -1605,10 +1616,11 @@ fn masp_incentives() -> Result<()> { Some(60) )?; let amt = (amt30 * masp_rewards[&(eth(), None)]).0 * (ep5.0 - ep3.0); - let denominated = DenominatedAmount {amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; - client.exp_string(&format!( - "nam: {}", denominated, - ))?; + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated,))?; client.assert_success(); // Assert NAM balance at MASP pool is @@ -1629,10 +1641,11 @@ fn masp_incentives() -> Result<()> { )?; let amt = ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep6.0 - ep0.0)) + ((amt30 * masp_rewards[&(eth(), None)]).0 * (ep5.0 - ep3.0)); - let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into()}; - client.exp_string(&format!( - "nam: {}", denominated, - ))?; + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated,))?; client.assert_success(); // Wait till epoch boundary to prevent conversion expiry during transaction diff --git a/tx_prelude/Cargo.toml b/tx_prelude/Cargo.toml index 6de40aed20..6d5f9c1433 100644 --- a/tx_prelude/Cargo.toml +++ b/tx_prelude/Cargo.toml @@ -15,7 +15,7 @@ abciplus = [ ] [dependencies] -masp_primitives = { git = "https://github.com/anoma/masp", rev = "402b08accff6cf656b7f63823d074931cdcf7773" } +masp_primitives = { git = "https://github.com/anoma/masp", rev = "ea4fcc68d9412b94042c1c2f0ea0212394e1be37" } namada_core = {path = "../core", default-features = false} namada_macros = {path = "../macros"} namada_proof_of_stake = {path = "../proof_of_stake", default-features = false} diff --git a/vm_env/Cargo.toml b/vm_env/Cargo.toml index 4e9bd48a47..f359161765 100644 --- a/vm_env/Cargo.toml +++ b/vm_env/Cargo.toml @@ -16,6 +16,6 @@ abciplus = [ namada_core = {path = "../core", default-features = false} borsh = "0.9.0" #libmasp = { git = "https://github.com/anoma/masp", branch = "murisi/masp-incentive" } -masp_primitives = { git = "https://github.com/anoma/masp", rev = "402b08accff6cf656b7f63823d074931cdcf7773" } -masp_proofs = { git = "https://github.com/anoma/masp", rev = "402b08accff6cf656b7f63823d074931cdcf7773" } +masp_primitives = { git = "https://github.com/anoma/masp", rev = "ea4fcc68d9412b94042c1c2f0ea0212394e1be37" } +masp_proofs = { git = "https://github.com/anoma/masp", rev = "ea4fcc68d9412b94042c1c2f0ea0212394e1be37" } hex = "0.4.3" diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index b4d4e87839..604abb3686 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -2425,7 +2425,7 @@ dependencies = [ [[package]] name = "masp_primitives" version = "0.5.0" -source = "git+https://github.com/anoma/masp?rev=402b08accff6cf656b7f63823d074931cdcf7773#402b08accff6cf656b7f63823d074931cdcf7773" +source = "git+https://github.com/anoma/masp?rev=ea4fcc68d9412b94042c1c2f0ea0212394e1be37#ea4fcc68d9412b94042c1c2f0ea0212394e1be37" dependencies = [ "aes", "bip0039", @@ -2456,7 +2456,7 @@ dependencies = [ [[package]] name = "masp_proofs" version = "0.5.0" -source = "git+https://github.com/anoma/masp?rev=402b08accff6cf656b7f63823d074931cdcf7773#402b08accff6cf656b7f63823d074931cdcf7773" +source = "git+https://github.com/anoma/masp?rev=ea4fcc68d9412b94042c1c2f0ea0212394e1be37#ea4fcc68d9412b94042c1c2f0ea0212394e1be37" dependencies = [ "bellman", "blake2b_simd 1.0.0", diff --git a/wasm/checksums.json b/wasm/checksums.json index ebd6ccc2b1..d40a0ba7b4 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,20 +1,20 @@ { - "tx_bond.wasm": "tx_bond.a63917a5f9971fd9e0c38039327c477f6a5ac70850a6714d6606c7d8c625ec5d.wasm", - "tx_change_validator_commission.wasm": "tx_change_validator_commission.494320a5fd32eb09f8d5ec4e0950f57ab4da68aae5940e0e5d27030cca4334bf.wasm", - "tx_ibc.wasm": "tx_ibc.bed0f324d800aa2da8492767d7addd607d85918159407735120c8b8d0cdff882.wasm", - "tx_init_account.wasm": "tx_init_account.025fef966350ee18b643b1d2b83d99d264b82e4f247be3d3d9709cd68db1e99d.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.02388a86b695c410830ce7eb03a948dcc1cb74b37cf2ff04488485a84a0fe4c6.wasm", - "tx_init_validator.wasm": "tx_init_validator.a2f7e28fc73d67b17d2dec7cda589d54c988ce34267d4a08d3b31c831973ac53.wasm", - "tx_reveal_pk.wasm": "tx_reveal_pk.da894ee43081312e9f0dc7a1ebc010ea087348d0e17588a4f9ed876b5a64466c.wasm", - "tx_transfer.wasm": "tx_transfer.569847cc57c5872b6be5485d8e51ae7fdbb2c147b442f3dced2d7a47dad997d3.wasm", - "tx_unbond.wasm": "tx_unbond.02932b2c5b5ddc5c5fa23955014eb7e40551c62e065d135d3666003c7f53c206.wasm", - "tx_update_vp.wasm": "tx_update_vp.1910af6c9d86bb241b7e8d2aede17aa131e77ea888f37ab125bc3f8163bc45ec.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.99f64828e6e8571b7f4ae0050c1ed3f36184f984ed595f3b588a9c639943aede.wasm", - "tx_withdraw.wasm": "tx_withdraw.6ccc14bcbbaaa4fe02cbc10b16301dee9c306cef6fad0b16091295cc105d6e79.wasm", - "vp_implicit.wasm": "vp_implicit.6434963d9844da7dc382fcc09808583bd57740f0ab0a6f13ad940e35ebe4dec3.wasm", - "vp_masp.wasm": "vp_masp.7e29cd91a45bf0e2f8976e9db739dcdb7ae6cbb38f8f5f6d82eedbe24857474b.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.b581d376d9b4454800bb82c32a4377cfe4dc67ac2a08cf8d7ad9a7a80bf6a16b.wasm", - "vp_token.wasm": "vp_token.246329086c51c170ff890a3d361db42f2f34af9781a8b7b8f12c81764fcad223.wasm", - "vp_user.wasm": "vp_user.74814ed9d67905454fdda739d2f4959f08bca96095c0b6a30abcdb60b7474af2.wasm", - "vp_validator.wasm": "vp_validator.fe9672f182cdbece544d9bf2c5bd86ed984b07add82ecabc47380ae6685f90c7.wasm" + "tx_bond.wasm": "tx_bond.9259e3386b68f7ee0e35a3dfe7e96b1543e4ee8a6215010eae76de0104c9c9c2.wasm", + "tx_change_validator_commission.wasm": "tx_change_validator_commission.fcc26c4678517d7097f41257c84c294b8c48eb4a0250b0ec74ee634d7935cf1e.wasm", + "tx_ibc.wasm": "tx_ibc.7ca7ee70182913d4167aeec2703b21712af82785856bab59a25f1fe732095dcc.wasm", + "tx_init_account.wasm": "tx_init_account.709c1a6b0de57f8cfb4bf84676f263370bf47256fcdbdf0cb91a8ef5fccb7cea.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.e2568a89aed19218367d64f63a14a74973d7768577f2dcfd8c3b0e0558df6890.wasm", + "tx_init_validator.wasm": "tx_init_validator.63709c3297d1752efb9b7a943c82edb27aa61c82a95857739273080bee9d42cf.wasm", + "tx_reveal_pk.wasm": "tx_reveal_pk.c78cb00453c5f2b0eaec49b36e895ac8d363261eb2af5e719dd641424847383f.wasm", + "tx_transfer.wasm": "tx_transfer.6a656bcb2a13ff3c4402ff7e275c080f4b1cf1de8f6112bbfbc03bf07167b297.wasm", + "tx_unbond.wasm": "tx_unbond.96ca5b536a02093d3f35a0e631670f10960c5bad220082e992003eb02c42f5f8.wasm", + "tx_update_vp.wasm": "tx_update_vp.8fc0dde790bf9304ce80fdbb85c85f6671dac6eedeb19cde17e825e6b477c23b.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.c5a856595b0a76b7191aec1bf1912d69e9d29e94e412ce7ebd7c96467900bfd8.wasm", + "tx_withdraw.wasm": "tx_withdraw.43cf91dc3fec04ffe740da865e55b00b6e6d71a6da9dcfde378506aedd715f24.wasm", + "vp_implicit.wasm": "vp_implicit.377e14b90713516ed12fd10975a13c9d74bd3a4de8f40be17e9aa4315953c282.wasm", + "vp_masp.wasm": "vp_masp.eb53123cad15bf82a09501dfe9da8dbfa0070468f6c8376dab4a62fd8ac58548.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.9059ec6cd09ce0a7bde581b487d1e53921a9c26f29c76071d3a8d68fdba9bdab.wasm", + "vp_token.wasm": "vp_token.cd4d62be7dbb08a1f983857a9a59689b8ac12856ce15480173e26d94647adf86.wasm", + "vp_user.wasm": "vp_user.79e5cd7e4cb39fbb2b54c8a59b2a142f53050b030ec79acab37a36fd24402ece.wasm", + "vp_validator.wasm": "vp_validator.257b596e176e2db8057b2a6489f978b77c30dbd77282dd5e57809d75dd6d7f0d.wasm" } \ No newline at end of file diff --git a/wasm/wasm_source/Cargo.toml b/wasm/wasm_source/Cargo.toml index 61d6e9f4b3..9d56f416e4 100644 --- a/wasm/wasm_source/Cargo.toml +++ b/wasm/wasm_source/Cargo.toml @@ -40,8 +40,8 @@ once_cell = {version = "1.8.0", optional = true} rust_decimal = {version = "1.26.1", optional = true} wee_alloc = "0.4.5" getrandom = { version = "0.2", features = ["custom"] } -masp_proofs = { git = "https://github.com/anoma/masp", rev = "402b08accff6cf656b7f63823d074931cdcf7773", optional = true } -masp_primitives = { git = "https://github.com/anoma/masp", rev = "402b08accff6cf656b7f63823d074931cdcf7773", optional = true } +masp_proofs = { git = "https://github.com/anoma/masp", rev = "ea4fcc68d9412b94042c1c2f0ea0212394e1be37", optional = true } +masp_primitives = { git = "https://github.com/anoma/masp", rev = "ea4fcc68d9412b94042c1c2f0ea0212394e1be37", optional = true } ripemd = "0.1.3" [dev-dependencies] diff --git a/wasm/wasm_source/src/vp_implicit.rs b/wasm/wasm_source/src/vp_implicit.rs index f8906a5f15..272d613bd4 100644 --- a/wasm/wasm_source/src/vp_implicit.rs +++ b/wasm/wasm_source/src/vp_implicit.rs @@ -129,10 +129,7 @@ fn validate_tx( } true } - KeyType::Token { - owner, - .. - } => { + KeyType::Token { owner, .. } => { if owner == &addr { let pre: token::Amount = ctx.read_pre(key)?.unwrap_or_default(); diff --git a/wasm/wasm_source/src/vp_user.rs b/wasm/wasm_source/src/vp_user.rs index c3f6424336..429008f660 100644 --- a/wasm/wasm_source/src/vp_user.rs +++ b/wasm/wasm_source/src/vp_user.rs @@ -102,10 +102,7 @@ fn validate_tx( for key in keys_changed.iter() { let key_type: KeyType = key.into(); let is_valid = match key_type { - KeyType::Token { - owner, - .. - } => { + KeyType::Token { owner, .. } => { if owner == &addr { let pre: token::Amount = ctx.read_pre(key)?.unwrap_or_default(); diff --git a/wasm/wasm_source/src/vp_validator.rs b/wasm/wasm_source/src/vp_validator.rs index 3309b75e26..f3cad12e93 100644 --- a/wasm/wasm_source/src/vp_validator.rs +++ b/wasm/wasm_source/src/vp_validator.rs @@ -102,10 +102,7 @@ fn validate_tx( for key in keys_changed.iter() { let key_type: KeyType = key.into(); let is_valid = match key_type { - KeyType::Token { - owner, - .. - } => { + KeyType::Token { owner, .. } => { if owner == &addr { let pre: token::Amount = ctx.read_pre(key)?.unwrap_or_default(); From ffd72ec7ce405079da5c1dab6b107c32096bc8f5 Mon Sep 17 00:00:00 2001 From: satan Date: Thu, 1 Jun 2023 17:35:55 +0200 Subject: [PATCH 071/151] WIP fixed bug in collecting unspent notes --- apps/src/lib/client/tx.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 01fa0391ad..4fd55c4c1b 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -485,7 +485,7 @@ pub fn is_amount_required(src: Amount, dest: Amount, delta: Amount) -> bool { if delta > Amount::zero() { let gap = dest - src; for (asset_type, value) in gap.components() { - if *value > 0 && delta[asset_type] > 0 { + if *value >= 0 && delta[asset_type] >= 0 { return true; } } @@ -1205,6 +1205,8 @@ impl ShieldedContext { let mut output = Amount::zero(); // Repeatedly exchange assets until it is no longer possible loop { + println!("\n\nInput {:?}\n\n", input); + let Some(((asset_epoch, token_addr), value)) = input.pop() else { break }; for denom in MaspDenom::iter() { let target_asset_type = make_asset_type( @@ -1294,7 +1296,14 @@ impl ShieldedContext { } } } - (output, conversions) + // finally convert the rewards in epoch 0. + let mut comp = MaspAmount::default(); + for ((_, key), val) in input.drain() { + comp.insert((target_epoch, key), val); + } + output += comp; + println!("\n\noutput {:?}\n\n", output); + (output.into(), conversions) } /// Collect enough unspent notes in this context to exceed the given amount From b4f616b2ff54bd0a667b7aa44614da07b4d992e1 Mon Sep 17 00:00:00 2001 From: satan Date: Fri, 2 Jun 2023 13:39:24 +0200 Subject: [PATCH 072/151] WIP --- apps/src/bin/namada-client/cli.rs | 36 ++++++++++++++++++++++++++++++- apps/src/lib/client/tx.rs | 31 +++++++++++++------------- 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/apps/src/bin/namada-client/cli.rs b/apps/src/bin/namada-client/cli.rs index 1a60d3f01b..cb86945d6c 100644 --- a/apps/src/bin/namada-client/cli.rs +++ b/apps/src/bin/namada-client/cli.rs @@ -4,18 +4,52 @@ use std::time::Duration; use color_eyre::eyre::Result; use namada_apps::cli::cmds::*; -use namada_apps::cli::{self, safe_exit}; +use namada_apps::cli::{self, args, Context, safe_exit}; use namada_apps::client::{rpc, tx, utils}; use namada_apps::facade::tendermint::block::Height; use namada_apps::facade::tendermint_config::net::Address as TendermintAddress; use namada_apps::facade::tendermint_rpc::{Client, HttpClient}; use tokio::time::sleep; +use namada::types::token::{Amount, DenominatedAmount, Denomination}; +use namada_apps::cli::args::{InputAmount, Tx}; +use namada_apps::cli::context::FromContext; +use namada_apps::config::TendermintMode; pub async fn main() -> Result<()> { + let test_cmd = NamadaClientWithContext::TxTransfer(TxTransfer(args::TxTransfer{ + tx: Tx { + dry_run: false, + dump_tx: false, + force: false, + broadcast_only: false, + ledger_address: TendermintAddress::Tcp {peer_id: None, host: "127.0.0.1".to_string(), port: 27657}, + initialized_account_alias: None, + fee_amount: InputAmount::Unvalidated(DenominatedAmount { amount: Amount::zero(), denom: Denomination(6) }), + fee_token: FromContext::new("NAM".into()), + gas_limit: Default::default(), + expiration: None, + signing_key: None, + signer: Some(FromContext::new("Bertha".into())), + }, + source: FromContext::new("xsktest1qqqqqqqqqqqqqqpagte43rsza46v55dlz8cffahv0fnr6eqacvnrkyuf9lmndgal7c2k4r7f7zu2yr5rjwr374unjjeuzrh6mquzy6grfdcnnu5clzaq2llqhr70a8yyx0p62aajqvrqjxrht3myuyypsvm725uyt5vm0fqzrzuuedtf6fala4r4nnazm9y9hq5yu6pq24arjskmpv4mdgfn3spffxxv8ugvym36kmnj45jcvvmm227vqjm5fq8882yhjsq97p7xrwqqd82s0".into()), + target: FromContext::new("Christel".into()), + token: FromContext::new("ETH".into()), + sub_prefix: None, + amount: InputAmount::Unvalidated(DenominatedAmount { amount: Amount::from_uint(30, 0).unwrap(), denom: Denomination(0) }), + })); match cli::namada_client_cli()? { cli::NamadaClient::WithContext(cmd_box) => { let (cmd, ctx) = *cmd_box; use NamadaClientWithContext as Sub; + /*let global_args = args::Global { + chain_id: None, + base_dir: " /tmp/.tmpmalzmo".into(), + wasm_dir: None, + mode: Some(TendermintMode::Full) + }; + + let ctx = Context::new(global_args).unwrap(); + println!("{:?}", test_cmd);*/ match cmd { // Ledger cmds Sub::TxCustom(TxCustom(args)) => { diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 4fd55c4c1b..f6f5607728 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -619,7 +619,7 @@ impl From for Amount { /// Represents the amount used of different conversions pub type Conversions = - HashMap, Change)>; + HashMap, i128)>; /// Represents an amount that is pub type MaspDenominatedAmount = Amount<(TokenAddress, MaspDenom)>; @@ -1110,7 +1110,7 @@ impl ShieldedContext { .insert(asset_type, (addr, sub_prefix, denom, ep)); // If the conversion is 0, then we just have a pure decoding if conv != Amount::zero() { - conv_entry.insert((conv.into(), path, Change::zero())); + conv_entry.insert((conv.into(), path, 0)); } } } @@ -1301,9 +1301,9 @@ impl ShieldedContext { for ((_, key), val) in input.drain() { comp.insert((target_epoch, key), val); } - output += comp; - println!("\n\noutput {:?}\n\n", output); - (output.into(), conversions) + output += Amount::from(comp); + println!("\n\nconversions {:?}\n\n", output); + (output, conversions) } /// Collect enough unspent notes in this context to exceed the given amount @@ -1373,9 +1373,6 @@ impl ShieldedContext { } } } - println!("val_acc: {:?}", val_acc); - println!("notes: {:?}", notes); - println!("conversions: {:?}", conversions); (val_acc, notes, conversions) } @@ -1682,20 +1679,22 @@ async fn gen_shielded_transfer( epoch, ) .await; + println!("used convs: {:?}", used_convs); // Commit the notes found to our transaction for (diversifier, note, merkle_path) in unspent_notes { + let decoded = ctx.shielded.decode_asset_type(client.clone(), note.asset_type.clone()).await.unwrap(); + println!("Adding note value: {:?}: {:?}", decoded, note.value); builder.add_sapling_spend(sk, diversifier, note, merkle_path)?; } // Commit the conversion notes used during summation for (conv, wit, value) in used_convs.values() { if value.is_positive() { - for denom in MaspDenom::iter() { - builder.add_convert( - conv.clone(), - denom.denominate(&token::Amount::from(*value)), - wit.clone(), - )?; - } + println!("adding conversion {:?} -> {}", conv.assets, *value as u64); + builder.add_convert( + conv.clone(), + *value as u64, + wit.clone(), + )?; } } } else { @@ -1753,6 +1752,8 @@ async fn gen_shielded_transfer( target_enc.as_ref(), )); for (denom, asset_type) in MaspDenom::iter().zip(asset_types.iter()) { + let decoded = ctx.shielded.decode_asset_type(client.clone(), asset_type.clone()).await.unwrap(); + println!("Adding transparent outpt: {:?}: {}", decoded, denom.denominate(&amt)); builder.add_transparent_output( &TransparentAddress::PublicKey(hash.into()), *asset_type, From d2a07edd3a2dc6341bd3a42eaa19cfdf87206e22 Mon Sep 17 00:00:00 2001 From: satan Date: Fri, 2 Jun 2023 16:23:32 +0200 Subject: [PATCH 073/151] WIP small bug fixes and a debugger can be attached --- apps/src/bin/namada-client/cli.rs | 22 +++++++------- apps/src/lib/client/tx.rs | 50 +++++++++++++++++-------------- 2 files changed, 40 insertions(+), 32 deletions(-) diff --git a/apps/src/bin/namada-client/cli.rs b/apps/src/bin/namada-client/cli.rs index cb86945d6c..3fff3c9bad 100644 --- a/apps/src/bin/namada-client/cli.rs +++ b/apps/src/bin/namada-client/cli.rs @@ -3,6 +3,7 @@ use std::time::Duration; use color_eyre::eyre::Result; +use eyre::Chain; use namada_apps::cli::cmds::*; use namada_apps::cli::{self, args, Context, safe_exit}; use namada_apps::client::{rpc, tx, utils}; @@ -10,6 +11,7 @@ use namada_apps::facade::tendermint::block::Height; use namada_apps::facade::tendermint_config::net::Address as TendermintAddress; use namada_apps::facade::tendermint_rpc::{Client, HttpClient}; use tokio::time::sleep; +use namada::types::chain::ChainId; use namada::types::token::{Amount, DenominatedAmount, Denomination}; use namada_apps::cli::args::{InputAmount, Tx}; use namada_apps::cli::context::FromContext; @@ -37,20 +39,20 @@ pub async fn main() -> Result<()> { sub_prefix: None, amount: InputAmount::Unvalidated(DenominatedAmount { amount: Amount::from_uint(30, 0).unwrap(), denom: Denomination(0) }), })); - match cli::namada_client_cli()? { - cli::NamadaClient::WithContext(cmd_box) => { - let (cmd, ctx) = *cmd_box; + //match cli::namada_client_cli()? { + //cli::NamadaClient::WithContext(cmd_box) => { + //let (cmd, ctx) = *cmd_box; use NamadaClientWithContext as Sub; - /*let global_args = args::Global { - chain_id: None, - base_dir: " /tmp/.tmpmalzmo".into(), + let global_args = args::Global { + chain_id: Some(ChainId("e2e-test.1842a9ed7737c1c61d588".into())), + base_dir: "/tmp/.tmpmalzmo".into(), wasm_dir: None, mode: Some(TendermintMode::Full) }; let ctx = Context::new(global_args).unwrap(); - println!("{:?}", test_cmd);*/ - match cmd { + + match test_cmd { // Ledger cmds Sub::TxCustom(TxCustom(args)) => { wait_until_node_is_synched(&args.tx.ledger_address).await; @@ -176,7 +178,7 @@ pub async fn main() -> Result<()> { rpc::query_protocol_parameters(ctx, args).await; } } - } + /*} cli::NamadaClient::WithoutContext(cmd, global_args) => match cmd { // Utils cmds Utils::JoinNetwork(JoinNetwork(args)) => { @@ -192,7 +194,7 @@ pub async fn main() -> Result<()> { utils::init_genesis_validator(global_args, args) } }, - } + }*/ Ok(()) } diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index f6f5607728..815e86b827 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -52,10 +52,7 @@ use namada::types::storage::{ BlockHeight, Epoch, Key, KeySeg, TxIndex, RESERVED_ADDRESS_PREFIX, }; use namada::types::time::DateTimeUtc; -use namada::types::token::{ - Change, DenominatedAmount, MaspDenom, TokenAddress, Transfer, HEAD_TX_KEY, - PIN_KEY_PREFIX, TX_KEY_PREFIX, -}; +use namada::types::token::{Change, DenominatedAmount, MaspDenom, TokenAddress, Transfer, HEAD_TX_KEY, PIN_KEY_PREFIX, TX_KEY_PREFIX, Denomination}; use namada::types::transaction::governance::{ InitProposalData, ProposalType, VoteProposalData, }; @@ -1155,38 +1152,45 @@ impl ShieldedContext { &mut self, client: &HttpClient, conv: AllowedConversion, - asset_type: AssetType, + asset_type: (Epoch, TokenAddress, MaspDenom), value: i128, usage: &mut i128, input: &mut MaspAmount, - output: &mut Amount, + output: &mut MaspAmount, ) { // If conversion if possible, accumulate the exchanged amount let conv: Amount = conv.into(); + //println!("apply converson: {:?}", conv); // The amount required of current asset to qualify for conversion - let threshold = -conv[&asset_type]; + let masp_asset = make_asset_type( + Some(asset_type.0), + &asset_type.1.address, + &asset_type.1.sub_prefix, + asset_type.2 + ); + let threshold = -conv[&masp_asset]; + //println!("threshold {}, value {}", threshold, value); if threshold == 0 { eprintln!( "Asset threshold of selected conversion for asset type {} is \ 0, this is a bug, please report it.", - asset_type + masp_asset ); } // We should use an amount of the AllowedConversion that almost // cancels the original amount - if threshold > value { - return - } let required = value / threshold; + //println!("required: {}", required); // Forget about the trace amount left over because we cannot // realize its value - let trace = Amount::from_pair(asset_type, value % threshold).unwrap(); + let trace = MaspAmount(HashMap::from([((asset_type.0 , asset_type.1), Change::from(value % threshold))])); // Record how much more of the given conversion has been used *usage += required; // Apply the conversions to input and move the trace amount to output *input += self - .decode_all_amounts(client, conv.clone() * required - &trace) - .await; + .decode_all_amounts(client, conv.clone() * required) + .await + - trace.clone(); *output += trace; } @@ -1202,7 +1206,7 @@ impl ShieldedContext { mut conversions: Conversions, ) -> (Amount, Conversions) { // Where we will store our exchanged value - let mut output = Amount::zero(); + let mut output = MaspAmount::default(); // Repeatedly exchange assets until it is no longer possible loop { println!("\n\nInput {:?}\n\n", input); @@ -1223,7 +1227,7 @@ impl ShieldedContext { ); let at_target_asset_type = target_epoch == asset_epoch; - let denom_value = denom.denominate(&token::Amount::from(value)) as i128; + let denom_value = denom.denominate_i128(&value); _ = self .query_allowed_conversion( client.clone(), @@ -1246,7 +1250,7 @@ impl ShieldedContext { self.apply_conversion( &client, conv.clone(), - asset_type, + (asset_epoch, token_addr.clone(), denom), denom_value, usage, &mut input, @@ -1275,7 +1279,7 @@ impl ShieldedContext { self.apply_conversion( &client, conv.clone(), - asset_type, + (asset_epoch, token_addr.clone(), denom), denom_value, usage, &mut input, @@ -1292,7 +1296,7 @@ impl ShieldedContext { comp.insert((*e, key.clone()), *val); } } - output += Amount::from(&comp); + output += comp; } } } @@ -1301,9 +1305,9 @@ impl ShieldedContext { for ((_, key), val) in input.drain() { comp.insert((target_epoch, key), val); } - output += Amount::from(comp); + output += comp; println!("\n\nconversions {:?}\n\n", output); - (output, conversions) + (output.into(), conversions) } /// Collect enough unspent notes in this context to exceed the given amount @@ -1373,6 +1377,8 @@ impl ShieldedContext { } } } + println!("val_acc {:?}", val_acc); + println!("notes {:?}", notes); (val_acc, notes, conversions) } @@ -1679,7 +1685,7 @@ async fn gen_shielded_transfer( epoch, ) .await; - println!("used convs: {:?}", used_convs); + //println!("used convs: {:?}", used_convs); // Commit the notes found to our transaction for (diversifier, note, merkle_path) in unspent_notes { let decoded = ctx.shielded.decode_asset_type(client.clone(), note.asset_type.clone()).await.unwrap(); From 5d5772deee191e3ce409696f075da8b0c5c9c54c Mon Sep 17 00:00:00 2001 From: satan Date: Mon, 5 Jun 2023 17:10:17 +0200 Subject: [PATCH 074/151] WIP fixed unspent note calcs --- apps/src/bin/namada-client/cli.rs | 50 +++------------------ apps/src/lib/client/tx.rs | 13 +++--- tests/src/e2e/ledger_tests.rs | 74 +++++++++++++++++++++---------- wasm/checksums.json | 4 +- 4 files changed, 65 insertions(+), 76 deletions(-) diff --git a/apps/src/bin/namada-client/cli.rs b/apps/src/bin/namada-client/cli.rs index 3fff3c9bad..1a60d3f01b 100644 --- a/apps/src/bin/namada-client/cli.rs +++ b/apps/src/bin/namada-client/cli.rs @@ -3,56 +3,20 @@ use std::time::Duration; use color_eyre::eyre::Result; -use eyre::Chain; use namada_apps::cli::cmds::*; -use namada_apps::cli::{self, args, Context, safe_exit}; +use namada_apps::cli::{self, safe_exit}; use namada_apps::client::{rpc, tx, utils}; use namada_apps::facade::tendermint::block::Height; use namada_apps::facade::tendermint_config::net::Address as TendermintAddress; use namada_apps::facade::tendermint_rpc::{Client, HttpClient}; use tokio::time::sleep; -use namada::types::chain::ChainId; -use namada::types::token::{Amount, DenominatedAmount, Denomination}; -use namada_apps::cli::args::{InputAmount, Tx}; -use namada_apps::cli::context::FromContext; -use namada_apps::config::TendermintMode; pub async fn main() -> Result<()> { - let test_cmd = NamadaClientWithContext::TxTransfer(TxTransfer(args::TxTransfer{ - tx: Tx { - dry_run: false, - dump_tx: false, - force: false, - broadcast_only: false, - ledger_address: TendermintAddress::Tcp {peer_id: None, host: "127.0.0.1".to_string(), port: 27657}, - initialized_account_alias: None, - fee_amount: InputAmount::Unvalidated(DenominatedAmount { amount: Amount::zero(), denom: Denomination(6) }), - fee_token: FromContext::new("NAM".into()), - gas_limit: Default::default(), - expiration: None, - signing_key: None, - signer: Some(FromContext::new("Bertha".into())), - }, - source: FromContext::new("xsktest1qqqqqqqqqqqqqqpagte43rsza46v55dlz8cffahv0fnr6eqacvnrkyuf9lmndgal7c2k4r7f7zu2yr5rjwr374unjjeuzrh6mquzy6grfdcnnu5clzaq2llqhr70a8yyx0p62aajqvrqjxrht3myuyypsvm725uyt5vm0fqzrzuuedtf6fala4r4nnazm9y9hq5yu6pq24arjskmpv4mdgfn3spffxxv8ugvym36kmnj45jcvvmm227vqjm5fq8882yhjsq97p7xrwqqd82s0".into()), - target: FromContext::new("Christel".into()), - token: FromContext::new("ETH".into()), - sub_prefix: None, - amount: InputAmount::Unvalidated(DenominatedAmount { amount: Amount::from_uint(30, 0).unwrap(), denom: Denomination(0) }), - })); - //match cli::namada_client_cli()? { - //cli::NamadaClient::WithContext(cmd_box) => { - //let (cmd, ctx) = *cmd_box; + match cli::namada_client_cli()? { + cli::NamadaClient::WithContext(cmd_box) => { + let (cmd, ctx) = *cmd_box; use NamadaClientWithContext as Sub; - let global_args = args::Global { - chain_id: Some(ChainId("e2e-test.1842a9ed7737c1c61d588".into())), - base_dir: "/tmp/.tmpmalzmo".into(), - wasm_dir: None, - mode: Some(TendermintMode::Full) - }; - - let ctx = Context::new(global_args).unwrap(); - - match test_cmd { + match cmd { // Ledger cmds Sub::TxCustom(TxCustom(args)) => { wait_until_node_is_synched(&args.tx.ledger_address).await; @@ -178,7 +142,7 @@ pub async fn main() -> Result<()> { rpc::query_protocol_parameters(ctx, args).await; } } - /*} + } cli::NamadaClient::WithoutContext(cmd, global_args) => match cmd { // Utils cmds Utils::JoinNetwork(JoinNetwork(args)) => { @@ -194,7 +158,7 @@ pub async fn main() -> Result<()> { utils::init_genesis_validator(global_args, args) } }, - }*/ + } Ok(()) } diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 815e86b827..a0d646160a 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -1158,9 +1158,12 @@ impl ShieldedContext { input: &mut MaspAmount, output: &mut MaspAmount, ) { + // we do not need to convert negative values + if value <= 0 { + return; + } // If conversion if possible, accumulate the exchanged amount let conv: Amount = conv.into(); - //println!("apply converson: {:?}", conv); // The amount required of current asset to qualify for conversion let masp_asset = make_asset_type( Some(asset_type.0), @@ -1169,7 +1172,6 @@ impl ShieldedContext { asset_type.2 ); let threshold = -conv[&masp_asset]; - //println!("threshold {}, value {}", threshold, value); if threshold == 0 { eprintln!( "Asset threshold of selected conversion for asset type {} is \ @@ -1180,7 +1182,6 @@ impl ShieldedContext { // We should use an amount of the AllowedConversion that almost // cancels the original amount let required = value / threshold; - //println!("required: {}", required); // Forget about the trace amount left over because we cannot // realize its value let trace = MaspAmount(HashMap::from([((asset_type.0 , asset_type.1), Change::from(value % threshold))])); @@ -1209,8 +1210,6 @@ impl ShieldedContext { let mut output = MaspAmount::default(); // Repeatedly exchange assets until it is no longer possible loop { - println!("\n\nInput {:?}\n\n", input); - let Some(((asset_epoch, token_addr), value)) = input.pop() else { break }; for denom in MaspDenom::iter() { let target_asset_type = make_asset_type( @@ -1228,7 +1227,7 @@ impl ShieldedContext { let at_target_asset_type = target_epoch == asset_epoch; let denom_value = denom.denominate_i128(&value); - _ = self + self .query_allowed_conversion( client.clone(), target_asset_type, @@ -1259,7 +1258,7 @@ impl ShieldedContext { .await; break; } - _ = self + self .query_allowed_conversion( client.clone(), asset_type, diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 453a2dd1c7..9b761098ad 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -1140,7 +1140,7 @@ fn masp_incentives() -> Result<()> { client.assert_success(); let amt20 = token::Amount::from_uint(20, BTC_DENOMINATION).unwrap(); - let amt30 = token::Amount::from_uint(30, ETH_DENOMINATION).unwrap(); + let amt10 = token::Amount::from_uint(10, ETH_DENOMINATION).unwrap(); // Assert NAM balance at VK(A) is 20*BTC_reward*(epoch_1-epoch_0) let mut client = run!( @@ -1258,7 +1258,7 @@ fn masp_incentives() -> Result<()> { // Wait till epoch boundary let ep3 = epoch_sleep(&test, &validator_one_rpc, 720)?; - // Send 30 ETH from Albert to PA(B) + // Send 10 ETH from Albert to PA(B) let mut client = run!( test, Bin::Client, @@ -1271,16 +1271,18 @@ fn masp_incentives() -> Result<()> { "--token", ETH, "--amount", - "30", + "10", "--node", &validator_one_rpc ], Some(300) )?; + client.exp_string("Transaction accepted")?; + client.exp_string("Transaction applied")?; client.exp_string("Transaction is valid")?; client.assert_success(); - // Assert ETH balance at VK(B) is 30 + // Assert ETH balance at VK(B) is 10 let mut client = run!( test, Bin::Client, @@ -1295,7 +1297,7 @@ fn masp_incentives() -> Result<()> { ], Some(60) )?; - client.exp_string("eth: 30")?; + client.exp_string("eth: 10")?; client.assert_success(); // Assert NAM balance at VK(B) is 0 @@ -1319,7 +1321,7 @@ fn masp_incentives() -> Result<()> { // Wait till epoch boundary let ep4 = epoch_sleep(&test, &validator_one_rpc, 720)?; - // Assert ETH balance at VK(B) is 30 + // Assert ETH balance at VK(B) is 10 let mut client = run!( test, Bin::Client, @@ -1334,10 +1336,10 @@ fn masp_incentives() -> Result<()> { ], Some(60) )?; - client.exp_string("eth: 30")?; + client.exp_string("eth: 10")?; client.assert_success(); - // Assert NAM balance at VK(B) is 30*ETH_reward*(epoch_4-epoch_3) + // Assert NAM balance at VK(B) is 10*ETH_reward*(epoch_4-epoch_3) let mut client = run!( test, Bin::Client, @@ -1352,7 +1354,7 @@ fn masp_incentives() -> Result<()> { ], Some(60) )?; - let amt = (amt30 * masp_rewards[&(eth(), None)]).0 * (ep4.0 - ep3.0); + let amt = (amt10 * masp_rewards[&(eth(), None)]).0 * (ep4.0 - ep3.0); let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into(), @@ -1361,7 +1363,7 @@ fn masp_incentives() -> Result<()> { client.assert_success(); // Assert NAM balance at MASP pool is - // 20*BTC_reward*(epoch_4-epoch_0)+30*ETH_reward*(epoch_4-epoch_3) + // 20*BTC_reward*(epoch_4-epoch_0)+10*ETH_reward*(epoch_4-epoch_3) let mut client = run!( test, Bin::Client, @@ -1377,7 +1379,7 @@ fn masp_incentives() -> Result<()> { Some(60) )?; let amt = ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep4.0 - ep0.0)) - + ((amt30 * masp_rewards[&(eth(), None)]).0 * (ep4.0 - ep3.0)); + + ((amt10 * masp_rewards[&(eth(), None)]).0 * (ep4.0 - ep3.0)); let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into(), @@ -1388,7 +1390,7 @@ fn masp_incentives() -> Result<()> { // Wait till epoch boundary let ep5 = epoch_sleep(&test, &validator_one_rpc, 720)?; - // Send 30 ETH from SK(B) to Christel + // Send 10 ETH from SK(B) to Christel let mut client = run!( test, Bin::Client, @@ -1401,7 +1403,7 @@ fn masp_incentives() -> Result<()> { "--token", ETH, "--amount", - "30", + "10", "--signer", BERTHA, "--node", @@ -1409,6 +1411,24 @@ fn masp_incentives() -> Result<()> { ], Some(300) )?; + println!("{:?}", vec![ + "transfer", + "--source", + B_SPENDING_KEY, + "--target", + CHRISTEL, + "--token", + ETH, + "--amount", + "10", + "--signer", + BERTHA, + "--node", + &validator_one_rpc + ]); + assert!(false); + client.exp_string("Transaction accepted")?; + client.exp_string("Transaction applied")?; client.exp_string("Transaction is valid")?; client.assert_success(); @@ -1432,7 +1452,7 @@ fn masp_incentives() -> Result<()> { let mut ep = get_epoch(&test, &validator_one_rpc)?; - // Assert NAM balance at VK(B) is 30*ETH_reward*(ep-epoch_3) + // Assert NAM balance at VK(B) is 10*ETH_reward*(ep-epoch_3) let mut client = run!( test, Bin::Client, @@ -1447,7 +1467,7 @@ fn masp_incentives() -> Result<()> { ], Some(60) )?; - let amt = (amt30 * masp_rewards[&(eth(), None)]).0 * (ep.0 - ep3.0); + let amt = (amt10 * masp_rewards[&(eth(), None)]).0 * (ep.0 - ep3.0); let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into(), @@ -1457,7 +1477,7 @@ fn masp_incentives() -> Result<()> { ep = get_epoch(&test, &validator_one_rpc)?; // Assert NAM balance at MASP pool is - // 20*BTC_reward*(epoch_5-epoch_0)+30*ETH_reward*(epoch_5-epoch_3) + // 20*BTC_reward*(epoch_5-epoch_0)+10*ETH_reward*(epoch_5-epoch_3) let mut client = run!( test, Bin::Client, @@ -1473,7 +1493,7 @@ fn masp_incentives() -> Result<()> { Some(60) )?; let amt = ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep.0 - ep0.0)) - + ((amt30 * masp_rewards[&(eth(), None)]).0 * (ep.0 - ep3.0)); + + ((amt10 * masp_rewards[&(eth(), None)]).0 * (ep.0 - ep3.0)); let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into(), @@ -1505,6 +1525,8 @@ fn masp_incentives() -> Result<()> { ], Some(300) )?; + client.exp_string("Transaction accepted")?; + client.exp_string("Transaction applied")?; client.exp_string("Transaction is valid")?; client.assert_success(); @@ -1566,7 +1588,7 @@ fn masp_incentives() -> Result<()> { Some(60) )?; let amt = ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep6.0 - ep0.0)) - + ((amt30 * masp_rewards[&(eth(), None)]).0 * (ep5.0 - ep3.0)); + + ((amt10 * masp_rewards[&(eth(), None)]).0 * (ep5.0 - ep3.0)); let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into(), @@ -1600,7 +1622,7 @@ fn masp_incentives() -> Result<()> { client.exp_string(&format!("nam: {}", denominated))?; client.assert_success(); - // Assert NAM balance at VK(B) is 30*ETH_reward*(epoch_5-epoch_3) + // Assert NAM balance at VK(B) is 10*ETH_reward*(epoch_5-epoch_3) let mut client = run!( test, Bin::Client, @@ -1615,7 +1637,7 @@ fn masp_incentives() -> Result<()> { ], Some(60) )?; - let amt = (amt30 * masp_rewards[&(eth(), None)]).0 * (ep5.0 - ep3.0); + let amt = (amt10 * masp_rewards[&(eth(), None)]).0 * (ep5.0 - ep3.0); let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into(), @@ -1624,7 +1646,7 @@ fn masp_incentives() -> Result<()> { client.assert_success(); // Assert NAM balance at MASP pool is - // 20*BTC_reward*(epoch_6-epoch_0)+30*ETH_reward*(epoch_5-epoch_3) + // 20*BTC_reward*(epoch_6-epoch_0)+10*ETH_reward*(epoch_5-epoch_3) let mut client = run!( test, Bin::Client, @@ -1640,7 +1662,7 @@ fn masp_incentives() -> Result<()> { Some(60) )?; let amt = ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep6.0 - ep0.0)) - + ((amt30 * masp_rewards[&(eth(), None)]).0 * (ep5.0 - ep3.0)); + + ((amt10 * masp_rewards[&(eth(), None)]).0 * (ep5.0 - ep3.0)); let denominated = DenominatedAmount { amount: amt, denom: NATIVE_MAX_DECIMAL_PLACES.into(), @@ -1652,7 +1674,7 @@ fn masp_incentives() -> Result<()> { // construction let _ep8 = epoch_sleep(&test, &validator_one_rpc, 720)?; - // Send 30*ETH_reward*(epoch_5-epoch_3) NAM from SK(B) to Christel + // Send 10*ETH_reward*(epoch_5-epoch_3) NAM from SK(B) to Christel let mut client = run!( test, Bin::Client, @@ -1665,7 +1687,7 @@ fn masp_incentives() -> Result<()> { "--token", NAM, "--amount", - &((amt30 * masp_rewards[&(eth(), None)]).0 * (ep5.0 - ep3.0)) + &((amt10 * masp_rewards[&(eth(), None)]).0 * (ep5.0 - ep3.0)) .to_string_native(), "--signer", BERTHA, @@ -1674,6 +1696,8 @@ fn masp_incentives() -> Result<()> { ], Some(300) )?; + client.exp_string("Transaction accepted")?; + client.exp_string("Transaction applied")?; client.exp_string("Transaction is valid")?; client.assert_success(); @@ -1702,6 +1726,8 @@ fn masp_incentives() -> Result<()> { ], Some(300) )?; + client.exp_string("Transaction accepted")?; + client.exp_string("Transaction applied")?; client.exp_string("Transaction is valid")?; client.assert_success(); diff --git a/wasm/checksums.json b/wasm/checksums.json index d40a0ba7b4..017817884b 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -15,6 +15,6 @@ "vp_masp.wasm": "vp_masp.eb53123cad15bf82a09501dfe9da8dbfa0070468f6c8376dab4a62fd8ac58548.wasm", "vp_testnet_faucet.wasm": "vp_testnet_faucet.9059ec6cd09ce0a7bde581b487d1e53921a9c26f29c76071d3a8d68fdba9bdab.wasm", "vp_token.wasm": "vp_token.cd4d62be7dbb08a1f983857a9a59689b8ac12856ce15480173e26d94647adf86.wasm", - "vp_user.wasm": "vp_user.79e5cd7e4cb39fbb2b54c8a59b2a142f53050b030ec79acab37a36fd24402ece.wasm", - "vp_validator.wasm": "vp_validator.257b596e176e2db8057b2a6489f978b77c30dbd77282dd5e57809d75dd6d7f0d.wasm" + "vp_user.wasm": "vp_user.ef4584780d875a7fd9d242356f5606dcc91613083dcadc942b3f44dd64e2afbe.wasm", + "vp_validator.wasm": "vp_validator.02274dc44079fa9c8d06d2b85eb08e7a8cbc7e7b4831b16b2c44b851b0708361.wasm" } \ No newline at end of file From 057b4d3e1f4e4747a4d6da3134e97d0413cc64fb Mon Sep 17 00:00:00 2001 From: satan Date: Wed, 7 Jun 2023 16:00:50 +0200 Subject: [PATCH 075/151] WIP masp incentives passes. But I did something reckless to get there. --- apps/src/lib/client/tx.rs | 19 +++++++++++------- core/src/ledger/storage/masp_conversions.rs | 2 +- tests/src/e2e/ledger_tests.rs | 16 --------------- wasm/checksums.json | 2 +- wasm/wasm_source/src/vp_masp.rs | 22 +++++++++++++++++++-- 5 files changed, 34 insertions(+), 27 deletions(-) diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index a0d646160a..28e5a0cd9c 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -479,12 +479,14 @@ pub fn find_valid_diversifier( /// Determine if using the current note would actually bring us closer to our /// target pub fn is_amount_required(src: Amount, dest: Amount, delta: Amount) -> bool { - if delta > Amount::zero() { - let gap = dest - src; - for (asset_type, value) in gap.components() { - if *value >= 0 && delta[asset_type] >= 0 { - return true; - } + println!("delta: {:?}", delta); + println!("src: {:?}", src); + println!("dest: {:?}", dest); + + let gap = dest - src; + for (asset_type, value) in gap.components() { + if *value >= 0 && delta[asset_type] >= 0 { + return true; } } false @@ -1303,9 +1305,11 @@ impl ShieldedContext { let mut comp = MaspAmount::default(); for ((_, key), val) in input.drain() { comp.insert((target_epoch, key), val); + } output += comp; - println!("\n\nconversions {:?}\n\n", output); + println!("\n\noutput {:?}\n\n", output); + println!("\n\nconversions {:?}\n\n", conversions); (output.into(), conversions) } @@ -1347,6 +1351,7 @@ impl ShieldedContext { // The amount contributed by this note before conversion let pre_contr = Amount::from_pair(note.asset_type, note.value) .expect("received note has invalid value or asset type"); + println!("note.asset_type: {:?}, note.value {:?}", note.asset_type, note.value); let input = self.decode_all_amounts(&client, pre_contr).await; let (contr, proposed_convs) = self .compute_exchanged_amount( diff --git a/core/src/ledger/storage/masp_conversions.rs b/core/src/ledger/storage/masp_conversions.rs index 4dec02d4f5..8cf7a3c21a 100644 --- a/core/src/ledger/storage/masp_conversions.rs +++ b/core/src/ledger/storage/masp_conversions.rs @@ -66,7 +66,7 @@ where // have to use. This trick works under the assumption that reward tokens // from different epochs are exactly equivalent. let reward_asset = - encode_asset_type(address::nam(), &None, MaspDenom::Zero, Epoch(0)); + encode_asset_type(address::nam(), &None, MaspDenom::Zero, wl_storage.storage.block.epoch); // Conversions from the previous to current asset for each address let mut current_convs = BTreeMap::<(Address, Option, MaspDenom), AllowedConversion>::new(); diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 9b761098ad..de2b11c9da 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -1411,22 +1411,6 @@ fn masp_incentives() -> Result<()> { ], Some(300) )?; - println!("{:?}", vec![ - "transfer", - "--source", - B_SPENDING_KEY, - "--target", - CHRISTEL, - "--token", - ETH, - "--amount", - "10", - "--signer", - BERTHA, - "--node", - &validator_one_rpc - ]); - assert!(false); client.exp_string("Transaction accepted")?; client.exp_string("Transaction applied")?; client.exp_string("Transaction is valid")?; diff --git a/wasm/checksums.json b/wasm/checksums.json index 017817884b..880551cd83 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -12,7 +12,7 @@ "tx_vote_proposal.wasm": "tx_vote_proposal.c5a856595b0a76b7191aec1bf1912d69e9d29e94e412ce7ebd7c96467900bfd8.wasm", "tx_withdraw.wasm": "tx_withdraw.43cf91dc3fec04ffe740da865e55b00b6e6d71a6da9dcfde378506aedd715f24.wasm", "vp_implicit.wasm": "vp_implicit.377e14b90713516ed12fd10975a13c9d74bd3a4de8f40be17e9aa4315953c282.wasm", - "vp_masp.wasm": "vp_masp.eb53123cad15bf82a09501dfe9da8dbfa0070468f6c8376dab4a62fd8ac58548.wasm", + "vp_masp.wasm": "vp_masp.38d624456bf2d4f0151d5d3c3a8ef52c4d412243830a14c9e4bd408b24bad6a3.wasm", "vp_testnet_faucet.wasm": "vp_testnet_faucet.9059ec6cd09ce0a7bde581b487d1e53921a9c26f29c76071d3a8d68fdba9bdab.wasm", "vp_token.wasm": "vp_token.cd4d62be7dbb08a1f983857a9a59689b8ac12856ce15480173e26d94647adf86.wasm", "vp_user.wasm": "vp_user.ef4584780d875a7fd9d242356f5606dcc91613083dcadc942b3f44dd64e2afbe.wasm", diff --git a/wasm/wasm_source/src/vp_masp.rs b/wasm/wasm_source/src/vp_masp.rs index 7fee57705b..a618aa165f 100644 --- a/wasm/wasm_source/src/vp_masp.rs +++ b/wasm/wasm_source/src/vp_masp.rs @@ -55,7 +55,13 @@ fn valid_transfer_amount( transparented value {}", unshielded_transfer_value, reporeted_transparent_value - ) + ); + log_string(format!( + "The unshielded amount {} disagrees with the calculated masp \ + transparented value {}", + unshielded_transfer_value, + reporeted_transparent_value + )) } res } @@ -165,6 +171,12 @@ fn validate_tx( beteween 1 and 4 but is {}", shielded_tx.vout.len() ); + + log_string(format!( + "Transparent output to a transaction to the masp must be \ + beteween 1 and 4 but is {}", + shielded_tx.vout.len() + )); return reject(); } @@ -194,10 +206,11 @@ fn validate_tx( // This is encoded via the asset types. continue; } - if valid_transfer_amount( + if !valid_transfer_amount( out.value, denom.denominate(&transfer.amount.amount), ) { + log_string("Invalid transfer amount"); return reject(); } @@ -229,6 +242,10 @@ fn validate_tx( "the public key of the output account does \ not match the transfer target" ); + log_string(format!( + "the public key of the output account does \ + not match the transfer target" + )); return reject(); } } @@ -238,6 +255,7 @@ fn validate_tx( // one or more of the denoms in the batch failed to verify // the asset derivation. if valid_count != out_length { + log_string("one or more of the denoms in the batch failed to verify the asset derivation."); return reject(); } } else { From 1899e65d238c211c5d5d688b467a943fa993ce86 Mon Sep 17 00:00:00 2001 From: satan Date: Thu, 8 Jun 2023 01:24:06 +0200 Subject: [PATCH 076/151] WIP fixed masp incentives --- apps/src/lib/client/tx.rs | 61 +++++++-------------- core/src/ledger/storage/masp_conversions.rs | 2 +- 2 files changed, 22 insertions(+), 41 deletions(-) diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 28e5a0cd9c..64203c4182 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -479,10 +479,6 @@ pub fn find_valid_diversifier( /// Determine if using the current note would actually bring us closer to our /// target pub fn is_amount_required(src: Amount, dest: Amount, delta: Amount) -> bool { - println!("delta: {:?}", delta); - println!("src: {:?}", src); - println!("dest: {:?}", dest); - let gap = dest - src; for (asset_type, value) in gap.components() { if *value >= 0 && delta[asset_type] >= 0 { @@ -520,7 +516,7 @@ pub struct MaspAmount(HashMap<(Epoch, TokenAddress), token::Change>); impl MaspAmount { pub fn pop(&mut self) -> Option<((Epoch, TokenAddress), token::Change)> { - let key = self.keys().find(|(e, _)| e.0 != 0)?.clone(); + let key = self.keys().next().cloned()?; let value = self.remove(&key).unwrap(); Some((key, value)) } @@ -560,7 +556,6 @@ impl std::ops::AddAssign for MaspAmount { } } -// please stop copying and pasting make a function impl std::ops::Sub for MaspAmount { type Output = MaspAmount; @@ -570,6 +565,7 @@ impl std::ops::Sub for MaspAmount { .and_modify(|val| *val -= value) .or_insert(value); } + self.0.retain(|_, v| !v.is_zero()); self } } @@ -1166,6 +1162,7 @@ impl ShieldedContext { } // If conversion if possible, accumulate the exchanged amount let conv: Amount = conv.into(); + println!("conv: {:?}", conv); // The amount required of current asset to qualify for conversion let masp_asset = make_asset_type( Some(asset_type.0), @@ -1212,7 +1209,10 @@ impl ShieldedContext { let mut output = MaspAmount::default(); // Repeatedly exchange assets until it is no longer possible loop { - let Some(((asset_epoch, token_addr), value)) = input.pop() else { break }; + let (asset_epoch, token_addr, value) = match input.iter().next() { + Some(((e, a), v)) => (*e, a.clone(), *v), + _ => break + }; for denom in MaspDenom::iter() { let target_asset_type = make_asset_type( Some(target_epoch), @@ -1229,16 +1229,20 @@ impl ShieldedContext { let at_target_asset_type = target_epoch == asset_epoch; let denom_value = denom.denominate_i128(&value); - self - .query_allowed_conversion( + self.query_allowed_conversion( client.clone(), target_asset_type, &mut conversions, ) .await; - + self.query_allowed_conversion( + client.clone(), + asset_type, + &mut conversions, + ) + .await; if let (Some((conv, _wit, usage)), false) = ( - conversions.get_mut(&target_asset_type), + conversions.get_mut(&asset_type), at_target_asset_type, ) { println!( @@ -1258,22 +1262,13 @@ impl ShieldedContext { &mut output, ) .await; - break; - } - self - .query_allowed_conversion( - client.clone(), - asset_type, - &mut conversions, - ) - .await; - if let (Some((conv, _wit, usage)), false) = - (conversions.get_mut(&asset_type), at_target_asset_type) + } else if let (Some((conv, _wit, usage)), false) = + (conversions.get_mut(&target_asset_type), at_target_asset_type) { println!( "converting latest asset type to target asset type..." ); - // Not at the target asset type, yes at the latest asset + // Not at the target asset type, yet at the latest asset // type. Apply inverse conversion to get // from latest asset type to the target // asset type. @@ -1293,23 +1288,15 @@ impl ShieldedContext { let mut comp = MaspAmount::default(); comp.insert((asset_epoch, token_addr.clone()), denom_value.into()); for ((e, key), val) in input.iter() { - if *key == token_addr { + if *key == token_addr && *e == asset_epoch { comp.insert((*e, key.clone()), *val); } } - output += comp; + output += comp.clone(); + input -= comp; } } } - // finally convert the rewards in epoch 0. - let mut comp = MaspAmount::default(); - for ((_, key), val) in input.drain() { - comp.insert((target_epoch, key), val); - - } - output += comp; - println!("\n\noutput {:?}\n\n", output); - println!("\n\nconversions {:?}\n\n", conversions); (output.into(), conversions) } @@ -1351,7 +1338,6 @@ impl ShieldedContext { // The amount contributed by this note before conversion let pre_contr = Amount::from_pair(note.asset_type, note.value) .expect("received note has invalid value or asset type"); - println!("note.asset_type: {:?}, note.value {:?}", note.asset_type, note.value); let input = self.decode_all_amounts(&client, pre_contr).await; let (contr, proposed_convs) = self .compute_exchanged_amount( @@ -1381,8 +1367,6 @@ impl ShieldedContext { } } } - println!("val_acc {:?}", val_acc); - println!("notes {:?}", notes); (val_acc, notes, conversions) } @@ -1689,7 +1673,6 @@ async fn gen_shielded_transfer( epoch, ) .await; - //println!("used convs: {:?}", used_convs); // Commit the notes found to our transaction for (diversifier, note, merkle_path) in unspent_notes { let decoded = ctx.shielded.decode_asset_type(client.clone(), note.asset_type.clone()).await.unwrap(); @@ -1762,8 +1745,6 @@ async fn gen_shielded_transfer( target_enc.as_ref(), )); for (denom, asset_type) in MaspDenom::iter().zip(asset_types.iter()) { - let decoded = ctx.shielded.decode_asset_type(client.clone(), asset_type.clone()).await.unwrap(); - println!("Adding transparent outpt: {:?}: {}", decoded, denom.denominate(&amt)); builder.add_transparent_output( &TransparentAddress::PublicKey(hash.into()), *asset_type, diff --git a/core/src/ledger/storage/masp_conversions.rs b/core/src/ledger/storage/masp_conversions.rs index 8cf7a3c21a..4dec02d4f5 100644 --- a/core/src/ledger/storage/masp_conversions.rs +++ b/core/src/ledger/storage/masp_conversions.rs @@ -66,7 +66,7 @@ where // have to use. This trick works under the assumption that reward tokens // from different epochs are exactly equivalent. let reward_asset = - encode_asset_type(address::nam(), &None, MaspDenom::Zero, wl_storage.storage.block.epoch); + encode_asset_type(address::nam(), &None, MaspDenom::Zero, Epoch(0)); // Conversions from the previous to current asset for each address let mut current_convs = BTreeMap::<(Address, Option, MaspDenom), AllowedConversion>::new(); From b380c20fa77edfa07a17b5b2965bdc3c29e48cbb Mon Sep 17 00:00:00 2001 From: satan Date: Fri, 9 Jun 2023 10:44:54 +0200 Subject: [PATCH 077/151] Masp e2e tests now passing --- apps/src/lib/client/rpc.rs | 4 +-- apps/src/lib/client/tx.rs | 38 ++++++++++------------ apps/src/lib/client/utils.rs | 60 +++++++++++++++++++++++++++++++++++ tests/src/e2e/ledger_tests.rs | 11 +++++-- tests/src/e2e/setup.rs | 3 +- 5 files changed, 88 insertions(+), 28 deletions(-) diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 317882f9a7..af64aa08d3 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -650,6 +650,7 @@ pub async fn query_pinned_balance(ctx: &mut Context, args: args::QueryBalance) { ) .await } + // Now print out the received quantities according to CLI arguments match (balance, args.token.as_ref(), args.sub_prefix.as_ref()) { (Err(PinnedBalanceError::InvalidViewingKey), _, _) => println!( @@ -668,8 +669,7 @@ pub async fn query_pinned_balance(ctx: &mut Context, args: args::QueryBalance) { sub_prefix: sub_prefix .map(|string| Key::parse(string).unwrap()), }; - - let total_balance = balance[&(epoch, token_address.clone())]; + let total_balance = balance.get(&(epoch, token_address.clone())).cloned().unwrap_or_default(); if total_balance.is_zero() { println!( diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 64203c4182..ef27aa8fe1 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -52,7 +52,7 @@ use namada::types::storage::{ BlockHeight, Epoch, Key, KeySeg, TxIndex, RESERVED_ADDRESS_PREFIX, }; use namada::types::time::DateTimeUtc; -use namada::types::token::{Change, DenominatedAmount, MaspDenom, TokenAddress, Transfer, HEAD_TX_KEY, PIN_KEY_PREFIX, TX_KEY_PREFIX, Denomination}; +use namada::types::token::{Change, DenominatedAmount, MaspDenom, TokenAddress, Transfer, HEAD_TX_KEY, PIN_KEY_PREFIX, TX_KEY_PREFIX}; use namada::types::transaction::governance::{ InitProposalData, ProposalType, VoteProposalData, }; @@ -72,6 +72,7 @@ use crate::client::rpc::{ }; use crate::client::signing::{find_keypair, sign_tx, tx_signer, TxSigningKey}; use crate::client::tendermint_rpc_types::{TxBroadcastData, TxResponse}; +use crate::client::utils::with_spinny_wheel; use crate::facade::tendermint_config::net::Address as TendermintAddress; use crate::facade::tendermint_rpc::endpoint::broadcast::tx_sync::Response; use crate::facade::tendermint_rpc::error::Error as RpcError; @@ -1162,7 +1163,6 @@ impl ShieldedContext { } // If conversion if possible, accumulate the exchanged amount let conv: Amount = conv.into(); - println!("conv: {:?}", conv); // The amount required of current asset to qualify for conversion let masp_asset = make_asset_type( Some(asset_type.0), @@ -1549,20 +1549,11 @@ fn convert_amount( sub_prefix: &Option<&String>, val: &token::Amount, ) -> ([AssetType; 4], Amount) { - println!( - "{}", - DenominatedAmount { - amount: val.clone(), - denom: 18.into() - } - ); let mut amount = Amount::zero(); let asset_types: [AssetType; 4] = MaspDenom::iter() .map(|denom| { let asset_type = make_asset_type(Some(epoch), token, sub_prefix, denom); - let inner = denom.denominate(val); - println!("{}", inner); // Combine the value and unit into one amount amount += Amount::from_nonnegative(asset_type, denom.denominate(val)) @@ -1675,14 +1666,11 @@ async fn gen_shielded_transfer( .await; // Commit the notes found to our transaction for (diversifier, note, merkle_path) in unspent_notes { - let decoded = ctx.shielded.decode_asset_type(client.clone(), note.asset_type.clone()).await.unwrap(); - println!("Adding note value: {:?}: {:?}", decoded, note.value); builder.add_sapling_spend(sk, diversifier, note, merkle_path)?; } // Commit the conversion notes used during summation for (conv, wit, value) in used_convs.values() { if value.is_positive() { - println!("adding conversion {:?} -> {}", conv.assets, *value as u64); builder.add_convert( conv.clone(), *value as u64, @@ -1745,18 +1733,24 @@ async fn gen_shielded_transfer( target_enc.as_ref(), )); for (denom, asset_type) in MaspDenom::iter().zip(asset_types.iter()) { - builder.add_transparent_output( - &TransparentAddress::PublicKey(hash.into()), - *asset_type, - denom.denominate(&amt), - )?; + let vout = denom.denominate(&amt); + if vout != 0 { + builder.add_transparent_output( + &TransparentAddress::PublicKey(hash.into()), + *asset_type, + vout, + )?; + } } } // Build and return the constructed transaction - builder - .build(consensus_branch_id, &prover) - .map(|(a, b)| Some((a, b, epoch))) + with_spinny_wheel( + "Building proofs for MASP transaction (this can take some time) ... ", + move || builder + .build(consensus_branch_id, &prover) + .map(|(a, b)| Some((a, b, epoch))) + ) } pub async fn submit_transfer(mut ctx: Context, mut args: args::TxTransfer) { diff --git a/apps/src/lib/client/utils.rs b/apps/src/lib/client/utils.rs index 9e901cfaf1..588fd48428 100644 --- a/apps/src/lib/client/utils.rs +++ b/apps/src/lib/client/utils.rs @@ -44,6 +44,41 @@ const DEFAULT_NETWORK_CONFIGS_SERVER: &str = /// We do pre-genesis validator set up in this directory pub const PRE_GENESIS_DIR: &str = "pre-genesis"; +/// Environment variable set to reduce the amount of printing the CLI +/// tools perform. Extra prints, while good for UI, clog up test tooling. +pub const REDUCED_CLI_PRINTING: &str = "REDUCED_CLI_PRINTING"; + +macro_rules! cli_print { + ($($arg:tt)*) => {{ + if std::env::var(REDUCED_CLI_PRINTING) + .map(|v| if v.to_lowercase().trim() == "true" { + false + } else { + true + }).unwrap_or(true) { + let mut stdout = std::io::stdout().lock(); + _ = stdout.write_all(format!("{}", std::format_args!($($arg)*)).as_bytes()); + _ = stdout.flush(); + } + }}; +} + +#[allow(unused)] +macro_rules! cli_println { + ($($arg:tt)*) => {{ + if std::env::var(REDUCED_CLI_PRINTING) + .map(|v| if v.to_lowercase().trim() == "true" { + false + } else { + true + }).unwrap_or(true) { + let mut stdout = std::io::stdout().lock(); + _ = stdout.write_all(format!("{}\n", std::format_args!($($arg)*)).as_bytes()); + _ = stdout.flush(); + } + }}; +} + /// Configure Namada to join an existing network. The chain must be released in /// the repository. pub async fn join_network( @@ -1066,6 +1101,30 @@ pub fn validator_pre_genesis_dir(base_dir: &Path, alias: &str) -> PathBuf { base_dir.join(PRE_GENESIS_DIR).join(alias) } +/// Add a spinning wheel to a message for long running commands. +/// Can be turned off for E2E tests by setting the `REDUCED_CLI_PRINTING` +/// environment variable. +pub fn with_spinny_wheel(msg: &str, func: F) -> Out +where + F: FnOnce() -> Out + Send + 'static, + Out: Send + 'static +{ + let task = std::thread::spawn(func); + let spinny_wheel = "|/-\\"; + print!("{}", msg); + _ = std::io::stdout().flush(); + for c in spinny_wheel.chars().cycle() { + cli_print!("{}", c); + std::thread::sleep(std::time::Duration::from_secs(1)); + cli_print!("{}", (8u8 as char)); + if task.is_finished() { + break; + } + } + println!(""); + task.join().unwrap() +} + fn is_valid_validator_for_current_chain( tendermint_node_pk: &common::PublicKey, genesis_config: &GenesisConfig, @@ -1078,3 +1137,4 @@ fn is_valid_validator_for_current_chain( } }) } + diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index de2b11c9da..3a6725e0d9 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -28,7 +28,7 @@ use namada_apps::config::genesis::genesis_config::{ GenesisConfig, ParametersConfig, PosParamsConfig, }; use namada_core::types::token::{ - DenominatedAmount, Denomination, NATIVE_MAX_DECIMAL_PLACES, + DenominatedAmount, NATIVE_MAX_DECIMAL_PLACES, }; use namada_test_utils::TestWasms; use serde_json::json; @@ -833,7 +833,7 @@ fn masp_txs_and_queries() -> Result<()> { } else { tx_args.clone() }; - let mut client = run!(test, Bin::Client, tx_args, Some(300))?; + let mut client = run!(test, Bin::Client, tx_args, Some(720))?; if *tx_result == "Transaction is valid" && !dry_run { client.exp_string("Transaction accepted")?; @@ -863,7 +863,7 @@ fn masp_pinned_txs() -> Result<()> { let test = setup::network( |genesis| { let parameters = ParametersConfig { - epochs_per_year: epochs_per_year_from_min_duration(60), + epochs_per_year: epochs_per_year_from_min_duration(120), ..genesis.parameters }; GenesisConfig { @@ -926,6 +926,9 @@ fn masp_pinned_txs() -> Result<()> { client.exp_string("has not yet been consumed")?; client.assert_success(); + // Wait till epoch boundary + let _ep1 = epoch_sleep(&test, &validator_one_rpc, 720)?; + // Send 20 BTC from Albert to PPA(C) let mut client = run!( test, @@ -945,6 +948,8 @@ fn masp_pinned_txs() -> Result<()> { ], Some(300) )?; + client.exp_string("Transaction accepted")?; + client.exp_string("Transaction applied")?; client.exp_string("Transaction is valid")?; client.assert_success(); diff --git a/tests/src/e2e/setup.rs b/tests/src/e2e/setup.rs index 103312271d..c19039f87d 100644 --- a/tests/src/e2e/setup.rs +++ b/tests/src/e2e/setup.rs @@ -26,6 +26,7 @@ use namada_apps::{config, wallet}; use rand::Rng; use serde_json; use tempfile::{tempdir, TempDir}; +use namada_apps::client::utils::REDUCED_CLI_PRINTING; use crate::e2e::helpers::generate_bin_command; @@ -125,7 +126,7 @@ pub fn network( eprintln!("Failed setting up colorful error reports {}", err); } }); - + env::set_var(REDUCED_CLI_PRINTING, "true"); let working_dir = working_dir(); let test_dir = TestDir::new(); From 61e9bd62a7d664752153ea0320834e1f48771c95 Mon Sep 17 00:00:00 2001 From: satan Date: Sun, 11 Jun 2023 12:04:12 +0200 Subject: [PATCH 078/151] [fix]: Fixed serialization bug with amounts. Fixed remaining masp e2e tests --- apps/src/lib/client/rpc.rs | 5 +- apps/src/lib/client/tx.rs | 53 +++++++++++-------- apps/src/lib/client/utils.rs | 3 +- .../lib/node/ledger/shell/finalize_block.rs | 4 +- apps/src/lib/node/ledger/shell/init_chain.rs | 15 +++--- apps/src/lib/node/ledger/storage/rocksdb.rs | 9 ++-- core/src/ledger/storage/mockdb.rs | 5 +- core/src/ledger/storage/mod.rs | 4 +- core/src/ledger/storage/wl_storage.rs | 4 +- core/src/types/token.rs | 29 ++++------ tests/src/e2e/ledger_tests.rs | 4 +- tests/src/e2e/setup.rs | 2 +- wasm/checksums.json | 2 +- wasm/wasm_source/src/vp_masp.rs | 8 +-- 14 files changed, 74 insertions(+), 73 deletions(-) diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index af64aa08d3..7dbc6f6923 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -669,7 +669,10 @@ pub async fn query_pinned_balance(ctx: &mut Context, args: args::QueryBalance) { sub_prefix: sub_prefix .map(|string| Key::parse(string).unwrap()), }; - let total_balance = balance.get(&(epoch, token_address.clone())).cloned().unwrap_or_default(); + let total_balance = balance + .get(&(epoch, token_address.clone())) + .cloned() + .unwrap_or_default(); if total_balance.is_zero() { println!( diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index ef27aa8fe1..f079e2ef4b 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -52,7 +52,10 @@ use namada::types::storage::{ BlockHeight, Epoch, Key, KeySeg, TxIndex, RESERVED_ADDRESS_PREFIX, }; use namada::types::time::DateTimeUtc; -use namada::types::token::{Change, DenominatedAmount, MaspDenom, TokenAddress, Transfer, HEAD_TX_KEY, PIN_KEY_PREFIX, TX_KEY_PREFIX}; +use namada::types::token::{ + Change, DenominatedAmount, MaspDenom, TokenAddress, Transfer, HEAD_TX_KEY, + PIN_KEY_PREFIX, TX_KEY_PREFIX, +}; use namada::types::transaction::governance::{ InitProposalData, ProposalType, VoteProposalData, }; @@ -1168,7 +1171,7 @@ impl ShieldedContext { Some(asset_type.0), &asset_type.1.address, &asset_type.1.sub_prefix, - asset_type.2 + asset_type.2, ); let threshold = -conv[&masp_asset]; if threshold == 0 { @@ -1183,7 +1186,10 @@ impl ShieldedContext { let required = value / threshold; // Forget about the trace amount left over because we cannot // realize its value - let trace = MaspAmount(HashMap::from([((asset_type.0 , asset_type.1), Change::from(value % threshold))])); + let trace = MaspAmount(HashMap::from([( + (asset_type.0, asset_type.1), + Change::from(value % threshold), + )])); // Record how much more of the given conversion has been used *usage += required; // Apply the conversions to input and move the trace amount to output @@ -1211,7 +1217,7 @@ impl ShieldedContext { loop { let (asset_epoch, token_addr, value) = match input.iter().next() { Some(((e, a), v)) => (*e, a.clone(), *v), - _ => break + _ => break, }; for denom in MaspDenom::iter() { let target_asset_type = make_asset_type( @@ -1230,21 +1236,20 @@ impl ShieldedContext { let denom_value = denom.denominate_i128(&value); self.query_allowed_conversion( - client.clone(), - target_asset_type, - &mut conversions, - ) - .await; + client.clone(), + target_asset_type, + &mut conversions, + ) + .await; self.query_allowed_conversion( client.clone(), asset_type, &mut conversions, ) .await; - if let (Some((conv, _wit, usage)), false) = ( - conversions.get_mut(&asset_type), - at_target_asset_type, - ) { + if let (Some((conv, _wit, usage)), false) = + (conversions.get_mut(&asset_type), at_target_asset_type) + { println!( "converting current asset type to latest asset type..." ); @@ -1262,9 +1267,10 @@ impl ShieldedContext { &mut output, ) .await; - } else if let (Some((conv, _wit, usage)), false) = - (conversions.get_mut(&target_asset_type), at_target_asset_type) - { + } else if let (Some((conv, _wit, usage)), false) = ( + conversions.get_mut(&target_asset_type), + at_target_asset_type, + ) { println!( "converting latest asset type to target asset type..." ); @@ -1281,12 +1287,15 @@ impl ShieldedContext { &mut input, &mut output, ) - .await; + .await; } else { // At the target asset type. Then move component over to // output. let mut comp = MaspAmount::default(); - comp.insert((asset_epoch, token_addr.clone()), denom_value.into()); + comp.insert( + (asset_epoch, token_addr.clone()), + denom_value.into(), + ); for ((e, key), val) in input.iter() { if *key == token_addr && *e == asset_epoch { comp.insert((*e, key.clone()), *val); @@ -1747,9 +1756,11 @@ async fn gen_shielded_transfer( // Build and return the constructed transaction with_spinny_wheel( "Building proofs for MASP transaction (this can take some time) ... ", - move || builder - .build(consensus_branch_id, &prover) - .map(|(a, b)| Some((a, b, epoch))) + move || { + builder + .build(consensus_branch_id, &prover) + .map(|(a, b)| Some((a, b, epoch))) + }, ) } diff --git a/apps/src/lib/client/utils.rs b/apps/src/lib/client/utils.rs index 588fd48428..94a1b84b99 100644 --- a/apps/src/lib/client/utils.rs +++ b/apps/src/lib/client/utils.rs @@ -1107,7 +1107,7 @@ pub fn validator_pre_genesis_dir(base_dir: &Path, alias: &str) -> PathBuf { pub fn with_spinny_wheel(msg: &str, func: F) -> Out where F: FnOnce() -> Out + Send + 'static, - Out: Send + 'static + Out: Send + 'static, { let task = std::thread::spawn(func); let spinny_wheel = "|/-\\"; @@ -1137,4 +1137,3 @@ fn is_valid_validator_for_current_chain( } }) } - diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index e9870d8ff1..189852d836 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -885,7 +885,6 @@ fn pos_votes_from_abci( #[cfg(test)] mod test_finalize_block { use std::collections::{BTreeMap, BTreeSet}; - use std::str::FromStr; use data_encoding::HEXUPPER; use namada::ledger::parameters::EpochDuration; @@ -1341,12 +1340,11 @@ mod test_finalize_block { // Collect all storage key-vals into a sorted map let store_block_state = |shell: &TestShell| -> BTreeMap<_, _> { - let prefix: Key = FromStr::from_str("").unwrap(); shell .wl_storage .storage .db - .iter_prefix(&prefix) + .iter_prefix(None) .map(|(key, val, _gas)| (key, val)) .collect() }; diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index 7f44315706..6d94bde41d 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -52,11 +52,11 @@ where let errors = self.wl_storage.storage.chain_id.validate(genesis_bytes); use itertools::Itertools; - // assert!( - // errors.is_empty(), - // "Chain ID validation failed: {}", - // errors.into_iter().format(". ") - // ); + assert!( + errors.is_empty(), + "Chain ID validation failed: {}", + errors.into_iter().format(". ") + ); } #[cfg(feature = "dev")] let genesis = genesis::genesis(num_validators); @@ -475,11 +475,9 @@ where #[cfg(test)] mod test { use std::collections::BTreeMap; - use std::str::FromStr; use namada::ledger::storage::DBIter; use namada::types::chain::ChainId; - use namada::types::storage; use crate::facade::tendermint_proto::abci::RequestInitChain; use crate::facade::tendermint_proto::google::protobuf::Timestamp; @@ -493,12 +491,11 @@ mod test { // Collect all storage key-vals into a sorted map let store_block_state = |shell: &TestShell| -> BTreeMap<_, _> { - let prefix: storage::Key = FromStr::from_str("").unwrap(); shell .wl_storage .storage .db - .iter_prefix(&prefix) + .iter_prefix(None) .map(|(key, val, _gas)| (key, val)) .collect() }; diff --git a/apps/src/lib/node/ledger/storage/rocksdb.rs b/apps/src/lib/node/ledger/storage/rocksdb.rs index ce3802e0fe..8dba637a37 100644 --- a/apps/src/lib/node/ledger/storage/rocksdb.rs +++ b/apps/src/lib/node/ledger/storage/rocksdb.rs @@ -380,7 +380,7 @@ impl RocksDB { let batch = Mutex::new(batch); tracing::info!("Restoring previous hight subspace diffs"); - self.iter_prefix(&Key::default()) + self.iter_prefix(None) .par_bridge() .try_for_each(|(key, _value, _gas)| -> Result<()> { // Restore previous height diff if present, otherwise delete the @@ -1138,7 +1138,7 @@ impl<'iter> DBIter<'iter> for RocksDB { fn iter_prefix( &'iter self, - prefix: &Key, + prefix: Option<&Key>, ) -> PersistentPrefixIterator<'iter> { iter_subspace_prefix(self, prefix) } @@ -1180,10 +1180,11 @@ impl<'iter> DBIter<'iter> for RocksDB { fn iter_subspace_prefix<'iter>( db: &'iter RocksDB, - prefix: &Key, + prefix: Option<&Key>, ) -> PersistentPrefixIterator<'iter> { let db_prefix = "subspace/".to_owned(); - let prefix = format!("{}{}", db_prefix, prefix); + let prefix_str = prefix.map(|k| k.to_string()).unwrap_or_default(); + let prefix = format!("{}{}", db_prefix, prefix_str); iter_prefix(db, db_prefix, prefix) } diff --git a/core/src/ledger/storage/mockdb.rs b/core/src/ledger/storage/mockdb.rs index 585edb1381..cb2a7a9679 100644 --- a/core/src/ledger/storage/mockdb.rs +++ b/core/src/ledger/storage/mockdb.rs @@ -478,9 +478,10 @@ impl DB for MockDB { impl<'iter> DBIter<'iter> for MockDB { type PrefixIter = MockPrefixIterator; - fn iter_prefix(&'iter self, prefix: &Key) -> MockPrefixIterator { + fn iter_prefix(&'iter self, prefix: Option<&Key>) -> MockPrefixIterator { let db_prefix = "subspace/".to_owned(); - let prefix = format!("{}{}", db_prefix, prefix); + let prefix_str = prefix.map(|k| k.to_string()).unwrap_or_default(); + let prefix = format!("{}{}", db_prefix, prefix_str); let iter = self.0.borrow().clone().into_iter(); MockPrefixIterator::new(MockIterator { prefix, iter }, db_prefix) } diff --git a/core/src/ledger/storage/mod.rs b/core/src/ledger/storage/mod.rs index 0560844d6a..5bc9a7bf3f 100644 --- a/core/src/ledger/storage/mod.rs +++ b/core/src/ledger/storage/mod.rs @@ -319,7 +319,7 @@ pub trait DBIter<'iter> { /// /// Read account subspace key value pairs with the given prefix from the DB, /// ordered by the storage keys. - fn iter_prefix(&'iter self, prefix: &Key) -> Self::PrefixIter; + fn iter_prefix(&'iter self, prefix: Option<&Key>) -> Self::PrefixIter; /// Read results subspace key value pairs from the DB fn iter_results(&'iter self) -> Self::PrefixIter; @@ -546,7 +546,7 @@ where &self, prefix: &Key, ) -> (>::PrefixIter, u64) { - (self.db.iter_prefix(prefix), prefix.len() as _) + (self.db.iter_prefix(Some(prefix)), prefix.len() as _) } /// Returns a prefix iterator and the gas cost diff --git a/core/src/ledger/storage/wl_storage.rs b/core/src/ledger/storage/wl_storage.rs index 88d44bf871..ada2f4ecef 100644 --- a/core/src/ledger/storage/wl_storage.rs +++ b/core/src/ledger/storage/wl_storage.rs @@ -235,7 +235,7 @@ where D: DB + for<'iter_> DBIter<'iter_>, H: StorageHasher, { - let storage_iter = storage.db.iter_prefix(prefix).peekable(); + let storage_iter = storage.db.iter_prefix(Some(prefix)).peekable(); let write_log_iter = write_log.iter_prefix_pre(prefix).peekable(); ( PrefixIter { @@ -260,7 +260,7 @@ where D: DB + for<'iter_> DBIter<'iter_>, H: StorageHasher, { - let storage_iter = storage.db.iter_prefix(prefix).peekable(); + let storage_iter = storage.db.iter_prefix(Some(prefix)).peekable(); let write_log_iter = write_log.iter_prefix_post(prefix).peekable(); ( PrefixIter { diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 910e51b180..b72804962f 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -392,12 +392,10 @@ impl<'de> serde::Deserialize<'de> for Amount { where D: serde::Deserializer<'de>, { - use serde::de::Error; let amount_string: String = serde::Deserialize::deserialize(deserializer)?; - Ok(Self { - raw: Uint::from_str(&amount_string).map_err(D::Error::custom)?, - }) + let amt = DenominatedAmount::from_str(&amount_string).unwrap(); + Ok(amt.amount) } } @@ -1065,6 +1063,14 @@ mod tests { assert_eq!(max.checked_sub(max), Some(zero)); } + #[test] + fn test_serialization_round_trip() { + let amount: Amount = serde_json::from_str(r#""1000000000""#).unwrap(); + assert_eq!(amount, Amount{raw: Uint::from(1000000000)}); + let serialized = serde_json::to_string(&amount).unwrap(); + assert_eq!(serialized, r#""1000000000""#); + } + #[test] fn test_amount_checked_add() { let max = Amount::max(); @@ -1150,21 +1156,6 @@ mod tests { let non_zero = Amount::from_uint(1, 0).expect("Test failed"); assert!(!non_zero.is_zero()); } - - #[test] - fn testy_poo() { - let change = Change::from(30000000000000000000i128); - let output = Change::from(6893488147419103231i128); - - let amt = DenominatedAmount { - amount: Amount::from(change), - denom: 18.into(), - }; - println!("{}", amt); - println!("{:?}", change.0.0); - println!("{:?}", output.0.0); - assert!(false); - } } /// Helpers for testing with addresses. diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 3a6725e0d9..0dad2ed1d8 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -27,9 +27,7 @@ use namada_apps::client::tx::ShieldedContext; use namada_apps::config::genesis::genesis_config::{ GenesisConfig, ParametersConfig, PosParamsConfig, }; -use namada_core::types::token::{ - DenominatedAmount, NATIVE_MAX_DECIMAL_PLACES, -}; +use namada_core::types::token::{DenominatedAmount, NATIVE_MAX_DECIMAL_PLACES}; use namada_test_utils::TestWasms; use serde_json::json; use setup::constants::*; diff --git a/tests/src/e2e/setup.rs b/tests/src/e2e/setup.rs index c19039f87d..fc72fcf262 100644 --- a/tests/src/e2e/setup.rs +++ b/tests/src/e2e/setup.rs @@ -21,12 +21,12 @@ use eyre::{eyre, Context}; use itertools::{Either, Itertools}; use namada::types::chain::ChainId; use namada_apps::client::utils; +use namada_apps::client::utils::REDUCED_CLI_PRINTING; use namada_apps::config::genesis::genesis_config::{self, GenesisConfig}; use namada_apps::{config, wallet}; use rand::Rng; use serde_json; use tempfile::{tempdir, TempDir}; -use namada_apps::client::utils::REDUCED_CLI_PRINTING; use crate::e2e::helpers::generate_bin_command; diff --git a/wasm/checksums.json b/wasm/checksums.json index 880551cd83..c23470d836 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -12,7 +12,7 @@ "tx_vote_proposal.wasm": "tx_vote_proposal.c5a856595b0a76b7191aec1bf1912d69e9d29e94e412ce7ebd7c96467900bfd8.wasm", "tx_withdraw.wasm": "tx_withdraw.43cf91dc3fec04ffe740da865e55b00b6e6d71a6da9dcfde378506aedd715f24.wasm", "vp_implicit.wasm": "vp_implicit.377e14b90713516ed12fd10975a13c9d74bd3a4de8f40be17e9aa4315953c282.wasm", - "vp_masp.wasm": "vp_masp.38d624456bf2d4f0151d5d3c3a8ef52c4d412243830a14c9e4bd408b24bad6a3.wasm", + "vp_masp.wasm": "vp_masp.68c7729e65ba47cfcca93009f6c1e7bfc36d785d6defb7f03b5d362276602c00.wasm", "vp_testnet_faucet.wasm": "vp_testnet_faucet.9059ec6cd09ce0a7bde581b487d1e53921a9c26f29c76071d3a8d68fdba9bdab.wasm", "vp_token.wasm": "vp_token.cd4d62be7dbb08a1f983857a9a59689b8ac12856ce15480173e26d94647adf86.wasm", "vp_user.wasm": "vp_user.ef4584780d875a7fd9d242356f5606dcc91613083dcadc942b3f44dd64e2afbe.wasm", diff --git a/wasm/wasm_source/src/vp_masp.rs b/wasm/wasm_source/src/vp_masp.rs index a618aa165f..186e0d21aa 100644 --- a/wasm/wasm_source/src/vp_masp.rs +++ b/wasm/wasm_source/src/vp_masp.rs @@ -59,8 +59,7 @@ fn valid_transfer_amount( log_string(format!( "The unshielded amount {} disagrees with the calculated masp \ transparented value {}", - unshielded_transfer_value, - reporeted_transparent_value + unshielded_transfer_value, reporeted_transparent_value )) } res @@ -255,7 +254,10 @@ fn validate_tx( // one or more of the denoms in the batch failed to verify // the asset derivation. if valid_count != out_length { - log_string("one or more of the denoms in the batch failed to verify the asset derivation."); + log_string( + "one or more of the denoms in the batch failed to verify \ + the asset derivation.", + ); return reject(); } } else { From 38e514c2e498f46d5d7d6ed93871688e72d8ec0e Mon Sep 17 00:00:00 2001 From: satan Date: Sun, 11 Jun 2023 12:27:34 +0200 Subject: [PATCH 079/151] [chore] Fixed clippy and formatting --- apps/src/lib/client/tx.rs | 10 +++---- apps/src/lib/client/utils.rs | 2 +- apps/src/lib/node/ledger/storage/rocksdb.rs | 8 +++--- core/src/types/token.rs | 7 ++++- wasm/wasm_source/src/vp_implicit.rs | 22 ++++------------ wasm/wasm_source/src/vp_masp.rs | 29 --------------------- wasm/wasm_source/src/vp_user.rs | 24 +++++------------ wasm/wasm_source/src/vp_validator.rs | 24 +++++------------ wasm_for_tests/wasm_source/Cargo.lock | 4 +-- 9 files changed, 35 insertions(+), 95 deletions(-) diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index f079e2ef4b..26e98bf5cf 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -1214,11 +1214,11 @@ impl ShieldedContext { // Where we will store our exchanged value let mut output = MaspAmount::default(); // Repeatedly exchange assets until it is no longer possible - loop { - let (asset_epoch, token_addr, value) = match input.iter().next() { - Some(((e, a), v)) => (*e, a.clone(), *v), - _ => break, - }; + while let Some(((asset_epoch, token_addr), value)) = input.iter().next() + { + let value = *value; + let asset_epoch = *asset_epoch; + let token_addr = token_addr.clone(); for denom in MaspDenom::iter() { let target_asset_type = make_asset_type( Some(target_epoch), diff --git a/apps/src/lib/client/utils.rs b/apps/src/lib/client/utils.rs index 94a1b84b99..bddbefb7ab 100644 --- a/apps/src/lib/client/utils.rs +++ b/apps/src/lib/client/utils.rs @@ -1121,7 +1121,7 @@ where break; } } - println!(""); + println!(); task.join().unwrap() } diff --git a/apps/src/lib/node/ledger/storage/rocksdb.rs b/apps/src/lib/node/ledger/storage/rocksdb.rs index 8dba637a37..9adea77f4d 100644 --- a/apps/src/lib/node/ledger/storage/rocksdb.rs +++ b/apps/src/lib/node/ledger/storage/rocksdb.rs @@ -380,9 +380,8 @@ impl RocksDB { let batch = Mutex::new(batch); tracing::info!("Restoring previous hight subspace diffs"); - self.iter_prefix(None) - .par_bridge() - .try_for_each(|(key, _value, _gas)| -> Result<()> { + self.iter_prefix(None).par_bridge().try_for_each( + |(key, _value, _gas)| -> Result<()> { // Restore previous height diff if present, otherwise delete the // subspace key @@ -401,7 +400,8 @@ impl RocksDB { } Ok(()) - })?; + }, + )?; // Delete any height-prepended key, including subspace diff keys let mut batch = batch.into_inner().unwrap(); diff --git a/core/src/types/token.rs b/core/src/types/token.rs index b72804962f..429388e6e3 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -1066,7 +1066,12 @@ mod tests { #[test] fn test_serialization_round_trip() { let amount: Amount = serde_json::from_str(r#""1000000000""#).unwrap(); - assert_eq!(amount, Amount{raw: Uint::from(1000000000)}); + assert_eq!( + amount, + Amount { + raw: Uint::from(1000000000) + } + ); let serialized = serde_json::to_string(&amount).unwrap(); assert_eq!(serialized, r#""1000000000""#); } diff --git a/wasm/wasm_source/src/vp_implicit.rs b/wasm/wasm_source/src/vp_implicit.rs index 272d613bd4..14570bb2a0 100644 --- a/wasm/wasm_source/src/vp_implicit.rs +++ b/wasm/wasm_source/src/vp_implicit.rs @@ -11,7 +11,7 @@ //! //! Any other storage key changes are allowed only with a valid signature. -use namada_vp_prelude::storage::{Key, KeySeg}; +use namada_vp_prelude::storage::KeySeg; use namada_vp_prelude::*; use once_cell::unsync::Lazy; @@ -19,8 +19,6 @@ enum KeyType<'a> { /// Public key - written once revealed Pk(&'a Address), Token { - token: &'a Address, - sub_prefix: Option, owner: &'a Address, }, PoS, @@ -32,22 +30,12 @@ impl<'a> From<&'a storage::Key> for KeyType<'a> { fn from(key: &'a storage::Key) -> KeyType<'a> { if let Some(address) = key::is_pk_key(key) { Self::Pk(address) - } else if let Some([token, owner]) = - token::is_any_token_balance_key(key) - { - Self::Token { - token, - owner, - sub_prefix: None, - } - } else if let Some((sub, [token, owner])) = + } else if let Some([_, owner]) = token::is_any_token_balance_key(key) { + Self::Token { owner } + } else if let Some((_, [_, owner])) = token::is_any_multitoken_balance_key(key) { - Self::Token { - token, - owner, - sub_prefix: Some(sub), - } + Self::Token { owner } } else if proof_of_stake::is_pos_key(key) { Self::PoS } else if gov_storage::is_vote_key(key) { diff --git a/wasm/wasm_source/src/vp_masp.rs b/wasm/wasm_source/src/vp_masp.rs index 186e0d21aa..eb0cd6cf3d 100644 --- a/wasm/wasm_source/src/vp_masp.rs +++ b/wasm/wasm_source/src/vp_masp.rs @@ -56,11 +56,6 @@ fn valid_transfer_amount( unshielded_transfer_value, reporeted_transparent_value ); - log_string(format!( - "The unshielded amount {} disagrees with the calculated masp \ - transparented value {}", - unshielded_transfer_value, reporeted_transparent_value - )) } res } @@ -117,7 +112,6 @@ fn validate_tx( transparent_tx_pool += shielded_tx.value_balance.clone(); if transfer.source != masp() { - log_string("transparent input"); // Handle transparent input // Note that the asset type is timestamped so shields // where the shielded value has an incorrect timestamp @@ -131,7 +125,6 @@ fn validate_tx( denom, ); - log_string(format!("transparent amount: {:?}", transp_amt)); // Non-masp sources add to transparent tx pool transparent_tx_pool += transp_amt; } @@ -153,7 +146,6 @@ fn validate_tx( } if transfer.target != masp() { - log_string("transparent output"); // Handle transparent output // The following boundary conditions must be satisfied // 1. One to 4 transparent outputs @@ -171,11 +163,6 @@ fn validate_tx( shielded_tx.vout.len() ); - log_string(format!( - "Transparent output to a transaction to the masp must be \ - beteween 1 and 4 but is {}", - shielded_tx.vout.len() - )); return reject(); } @@ -209,7 +196,6 @@ fn validate_tx( out.value, denom.denominate(&transfer.amount.amount), ) { - log_string("Invalid transfer amount"); return reject(); } @@ -241,10 +227,6 @@ fn validate_tx( "the public key of the output account does \ not match the transfer target" ); - log_string(format!( - "the public key of the output account does \ - not match the transfer target" - )); return reject(); } } @@ -254,10 +236,6 @@ fn validate_tx( // one or more of the denoms in the batch failed to verify // the asset derivation. if valid_count != out_length { - log_string( - "one or more of the denoms in the batch failed to verify \ - the asset derivation.", - ); return reject(); } } else { @@ -267,7 +245,6 @@ fn validate_tx( // Satisfies 1. if !shielded_tx.vout.is_empty() { - log_string(format!("transparent vout {:?}", shielded_tx.vout)); debug_log!( "Transparent output to a transaction from the masp must \ be 0 but is {}", @@ -286,11 +263,6 @@ fn validate_tx( ); // Section 3.4: The remaining value in the transparent // transaction value pool MUST be nonnegative. - log_string(format!( - "would give the masp a negative balance; transparent tx \ - {:?}", - transparent_tx_pool - )); return reject(); } _ => {} @@ -298,6 +270,5 @@ fn validate_tx( } // Do the expensive proof verification in the VM at the end. - log_string("reached proof verification"); ctx.verify_masp(data) } diff --git a/wasm/wasm_source/src/vp_user.rs b/wasm/wasm_source/src/vp_user.rs index 429008f660..7abc85df0b 100644 --- a/wasm/wasm_source/src/vp_user.rs +++ b/wasm/wasm_source/src/vp_user.rs @@ -9,16 +9,12 @@ //! Any other storage key changes are allowed only with a valid signature. use namada_vp_prelude::address::masp; -use namada_vp_prelude::storage::{Key, KeySeg}; +use namada_vp_prelude::storage::KeySeg; use namada_vp_prelude::*; use once_cell::unsync::Lazy; enum KeyType<'a> { - Token { - token: &'a Address, - sub_prefix: Option, - owner: &'a Address, - }, + Token { owner: &'a Address }, PoS, Vp(&'a Address), Masp, @@ -28,20 +24,12 @@ enum KeyType<'a> { impl<'a> From<&'a storage::Key> for KeyType<'a> { fn from(key: &'a storage::Key) -> KeyType<'a> { - if let Some([token, owner]) = token::is_any_token_balance_key(key) { - Self::Token { - token, - owner, - sub_prefix: None, - } - } else if let Some((sub, [token, owner])) = + if let Some([_, owner]) = token::is_any_token_balance_key(key) { + Self::Token { owner } + } else if let Some((_, [_, owner])) = token::is_any_multitoken_balance_key(key) { - Self::Token { - token, - owner, - sub_prefix: Some(sub), - } + Self::Token { owner } } else if proof_of_stake::is_pos_key(key) { Self::PoS } else if gov_storage::is_vote_key(key) { diff --git a/wasm/wasm_source/src/vp_validator.rs b/wasm/wasm_source/src/vp_validator.rs index f3cad12e93..92fe809c68 100644 --- a/wasm/wasm_source/src/vp_validator.rs +++ b/wasm/wasm_source/src/vp_validator.rs @@ -12,16 +12,12 @@ //! //! Any other storage key changes are allowed only with a valid signature. -use namada_vp_prelude::storage::{Key, KeySeg}; +use namada_vp_prelude::storage::KeySeg; use namada_vp_prelude::*; use once_cell::unsync::Lazy; enum KeyType<'a> { - Token { - token: &'a Address, - sub_prefix: Option, - owner: &'a Address, - }, + Token { owner: &'a Address }, PoS, Vp(&'a Address), GovernanceVote(&'a Address), @@ -30,20 +26,12 @@ enum KeyType<'a> { impl<'a> From<&'a storage::Key> for KeyType<'a> { fn from(key: &'a storage::Key) -> KeyType<'a> { - if let Some([token, owner]) = token::is_any_token_balance_key(key) { - Self::Token { - token, - owner, - sub_prefix: None, - } - } else if let Some((sub, [token, owner])) = + if let Some([_, owner]) = token::is_any_token_balance_key(key) { + Self::Token { owner } + } else if let Some((_, [_, owner])) = token::is_any_multitoken_balance_key(key) { - Self::Token { - token, - owner, - sub_prefix: Some(sub), - } + Self::Token { owner } } else if proof_of_stake::is_pos_key(key) { Self::PoS } else if gov_storage::is_vote_key(key) { diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index 04f4c6989b..333360873e 100644 --- a/wasm_for_tests/wasm_source/Cargo.lock +++ b/wasm_for_tests/wasm_source/Cargo.lock @@ -2425,7 +2425,7 @@ dependencies = [ [[package]] name = "masp_primitives" version = "0.5.0" -source = "git+https://github.com/anoma/masp?rev=bee40fc465f6afbd10558d12fe96eb1742eee45c#bee40fc465f6afbd10558d12fe96eb1742eee45c" +source = "git+https://github.com/anoma/masp?rev=ea4fcc68d9412b94042c1c2f0ea0212394e1be37#ea4fcc68d9412b94042c1c2f0ea0212394e1be37" dependencies = [ "aes", "bip0039", @@ -2456,7 +2456,7 @@ dependencies = [ [[package]] name = "masp_proofs" version = "0.5.0" -source = "git+https://github.com/anoma/masp?rev=bee40fc465f6afbd10558d12fe96eb1742eee45c#bee40fc465f6afbd10558d12fe96eb1742eee45c" +source = "git+https://github.com/anoma/masp?rev=ea4fcc68d9412b94042c1c2f0ea0212394e1be37#ea4fcc68d9412b94042c1c2f0ea0212394e1be37" dependencies = [ "bellman", "blake2b_simd 1.0.0", From 5eea24439362e55b81ac819868e8042db0330827 Mon Sep 17 00:00:00 2001 From: satan Date: Thu, 15 Jun 2023 08:01:42 +0200 Subject: [PATCH 080/151] [fix]: Fixed unit tests other than flaky pos test --- .../lib/node/ledger/shell/finalize_block.rs | 26 ++++++++----------- .../tests/state_machine.txt | 1 + proof_of_stake/src/tests.rs | 15 +++-------- 3 files changed, 16 insertions(+), 26 deletions(-) diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index 69ce68961c..2984322e1b 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -920,7 +920,7 @@ mod test_finalize_block { use namada::types::key::tm_consensus_key_raw_hash; use namada::types::storage::Epoch; use namada::types::time::DurationSecs; - use namada::types::token::Amount; + use namada::types::token::{Amount, NATIVE_MAX_DECIMAL_PLACES}; use namada::types::transaction::governance::{ InitProposalData, ProposalType, VoteProposalData, }; @@ -2664,10 +2664,9 @@ mod test_finalize_block { - self_unbond_2_amount) + Dec::from(del_2_amount); - let val = (exp_pipeline_stake - Dec::from(post_stake_10)).abs(); assert!( - (exp_pipeline_stake - Dec::from(post_stake_10)).abs() - <= Uint::from(Amount::native_whole(1u64)) + exp_pipeline_stake.abs_diff(&Dec::from(post_stake_10)) + <= Dec::new(1, NATIVE_MAX_DECIMAL_PLACES).unwrap() ); // Check the balance of the Slash Pool @@ -2828,12 +2827,10 @@ mod test_finalize_block { // TODO: decimal mult issues should be resolved with PR 1282 assert!( (del_details.bonds[0].slashed_amount.unwrap().change() - - (std::cmp::min( + - std::cmp::min( Dec::one(), Dec::new(3, 0).unwrap() * cubic_rate - ) * del_1_amount - - del_unbond_1_amount) - .change()) + ) * (del_1_amount.change() - del_unbond_1_amount.change())) .abs() <= Uint::from(2) ); @@ -2852,15 +2849,14 @@ mod test_finalize_block { // TODO: not sure why this is correct??? (with + self_bond_1_amount - // self_unbond_2_amount) // TODO: Make sure this is sound and what we expect - assert_eq!( - self_details.bonds[0].slashed_amount, - Some( - std::cmp::min(Dec::one(), Dec::new(3, 0).unwrap() * cubic_rate) - * initial_stake + assert!( + (self_details.bonds[0].slashed_amount.unwrap().change() - + (std::cmp::min(Dec::one(), Dec::new(3, 0).unwrap() * cubic_rate) + * (initial_stake - self_unbond_1_amount + self_bond_1_amount - - self_unbond_2_amount - ) + - self_unbond_2_amount) + ).change()) <= Amount::from_uint(1000, NATIVE_MAX_DECIMAL_PLACES).unwrap().change() ); // Check delegation unbonds diff --git a/proof_of_stake/proptest-regressions/tests/state_machine.txt b/proof_of_stake/proptest-regressions/tests/state_machine.txt index fd37a6f64a..4c02bc0ede 100644 --- a/proof_of_stake/proptest-regressions/tests/state_machine.txt +++ b/proof_of_stake/proptest-regressions/tests/state_machine.txt @@ -5,3 +5,4 @@ # It is recommended to check this file in to source control so that # everyone who runs the test benefits from these saved cases. cc 3076c8509d56c546d5915febcf429f218ab79a7bac34c75c288f531b88110bc3 # shrinks to (initial_state, transitions) = (AbstractPosState { epoch: Epoch(0), params: PosParams { max_validator_slots: 4, pipeline_len: 2, unbonding_len: 4, tm_votes_per_token: 0.0614, block_proposer_reward: 0.125, block_vote_reward: 0.1, max_inflation_rate: 0.1, target_staked_ratio: 0.6667, duplicate_vote_min_slash_rate: 0.001, light_client_attack_min_slash_rate: 0.001, cubic_slashing_window_length: 1 }, genesis_validators: [GenesisValidator { address: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, tokens: Amount { micro: 9185807 }, consensus_key: Ed25519(PublicKey(VerificationKey("ee1aa49a4459dfe813a3cf6eb882041230c7b2558469de81f87c9bf23bf10a03"))), commission_rate: 0.05, max_commission_rate_change: 0.01 }, GenesisValidator { address: Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6, tokens: Amount { micro: 5025206 }, consensus_key: Ed25519(PublicKey(VerificationKey("17888c2ca502371245e5e35d5bcf35246c3bc36878e859938c9ead3c54db174f"))), commission_rate: 0.05, max_commission_rate_change: 0.01 }, GenesisValidator { address: Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc, tokens: Amount { micro: 4424807 }, consensus_key: Ed25519(PublicKey(VerificationKey("478243aed376da313d7cf3a60637c264cb36acc936efb341ff8d3d712092d244"))), commission_rate: 0.05, max_commission_rate_change: 0.01 }, GenesisValidator { address: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, tokens: Amount { micro: 4119410 }, consensus_key: Ed25519(PublicKey(VerificationKey("c5bbbb60e412879bbec7bb769804fa8e36e68af10d5477280b63deeaca931bed"))), commission_rate: 0.05, max_commission_rate_change: 0.01 }, GenesisValidator { address: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, tokens: Amount { micro: 3619078 }, consensus_key: Ed25519(PublicKey(VerificationKey("4f44e6c7bdfed3d9f48d86149ee3d29382cae8c83ca253e06a70be54a301828b"))), commission_rate: 0.05, max_commission_rate_change: 0.01 }, GenesisValidator { address: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, tokens: Amount { micro: 2691447 }, consensus_key: Ed25519(PublicKey(VerificationKey("ff87a0b0a3c7c0ce827e9cada5ff79e75a44a0633bfcb5b50f99307ddb26b337"))), commission_rate: 0.05, max_commission_rate_change: 0.01 }, GenesisValidator { address: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, tokens: Amount { micro: 224944 }, consensus_key: Ed25519(PublicKey(VerificationKey("191fc38f134aaf1b7fdb1f86330b9d03e94bd4ba884f490389de964448e89b3f"))), commission_rate: 0.05, max_commission_rate_change: 0.01 }, GenesisValidator { address: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, tokens: Amount { micro: 142614 }, consensus_key: Ed25519(PublicKey(VerificationKey("e2e8aa145e1ec5cb01ebfaa40e10e12f0230c832fd8135470c001cb86d77de00"))), commission_rate: 0.05, max_commission_rate_change: 0.01 }], bonds: {BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }: {Epoch(0): 142614}, BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }: {Epoch(0): 4119410}, BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }: {Epoch(0): 9185807}, BondId { source: Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6, validator: Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6 }: {Epoch(0): 5025206}, BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }: {Epoch(0): 2691447}, BondId { source: Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc, validator: Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc }: {Epoch(0): 4424807}, BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }: {Epoch(0): 224944}, BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }: {Epoch(0): 3619078}}, validator_stakes: {Epoch(0): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: 142614, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: 4119410, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: 9185807, Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6: 5025206, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: 2691447, Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc: 4424807, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: 224944, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: 3619078}, Epoch(1): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: 142614, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: 4119410, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: 9185807, Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6: 5025206, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: 2691447, Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc: 4424807, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: 224944, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: 3619078}, Epoch(2): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: 142614, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: 4119410, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: 9185807, Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6: 5025206, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: 2691447, Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc: 4424807, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: 224944, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: 3619078}}, consensus_set: {Epoch(0): {Amount { micro: 4119410 }: [Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3], Amount { micro: 4424807 }: [Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc], Amount { micro: 5025206 }: [Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6], Amount { micro: 9185807 }: [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6]}, Epoch(1): {Amount { micro: 4119410 }: [Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3], Amount { micro: 4424807 }: [Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc], Amount { micro: 5025206 }: [Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6], Amount { micro: 9185807 }: [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6]}, Epoch(2): {Amount { micro: 4119410 }: [Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3], Amount { micro: 4424807 }: [Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc], Amount { micro: 5025206 }: [Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6], Amount { micro: 9185807 }: [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6]}}, below_capacity_set: {Epoch(0): {ReverseOrdTokenAmount(Amount { micro: 142614 }): [Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6], ReverseOrdTokenAmount(Amount { micro: 224944 }): [Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv], ReverseOrdTokenAmount(Amount { micro: 2691447 }): [Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk], ReverseOrdTokenAmount(Amount { micro: 3619078 }): [Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd]}, Epoch(1): {ReverseOrdTokenAmount(Amount { micro: 142614 }): [Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6], ReverseOrdTokenAmount(Amount { micro: 224944 }): [Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv], ReverseOrdTokenAmount(Amount { micro: 2691447 }): [Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk], ReverseOrdTokenAmount(Amount { micro: 3619078 }): [Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd]}, Epoch(2): {ReverseOrdTokenAmount(Amount { micro: 142614 }): [Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6], ReverseOrdTokenAmount(Amount { micro: 224944 }): [Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv], ReverseOrdTokenAmount(Amount { micro: 2691447 }): [Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk], ReverseOrdTokenAmount(Amount { micro: 3619078 }): [Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd]}}, validator_states: {Epoch(0): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: BelowCapacity, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: Consensus, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: Consensus, Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6: Consensus, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: BelowCapacity, Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc: Consensus, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: BelowCapacity, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: BelowCapacity}, Epoch(1): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: BelowCapacity, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: Consensus, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: Consensus, Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6: Consensus, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: BelowCapacity, Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc: Consensus, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: BelowCapacity, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: BelowCapacity}, Epoch(2): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: BelowCapacity, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: Consensus, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: Consensus, Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6: Consensus, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: BelowCapacity, Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc: Consensus, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: BelowCapacity, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: BelowCapacity}}, unbonds: {}, validator_slashes: {}, enqueued_slashes: {}, validator_last_slash_epochs: {}, unbond_records: {} }, [InitValidator { address: Established: atest1v4ehgw36xgunxvj9xqmny3jyxycnzdzxxqeng33ngvunqsfsx5mnwdfjgvenvwfk89prwdpjd0cjrk, consensus_key: Ed25519(PublicKey(VerificationKey("bea04de1e5be8ca0ae27be8ad935df8d757e96c1e067e96aedeba0ded0df997d"))), commission_rate: 0.39428, max_commission_rate_change: 0.12485 }]) +cc c0ffe7b368967ea0c456da20046f7d8a78c232c066ea116d3a123c945b7882fb # shrinks to (initial_state, transitions) = (AbstractPosState { epoch: Epoch(0), params: PosParams { max_validator_slots: 4, pipeline_len: 2, unbonding_len: 7, tm_votes_per_token: Dec(900700.000000), block_proposer_reward: Dec(125000.000000), block_vote_reward: Dec(100000.000000), max_inflation_rate: Dec(100000.000000), target_staked_ratio: Dec(666700.000000), duplicate_vote_min_slash_rate: Dec(1000.000000), light_client_attack_min_slash_rate: Dec(1000.000000), cubic_slashing_window_length: 1 }, genesis_validators: [GenesisValidator { address: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, tokens: Amount { raw: 8937727 }, consensus_key: Ed25519(PublicKey(VerificationKey("e2e8aa145e1ec5cb01ebfaa40e10e12f0230c832fd8135470c001cb86d77de00"))), commission_rate: Dec(50000.000000), max_commission_rate_change: Dec(10000.000000) }, GenesisValidator { address: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, tokens: Amount { raw: 8738693 }, consensus_key: Ed25519(PublicKey(VerificationKey("ff87a0b0a3c7c0ce827e9cada5ff79e75a44a0633bfcb5b50f99307ddb26b337"))), commission_rate: Dec(50000.000000), max_commission_rate_change: Dec(10000.000000) }, GenesisValidator { address: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, tokens: Amount { raw: 8373784 }, consensus_key: Ed25519(PublicKey(VerificationKey("c5bbbb60e412879bbec7bb769804fa8e36e68af10d5477280b63deeaca931bed"))), commission_rate: Dec(50000.000000), max_commission_rate_change: Dec(10000.000000) }, GenesisValidator { address: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, tokens: Amount { raw: 3584214 }, consensus_key: Ed25519(PublicKey(VerificationKey("4f44e6c7bdfed3d9f48d86149ee3d29382cae8c83ca253e06a70be54a301828b"))), commission_rate: Dec(50000.000000), max_commission_rate_change: Dec(10000.000000) }, GenesisValidator { address: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, tokens: Amount { raw: 553863 }, consensus_key: Ed25519(PublicKey(VerificationKey("ee1aa49a4459dfe813a3cf6eb882041230c7b2558469de81f87c9bf23bf10a03"))), commission_rate: Dec(50000.000000), max_commission_rate_change: Dec(10000.000000) }, GenesisValidator { address: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, tokens: Amount { raw: 218044 }, consensus_key: Ed25519(PublicKey(VerificationKey("191fc38f134aaf1b7fdb1f86330b9d03e94bd4ba884f490389de964448e89b3f"))), commission_rate: Dec(50000.000000), max_commission_rate_change: Dec(10000.000000) }], bonds: {BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }: {Epoch(0): 8.937727}, BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }: {Epoch(0): 8.373784}, BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }: {Epoch(0): 0.553863}, BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }: {Epoch(0): 8.738693}, BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }: {Epoch(0): 0.218044}, BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }: {Epoch(0): 3.584214}}, validator_stakes: {Epoch(0): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: 8.937727, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: 8.373784, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: 0.553863, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: 8.738693, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: 0.218044, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: 3.584214}, Epoch(1): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: 8.937727, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: 8.373784, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: 0.553863, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: 8.738693, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: 0.218044, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: 3.584214}, Epoch(2): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: 8.937727, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: 8.373784, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: 0.553863, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: 8.738693, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: 0.218044, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: 3.584214}}, consensus_set: {Epoch(0): {Amount { raw: 3584214 }: [Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd], Amount { raw: 8373784 }: [Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3], Amount { raw: 8738693 }: [Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk], Amount { raw: 8937727 }: [Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6]}, Epoch(1): {Amount { raw: 3584214 }: [Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd], Amount { raw: 8373784 }: [Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3], Amount { raw: 8738693 }: [Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk], Amount { raw: 8937727 }: [Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6]}, Epoch(2): {Amount { raw: 3584214 }: [Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd], Amount { raw: 8373784 }: [Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3], Amount { raw: 8738693 }: [Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk], Amount { raw: 8937727 }: [Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6]}}, below_capacity_set: {Epoch(0): {ReverseOrdTokenAmount(Amount { raw: 218044 }): [Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv], ReverseOrdTokenAmount(Amount { raw: 553863 }): [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6]}, Epoch(1): {ReverseOrdTokenAmount(Amount { raw: 218044 }): [Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv], ReverseOrdTokenAmount(Amount { raw: 553863 }): [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6]}, Epoch(2): {ReverseOrdTokenAmount(Amount { raw: 218044 }): [Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv], ReverseOrdTokenAmount(Amount { raw: 553863 }): [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6]}}, validator_states: {Epoch(0): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: Consensus, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: Consensus, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: BelowCapacity, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: Consensus, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: BelowCapacity, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: Consensus}, Epoch(1): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: Consensus, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: Consensus, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: BelowCapacity, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: Consensus, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: BelowCapacity, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: Consensus}, Epoch(2): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: Consensus, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: Consensus, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: BelowCapacity, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: Consensus, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: BelowCapacity, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: Consensus}}, unbonds: {}, validator_slashes: {}, enqueued_slashes: {}, validator_last_slash_epochs: {}, unbond_records: {} }, [Unbond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 267 } }, NextEpoch, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 7610143 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 9863718 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 7102818 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 63132 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 9663084 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 2694963 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 7453740 } }, NextEpoch, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 14974324 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 2628172 } }, NextEpoch, NextEpoch, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 282055 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 11228090 } }, Bond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 2027105 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 2034080 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }, amount: Amount { raw: 3329590 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 854661 } }, Misbehavior { address: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, slash_type: DuplicateVote, infraction_epoch: Epoch(1), height: 0 }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 227931 } }, NextEpoch, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 2701887 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 1776100 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }, amount: Amount { raw: 3717491 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }, amount: Amount { raw: 5281559 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }, amount: Amount { raw: 2426117 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 2005749 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 7883312 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 7300122 } }, Bond { id: BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 3388459 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }, amount: Amount { raw: 195542 } }, NextEpoch, Unbond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 2251455 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 1237777 } }, NextEpoch, NextEpoch, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 691613 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 1244599 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 2645543 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 8384136 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 590662 } }, NextEpoch, InitValidator { address: Established: atest1v4ehgw368qcrqd2ygvmyyvf4g9qnvv3kxucrwv3hxg6ryve4x56r233cxucnysjrxsmygdj9yer4pz, consensus_key: Ed25519(PublicKey(VerificationKey("afa2335747c0249f66eca84e88fba1a0e3ccec6a8f6f97f3177a42ffbb216492"))), commission_rate: Dec(195450.000000), max_commission_rate_change: Dec(954460.000000) }, Bond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 1687952 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 12754717 } }, Misbehavior { address: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, slash_type: LightClientAttack, infraction_epoch: Epoch(4), height: 0 }, Bond { id: BondId { source: Implicit: atest1d9khqw36xqunjdeegge5xdpcg5mnqwzp8yerzde58pq5g3pcxu6yvvphg3zr23z9gg6yvs3cmzdz9u, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 8952712 } }, NextEpoch, Withdraw { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv } }, Unbond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 519835 } }, UnjailValidator { address: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, Unbond { id: BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 2207493 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 236124 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 71122 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw36xqunjdeegge5xdpcg5mnqwzp8yerzde58pq5g3pcxu6yvvphg3zr23z9gg6yvs3cmzdz9u, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 1158688 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 267618 } }, InitValidator { address: Established: atest1v4ehgw36xucy2dfcxdzrxvpjx5uygwzrxpzrjs3jx4p5vvjrxdq5yvpjx5e5zs3jxdqng3pcplv2ch, consensus_key: Ed25519(PublicKey(VerificationKey("822cfec1ec829a50306424ac3d11115e880b952f5f54ac9a624277898991ee70"))), commission_rate: Dec(614520.000000), max_commission_rate_change: Dec(369920.000000) }, Bond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 8634884 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 8660668 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g9rryv3sx5c5v33sgsmrsd3egerrgdenx3zy2sfex4prvsehxcurydjx8qu5zdz9f2npes, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 8436873 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 515615 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 46481 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 4153966 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 2272563 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g5eyzwf3xqc5gwzxg3pnq3jpgsenxwp3x56rjvz9x5crwsf3gerrgwphxqen2sjz4hscvd, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 7491749 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 1921487 } }, Bond { id: BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 8316111 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 11873152 } }, NextEpoch, Withdraw { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv } }, Withdraw { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk } }, Withdraw { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 } }, Bond { id: BondId { source: Implicit: atest1d9khqw368yenjvpjxcu5vv33x3zrqw2zgg6nsvzrx9prxd2pgsmyxwfjxgunvs3exerrydp3csdkvr, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 4728535 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g5eyzwf3xqc5gwzxg3pnq3jpgsenxwp3x56rjvz9x5crwsf3gerrgwphxqen2sjz4hscvd, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 2828807 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g5eyzwf3xqc5gwzxg3pnq3jpgsenxwp3x56rjvz9x5crwsf3gerrgwphxqen2sjz4hscvd, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 655500 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 234416 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 330322 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 222600 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw36xqunjdeegge5xdpcg5mnqwzp8yerzde58pq5g3pcxu6yvvphg3zr23z9gg6yvs3cmzdz9u, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 2538059 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 168498 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw368yenjvpjxcu5vv33x3zrqw2zgg6nsvzrx9prxd2pgsmyxwfjxgunvs3exerrydp3csdkvr, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 510701 } }, Misbehavior { address: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, slash_type: DuplicateVote, infraction_epoch: Epoch(8), height: 0 }, InitValidator { address: Established: atest1v4ehgw36ggcrz3zygyunqsfjggmnq33h8ycnsdphxepnsve4gerrss2pgfp5z3psgccrj33klenl5r, consensus_key: Ed25519(PublicKey(VerificationKey("afc853489cf37abedeb6a97d036f3dc60934194af7169a2cc15fb3f85e4e287c"))), commission_rate: Dec(52690.000000), max_commission_rate_change: Dec(56470.000000) }, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 7098849 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw36xqunjdeegge5xdpcg5mnqwzp8yerzde58pq5g3pcxu6yvvphg3zr23z9gg6yvs3cmzdz9u, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 2180088 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 243441 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw36xqunjdeegge5xdpcg5mnqwzp8yerzde58pq5g3pcxu6yvvphg3zr23z9gg6yvs3cmzdz9u, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 1621261 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 7650954 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 1201023 } }, Bond { id: BondId { source: Implicit: atest1d9khqw36xqunjdeegge5xdpcg5mnqwzp8yerzde58pq5g3pcxu6yvvphg3zr23z9gg6yvs3cmzdz9u, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 9702706 } }, InitValidator { address: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu, consensus_key: Ed25519(PublicKey(VerificationKey("f8506f129faaf3bac1397ad0ab3bfa6d1a00d5c1064c4fafe740f2844be8fb04"))), commission_rate: Dec(575190.000000), max_commission_rate_change: Dec(602710.000000) }, Unbond { id: BondId { source: Implicit: atest1d9khqw368yenjvpjxcu5vv33x3zrqw2zgg6nsvzrx9prxd2pgsmyxwfjxgunvs3exerrydp3csdkvr, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 347187 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 5536481 } }, Bond { id: BondId { source: Implicit: atest1d9khqw36xc6nvvf4g9znxvf3xdrrgvfexuen2dek8qmnqse58q6ygdpkxeznz3j9xyeyydfht747xe, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 1859243 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 1907757 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw368yenjvpjxcu5vv33x3zrqw2zgg6nsvzrx9prxd2pgsmyxwfjxgunvs3exerrydp3csdkvr, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 3007741 } }, Misbehavior { address: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, slash_type: DuplicateVote, infraction_epoch: Epoch(9), height: 0 }, Bond { id: BondId { source: Established: atest1v4ehgw36g9rryv3sx5c5v33sgsmrsd3egerrgdenx3zy2sfex4prvsehxcurydjx8qu5zdz9f2npes, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 8226972 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw368yenjvpjxcu5vv33x3zrqw2zgg6nsvzrx9prxd2pgsmyxwfjxgunvs3exerrydp3csdkvr, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 602759 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw36xqunjdeegge5xdpcg5mnqwzp8yerzde58pq5g3pcxu6yvvphg3zr23z9gg6yvs3cmzdz9u, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 8350223 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 3787232 } }, InitValidator { address: Established: atest1v4ehgw36gc6njdpcxycnwv2zx9zrsdjxg9zrqvjzxuurxve5x3rryde48pqnjsekg3przs2z8dz595, consensus_key: Ed25519(PublicKey(VerificationKey("0b88c50c1b9b5b1e83c89110e388908dc3cc18ce0551494ab1c82bece24b2714"))), commission_rate: Dec(674000.000000), max_commission_rate_change: Dec(247230.000000) }, Bond { id: BondId { source: Established: atest1v4ehgw36gdp52wp4xv6yyd3nx9pnysfn89znjsen8quyvwfkgycnjs29x9ryxveh8prygsfecye5dj, validator: Established: atest1v4ehgw36ggcrz3zygyunqsfjggmnq33h8ycnsdphxepnsve4gerrss2pgfp5z3psgccrj33klenl5r }, amount: Amount { raw: 1391049 } }, Bond { id: BondId { source: Implicit: atest1d9khqw36gve5zdf4gccygv6zxgcnxwzrgv65x32rg4zrxv34g9prvs2pxqmnzve5xvuns33czq9awp, validator: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu }, amount: Amount { raw: 4008194 } }, Bond { id: BondId { source: Implicit: atest1d9khqw368pq5g3f3gceygvpjxuenyveexary2wzx8ycnw3zpg9zrvvp4xger2dzyxuunwvjz4n93ww, validator: Established: atest1v4ehgw36gc6njdpcxycnwv2zx9zrsdjxg9zrqvjzxuurxve5x3rryde48pqnjsekg3przs2z8dz595 }, amount: Amount { raw: 9368360 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }, amount: Amount { raw: 9140634 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 600383 } }, Misbehavior { address: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, slash_type: DuplicateVote, infraction_epoch: Epoch(7), height: 0 }, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 8599835 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 345454 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g9rryv3sx5c5v33sgsmrsd3egerrgdenx3zy2sfex4prvsehxcurydjx8qu5zdz9f2npes, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 12448069 } }, NextEpoch, Withdraw { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 5151682 } }, Bond { id: BondId { source: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu, validator: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu }, amount: Amount { raw: 1862578 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 10904134 } }, Bond { id: BondId { source: Implicit: atest1d9khqw368pq5g3f3gceygvpjxuenyveexary2wzx8ycnw3zpg9zrvvp4xger2dzyxuunwvjz4n93ww, validator: Established: atest1v4ehgw36gc6njdpcxycnwv2zx9zrsdjxg9zrqvjzxuurxve5x3rryde48pqnjsekg3przs2z8dz595 }, amount: Amount { raw: 773655 } }, Bond { id: BondId { source: Implicit: atest1d9khqw3689rrqdp58pznydecgyu5xs3cxdznvd6xxsmng32zxumrxvpj8qenydejgfzygwzxlu6r7s, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 8927299 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu, validator: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu }, amount: Amount { raw: 1288039 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 2861830 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu, validator: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu }, amount: Amount { raw: 445593 } }, Bond { id: BondId { source: Implicit: atest1d9khqw368pq5g3f3gceygvpjxuenyveexary2wzx8ycnw3zpg9zrvvp4xger2dzyxuunwvjz4n93ww, validator: Established: atest1v4ehgw36gc6njdpcxycnwv2zx9zrsdjxg9zrqvjzxuurxve5x3rryde48pqnjsekg3przs2z8dz595 }, amount: Amount { raw: 8204875 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 602527 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 5812026 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw3689rrqdp58pznydecgyu5xs3cxdznvd6xxsmng32zxumrxvpj8qenydejgfzygwzxlu6r7s, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 211165 } }, NextEpoch, Bond { id: BondId { source: Implicit: atest1d9khqw36xsun2decx9p52v2xg5cr2vphxym5vve58yerqve5x5c5yve3gepyzs3ngycy233eufckzz, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 350302 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 4560437 } }, Bond { id: BondId { source: Implicit: atest1d9khqw36xqunjdeegge5xdpcg5mnqwzp8yerzde58pq5g3pcxu6yvvphg3zr23z9gg6yvs3cmzdz9u, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 3515009 } }, Bond { id: BondId { source: Established: atest1v4ehgw36xucy2dfcxdzrxvpjx5uygwzrxpzrjs3jx4p5vvjrxdq5yvpjx5e5zs3jxdqng3pcplv2ch, validator: Established: atest1v4ehgw36xucy2dfcxdzrxvpjx5uygwzrxpzrjs3jx4p5vvjrxdq5yvpjx5e5zs3jxdqng3pcplv2ch }, amount: Amount { raw: 4956849 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw36xsun2decx9p52v2xg5cr2vphxym5vve58yerqve5x5c5yve3gepyzs3ngycy233eufckzz, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 290427 } }, NextEpoch, Unbond { id: BondId { source: Implicit: atest1d9khqw36gve5zdf4gccygv6zxgcnxwzrgv65x32rg4zrxv34g9prvs2pxqmnzve5xvuns33czq9awp, validator: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu }, amount: Amount { raw: 3261985 } }, Bond { id: BondId { source: Established: atest1v4ehgw36xucy2dfcxdzrxvpjx5uygwzrxpzrjs3jx4p5vvjrxdq5yvpjx5e5zs3jxdqng3pcplv2ch, validator: Established: atest1v4ehgw36xucy2dfcxdzrxvpjx5uygwzrxpzrjs3jx4p5vvjrxdq5yvpjx5e5zs3jxdqng3pcplv2ch }, amount: Amount { raw: 8946479 } }, Withdraw { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 } }, NextEpoch, InitValidator { address: Established: atest1v4ehgw36gvcrgdeex5ensvfkgccyxve3x3pnys6xxpzr2s6rxuurv3j9g4pyysjzxq6ygdzyt2wxa3, consensus_key: Ed25519(PublicKey(VerificationKey("a856fc650a2404e2d0c152d89c1c221bd9056a6103980e1d821b0cbae213ff44"))), commission_rate: Dec(324920.000000), max_commission_rate_change: Dec(512260.000000) }, Withdraw { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu, validator: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu }, amount: Amount { raw: 82795 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 128956 } }, Bond { id: BondId { source: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu, validator: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu }, amount: Amount { raw: 2043203 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 6764953 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g5eyzwf3xqc5gwzxg3pnq3jpgsenxwp3x56rjvz9x5crwsf3gerrgwphxqen2sjz4hscvd, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 6413168 } }, Bond { id: BondId { source: Implicit: atest1d9khqw368pq5g3f3gceygvpjxuenyveexary2wzx8ycnw3zpg9zrvvp4xger2dzyxuunwvjz4n93ww, validator: Established: atest1v4ehgw36gc6njdpcxycnwv2zx9zrsdjxg9zrqvjzxuurxve5x3rryde48pqnjsekg3przs2z8dz595 }, amount: Amount { raw: 6384185 } }, Misbehavior { address: Established: atest1v4ehgw36gc6njdpcxycnwv2zx9zrsdjxg9zrqvjzxuurxve5x3rryde48pqnjsekg3przs2z8dz595, slash_type: LightClientAttack, infraction_epoch: Epoch(13), height: 0 }, Bond { id: BondId { source: Established: atest1v4ehgw36gc6njdpcxycnwv2zx9zrsdjxg9zrqvjzxuurxve5x3rryde48pqnjsekg3przs2z8dz595, validator: Established: atest1v4ehgw36gc6njdpcxycnwv2zx9zrsdjxg9zrqvjzxuurxve5x3rryde48pqnjsekg3przs2z8dz595 }, amount: Amount { raw: 8314982 } }, Bond { id: BondId { source: Implicit: atest1d9khqw36xscrsve3geqnwd2x8qmrzwpe89z5zsekgvenqwp5x4p5ydzp8qmrz3zpgcmnydjptyfc40, validator: Established: atest1v4ehgw36gvcrgdeex5ensvfkgccyxve3x3pnys6xxpzr2s6rxuurv3j9g4pyysjzxq6ygdzyt2wxa3 }, amount: Amount { raw: 9139532 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu, validator: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu }, amount: Amount { raw: 34693 } }, Bond { id: BondId { source: Implicit: atest1d9khqw3689rrqdp58pznydecgyu5xs3cxdznvd6xxsmng32zxumrxvpj8qenydejgfzygwzxlu6r7s, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 9487215 } }, NextEpoch, NextEpoch, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 799953 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw36xscrsve3geqnwd2x8qmrzwpe89z5zsekgvenqwp5x4p5ydzp8qmrz3zpgcmnydjptyfc40, validator: Established: atest1v4ehgw36gvcrgdeex5ensvfkgccyxve3x3pnys6xxpzr2s6rxuurv3j9g4pyysjzxq6ygdzyt2wxa3 }, amount: Amount { raw: 3334636 } }, NextEpoch, Withdraw { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xucy2dfcxdzrxvpjx5uygwzrxpzrjs3jx4p5vvjrxdq5yvpjx5e5zs3jxdqng3pcplv2ch, validator: Established: atest1v4ehgw36xucy2dfcxdzrxvpjx5uygwzrxpzrjs3jx4p5vvjrxdq5yvpjx5e5zs3jxdqng3pcplv2ch }, amount: Amount { raw: 7942329 } }, NextEpoch, Unbond { id: BondId { source: Established: atest1v4ehgw36gdp52wp4xv6yyd3nx9pnysfn89znjsen8quyvwfkgycnjs29x9ryxveh8prygsfecye5dj, validator: Established: atest1v4ehgw36ggcrz3zygyunqsfjggmnq33h8ycnsdphxepnsve4gerrss2pgfp5z3psgccrj33klenl5r }, amount: Amount { raw: 878389 } }, Withdraw { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 } }, UnjailValidator { address: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, UnjailValidator { address: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, Bond { id: BondId { source: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu, validator: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu }, amount: Amount { raw: 5376602 } }, UnjailValidator { address: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }, Unbond { id: BondId { source: Implicit: atest1d9khqw36xc6nvvf4g9znxvf3xdrrgvfexuen2dek8qmnqse58q6ygdpkxeznz3j9xyeyydfht747xe, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 1118174 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 286221 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 73579 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g9rryv3sx5c5v33sgsmrsd3egerrgdenx3zy2sfex4prvsehxcurydjx8qu5zdz9f2npes, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 2010212 } }, Bond { id: BondId { source: Implicit: atest1d9khqw3689rrqdp58pznydecgyu5xs3cxdznvd6xxsmng32zxumrxvpj8qenydejgfzygwzxlu6r7s, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 4276553 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw368yenjvpjxcu5vv33x3zrqw2zgg6nsvzrx9prxd2pgsmyxwfjxgunvs3exerrydp3csdkvr, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 54860 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gdp52wp4xv6yyd3nx9pnysfn89znjsen8quyvwfkgycnjs29x9ryxveh8prygsfecye5dj, validator: Established: atest1v4ehgw36ggcrz3zygyunqsfjggmnq33h8ycnsdphxepnsve4gerrss2pgfp5z3psgccrj33klenl5r }, amount: Amount { raw: 145154 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }, amount: Amount { raw: 1941194 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 93 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw3689rrqdp58pznydecgyu5xs3cxdznvd6xxsmng32zxumrxvpj8qenydejgfzygwzxlu6r7s, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 9992596 } }, Bond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 504024 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 5640962 } }, InitValidator { address: Established: atest1v4ehgw368qmnzsfeg5urqw2p8pq5gsf4ggcnqdz9xvc5vsfjxc6nvsekgsmyv3jp8ym52wph0hm33r, consensus_key: Ed25519(PublicKey(VerificationKey("2bccbdf7490f98b2e258a399b75c74bd1b71e9f6f4cc2160edbe3186e23d30e4"))), commission_rate: Dec(427420.000000), max_commission_rate_change: Dec(574220.000000) }, Misbehavior { address: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, slash_type: DuplicateVote, infraction_epoch: Epoch(12), height: 0 }, Bond { id: BondId { source: Implicit: atest1d9khqw368pq5g3f3gceygvpjxuenyveexary2wzx8ycnw3zpg9zrvvp4xger2dzyxuunwvjz4n93ww, validator: Established: atest1v4ehgw36gc6njdpcxycnwv2zx9zrsdjxg9zrqvjzxuurxve5x3rryde48pqnjsekg3przs2z8dz595 }, amount: Amount { raw: 4019468 } }, Bond { id: BondId { source: Implicit: atest1d9khqw36xscrsve3geqnwd2x8qmrzwpe89z5zsekgvenqwp5x4p5ydzp8qmrz3zpgcmnydjptyfc40, validator: Established: atest1v4ehgw36gvcrgdeex5ensvfkgccyxve3x3pnys6xxpzr2s6rxuurv3j9g4pyysjzxq6ygdzyt2wxa3 }, amount: Amount { raw: 5683219 } }, Bond { id: BondId { source: Implicit: atest1d9khqw368pz5zd3sgeqnxve4g9ryv3zzggerqdf3xqmrywfng4zrs3pkx5enydesg5mr2v6p4v8rst, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 6886837 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g9rryv3sx5c5v33sgsmrsd3egerrgdenx3zy2sfex4prvsehxcurydjx8qu5zdz9f2npes, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 7852494 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 749047 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gdp52wp4xv6yyd3nx9pnysfn89znjsen8quyvwfkgycnjs29x9ryxveh8prygsfecye5dj, validator: Established: atest1v4ehgw36ggcrz3zygyunqsfjggmnq33h8ycnsdphxepnsve4gerrss2pgfp5z3psgccrj33klenl5r }, amount: Amount { raw: 9097957 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g9rryv3sx5c5v33sgsmrsd3egerrgdenx3zy2sfex4prvsehxcurydjx8qu5zdz9f2npes, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 6781624 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw36gve5zdf4gccygv6zxgcnxwzrgv65x32rg4zrxv34g9prvs2pxqmnzve5xvuns33czq9awp, validator: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu }, amount: Amount { raw: 123577 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gvmrzsf58yurxsjxgfqnqv6yg56nwv69xv6yv3zpx9znv3jpg4p5zdpnxpznzv3hq7q2az, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 1515359 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 9136180 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw368yenjvpjxcu5vv33x3zrqw2zgg6nsvzrx9prxd2pgsmyxwfjxgunvs3exerrydp3csdkvr, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 190090 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw368pz5zd3sgeqnxve4g9ryv3zzggerqdf3xqmrywfng4zrs3pkx5enydesg5mr2v6p4v8rst, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 2817512 } }, NextEpoch, Bond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 5207922 } }, Bond { id: BondId { source: Implicit: atest1d9khqw36x5uyvv2px4pr2d3cgdpry3zzxq6nsd6yg5mnwsjzgcervdpegsunqd3kgy6ygvpjyvyhzj, validator: Established: atest1v4ehgw368qcrqd2ygvmyyvf4g9qnvv3kxucrwv3hxg6ryve4x56r233cxucnysjrxsmygdj9yer4pz }, amount: Amount { raw: 70961 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gdzns33sgsmr2wz9x4rrxdenx3zyysfcxcmry32pgeznjw2zx4zrysjxgeryxsfc2etu33, validator: Established: atest1v4ehgw36ggcrz3zygyunqsfjggmnq33h8ycnsdphxepnsve4gerrss2pgfp5z3psgccrj33klenl5r }, amount: Amount { raw: 9056961 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gvmrzsf58yurxsjxgfqnqv6yg56nwv69xv6yv3zpx9znv3jpg4p5zdpnxpznzv3hq7q2az, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 1451932 } }, Bond { id: BondId { source: Implicit: atest1d9khqw36gcunwdzyxpz5xs2rxuuyxvfcgfznzd3hg9zrzdfnx5crwv69ggcnvsjpgc65gd33uuymj8, validator: Established: atest1v4ehgw36xucy2dfcxdzrxvpjx5uygwzrxpzrjs3jx4p5vvjrxdq5yvpjx5e5zs3jxdqng3pcplv2ch }, amount: Amount { raw: 1463719 } }, Withdraw { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 } }, Bond { id: BondId { source: Implicit: atest1d9khqw36x5uyvv2px4pr2d3cgdpry3zzxq6nsd6yg5mnwsjzgcervdpegsunqd3kgy6ygvpjyvyhzj, validator: Established: atest1v4ehgw368qcrqd2ygvmyyvf4g9qnvv3kxucrwv3hxg6ryve4x56r233cxucnysjrxsmygdj9yer4pz }, amount: Amount { raw: 792907 } }, InitValidator { address: Established: atest1v4ehgw36xy65xd3cgvcyxsesgsunys3hgg6nyvekxgerz3fjxaprqvfhxser2wphg5mnjdzpf7edt5, consensus_key: Ed25519(PublicKey(VerificationKey("8f6eeade76a7ce1ccf1d3138807774696d51fcf2c8879e53aa2b082e34eec42b"))), commission_rate: Dec(592790.000000), max_commission_rate_change: Dec(854710.000000) }]) diff --git a/proof_of_stake/src/tests.rs b/proof_of_stake/src/tests.rs index ac0ab22c09..d31011d2fb 100644 --- a/proof_of_stake/src/tests.rs +++ b/proof_of_stake/src/tests.rs @@ -1007,20 +1007,13 @@ fn test_slashes_with_unbonding_aux( .rate; println!("Slash 0 rate {slash_rate_0}, slash 1 {slash_rate_1}"); - let expected_withdrawn_amount = Dec::one() - - Dec::from(slash_rate_1 * (Dec::one() - slash_rate_0) * unbond_amount); + let expected_withdrawn_amount = Dec::from((Dec::one() - Dec::from(slash_rate_1)) + * (Dec::one() - slash_rate_0) * unbond_amount); // Allow some rounding error, 1 NAMNAM per each slash - let rounding_error_tolerance = Uint::from(2); + let rounding_error_tolerance = Dec::new(2, NATIVE_MAX_DECIMAL_PLACES).unwrap(); assert!( dbg!( - (Amount::from_uint( - expected_withdrawn_amount.abs(), - NATIVE_MAX_DECIMAL_PLACES - ) - .unwrap() - .change() - - withdrawn_tokens.change()) - .abs() + expected_withdrawn_amount.abs_diff(&Dec::from(withdrawn_tokens)) ) <= rounding_error_tolerance ); From cb5a5475c85140dcf8cf4c6beeab86c9be43a54b Mon Sep 17 00:00:00 2001 From: satan Date: Thu, 15 Jun 2023 12:39:42 +0200 Subject: [PATCH 081/151] [fix]: Cleanup imports --- proof_of_stake/src/tests.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/proof_of_stake/src/tests.rs b/proof_of_stake/src/tests.rs index d31011d2fb..58707c71e9 100644 --- a/proof_of_stake/src/tests.rs +++ b/proof_of_stake/src/tests.rs @@ -19,8 +19,7 @@ use namada_core::types::key::testing::{ arb_common_keypair, common_sk_from_simple_seed, }; use namada_core::types::storage::{BlockHeight, Epoch}; -use namada_core::types::token::{Amount, NATIVE_MAX_DECIMAL_PLACES}; -use namada_core::types::uint::Uint; +use namada_core::types::token::NATIVE_MAX_DECIMAL_PLACES; use namada_core::types::{address, key, token}; use proptest::prelude::*; use proptest::test_runner::Config; From ade47862229358cda623135910410d7492469357 Mon Sep 17 00:00:00 2001 From: Murisi Tarusenga Date: Sat, 17 Jun 2023 20:03:09 +0200 Subject: [PATCH 082/151] Increased the number of init-proposal and vote-proposal test vectors. Moved init-proposal content into extra data. --- apps/src/lib/client/tx.rs | 16 +- .../lib/node/ledger/shell/finalize_block.rs | 4 +- core/src/ledger/storage_api/governance.rs | 3 +- core/src/types/transaction/governance.rs | 11 +- scripts/generator.sh | 251 ++++++++++++++++-- shared/src/ledger/signing.rs | 21 +- wasm/checksums.json | 38 +-- wasm/wasm_source/src/tx_init_proposal.rs | 14 +- wasm_for_tests/tx_memory_limit.wasm | Bin 395402 -> 391565 bytes wasm_for_tests/tx_mint_tokens.wasm | Bin 479096 -> 473128 bytes wasm_for_tests/tx_no_op.wasm | Bin 352006 -> 348309 bytes wasm_for_tests/tx_proposal_code.wasm | Bin 421303 -> 414982 bytes wasm_for_tests/tx_read_storage_key.wasm | Bin 400179 -> 398557 bytes wasm_for_tests/tx_write.wasm | Bin 403604 -> 402105 bytes wasm_for_tests/tx_write_storage_key.wasm | Bin 465585 -> 465585 bytes wasm_for_tests/vp_always_false.wasm | Bin 374173 -> 370245 bytes wasm_for_tests/vp_always_true.wasm | Bin 374174 -> 370245 bytes wasm_for_tests/vp_eval.wasm | Bin 433069 -> 430283 bytes wasm_for_tests/vp_memory_limit.wasm | Bin 411863 -> 407739 bytes wasm_for_tests/vp_read_storage_key.wasm | Bin 419348 -> 415058 bytes 20 files changed, 281 insertions(+), 77 deletions(-) diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 20bcf7f83b..e037744b25 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -541,8 +541,8 @@ pub async fn submit_init_proposal( Ok(()) } else { let signer = ctx.get(&signer); - let tx_data: Result = proposal.clone().try_into(); - let init_proposal_data = if let Ok(data) = tx_data { + let tx_data: Result<(InitProposalData, Vec), _> = proposal.clone().try_into(); + let (mut init_proposal_data, init_proposal_content) = if let Ok(data) = tx_data { data } else { eprintln!("Invalid data for init proposal transaction."); @@ -563,7 +563,7 @@ pub async fn submit_init_proposal( safe_exit(1); } - if init_proposal_data.content.len() + if init_proposal_content.len() > governance_parameters.max_proposal_content_size as usize { eprintln!("Proposal content size too big.",); @@ -571,14 +571,18 @@ pub async fn submit_init_proposal( } let mut tx = Tx::new(TxType::Raw); - let data = init_proposal_data - .try_to_vec() - .expect("Encoding proposal data shouldn't fail"); let tx_code_hash = query_wasm_code_hash(client, args::TX_INIT_PROPOSAL) .await .unwrap(); tx.header.chain_id = ctx.config.ledger.chain_id.clone(); tx.header.expiration = args.tx.expiration; + let content_sec = tx.add_section(Section::ExtraData(Code::new(init_proposal_content))); + let content_sec_hash = + Hash(content_sec.hash(&mut Sha256::new()).finalize_reset().into()); + init_proposal_data.content = content_sec_hash; + let data = init_proposal_data + .try_to_vec() + .expect("Encoding proposal data shouldn't fail"); tx.set_data(Data::new(data)); tx.set_code(Code::from_hash(tx_code_hash)); diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index e992544051..ff51c3d49d 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -916,6 +916,7 @@ mod test_finalize_block { validator_consensus_key_handle, validator_rewards_products_handle, validator_slashes_handle, validator_state_handle, write_pos_params, }; + use namada::types::hash::Hash; use namada::proto::{Code, Data, Section, Signature}; use namada::types::governance::ProposalVote; use namada::types::key::tm_consensus_key_raw_hash; @@ -1301,7 +1302,7 @@ mod test_finalize_block { let proposal = InitProposalData { id: Some(proposal_id), - content: vec![], + content: Hash::default(), author: validator.clone(), voting_start_epoch: Epoch::default(), voting_end_epoch: Epoch::default().next(), @@ -1312,6 +1313,7 @@ mod test_finalize_block { storage_api::governance::init_proposal( &mut shell.wl_storage, proposal, + vec![], ) .unwrap(); diff --git a/core/src/ledger/storage_api/governance.rs b/core/src/ledger/storage_api/governance.rs index b71f4a6e40..d7ea0c8fd2 100644 --- a/core/src/ledger/storage_api/governance.rs +++ b/core/src/ledger/storage_api/governance.rs @@ -11,6 +11,7 @@ use crate::types::transaction::governance::{ pub fn init_proposal( storage: &mut S, data: InitProposalData, + content: Vec, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, @@ -23,7 +24,7 @@ where }; let content_key = storage::get_content_key(proposal_id); - storage.write_bytes(&content_key, data.content)?; + storage.write_bytes(&content_key, content)?; let author_key = storage::get_author_key(proposal_id); storage.write(&author_key, data.author.clone())?; diff --git a/core/src/types/transaction/governance.rs b/core/src/types/transaction/governance.rs index 3b1f183eaa..dfd71fdae5 100644 --- a/core/src/types/transaction/governance.rs +++ b/core/src/types/transaction/governance.rs @@ -7,6 +7,7 @@ use crate::types::address::Address; use crate::types::governance::{ self, Proposal, ProposalError, ProposalVote, VoteType, }; +use crate::types::hash::Hash; use crate::types::storage::Epoch; /// The type of a Proposal @@ -89,7 +90,7 @@ pub struct InitProposalData { /// The proposal id pub id: Option, /// The proposal content - pub content: Vec, + pub content: Hash, /// The proposal author address pub author: Address, /// The proposal type @@ -123,18 +124,18 @@ pub struct VoteProposalData { pub delegations: Vec
, } -impl TryFrom for InitProposalData { +impl TryFrom for (InitProposalData, Vec) { type Error = ProposalError; fn try_from(proposal: Proposal) -> Result { - Ok(InitProposalData { + Ok((InitProposalData { id: proposal.id, - content: proposal.content.try_to_vec().unwrap(), + content: Hash::default(), author: proposal.author, r#type: proposal.r#type.try_into()?, voting_start_epoch: proposal.voting_start_epoch, voting_end_epoch: proposal.voting_end_epoch, grace_epoch: proposal.grace_epoch, - }) + }, proposal.content.try_to_vec().unwrap())) } } diff --git a/scripts/generator.sh b/scripts/generator.sh index 9c8a2de352..89ef6cc43a 100755 --- a/scripts/generator.sh +++ b/scripts/generator.sh @@ -12,26 +12,6 @@ NAMADA_DIR="$(pwd)" export NAMADA_LEDGER_LOG_PATH="$(pwd)/vectors.json" export NAMADA_TX_LOG_PATH="$(pwd)/debugs.txt" -echo '{ - "content": { - "title": "TheTitle", - "authors": "test@test.com", - "discussions-to": "www.github.com/anoma/aip/1", - "created": "2022-03-10T08:54:37Z", - "license": "MIT", - "abstract": "Ut convallis eleifend orci vel venenatis. Duis vulputate metus in lacus sollicitudin vestibulum. Suspendisse vel velit ac est consectetur feugiat nec ac urna. Ut faucibus ex nec dictum fermentum. Morbi aliquet purus at sollicitudin ultrices. Quisque viverra varius cursus. Praesent sed mauris gravida, pharetra turpis non, gravida eros. Nullam sed ex justo. Ut at placerat ipsum, sit amet rhoncus libero. Sed blandit non purus non suscipit. Phasellus sed quam nec augue bibendum bibendum ut vitae urna. Sed odio diam, ornare nec sapien eget, congue viverra enim.", - "motivation": "Ut convallis eleifend orci vel venenatis. Duis vulputate metus in lacus sollicitudin vestibulum. Suspendisse vel velit ac est consectetur feugiat nec ac urna. Ut faucibus ex nec dictum fermentum. Morbi aliquet purus at sollicitudin ultrices.", - "details": "Ut convallis eleifend orci vel venenatis. Duis vulputate metus in lacus sollicitudin vestibulum. Suspendisse vel velit ac est consectetur feugiat nec ac urna. Ut faucibus ex nec dictum fermentum. Morbi aliquet purus at sollicitudin ultrices. Quisque viverra varius cursus. Praesent sed mauris gravida, pharetra turpis non, gravida eros.", - "requires": "2" - }, - "author": "atest1v4ehgw36xvcyyvejgvenxs34g3zygv3jxqunjd6rxyeyys3sxy6rwvfkx4qnj33hg9qnvse4lsfctw", - "voting_start_epoch": 12, - "voting_end_epoch": 24, - "grace_epoch": 30, - "type": { "Default": "'"$NAMADA_DIR"'/wasm_for_tests/tx_no_op.wasm" } -} -' > valid_proposal.json - if [ "$#" -ne 1 ]; then echo "Illegal number of parameters" elif [ "$1" = "server" ]; then @@ -56,6 +36,225 @@ elif [ "$1" = "client" ]; then echo > $NAMADA_TX_LOG_PATH echo $'[' > $NAMADA_LEDGER_LOG_PATH + + ALBERT_ADDRESS=$(cargo run --bin namadaw -- address find --alias albert | sed 's/^Found address Established: //') + + echo '{ + "author":"'$ALBERT_ADDRESS'", + "content":{ + "abstract":"Ut convallis eleifend orci vel venenatis. Duis vulputate metus in lacus sollicitudin vestibulum. Suspendisse vel velit ac est consectetur feugiat nec ac urna. Ut faucibus ex nec dictum fermentum. Morbi aliquet purus at sollicitudin ultrices. Quisque viverra varius cursus. Praesent sed mauris gravida, pharetra turpis non, gravida eros. Nullam sed ex justo. Ut at placerat ipsum, sit amet rhoncus libero. Sed blandit non purus non suscipit. Phasellus sed quam nec augue bibendum bibendum ut vitae urna. Sed odio diam, ornare nec sapien eget, congue viverra enim.", + "authors":"test@test.com", + "created":"2022-03-10T08:54:37Z", + "details":"Ut convallis eleifend orci vel venenatis. Duis vulputate metus in lacus sollicitudin vestibulum. Suspendisse vel velit ac est consectetur feugiat nec ac urna. Ut faucibus ex nec dictum fermentum. Morbi aliquet purus at sollicitudin ultrices. Quisque viverra varius cursus. Praesent sed mauris gravida, pharetra turpis non, gravida eros.", + "discussions-to":"www.github.com/anoma/aip/1", + "license":"MIT", + "motivation":"Ut convallis eleifend orci vel venenatis. Duis vulputate metus in lacus sollicitudin vestibulum. Suspendisse vel velit ac est consectetur feugiat nec ac urna. Ut faucibus ex nec dictum fermentum. Morbi aliquet purus at sollicitudin ultrices.", + "requires":"2", + "title":"TheTitle" + }, + "grace_epoch":30, + "type":{ + "Default":"'$NAMADA_DIR'/wasm_for_tests/tx_proposal_code.wasm" + }, + "voting_end_epoch":24, + "voting_start_epoch":12 +} +' > proposal_submission_valid_proposal.json + + echo '{ + "content": { + "abstract": "Ut convallis eleifend orci vel venenatis. Duis vulputate metus in lacus sollicitudin vestibulum. Suspendisse vel velit ac est consectetur feugiat nec ac urna. Ut faucibus ex nec dictum fermentum. Morbi aliquet purus at sollicitudin ultrices. Quisque viverra varius cursus. Praesent sed mauris gravida, pharetra turpis non, gravida eros. Nullam sed ex justo. Ut at placerat ipsum, sit amet rhoncus libero. Sed blandit non purus non suscipit. Phasellus sed quam nec augue bibendum bibendum ut vitae urna. Sed odio diam, ornare nec sapien eget, congue viverra enim.", + "authors": "test@test.com", + "created": "2022-03-10T08:54:37Z", + "details": "Ut convallis eleifend orci vel venenatis. Duis vulputate metus in lacus sollicitudin vestibulum. Suspendisse vel velit ac est consectetur feugiat nec ac urna. Ut faucibus ex nec dictum fermentum. Morbi aliquet purus at sollicitudin ultrices. Quisque viverra varius cursus. Praesent sed mauris gravida, pharetra turpis non, gravida eros.", + "discussions-to": "www.github.com/anoma/aip/1", + "license": "MIT", + "motivation": "Ut convallis eleifend orci vel venenatis. Duis vulputate metus in lacus sollicitudin vestibulum. Suspendisse vel velit ac est consectetur feugiat nec ac urna. Ut faucibus ex nec dictum fermentum. Morbi aliquet purus at sollicitudin ultrices.", + "requires": "2", + "title": "TheTitle" + }, + "author": "'$ALBERT_ADDRESS'", + "tally_epoch": 18, + "signature": { + "Ed25519": { + "R_bytes": [ + 113, + 196, + 231, + 134, + 101, + 191, + 75, + 17, + 245, + 19, + 50, + 231, + 183, + 80, + 162, + 38, + 108, + 72, + 72, + 2, + 116, + 112, + 121, + 33, + 197, + 67, + 64, + 116, + 21, + 250, + 196, + 121 + ], + "s_bytes": [ + 87, + 163, + 134, + 87, + 42, + 156, + 121, + 211, + 189, + 19, + 255, + 5, + 23, + 178, + 143, + 39, + 118, + 249, + 37, + 53, + 121, + 136, + 59, + 103, + 190, + 91, + 121, + 95, + 46, + 54, + 168, + 9 + ] + } + }, + "address": "'$ALBERT_ADDRESS'" +} +' > proposal_offline_proposal + + echo '{ + "author":"'$ALBERT_ADDRESS'", + "content":{ + "abstract":"Ut convallis eleifend orci vel venenatis. Duis vulputate metus in lacus sollicitudin vestibulum. Suspendisse vel velit ac est consectetur feugiat nec ac urna. Ut faucibus ex nec dictum fermentum. Morbi aliquet purus at sollicitudin ultrices. Quisque viverra varius cursus. Praesent sed mauris gravida, pharetra turpis non, gravida eros. Nullam sed ex justo. Ut at placerat ipsum, sit amet rhoncus libero. Sed blandit non purus non suscipit. Phasellus sed quam nec augue bibendum bibendum ut vitae urna. Sed odio diam, ornare nec sapien eget, congue viverra enim.", + "authors":"test@test.com", + "created":"2022-03-10T08:54:37Z", + "details":"Ut convallis eleifend orci vel venenatis. Duis vulputate metus in lacus sollicitudin vestibulum. Suspendisse vel velit ac est consectetur feugiat nec ac urna. Ut faucibus ex nec dictum fermentum. Morbi aliquet purus at sollicitudin ultrices. Quisque viverra varius cursus. Praesent sed mauris gravida, pharetra turpis non, gravida eros.", + "discussions-to":"www.github.com/anoma/aip/1", + "license":"MIT", + "motivation":"Ut convallis eleifend orci vel venenatis. Duis vulputate metus in lacus sollicitudin vestibulum. Suspendisse vel velit ac est consectetur feugiat nec ac urna. Ut faucibus ex nec dictum fermentum. Morbi aliquet purus at sollicitudin ultrices.", + "requires":"2", + "title":"TheTitle" + }, + "grace_epoch":18, + "type":{ + "Default":null + }, + "voting_end_epoch":9, + "voting_start_epoch":3 +}' > proposal_offline_valid_proposal.json + + echo '{ + "author":"'$ALBERT_ADDRESS'", + "content":{ + "abstract":"Ut convallis eleifend orci vel venenatis. Duis vulputate metus in lacus sollicitudin vestibulum. Suspendisse vel velit ac est consectetur feugiat nec ac urna. Ut faucibus ex nec dictum fermentum. Morbi aliquet purus at sollicitudin ultrices. Quisque viverra varius cursus. Praesent sed mauris gravida, pharetra turpis non, gravida eros. Nullam sed ex justo. Ut at placerat ipsum, sit amet rhoncus libero. Sed blandit non purus non suscipit. Phasellus sed quam nec augue bibendum bibendum ut vitae urna. Sed odio diam, ornare nec sapien eget, congue viverra enim.", + "authors":"test@test.com", + "created":"2022-03-10T08:54:37Z", + "details":"Ut convallis eleifend orci vel venenatis. Duis vulputate metus in lacus sollicitudin vestibulum. Suspendisse vel velit ac est consectetur feugiat nec ac urna. Ut faucibus ex nec dictum fermentum. Morbi aliquet purus at sollicitudin ultrices. Quisque viverra varius cursus. Praesent sed mauris gravida, pharetra turpis non, gravida eros.", + "discussions-to":"www.github.com/anoma/aip/1", + "license":"MIT", + "motivation":"Ut convallis eleifend orci vel venenatis. Duis vulputate metus in lacus sollicitudin vestibulum. Suspendisse vel velit ac est consectetur feugiat nec ac urna. Ut faucibus ex nec dictum fermentum. Morbi aliquet purus at sollicitudin ultrices.", + "requires":"2", + "title":"TheTitle" + }, + "grace_epoch":30, + "type":"ETHBridge", + "voting_end_epoch":24, + "voting_start_epoch":12 +}' > eth_governance_proposal_valid_proposal.json + + echo '{ + "author":"'$ALBERT_ADDRESS'", + "content":{ + "abstract":"Ut convallis eleifend orci vel venenatis. Duis vulputate metus in lacus sollicitudin vestibulum. Suspendisse vel velit ac est consectetur feugiat nec ac urna. Ut faucibus ex nec dictum fermentum. Morbi aliquet purus at sollicitudin ultrices. Quisque viverra varius cursus. Praesent sed mauris gravida, pharetra turpis non, gravida eros. Nullam sed ex justo. Ut at placerat ipsum, sit amet rhoncus libero. Sed blandit non purus non suscipit. Phasellus sed quam nec augue bibendum bibendum ut vitae urna. Sed odio diam, ornare nec sapien eget, congue viverra enim.", + "authors":"test@test.com", + "created":"2022-03-10T08:54:37Z", + "details":"Ut convallis eleifend orci vel venenatis. Duis vulputate metus in lacus sollicitudin vestibulum. Suspendisse vel velit ac est consectetur feugiat nec ac urna. Ut faucibus ex nec dictum fermentum. Morbi aliquet purus at sollicitudin ultrices. Quisque viverra varius cursus. Praesent sed mauris gravida, pharetra turpis non, gravida eros.", + "discussions-to":"www.github.com/anoma/aip/1", + "license":"MIT", + "motivation":"Ut convallis eleifend orci vel venenatis. Duis vulputate metus in lacus sollicitudin vestibulum. Suspendisse vel velit ac est consectetur feugiat nec ac urna. Ut faucibus ex nec dictum fermentum. Morbi aliquet purus at sollicitudin ultrices.", + "requires":"2", + "title":"TheTitle" + }, + "grace_epoch":30, + "type":"PGFCouncil", + "voting_end_epoch":24, + "voting_start_epoch":12 +}' > pgf_governance_proposal_valid_proposal.json + + # proposal_submission + + cargo run --bin namadac --features std -- --mode full bond --validator validator-0 --source Bertha --amount 900 --gas-amount 0 --gas-limit 0 --gas-token NAM --node 127.0.0.1:27657 + + cargo run --bin namadac --features std -- --mode full init-proposal --force --data-path proposal_submission_valid_proposal.json --node 127.0.0.1:27657 + + cargo run --bin namadac --features std -- --base-dir $NAMADA_BASE_DIR/setup/validator-0/.namada --mode validator vote-proposal --proposal-id 0 --vote yay --signer validator-0 --node 127.0.0.1:27657 + + cargo run --bin namadac --features std -- --mode full vote-proposal --proposal-id 0 --vote nay --signer Bertha --node 127.0.0.1:27657 + + cargo run --bin namadac --features std -- --mode full vote-proposal --proposal-id 0 --vote yay --signer Albert --node 127.0.0.1:27657 + + # proposal_offline + + cargo run --bin namadac --features std -- --mode full bond --validator validator-0 --source Albert --amount 900 --gas-amount 0 --gas-limit 0 --gas-token NAM --node 127.0.0.1:27657 + + cargo run --bin namadac --features std -- --mode full init-proposal --force --data-path proposal_offline_valid_proposal.json --offline --node 127.0.0.1:27657 + + cargo run --bin namadac --features std -- --mode full vote-proposal --data-path proposal_offline_proposal --vote yay --signer Albert --offline --node 127.0.0.1:27657 + + # eth_governance_proposal + + cargo run --bin namadac --features std -- --mode full bond --validator validator-0 --source Bertha --amount 900 --gas-amount 0 --gas-limit 0 --gas-token NAM --ledger-address 127.0.0.1:27657 + + cargo run --bin namadac --features std -- --mode full init-proposal --force --data-path eth_governance_proposal_valid_proposal.json --ledger-address 127.0.0.1:27657 + + cargo run --bin namadac --features std -- --mode full vote-proposal --proposal-id 0 --vote yay --eth '011586062748ba53bc53155e817ec1ea708de75878dcb9a5713bf6986d87fe14e7 fd34672ab5' --signer Bertha --ledger-address 127.0.0.1:27657 + + cargo run --bin namadac --features std -- --base-dir $NAMADA_BASE_DIR/setup/validator-0/.namada --mode validator vote-proposal --proposal-id 0 --vote yay --eth '011586062748ba53bc53155e817ec1ea708de75878dcb9a5713bf6986d87fe14e7 fd34672ab5' --signer validator-0 --ledger-address 127.0.0.1:27657 + + # pgf_governance_proposal + + cargo run --bin namadac --features std -- --mode full bond --validator validator-0 --source Bertha --amount 900 --gas-amount 0 --gas-limit 0 --gas-token NAM --ledger-address 127.0.0.1:27657 + + cargo run --bin namadac --features std -- --mode full init-proposal --force --data-path pgf_governance_proposal_valid_proposal.json --ledger-address 127.0.0.1:27657 + + cargo run --bin namadac --features std -- --mode full init-proposal --force --data-path pgf_governance_proposal_valid_proposal.json --ledger-address 127.0.0.1:27657 + + cargo run --bin namadac --features std -- --base-dir $NAMADA_BASE_DIR/setup/validator-0/.namada --mode validator vote-proposal --proposal-id 0 --vote yay --pgf "$ALBERT_ADDRESS 1000" --signer validator-0 --ledger-address 127.0.0.1:27657 + + cargo run --bin namadac --features std -- --mode full vote-proposal --proposal-id 0 --vote yay --pgf "$ALBERT_ADDRESS 900" --signer Bertha --ledger-address 127.0.0.1:27657 + + cargo run --bin namadac --features std -- --mode full vote-proposal --proposal-id 1 --vote yay --pgf "$ALBERT_ADDRESS 900" --signer Bertha --ledger-address 127.0.0.1:27657 + + # non-proposal tests cargo run --bin namadac --features std -- transfer --source bertha --target christel --token btc --amount 23 --force --signing-key bertha-key --ledger-address 127.0.0.1:27657 @@ -77,8 +276,6 @@ elif [ "$1" = "client" ]; then cargo run --bin namadac --features std -- ibc-transfer --source bertha --receiver christel --token btc --amount 24 --channel-id channel-141 --signing-key bertha-key --force --ledger-address 127.0.0.1:27657 - #cargo run --bin namadac -- init-proposal --data-path valid_proposal.json --epoch 2 --force --signing-key bertha-key --ledger-address 127.0.0.1:27657 - cargo run --bin namadaw -- masp add --alias a_spending_key --value xsktest1qqqqqqqqqqqqqq9v0sls5r5de7njx8ehu49pqgmqr9ygelg87l5x8y4s9r0pjlvu69au6gn3su5ewneas486hdccyayx32hxvt64p3d0hfuprpgcgv2q9gdx3jvxrn02f0nnp3jtdd6f5vwscfuyum083cvfv4jun75ak5sdgrm2pthzj3sflxc0jx0edrakx3vdcngrfjmru8ywkguru8mxss2uuqxdlglaz6undx5h8w7g70t2es850g48xzdkqay5qs0yw06rtxcvedhsv --unsafe-dont-encrypt cargo run --bin namadaw -- masp add --alias b_spending_key --value xsktest1qqqqqqqqqqqqqqpagte43rsza46v55dlz8cffahv0fnr6eqacvnrkyuf9lmndgal7c2k4r7f7zu2yr5rjwr374unjjeuzrh6mquzy6grfdcnnu5clzaq2llqhr70a8yyx0p62aajqvrqjxrht3myuyypsvm725uyt5vm0fqzrzuuedtf6fala4r4nnazm9y9hq5yu6pq24arjskmpv4mdgfn3spffxxv8ugvym36kmnj45jcvvmm227vqjm5fq8882yhjsq97p7xrwqqd82s0 --unsafe-dont-encrypt @@ -101,6 +298,16 @@ elif [ "$1" = "client" ]; then cargo run --bin namadac --features std -- --mode full transfer --source b_spending_key --target bb_payment_address --token btc --amount 6 --force --ledger-address 127.0.0.1:27657 + rm proposal_submission_valid_proposal.json + + rm proposal_offline_proposal + + rm proposal_offline_valid_proposal.json + + rm eth_governance_proposal_valid_proposal.json + + rm pgf_governance_proposal_valid_proposal.json + perl -0777 -i.original -pe 's/,\s*$//igs' $NAMADA_LEDGER_LOG_PATH echo $'\n]' >> $NAMADA_LEDGER_LOG_PATH diff --git a/shared/src/ledger/signing.rs b/shared/src/ledger/signing.rs index 864ffcef2e..635207ca8c 100644 --- a/shared/src/ledger/signing.rs +++ b/shared/src/ledger/signing.rs @@ -1,5 +1,5 @@ //! Functions to sign transactions -use std::collections::{BTreeMap, HashMap}; +use std::collections::HashMap; #[cfg(feature = "std")] use std::env; #[cfg(feature = "std")] @@ -684,15 +684,7 @@ pub async fn to_ledger_vector< ), format!("Grace epoch : {}", init_proposal_data.grace_epoch), ]); - let content: BTreeMap = - BorshDeserialize::try_from_slice(&init_proposal_data.content)?; - if !content.is_empty() { - for (key, value) in &content { - tv.output.push(format!("Content {} : {}", key, value)); - } - } else { - tv.output.push("Content : (none)".to_string()); - } + tv.output.push(format!("Content: {}", init_proposal_data.content)); tv.output_expert.extend(vec![ format!("ID : {}", init_proposal_data_id), @@ -707,14 +699,7 @@ pub async fn to_ledger_vector< ), format!("Grace epoch : {}", init_proposal_data.grace_epoch), ]); - if !content.is_empty() { - for (key, value) in content { - tv.output_expert - .push(format!("Content {} : {}", key, value)); - } - } else { - tv.output_expert.push("Content : none".to_string()); - } + tv.output.push(format!("Content: {}", init_proposal_data.content)); } else if code_hash == vote_proposal_hash { let vote_proposal = VoteProposalData::try_from_slice(&tx.data().ok_or_else(|| { diff --git a/wasm/checksums.json b/wasm/checksums.json index 718af73cca..beddffc7db 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,21 +1,21 @@ { - "tx_bond.wasm": "tx_bond.9d034041d7085d261e2d75e548b74e0635b7c87f14173aecddbd64d8a8646d58.wasm", - "tx_change_validator_commission.wasm": "tx_change_validator_commission.5d172f38a22a045902e83d10ebdcaef88f75a0511dc5c3e224ef89d7fc594b78.wasm", - "tx_ibc.wasm": "tx_ibc.462075ed66348d74f60a7af04d5200acff673ddb1a1d16497fbc97ee846e1851.wasm", - "tx_init_account.wasm": "tx_init_account.7e827fb86331b0b62eb1162f917e522f6f426df9608d67c13caed089d63cd25f.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.4fe5500d95d040ca3f2e51446bfb96cfc99596302e4e2368ee599f1a610f5968.wasm", - "tx_init_validator.wasm": "tx_init_validator.a6db44f07090f6c19dea71f1865f6acba1a4a946c29057231312188dfbfd1c9e.wasm", - "tx_reveal_pk.wasm": "tx_reveal_pk.3bb71bb884737ab184b4f543b503e512431d0e8cad24d202981c06063a11614c.wasm", - "tx_transfer.wasm": "tx_transfer.8c24cc4bb4e947a7fab4708039300cfa36b0513db55e6ca45b1d7276db1eb02c.wasm", - "tx_unbond.wasm": "tx_unbond.99aacaa049edea0704a147d2feb79702fbae8a014092f41b8483daeff5eee181.wasm", - "tx_unjail_validator.wasm": "tx_unjail_validator.d9fe28aadc5d21d9d0c8e89365e5e8e68420c8f5c15c5c12c8587120822b9ceb.wasm", - "tx_update_vp.wasm": "tx_update_vp.c5706b7f5223deb15f2fae1646ee487948dd0ac25450ca460d9dfa55f29ab2c5.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.df21ee966e13f9c5731deea7c8a2ed62612d4706691b35564b058ed175b476ef.wasm", - "tx_withdraw.wasm": "tx_withdraw.cbb043216f2fc75b88a32bd3fbad8a05b48df4c3d6bdbc8284606a71b9be9d38.wasm", - "vp_implicit.wasm": "vp_implicit.c3b78b8b0bb9072a9a4c9e2aa1e89e04db9fdc41eecd53850698bfea89631103.wasm", - "vp_masp.wasm": "vp_masp.8c4ea33db73f13ad9c6efd1634780fd872e7acadb5a615e96f4b2cbbf8626463.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.403d3d09e582968305223e66a0c35c7b91fd62ac9bc07bab32250bc9120d2114.wasm", - "vp_token.wasm": "vp_token.d1f3cbfd1fccc432e9070b3b94ce1fe21798dc41c557c9201d58d52a02c61337.wasm", - "vp_user.wasm": "vp_user.7514d52275b0d2cb403db04f62656148dbb8570c47d2ec5bd21bb53fa45987a7.wasm", - "vp_validator.wasm": "vp_validator.02eab5750ce4773c2bd350cc642f39aa5d50e58aaa2a2974313329a138335566.wasm" + "tx_bond.wasm": "tx_bond.5825abd216c8af2fd745b43a778e5be800c0577c0e440ff5ff0f5c9f07af9cfa.wasm", + "tx_change_validator_commission.wasm": "tx_change_validator_commission.6a7f3eb89e8785967e444c10b470e3a61c02c940ab32ae44a51ec1c92cd714f1.wasm", + "tx_ibc.wasm": "tx_ibc.d65db46f582a9f74cd0858642ed10d280ebbd911591716a8c54743cbe5cee111.wasm", + "tx_init_account.wasm": "tx_init_account.1e5d5e73ecee4c02ba1bec8f1c50c4cdfa2a934c4e7c91fcfa594d66812b16b9.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.6a713fa141b6258d65e9799d16bfe466c581e1a0825957b30a88d2d5cebe00ae.wasm", + "tx_init_validator.wasm": "tx_init_validator.32f4abbbe427e3b9d99d69ce86e7937f35221bb396d2147a1febe0c35c632ed1.wasm", + "tx_reveal_pk.wasm": "tx_reveal_pk.920523458b8f18291f343aacbc25ed557d06cef3e26f5ce71a3e1395e5a88a2e.wasm", + "tx_transfer.wasm": "tx_transfer.acda70165297050221b352c77c01f7b8466b3f1af677bf1bf6e862765ab2943f.wasm", + "tx_unbond.wasm": "tx_unbond.91636a6fe7b4b7089ec054f5a3cda101a67eafb3a6bc2d3c6efee5a8483bdacb.wasm", + "tx_unjail_validator.wasm": "tx_unjail_validator.b06a7b9bf7f3b59520f45e8a631be6bd261c8cb7188694eae0b4d5bde691d65c.wasm", + "tx_update_vp.wasm": "tx_update_vp.59fd628215637f846b08a13fdb6e02b9f529a0bfde1ebfafbaadc8ab2a742b91.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.d6f2e729a99fd7ea1a20d79e6f03fc9881bb1a070cac0d176df1bf995dd7b623.wasm", + "tx_withdraw.wasm": "tx_withdraw.0c4df1acdfb5cd22be594a991f33e27495e22879d1f4a2accf71d778487b60e7.wasm", + "vp_implicit.wasm": "vp_implicit.a64bea341d993b015128d5bc353a54b0ab5e2068570eaf68c199255793605591.wasm", + "vp_masp.wasm": "vp_masp.eacacbcdc94c8028f805673819cf82b0cd45acabc7a9e4724d71ccf6560bb23c.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.73b81491707e224e2782febea3f6f7226557c3361735d65c3286bdf354c3a799.wasm", + "vp_token.wasm": "vp_token.df23e798f7df11cc6526ec25a7f8503bac2a5427e0adfaf63c95a0f8af493b46.wasm", + "vp_user.wasm": "vp_user.069581f25be255ca4af3ead226d518588370e7451b687eb20b70f076e411a71c.wasm", + "vp_validator.wasm": "vp_validator.71ab63e17a359d7fb449dc7b48d5e6c53e0fe003f570bb64050f1571d041eb0d.wasm" } \ No newline at end of file diff --git a/wasm/wasm_source/src/tx_init_proposal.rs b/wasm/wasm_source/src/tx_init_proposal.rs index 2507a18d21..21a895f081 100644 --- a/wasm/wasm_source/src/tx_init_proposal.rs +++ b/wasm/wasm_source/src/tx_init_proposal.rs @@ -3,13 +3,17 @@ use namada_tx_prelude::*; #[transaction] -fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { - let signed = tx_data; - let data = signed.data().ok_or_err_msg("Missing data")?; +fn apply_tx(ctx: &mut Ctx, tx: Tx) -> TxResult { + let data = tx.data().ok_or_err_msg("Missing data")?; let tx_data = transaction::governance::InitProposalData::try_from_slice(&data[..]) - .wrap_err("failed to decode InitProposalData")?; + .wrap_err("failed to decode InitProposalData")?; + let content = tx + .get_section(&tx_data.content) + .ok_or_err_msg("Missing content")? + .extra_data() + .ok_or_err_msg("Missing full content")?; log_string("apply_tx called to create a new governance proposal"); - governance::init_proposal(ctx, tx_data) + governance::init_proposal(ctx, tx_data, content) } diff --git a/wasm_for_tests/tx_memory_limit.wasm b/wasm_for_tests/tx_memory_limit.wasm index 3e7a8f98f6df30e5aeb3d5357d64a705a0d3450d..9da6f76584a9b2260800478e723f482505482bbd 100755 GIT binary patch delta 137753 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 NUefZWS&}u{_J2jT70&g$D?@9H~Pp|j9>w}vq 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
n(t(hQ4bY-f!RG+QiKOnwD)#U{W>{7-tjcO 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 diff --git a/wasm_for_tests/tx_no_op.wasm b/wasm_for_tests/tx_no_op.wasm index 5bcb417863e93612f9d069e23dcb839571ec7d89..a5cf5b570fb03f0e9b4d29e4df1abaab21296d97 100755 GIT binary patch delta 116355 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 z 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 z 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 X#ct(VS3AC@t>89YOeP-4ndbaosVCV~ literal 421303 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 Iw&wi50i8TpQ2+n{ diff --git a/wasm_for_tests/tx_read_storage_key.wasm b/wasm_for_tests/tx_read_storage_key.wasm index 385ed891bdde707a973a5c60568da26112f7389c..b52cf65c7ec25e450da6aa8788b658e8e640d741 100755 GIT binary patch delta 115846 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 delta 118204 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 K?MA(9^}PUGoD*UI delta 59 zcmdnERc7N>nT8g|Ele#vocW4u3M`IK7;>lg_Apg4vQB63Wzu3~Yd7j;0%B$$W&vW> K?MA(9^}PUGr4wQR diff --git a/wasm_for_tests/vp_always_false.wasm b/wasm_for_tests/vp_always_false.wasm index 875dcc4a426bd95dc347b75d495c202615cfa450..be883b4c4564316081d7537d8254141ad2252389 100755 GIT binary patch delta 117818 zcmeFa3w%|@wLiXR_IW>&llSA~F?$mr0RjP)Kmf^(B0f-1(fUBeS``yif|S|{oFJf4 zQG*9NXwk+eQ9)5`K}C(NwNz2@O|M=}Tia4?HC9wqg#UNV-us*rz$&-5_x}37z$a(+ z%$`}ZX3d&4Yi8E0eariWzuH_F(ayf)LZ)dNdyJJViQyF zswWZ)wCS2QU1ddrADI>`po~6xq6d0~Ol$#LFkMFqa?v~dbMiZnK@_zAX~?$#Zx>7t zxJfAMRqt)UD5B}Fu$NevbEdI~?qxdH{2F73`=Yw8WpSPRIM@BUpNI79aLB_rV;cT^ zF{U$5ASXAd`x*Y}S`aUg?#rVn{&8JrET1P3aveZ8fHUsZ0gihS?eh<*^eUu#xCglq z;ap?fqZgnqLWeRCd=KY=02M_>PoPlM#JoY~^D-ZZUZiPWtvINAylfUb>@cs+l9I)O zZPz}fg%g_9mR!p!*oNebtUPfZ(<~NlsvqB&H`F?A^6^tvvRjyb8=LVcdd|C%Jtgq?!L?Vi``Hk2>M3j3 zYW7R^H2W2Ml0C(K!9ovpXT8M6GnVM5S)#?%sx+ZlWz8W=8_DVM;YGQ>44PM_O#YwhD6BH zCnrygtjIxXmXjJ2F-qMT`OMdTKbq1n`FZpV-wOM2T9(PJKypD=iB~WyaO}EUv#w*t z*2Pbu1w;*Mm0+Mrq#0y+O0zcOC)S-jHg?fL8F6OlVT(!$UdJi$)uSx+-JW`o|@Cnw=Q1yha4Z{DE5AC9YxFY zrhza=uDdM%5}kD>KP(!lAI(^DQ1L;9gk)?JDf+i-;g(QAPVRSe)1B{wCHAAM?eDuYL5NIo+93kS6!3bi4V z*FDwnYF@m{1ypS{wg58~2dKXX!%6E;h3lR11)Qdr`hx%j5V8qRN^KidXX4tHiK@W;OeQHGK*$YAm~Om9|#!7b7PBH zf}X0tV~^HsmWmSfya8E4NZWxl!v$qcT8m_2q+TSOspKxC(m36Q)P8O%)jPdj@AL=^ z2q)1aTwJ{(n0)fwq5)=qlyt3m1Mw^h@qMw=^*z1q7`}O#_`cHFy)^ z)tNOn*6Y+_;ezGX3f64qI$5X*9f;>SaYDWw@%&5>ZOp;mt~j9yk4V?KuiC6mz=%6_ zg3O3ziBixrB+@mf>)dy3E&)6#0(b=xHCc6je!#4;p(m%DUyz81Jv&(u+pWqY+^W1{ zud0YRRvT|N%TnleB332h3SDk(K&&8xTx$_8%pg}6x0=nOOnP;4MK7NaBC1$xRI`~a zbT^At*KEdwCl#%3Hj__Xm^WB}I^Z+Vs@!>}7!2_WC$3lv@yg`P8S%s*CzTqo1*wCb zxMBmuhd6OavKqY`eW;q+4fKms+>q_$35%HX6cIU@<%HAqE(JWoZF};A3$l~(3nC-T z+7vjT5AbQA7$1nIL1Sbfo|8Q5f`Vhy=%*3iH_B~#vpFu7X(LlFcJrf_X({ z@}moiN0`G>#limomj(jdi5TWeD>>g%plO!Z->o_y_s>hBVJp)32RfVk}rr`jD6m6@H`;|^dn9`sK) z3ynuSJvz{M#PgF+wSZ=^R0WtXfpxkX@EhVTkqOgHh*u_aW*#TZoK&%Nfpk{NR6xWS z;xxmR5nh42+9GsRDyj1%~WhpqmjA5JLMzK_nlVlNbL783@OJ_xI)B*bD#eE&NyGuP9jZ zkNEgsjlsV=d__SLJNq1l|9==||LPt(KtV-;*#Bi7QWVJfzf6Jukx@np)B!o=uC(^h z*uJR;6pv;fpy1bYkfKoS0SbKsg=jzNz$pALj>2ymrT^RS)fgCdpojl;9DYE`{BQI4 z|91U~18NS8v48Oxqs^!T^VHYOQ;G-sAE3|y4oPj-?y;Uw974Ow2gcAp8bewCmno!p zWa$583jDLWgrea8X&(9}3LcO{--<#9c<5`666p-%z$iPwL*GP!Z+d?~4t*;M9gsu+ zzm`L46o`LkjqjUVko@SbF>%+FQG?}W%9>Flo7rG;;@#!!+T`rJN00%BcVg?h)pz%2 z!!Exb2BT%xP6RQN43_ayhw;$L4kYw*63BXOsYq&NN&9$gGHXEk8*~cUwU;QKA z`{U&3HD^(9^O{No@}uWizHj@@sBA#;wKX-79aqsKfa_LIGIalW`HO#&N?4{_@7g(Q zpB=+@-m~th`zJEj?0m=7$Y&-C)(&CS$)>fxD_DX0$(UNRDo1h;f*m(>YsrW{AZW1| zEYolIgkZdzpSkf_fyxy%@)z-V$ zsJ4iQ%k#)+d?YUxyouE-WBZ-gCZ{|+GA{!n7@U;WHTUiN;)-dUC}O|E{wd_W#7wXGVPvWu@HQfuK{WxZ{At&p__NwF7M za{JHgd^_-Lg_6VHEA)MkdSri0p0Pg8-cCNaKCZuhUAH{@F1>`kNKy8TdbK|Jm-XNC zzjPl8zjV|{)Z|_T9yc zDmbDw`OagF=rghLLi}F0v5qZIKDn_rI($MjhygmJ8Fp+VlAmupwy;Y0Z~-67cVzca{GQu=Jqmo%J&g*S_|#}NB6;moJ*;QlMZXxx_{POa z`OD2Yy~{w|GHV-#MriqvVsn>S9+XXFOxz_v&ZP!0)za z2I4pN>*@k^pFzX852B6D)k&0_ps_nX^dDeL}@ z2TFTRXr{8+yIfn>uAD@k;2ec>FsR59+wt=zZyeuA2ezudt#C0D^UTi6Deg&wAVBCVp#;kPCE484(vR907FqLg{^6^*V zrQj!AVrkG|3u+zQY^_Bxj50|4K=SXe99M1zM)Eolpb0B>I4)jzG0RyiX}BPsJoD8; z_Tswlzv^Q(hZ}SwA!awzxzzFM8X*20Qx`UXr41z6``RDkUz_`RmK^uSA-uOcIp>X5 zzP&s7=QnD`_I5+c1Aw)A_inA#0JU`>l1eR@YceZwN%I6ugVbAw$ND8^K8%xJdXG&` z+FDWB`;(rZ2ZYhXAadDYf#j2e-?e(RORZ#;^| z4zlih3_&-u-rtDu=X&heXPu5`U|KO-hb?tC21kxz~;YB0v|6u6ye@wGBjn+%W+cq0>CB01a9> z)?3uf7g@Lr8HjW4+m$@)?f#*YKpe;v7CUL(b#LDiW}A~k-ixzG>DPDfCV+{NoMkJL zZSS2-V!Y&iGx^1P<@9(O9>=`D51j8zHow1*siS?}eec%<*=fnww!f-h9V^52mqE$y z6TK{v{Etrt<=|cl!}S!pj#8tqU`Bjh-lsP(R+7B;vjwblUCoY@IEyC}Uzqs4{fj2E zY`$480#nz(qS`OAPhK-`e$)6Xp(2Gu>B(#6Bds*)`%)B_3jfqK^UOfguJxbqexU6y zZ@;1~GV?@v^3X2_v9ja^UoHZ?zkMlKX)(*eDZAR*KA;2KbR=blDrJFk%v>2#lRh#XupvKy^)@{Pd$g?)obE%uR_UMh=ou zl#(y+xVEix_nKd9)s`5!g4O7aO}j9XDwh~}$;RFJiF{!>gD7>E`=Z-%z~7D5jKHA574NHF}^A7NwM=IZzTt997|x5t57u9&xdV9;0}~ zZ6bQaC8`>Ix{+;p3{p5nv&oFG-pKJEvlDCeG=fy23E+8c(rglNcnyIrAhnK&;wqE@ z0YV!<14V|fE{yN{$i zWN8bo)v0V@+@At?+|a{BtQi5zZ+K;AFU!C2Lza_e1)yD0m8dx|S!OXd>3E1*BPaq< z7AEB#YBLi-!wY?!LtGdkp-1t+!iEkcbkz(q1RWd~`AC^Z`;gaepBF9i8DaS#W3?6%Qx=|PsHbc^ zO;=B`MHxL>ub-k4a~5TGYP~*1rRFY5^{c#$vv4ABk<+Whyi`?wUtptDV!QriB z8W>o$UawZEMQMmaB^39CSgaCDY={zfTeGdwJpe)S0+VC{n3Bu+0Ju9-l((Ru(&+S!v-U`#>2)AECW-HB2J}|t6OVv1*sfK zYEaopT}Y~sA9z?r2zQWlbxW*peXf^HVR0&5?3Si-F*i~n=lfV$qF5ASRjsS*YgOPU zd#u=i=?Ytaq}hYUPNQpV1CVBq9+jpBP`%#9AQ>3}a*!=Y3?Q6X1!5@dHiQb{2AmXr z7Cg^1aHoXuAav6h5WeZU=?YF2IO(k(NN{71rT3OC4{>zz6j3&@)kcUK8&hJ zXQ3+6S*QwIu-Oem6}afv$pGDHl;bAU(0g*ks7 zZsmDs4)8QBY-?+qwaBa#IUDu;yF*@#^iVvqoGZHN^e zR-&4q<6@1kPraptXr3scTJ!4lE>IFVmaM(<`L>0ht~_XwSuOJCnfY|TMux%+y9Dz@ zzNnVN!z{1Jp$BHaoO!0N2{M0o_B^vxo)c!{F);~wSC}2DgB1S+|&X@E?4BR5lDM3hfTyj3_&-&^?Cp}V2;RS zl>h+CgL2ux3a)e{EIx=^>(SxZ2+^q1?yN9bLeAGQaIQzjA`*M(x3#qGg9Qu|tX8Wu z`^n$rvR|_-xh#*(iD4jR){-R)@-Zo8$;0wl7nMC9-y*6_72-zvdaZYgF52P3GDZW>4B ztP(a{#){ccJtCsgDrRTy`SQ2LtoCbOp5>+wk~>S-7#5MEN?8>L*lDHg{6r)rr6Jq4 z;F_Z%7cMp0WK#EU!S>i8tgt$-RQjmhNV*1FX|2diqKm%5>c~uLcOz@LC5#CEqBKsd zEp`Pw*c_{XW)eobCdb(kWwwcXAvUtGIoB* zj^r;fvgFb-R*c5oRff4RDnBg)^F(D;IXgFmtH^fOoAUZ{R)$Qg%i({*lowa9Lk@=~ zyp}_lV z%&%mV!B*d^WYuwi$>2ibKH^E@#)!P5k}ZlsUyYRty;f_7g4rti$wLJj07TCfY#f3+ z1Un0`bc2HR=NN1V8#Nu0!dW1sH2h39)JT41vZ_Wx&RVhl*CuOV`f^VMn^DEGxC*g_ z^2923ctP9dZVgar$HIOJG{+8=tE-preeeJX^m38%OAA733oA6_0M{gKvDgc8$|&|i@=V&K%J4ywyfv#) zp7(Rs%q@@H@^f;|p zw7+yI3j0R#nF#DX<|&XF*n2)432Wt&v1}v@%O}UOvFu{Gb1WvxS<)KE`g_8YJ&p3g zaqM7UV+%IeTji>8>=?ZKcpST-st@RuG*0&c`<_GDj2r@k*HAeu9D20GE|&erv*Xxo zIcNMn-nPnK`|bEfwv269|578nlQ(VN(oH)*+YzX93Ox#MD?Rqs`r?=DM5~=Ljj%R4 znXKMFt3oTb(fjr54`X9}70Z#?T890Zmz_cz@PG}ccU63S&e7~v-qigf(gs@F5UARC zMr}KsL~AE#YaL{*b>n?&`)Cp?^G5psm+TUD8k-}hT>{#_{I0z56813fepend2TIBE z^<8t=XvW*#lYjU=dmG{UU(RLQnLO@P7Ux^ul`W^T#q7JX_f*KCE;;x#b~4}csa$j# zJBKZoe?E;hL>FK;4T6G0v&aswpL79>vx=4Pc5A~lZHz{IhrPt*h}URZy^1fDA704D z@@;>VwHL7~<DF2= z?ea0KgmJ|mPDQ{7>KTfJla4VTJ6m3K4jaz4%1&(Wcz|e^y#0K(ky<-_2Aj#Drf#0W zYCR@gm&n~S*s!?A`kAT%lOA>tq2M(DWwcIju3F!5I`rmIZSP$dF#Oneu!_N)L6gyj zB#Bz<{?{@ZiALX@8NXFNxCHvvd1tWx)beZ3VBhoEpk&=VmS3?Fnfp*Iv+z>8aO0V& z!twQ&pUEQBZ|ghGVxKYj>P+_4wqg*wMEglLg!W|QK##g!| z@4{37I=KnxS^G>Zg0nh#Xe%pQ|KvIB=L)4CoXZCB^{2J6+gNzVN8o0hV?B^nAuCT{ z2SM{Z{scA=_1ts<8%AC6>NvJEk9KwGUmQ8w-XkTSi$a_y_n?i8l zqFhj5p?B$b*yP+w5qBGBI~PDIT>nRXnaoO)w zHaO?(l$R?G6`ETMrFA-MDCmBrn_`%=ohLPC|LjsWVA#elGJ2faha2cy1j{C!9>=5en(s^vqesNppJiFi{te&;YUtWdPU%UMDDs~DrY0A~CA`m1- z#5B8p=GCk=Yf2YQH&oDTg{@R1$BI}RoHs5)UGEX@7#^K8QPo*n5UBX_KOrux9u=2= zS;20K_L*Je^*6B=c9s0{Cd?35t?#{=4d6#Pt=xi1@tc97Qm2v*r&0(MD8g#$`0#cJ z0GGfWe<65s`km|~dG;;r2DVskzXcQ2)~)i@TUo7P&-$~ldPY}%4?}GGZ%OBvjwdW~ zt)0J>=1>^p#$^ODFTJO6JZt1GpAv43No_V*&!FdzSsfP7e1gP`jmHMgrpZpvZa3Y!knD7h6c|H|6e>hBfPMwsXI0 znQ{gzl(#<0g7VQ-EPwp3uFMc1#gI6eaR`_VnQ@GlwVCm*E9FbO0IpvbR%Si&_%00c z2KjauJIy^S1$(<^r5esk<#G40Q=6Q_(mmdV4J6Ro0iqBi0eu3_P92-}iM=HMau4ed z^)mZjwt+2^o9<;v_B}c6K31EHf#R9%ie#GYm&sf1V>jdVsMXM2mdP7evpd-7a`YP3 zlD!k1nG&Ddd*r$`Om#u;8rEbi{vA3vCw2viao(V9;*lU7->OH1m?uuWpIycRH&VXb z*cI~i2iR~DJi)c>l>P1(NNQE}^K03iuIsyPe*|xBVe-a@*wdtaoxTp5)bdv(@|4Qw z*RgY`wh<3Q+<`G%zjzQ4OCN@|wwJ5CD|}6uZ?G-&fX)ks@mPz%DRBU278F9bU?&Z0 zs6CzXy$C;&kZH+<#i#S-Ecu_scgQUpSV_)arO9WXzVP5mX*|N>I#yp;g$a4aBdmVE zX#u7h!#92Q<40K$3&`R}S=m%Nkg=G-nv{uBK!lD$yfjmp0`uN|nj0AxP z6wo1ul_>@G$U-f4nn#4lbVk4j<&jX_VBKNsa18nJj!smd`H0X+-VGAsR7Hej{$s2k zJ4}vx47(FydD3HS6oTs>V-w3>eTeGXKsh0II3RDrGi_&Co${l{*uiC;|0svGk)6d~ z?UkcHf&TNvMs{Ce1p9EY7So4;n4DsUOgzpiLw>Ahd^lt?!?NpfR^Aj6zF``j z5rtPNBN1IhV0!}Bmr+Jf`67s8u?Tnq=V^4T1ut^26B7{&id!s9G?5?uisgH7>g1P| zPq3m<{vjGuc4vOfM#2v(BQ;@J97oEA@YQM_!^0afCn|G<416AVoE0A02Oq)!z3kv} z_&#wt+{UHxe*%{oDO_gm6PKAbE;IJT<;EvjzcgJys{Nu%?~|-B0TUtWrx5xHCLEEA zj4V(PP0HR0if7 z^XxL0;sg zyP)@4b)-4&X02B6SinI?h%oFPnnF!l>^F=oRACN+{WUX&)Z>xEJ`py z(h%9S+gohfgn+Qr=UCkx-RjiQNhHc3Pe5%8gB0VhJONTa0Ggs#gLDRu3PR|b1l;!0 zXbc!0sEA}Epp0Ej==R4UStkYnZbW}0N(c4W4QAH(R@zOqDv^fZEb5o2<(nfyvylML zyC~!2<;aWK#y4j+N&r(E+AL~C2xTG(yijnXR_+{Z()DVs!cV>K7MqR34stf^A_*d6 zb|5gABATfd@K=lQH-}MyACp)V3VpnhrJWIEjMgYS`&ya78i+olIC^Lbd+tj4FHbK|VDOJJRE@EY}~l zbVIj|HB^6NGI|E99>4s~Gpyftf@(sdRT@@Kev6Z`K$W1bAhl2ph-1hgx>n<`)~q`U zWFCFYY^3-tJtMbwh^9mSl36wAAYXch!ON~a+bN3^kg)~t`B643FM81LR^Y>^z}_kS z*Fe~Y$?)oE4LcV5;?_tT1<&NTLWX}`FcY!5myz6& zKln9k8RWM@Gexu+8h~onSWH!v5=y0Xl+A<};1-}Cfudo8^efN{&Mbo6P%jNzjI4<8 zj^v0(fV6@+_;) z3s~qMnrtI8agc+K!32BFv&;aXHF#eYSv`MVQ`@rVH~yK;H^awA0Rgz?SZ&MSe+vxZ zC7CQgf0m6dh^!_deIi6OPDNs7cpgq7!gAtsOhC_^@f<6=5SoL>dhG9mm1!gCt-AF$ z>~wmp2R{j13_>EnwFKaz9T~?M_!}p!fx?SG@Q}6fv%oP?FHe9mdw&f12)Tm&y#UCH z6b!2V1w5zpPs#zk=4?R1_IlJ{?Bb8{5cJBU=EK8yha z@{s4*tq~~X)UkJRv{SzGJkEpzvi1cmD#CKY3#=;Wx1w`I6f#uKeu14_5FN>T9i1Gd zppQ4PSdIM87g#(34y=ZnZlHgo((@u4h7LRUMGT{Wocki{7lwAlNQ7aUkaxbw+E|_J zzl9Avl-L!`LE*KW49AE$NZ^E>)D3RxMh-CoMpSv<v#JZ6~s)-i@wCxUWi?#64?;j<{zr+d((bOJl z>IOCPum};6)oAOemta$r@y^w1g3xL>tyRE10cBv0IRT4CbS(5nDE#oUZ)q4hk(Sl5 zfh3SI6~g^Arr9Q(9pRs6zOtriYrAwR23rubGv?1AfYB*itUsj$Cg4rLO0;#o{)m4v zh=sHWY6y|43eL25$}ka9uLf}fs1yS0!ByRw)!*9K1(66Fa(BaO^*xAV!wOvj5BE=l z^fmehI+fPo9G@cH>`=hbFv(%v2iys2?kZE0C%lXW_2y6HbuY6qjnO`rEQsf=ZC3#@ zHm9vFT=787TM($4m+nBk9)yBCvo>Ha%If@F247+Q@ll0gudt?3SY$)jYDz3=YFl{u z;`WZt?zY4t147i`dZS=dlrG3&B}3O{$V?RP=<<+!;1xFHV!?j2%F^drGt_7)ZlRr1 zObSpOt*llnGO-op^J5xN=|oGvl}{18XR#2OJO-#T4=&=65)bRaW~+FniH$EPi{L|G zC$tuzA3*N#oAUTq+1bSyw{r!=`#>#^_loCL|Il=X> zvAV;_&=D#EEGCf|(=?dQ-nmf<1Thj4W532hkIzcru9Kyy7s3mf0#=GW!wiqqU&ofR zM;`V%JG7W?LuF&qBmd$cY3ydU$_%#RbZ6!3tRi1;2Io=Zh+@FPOK)CyofXI7rC#*| zZI~dHDaBmrdmhQ&V5J78>7Z3I*MwTD5r`IKqirzabxQKcvB)N;yveHMkKSNIkF&QW zi-ktpk`WMU#j~_S5Kw(@^&&#onrv1@B2CkpA}5UqtofA`NOh>(@dg`Fg`Pprno1jN z!%7F$qDbNr^fhVPdC2FCLy@?O*Vu*As>B{ zU0V%B22xmqB#S_GBaLcuobEClN!&_G4NM5Je{#FL_$^kF!08n>*GS|cj8GW6o%nh; zE=C-QcG=k(5=n(32>Ve28VtH(@2W`&_E4YcL-q%t9QmKDK4L*s;_DPZH!ykq5C42vAdr$|TEI0H(nOQCm{~a^I7WTc&b{2L!; z?37|3_?^r+=IuXc#<3OmVP;%3Tc2gd`(v+NX8!?I)%~8uj=wka9bi9{83*h~Gvk2$ zi_AD+Kbsi`>=$j=_Y>@|KIn`0lFYn-eQjnOuy4$a1NJSMalpQvVAr0oWe>1hGvff; zlNkrtZJBX^?ahn>>?a?{*dMT;ckdt251T%a&;J4A1pV!an$ojW*qfAl^I7p&!~Fj?mw}dvj*|X?wwX_dV%-j}3r$8S)+*$N%`AY!u*K_3v{0UvbEfaMoYhL#5EPplhxAs#~)L(`yZ* z=kJK@{8*0t8!H{W60I7^1zrKbBm}Ef!jXWe%)#lF8i%%Mz5Bjg_&3&udxcW}fE^nC z35FOcuPpWxIq3toYQK*uQpX24tB_|;=Y{h7AF?_74W_tPop#8Rdf8gmDSaQYoA}Bv zQTzVKIJMP|l(&2g zi>t%r?>}ZM*{d?KofY!#kEGnrh5~Un`u28qs&hbbM8A|yO#Qa?BlKJ6=g?>Dj&E!M z@jK=1pRnoFV$Y{`i%@)5T7ZQ@&unuRGzK`2{=Oy$%YcEA#Zz zP#c)sYWuo#O-#wo`Dgeu8Tyj-lf%DcjMBz8J`2V-gT34wO2$91r|@7MuwL>v zTcvZGttV+WtyRA3=RYcNJ(FN#3kz3#^T>xyVR=b_k82{fFa{uPE7G#5xBX(N+a$=CzSnfBVApN4W z&d*9^+7Tg7CGKSkpoch4FH`TgA&w&j`CyQjFrR!b$WJUbgR%2~FAkzM(E1GFMnAaI zCl3zsX^B)5I3Adno7yEK*8V;!}uLFJ96XK?lOT*!iHY z+9}fQ{C+zK*Z~gu&~SN6jOPpwVq+l>SZ1dT<<&h1MH%d4IL*T`tn=iPr-pg_kQDCS znYeGr#C>fh?p=uZwW>z%iI_3mOCtj900!AYfdaJ<$r1_{t`8s*wl9NW^AM4uh*03bX^8oR z11~0xD9;PHv{a}bLYur>kc(*k?P-`3<^B2s8Wu#0|JOl)@_D#-}pd&;+R+HjZW15F;PQ*3?*0sCj?pq4XVidkw5=%ct$Re0lLeUX|r6 zVVzA3dD}ofSk?{Tp#<$#pm}O9z+U`eMT|JvgV;-NtRoPoeF1ybhWcI-jC-Vn}LzL9FZ|18ydL*gG(?GQyUG^r24 z(5gNJL&N$I3{C4pFtl#p1VjDU(#j-QcP7C$gqx2@6KriJ!MYI1zk_#p{=WSm=_paC|k&_l#> zXrl-bOF7#!Xr_o}upC-SW?CthLtAZ%J1mFh+7#bw=ZDVic4Shp_sC2VZbKwBV0#cr zvD{WfGAX!)j0IK8oHhmJM+fn$Q>igqkk_jQC^f>~tRnR7ttvvCo+7CoHWM%$r zB6;rq-3mks-Tk{^9f$?o{kwKVys7;=>>|OAY9GBgn0m1ddGpbW@U`gl4z|Lar$WVk z)Qd2#9>OcL_XVYYjyO>M0R0Z|&w=q{OZ)@!<9}3sLW<`@j$_-{H2onFLw z=KoMH&cu=iQ-A72a|Z3^PNjPg@1JpMy$$gwoPfPF0(X_J8OI9~ zewfyhVMf^6wQKkInPe5WIMM=b99KSYzHPy=vaV9^L@GQR7S6@-zi*Dp&_NmA?sbZ_ zQ86D*@n=G}r-OVL>jq~NpcM|F@iU=Adr=`WdQ=WwibHuI;iFtjsl2X|{i;}}A1Am0 zJG)<{2UPlpan)x0A7lvK_6wVE{hb&2ayaaHj-f6mvk$lsRB&@ps~BkK zfkKW;49W_me`~9#LLS=^I_H%CN)D)=rqha(u+B)+I) z>G~f1Ix<8(yIfA`_Rx361-&KUkb|m!NXP-FG8?Cjze7Kehtyvl&ietN;!Mik4)n^% zmMaPXO6Wkmq9+3rKrFw|XXBnzDlkhnDxVo<)_OjBiArAp8 zqco(S8ibXtgvkoLR;_bOXC-FaQqF!4=?G{U#ly2frBvmec&!NH7LPcRayEnxa$%%9 zlp=F3C5$}#l0p$0+qha+aY?JCpdT?C_qNHv48%X5z40}E&nqjh)d0+z*88ca;rd4vs_ri@Kqs%E+}t28m# z$*3~dGRbuU^1`c>8e=vhZy*+N(IyzxbQ5<$E6G;YI@`=yBMO?t6^jkSqRXE|d5}Zo zG-%ik!8fNya;YE54S1pIexQpGioV_8$&55 zHHJ}8VkGJ)EH;KyP-KjtpwKvof&$}U3i6FZDA0|O6mX-S0%nY&AZm=JAYwF#+#1{n zgQt2J$r`ENH0<6KdDD<3v=^kfpwR&{U1*(QC|NIq+y0$HnNX z>JH=_rQV8qJ5vCeM%s_DD;6&nDqCA`}@E zbqt@tisjT}`0%2r{eqfSW`1%j4~OSS0FR|hV;$6?Q1gIev|je+{2q* z{Hn}FCk~JU5@~U@a?1fSe z6tZCg6f30OVRxT$;_9pdibE^{W96%tIGjDsGZj`n4#+z7iu z#DJRHQp7^2@Lf&4?7@ue#fhI?a1+ReFBL^p$)L!kGV;FTc>nAih-I5)Xrl4UcaGx) zXOgL64`^EGzND_&3ct#mKr{$njw*0P9d~q3Min)x$xwb2k)ZOZbT8I?Q*o)iFvW9% z9I?*_vz|Q#%v#_wYhF47DMj<)*e1=j#6OCqbJDqJW&xOtTq%YV>0C75d<`tPer_6W z^CgD5{ZC(nYPJ$NgFrj-|0>#vuR%K(%)H4SS6-2e7SJ3?2~bIisYy$Ptuw@KWOl4v zNtyX(fqm8%qu$-(U9w2TmYC}65B6!Oa=U{$UUj~Ai6}sZ#1eN7CyhAEp5;}J9w)~V zTf@cRg#HYdJ?I7-#I{rhe2W4yiolXlGKHH`@1PR*fU$t~Nw94$0m@Kfwqo42Dzppe z+cyT%UEwE6h@*y2<@veA)ZJj0C5!OA8M2a&sr2F^CtVaT5ym3?(%WqJZKeCRNWCpm zZ{f9rxHLae;J&R?Z!6VXo)&8v>>~Q{Mj6&ixT{|yQoqf zqPw(SA}SX-T!i-FzmrLwth=!6Om6EHBy?EORA`H(PIYz`hcc!3Wy0Pz!3i3DE8vEM zjsmaBP}Dn-Mtc3SjI=JKkwSk{M%n{N%f>0GtBdzS7soyThLtV0cOZ@#E){P>{`4IM z9}0nU05pq4&q_(vV$u@>fkWBz$k!(@felR?lF;g4EXlO;P)7C%?!1~V_?xy zqi^#VMW_@GPC%@51L2wj1V}fK98_m~VgSyD>%U-_7IH3UFbz_iaXfQm3DoN zu0Ta6(;Tm~zl{38Xs8jQ174QG*wNC&pn6`7U~oMb2!_b_PUTk&AJw3BF+}Sdw0kHV z*`VD^VQzzVABEU)T1_F=W@{)6%G*xkl{qyH+Cvm=XwcSCI9k4R8XtIkv_X4>qGb)* zqZA&}pgl(6!MKJ*;Xw`B;}ni)(4L@hc!TyNg>?;DH-*C*w5KQ>+MxY{!diLM>3pD3 zQqManT2v1gMhN5ea6^T#w4S$92(9;Yeo$_xL90W|K(n#qi>Dm<`ssW)U7!t43Pwg}#D+EcOBme=0A#kX!4w zp2I6x7Cdk$w2>8{V)+J@Z`kI zc4mZMS~!t2Bkb(7z#+`t=ks5TBUy@VC!*DMA6)v7mm5S(4=BNKBNor-7-Oal9G?e+ z>n!r}(t(fh>hi7`d?eH5TQhiN|F0{7$xM~l0%Jg&#ay6DeCGl_z>g&=d<5!p@df-R zj7iaqt!j@P)yxgVPHX1zA~o9p12{LPnNha`k6dc!xgU8@;@M_CET3E^pe-@gk>nYa zeBMM`c%A#Ey3d>ATliEKUVnEBpUw7QG;)%@{zn(`8dd?Wq~?3(!0!#z0V0f-T*PRk z*iDvCT?FS)j>mS#!+PGu{E8e%iJO1_^cTN*Z1W0#c;{Bn#kf~JUfrvfyKuQW_2f%E z<$Be1YxzziE6F2w`R?s&`+0%SPI8%+`Q1rB1b`p;7d~%QMj2KvnedYrWtNbh*sko zcL8}aWX2IqYG1WLl;C^B*eG^!$Gw|9HvCjuGCl#J+LCb#J&IfCZo_Z~T^7&gb>s`; z=-F5@mqN`gNFx&z1=w|0$P|#9Yic?X?Fjk|_z_q`6_A_jAX-&05;R5_#P+AXNvUsw z$3A*j1FDA&bc5=qBSa(3&9TEU;#DsZ^Qt^1Ym)EI=6OJ(v0nA=5%iUL^mRS2LTONX zEvN>6Sb7&is|t>E0YsZZq6@jOlo*R%DYce*!Lzh?3v_Uiobtly-(rM#3Sb|wjgm0u zy#DiloAp#WmDQ~?CK z3ZU(<8vy??yl9kwZGj36TG8idLKtf{_N8+7CA`pEiq$KZznh_#`mhYcCb47}==mXH zW97}ZRGu=2_wfYsZFoRFIR~E>K*#T#qqwjH;@IIq5}c~jK|ovsH!Gkid3mG#UsRAU z0b*rm*sbt3$0V~m6Mo*9QSmkJ@VUy2B6p!kEga~>NouK8W{+S3l}e{hLb8!7*M6TH zlS?hUH7j8@YnITMNDTpU4B2ceyS@J#LETj7-d1717Q5iv?vD{wtR z&;~r!h*_O#gr~<4;stOFOqf!IKllM3yx$eRgbM3WAy&b*a+t0VJxmqqY6qRF(o_UCIZ^>o4V1vA!U{p})M8k0xaZS&kczx3CL!D{!>Wup>41Mf8G9q8G?( ze#rZLIpbT=GCDhhy{RR^R7;doY*A37F`P^Ev$n!HUnQmv(XX0s^_Yd2HJ~l`tKn~X z!ltHH5<#J@G76{@`dK~dT;9l|?C;XT65(64s&ygV$1qYHPaF1_k&pd|V=|Uoe#A@l z%Ge$9Z$IL3oO=XOkK$;2|Cu~*RN{ybPN^YL6i>;T%edI@KJ7VISL6C;m+_%z7l^zG zqL7p*G)p;zoDIX&f~QSutipyE!?4K&!r&d+vpZBf*Np$Vq|Xh5T`5N_zyf%Z;{)2q8hy0IaG^b zpwgItA5n6^9IMrWSUFOj+>AWA%_HT>^Z7yKs^*&cd>Xl``OEyiiyHD`n(5DlJdK*fQY0{UjEcYj2whL=5tWMArPK_X ziw<)IJBozl=WTq5Bb5fp;}`M~Meb5KH38U?U*5csv?0IVLl3A)GHjx_kG1vuV^MGh^gkW)GKs@(o8V78OJ#4b88S(F#199DwSVd z&dc_jXGkvBx&y``JI*K77O_EJRU);GG-7ZtY00Q(?IK>07}2Du*H)Cab*(5IbOv#3 zIa7I)g%%InKgeRGCX&iW{jStAj7X)Py$V%gI@Wl$Qqdo|BlwHqFAsl(fUY4l@K;GS zAsm2E#~;UY6C8dU;}98%@I?Gsc%Fd2N%%Vq@ly~^MJNzXMtBm!(-5AEzw_{XI>KrA zI|I)b<8KyL@f~3}8?C}J-2D&aNXfy+31Cx^R&qk{HYHzJ%zc@}#!9%4bPDYuokAp? zpyZhHtT%aB)-B;#hq+`A*`uOADL8Vx`S6KbqBXgNZqNI3zzUx zHbkym!pn+=WchtwkIvzVmRwG61GsD`aJIAd-!I{J>^H&>Ut!PMA6>z#Do+8-uY6>v zVw{fsRQ^|=F&Rnie)2nRoGTB$k`KzR1j|_1xWh>ig30p2EBVMmOy+ienu_r)hHAde zXBNswuH*yQ9kS<2Ug)zYMY;1zK7yQ}!ml(=k!XcWfUv^FuJ-IiuD#b%i8UvhkK@k? z(YQrYN$CJ4X3;s2%^;@;9Rq$O#yvI;FvT;W55%n3?|p61QTUjITBzh<5~#Fbr&bK1 zpNA|Yp({l$Du$3&3LdR-ifKY4h?Q2Xu{qWZ^hvp-DI`}Z3)DS?fQXUQ!*~HuwZIQr z3)|pJ`v{0s$dNJ)Z{+N&V5e3nyRO1~TZuse-(jTCz+wj|2KfT3XX#VqR2n-tP23o0 z^&*NTk*-qvCNxo8fZO~G34ZpIy{>}Dik;4cvW?%Y0>v^gn=7HJmDcz(meE0q`8JGqfi55 z2=RO(ACWa6Oh{>L!wHi(f%+4UfMN84gQ0Ce4krotrJCUx8m$Ij?ev-BuvZ0d{#sK> zyE&n0x3;Lsb>z(!7&^v0J~NFoiFzH=JxXlWjVcOX)o~bsaEH&ZpkA7BBSg%KHjG0< ztyVLaINuy5ueyf!OAJK%K=hIrCWbY!D;8yX3!egK&tQ&nP{J&4Hq=JLz#jp>f!JFU z0~bNdEkm1tBKZmzxy^vCJcftSZm>eSsqinHl0{`0?^x<#BUluP;h3R_N)u5NOrRm4 zL=nDn5LMrUDuU^n^OmRH%Cna8qRNa{EcJ>CqE|V-O>pB$vDhum4zSTjUEslgd=$(_KvapaUcUrxOi z=dXnpF%v|W;wrmC?QStG5pOkz?2QV#!3r25^hihy!7c=319&h5zzVs2l~7xfDWry^ zk@8#ESJ7;Y3Wi{O0!*`*u*%x4wVFXvcPs+m(u!o%B2*F>#F`;TT*nh6y72K1&CE*o zqI~)~K9N<*$o2e+#I>#@oJ5U+6s)IlHOas+eQX7bph8z}r*eV`oxB#Z?)7>gDF97Q zjuSDunvly9P4IgMkA?s^30e}8>S^?hbJ7O76=I^ueDAz&bYExSlYowTTn8@-k4xc@ zkh41aE(O2UEcjdpCKY@{5tB->6w7#!ohJt^gFZ?#{REO}-E?C#fLz@cL&W7f%Xz%`0_v{b0OTYIoB+&r!elj@CuiKijhtK(=S--RTT;g><;^$n zGousiLS`k9o*=zRe)xV*=a(ikDF1wt*Y%r5;t3#%83E{A~A;C z*OC`TL-?GTLtY}~9@Nd-FX|3(`enb!JD@Ll2iW9QbY=F5IFUD3kv9*VKz(9PQy`$09H#&VNsnlO;cnCFHG= z58Cg}`AqUL1NKk}L!^nDhrwLK7)YP(0*cVO&XA|9;FV37u9`4!zN+S_)I1RoMG&e1 z>tl?82848&a;V)+7MpEUW%OBxd}IYT&N?)MSrwO8i$jT}xkK|^*bFTcRoL9BwZGDf zK8FQco~TX@mk$xgceiL1D3-4phL41^o#=jXs2qJ0A6pGnU;q%J*9{~%$soawWRr1> zTd+{xauYWz4fU-agN&q=;z)b-X#19sQ4ahK`SwkGATy-zW!sbBry_8vtjKe_IEx5*iHH+kt z;BJHMo^5Jj3y~#=$K}wU@PT!OigcGl2GnZpesi$0&<-nnbr|RqrIlt?t6k+ckS>?} zgrC(vOcPEJW;V@Yu>JTktPRQ?(gl?nc3cKm^6G437&{YtWntToY@TdH;t_e459$X- zIIXJir9y;RVGu_R>^h=H9m&#DM4eFO>4tdX67=>Mb|$q4hLIzH%l#{PMgGB1pdk%i zIR_DIj*&epVL)DJMJ8hvPKC)*qeT=>jEo$AG1v=C~;lXmst-K;HZ;$$8 zsDI(DFxoMQK$u3dh(JazZxn?NM)=B03ZL9GKDjnNkhu;%c<17iOZd3MEtl}2jYArH zxi&tz#9V^d&*_o`-k?Hg+|)Jny_QV)Ax8?R>NhEJ?0;>kuBH)7T^^(^M^!W?5_Xaj zKL(hAHG>C81h^~{-vhnO?r_mlz117Adn-kZZ__yqvb>WI8Sd0e;{bE|;0$Tkj(Av% zPVr6$;-;vPKkDRli2*6|C@hn}Z?$5uo00lthX)I)0D9D@=BL=Z3Yu}IJx0$I+`!5K ztQs@&NX}VrY0bkJRxBjlMaT@&ARp=uas!{YgY1Xw^B!d{6hygsLdf!Boj`j4c;Sao zv@j0Sa=>upKzSG*cw9L!AT0+}6u5RWP}Rtfr$;{VIySufZ?euv@txhXgNGUzH}F^uTH;%M-oFF@i-LliC9Ef0-}aS9}Hp&1NA}L zaxrwW8KG4MI!fUX16c?M7loLOLd-@1yCrE9NaTwFL<`^}-@k*K_#jNyo!I|VSbt-Y z7)|xHc6l1UHr^l|O=}S3(B>6t<(Rc`r;&0KmYBj7j(D(TLQo8~rGWdoN|ezK!B7%- zS;yjFK{W{*K+SN6ZbnFf73K29JNf9GJo-$d2T-XxYk?RlOYY(~RelSy7l7g!7!Oq# zLujK?Xgh-_P@gN!b5PDh{}Afb#VOR?L>pg}QqqiuvxpoE=2QIOB5nY2s*`k>xAbOt z?%h1^;Gx8-S=eGSvQU9yZ-X}f47G|?&4c)+=4GLIgIchx7%Era&HJ&Ta?9Pkc(eil z|Mp}<1181X)-r}o0|NF7Cp9w(-Ht(&CyiD3%w1ZdL6*tMt9a2++JtfBln9!(C{x&s zu(=ogd$7$ncp4*@ui_W7GbQihM}Bkdi!qe3gfwQ!NAKmMY7c>|9EHCr_)}Za{WPn)xWz2a*cD!&WO25cD;IX~ z{E-;i7%-XN9r#u%<>Tk*&Q9=EPx7qPiB-tn2|mQ4@9hepL(Mz{mq0CzC~ydlS}cvG zv_sTT!CU%*8r=o3KmVGm_AfDSdq4w>F)~>h3jG+Hq(d=S@G(;i`wqXoOHVduqL}JP zz(mtNl)_mR9ZD5j8}PYO&un8N9(%K!jUy3u7B%DISFzPm*o-S<#n$`z_;?}0<%kbJ z*k0I7D~xJ9E(HG!Lf9Q`HjdI8n=Z#rXxn?&1qeNl83#tBQ^bjjQ6JVT4&qg+^E8~N zkF~bIj@`(@HmHVe0i7>sBAMaAOjjzZpn_nQBnV#T6fqYI6guxg=5)4d94SXsHXM*r zMR=|v>oa^HK;w`RmH<3am8jOdSUnKDnv9G!7~e$3VryHBOp-}DI|^{I)q^BbUQ#-7 zv9%RRWk^bG2mF7$y$PJ1MV0q|?^E5K-utHaPH)e1=_K7*$U?{twVH>D;X$+!bXb>)$aTc<*Me6U&cyrlm9 zE~BLOeJ-P!P&^_tr)06z;2w-lK4TUG!ds2kW9s z6b{x$mntl5s7II~vavo|u5d?PbfLnXb2h7&` zXr{t#_0dTRchqy7PPnrkcAs!peZ(}fH0{$ed+M*wAEV6O^<2T*(gx@iGT|}AH!B{v z=tf1SxabB&j|oqrz@VZtUGBkFm%EDG{XV*q=sq7ki|7^~J(K8WAH9g^MjyTKx;)(F z1|^m+eDQ|DreFR2J@?+sb&+GPBblbS*P#-}E`0fpmwx%$tG9gfQw3L|B*_&b6}fcb zwHt35-17Q;-`JCJMM{!5I8%{J7UEsvb#9f=gpVPj=)-##zE~(WIkE#gRpLE=wgWp< zWZj?bz%~_Gdx#Ef#Lfg@a^ zc453=b-vFNG3ax?)2;Wabg@Owp&DGNa1!_u@qTRy6=IiJLZ?Z4BIMFAXl?O)zqZN< z+Y(vQ(IBS`-4VFD*f;obeZ0pftNqpJ-M_P+`ywMyFZX7Ov^Es?>U${{nqFfn!i#z( zj1pcP0!u1=rKH`F5bp8ptkxj-;#y4Gqd#Qzl~UAgF2fDxOmU#?@m7~J1gSVR5g2Z% z=JaTA@K0PxMjq?d#r@YuT@js1wD-MSkE%tX(F&JEAuze&I{0sta{!h7)R%&Bg_+@e z=|0)w*Z?t zebDqbD>R3nb|)JDmV%UeTK<}37j(3PWU~I>K$VE$49s<+XMiPf+CQ?Cxk#dWDd-|zEPWC1u{N7^3tyDn-khV6wGa%Rh>-)?fn5e?i@O{w@JFJ#egjo)g#0nvL=!R;3lobyUD5{tE2;q-N#)` za5bJxO9T>sRUH=m3^Sei=@`)G^ZYK~Dw(J;tPWd~WuUO6npU?At#NcY9!M&A;)XEfw1OX6fpAPu`K;dYLuWdx(&@ zTF-VwSN7BuAbSZcIr$niWSera8r~$)sS=Qe0Cn_BM;dpz8&m_ljE4=XkjF8|+n1jL_y=zJ5Q0?z}1rl&G8gR1czO?D;y}|G;eWs z7K)0&_IRpJSkh~it^>Q8X!JUPkkaQE0}^agQ=m~0dEE~fZYIvWCt@4PIP^Bb}$1H5wU|AfLc^h zIOok^G#WwZM2*WC_^S&-`3$^}DisYw7A-Y{nFvq}0EcwGput784y$pMq^3c^aHdxd z0fP=OQjn;MpF!2b8}1p%+CHM_%ZP@jLQpUl3g<&m2;=AZr1dKk#8<^HlXOtyQm>M7 zFr}1z;6ZLT2vO1HtL&`?h9SZFs`%5ShE7`UBIP&_l?L`ItU@!f_(K#f{AY zkIYdI4`(|K$&s43no8C0v|c_?C zVcx;t;rxx}Zycb%JBKUxR3GE_I`p6?6|W$G4*z#LnUwiYm~VzIkirnEq?o!=Ls&+YoF2qgY5#bSWlhe5D&X#CgnuSi>9&OfUxJeK z8%@kfj$azfVTAj?z@Fxq4K^s8Q0Sy)NFuZccLwunlcYV0wOzfH1^?6EiCWc+~unbX7X(nt_J!vL#BO$R3Vjs8oIOc8G zs`~iu2I8AlmQnLB)0KOFupoqs*uk^%$Cuf*Z(!YkFRRU!B0SY{de|KAghO)3=Car} z-j`jp+% zdTqws%I|xOOgb5q3~*;?B&RwyA{`5lnw5@4OhhhhhKb;~L*Rrn&oGyJm$G)(x^rNv z<7?#JxP|i*qwPjs(9oTO#&pom-(xyx*=`h_0LUh$csXCzFrIPH5IG@cKf(@j4=1OB zpoCXovNzOl>46S%Z=*_WZm2?yJR!oLL#h318jm>X+UW`HGfS3N{sME$0& zi6(}~xIi3tph7&+IpZDTk5ni0Y45028H(#6knC!-G$g=rQ zU7erERk)z0Cgv~!Ed)2?bX?l7D*pbS;+x;F^sC}WNo&w-Wwr6c#l#TZ!_d;6_wPuu zsh)xP6Iu?ifYof@Q$@{Cu-B$`;+lH8B|`{o@JAK{iWzNV!!;-zwNF_2uUil zpq!xz35TIbL{TkF8Z=fbKV{!5kqxJ7`D!$h=?)S(>P|-1MllZsd~~bKF8^O@-b<5D ziJo|xz|NYx&`|`iOiCgxN@X;IMUbg1I+WYIdcZZSco z;~aZDGuYwFj@CzuJ;RV&pXdxy$c0d`&>OO{ zvGnm9b>BYEImBD+OS^)m^P1!5syMar@cY1Z4-wl34a9^yc#1GS@VJzDdlZGD$A2Q3 z)9JlS0$R9sMWbJJDppR8j6ls!OBHs__WVbJjw7LRh#WMz@ShHT;@2Pj>6i9=mqhLn z?E*aN?!Mgh{eQUT-q-JY{+aZPz3-8r9T4A-GL{zJM_G>A)OQ*hFqdAH7qoClM^?y{ z_cQLh%6~cAf7$!SFH8IvTr>{-hEA!)bJP{6vuIMHZDfaPKsINpbuyuU54wK`*{<0k zaK$?GTjlI-fbDKzVTkV`z9Wh6CB8F>?<2ko=P}IN`Lw7lkox0o4XbkJp}_E+D4EnF z>NH0X=V7x76SR;qU!7ME?tkIdFFf-2uXg-e!_bs|^4mdsSF?j+KEGjJ<4U{j5!A`W znHBG~V0Cd~`~4(MEt#XdoAGx4A$-G!O8vV1%jmxo6&Yb^gq7 z`PKQE3t!v*hu6O__{>kgpSdbOg`(OD(}s4{8p_ZRvPm;1PczTBFFcd~m!EOp)mJS%<|Fj$SoQ1rZ6Ep1!E1hZ z+YxjtyoB<5H3o+s;aU8DQChxmR(PT6FZn?xyp$j8EN$S}@Jt4+2EvtBFWmLY%a4BX z*=K(D+ssw2?tR1SzA&jfTs6F?YTZ>Q`%uSO?u3-!xDAS@2yDa-(H6hgu}i}{6%lYN zfIoqc;%DaqUC9nX?$P9mrA@7zsDRu7a>doACQS|(#W{-&cih(!Oo`gXJQsqbHa!8m zm|XOzt%(NnG?LVkOOnmuR2OqJ5F$%e!pNnBA{p~?ogwlKK5Q$B!fA87ew5RP29j1C z1Ab#k20IM+w`o+$RyT(;W=d3860TP7Qr3MjNxGG??Wd8XTh-}AghE;|-*f~)f@3Xp z1wh-%x`-Z~#`&1w&L#IKsS`7D*j*3 zQyIXs;jZ_c$&7J-xHy%En8hx-T2NmnAYs#nUy7qi(1uS8kSMB;(Z0HD*T&G~49&}- zr-YR%PKh>RrldBRl8rS5rS~YE%*kd~VsA;#7MHUx&8ecGJKsYiKH1gg)_XGj6eLs> z7t=vSHFMkDggOs$Yz89)>H)a4@hR`#PB*xEtPq;u)47Ngl7aXpFh- z4R3{F+(zFGddGB0T`u$feAQC)vQn2PqN+XTyBI;qqycGa)EgAz>~cejafrJV<4)j< zU^Q@*DJ|K(3dLh6q}4kWqF>&k&~05ag9nHwn9J~TR}IOn3($+V&qOM$h7I(L#{=b) zz0SDVHhd4eT;?Ct%t)w}1j|P$g`5QIaNQ+99f#sB!9+XECQ+1EB?GB@z{X-agGY=6 zFeR#T#kL5hG`FR^D5^$LK`QUG_mWk;$~%#~i_+>QuJ^-AW7v)`=M;*>_rWWp-S%Qr z4wW}~$V=`j6@&m$X{I}I`~Gm1x07kXRyXhhqo^t!xmfQo9l7}K;>g8x4~@Ji#-cYU zQF+I^N3;eXUet8QkNkqJOpX6UfReI?^hv`riZVdM-y{PRMSz#SAI$3?E&^zNfQlP% zXa(z!7Zm1pw`|{L+A|&x`Z~J8nW@UXi}?%e}(u%zh&QFlZbG7JdncDrT2GIxWw<U2u4+2vHSAkhTfD((pM3(?&!D01YT|V+Mx7=lHCDj< zCb_KHDGnPYb*Feg3;+@(btfgHq{b#7QBtcZp_$W03b)nna3%FrxbIFNhhEbE&1hZ_ zY6XE=-?#?uHa7=1Zsh5>(O=sy`~cE0#cic-lup}sM0l!1%ab*O8)5x3;mMw^p&)jn z$Y=pYvvjQE0H0PI85r8G*@UFtaICF%Mz$2m-4v|Py+d=B6s^siP{SQyBtix@p+>@J zyc;Q(%FAnF?=%QumU~C&{^F;eDCz^GxQc&RnttpFd;1TA$?xEnMLFl+&aFUqoWfzZ zQw&Jz#QO?L2P!Rd6ej)-l@uus^5Qp3E2WXIm8QRJ2lfPqkE4yU&{z3}rfRTqIH@K) z39HVl_XLwCxaGjO%Po+*;Kpkp*)}u#fKvrg6Qn;zz-RlbDB` z*43+if3gOYbUnizLwYH@p0un3TkX|93ZiVIz2iqg z=ihufKHLIOy5(_C2J1O{>RIvB#a9ekPq2(m^3O??sIU!!|m;$z^n;+9x+0Y&du1G%_6E=TWzP zB}vqB%!rl~U-x%&l+tOB{z=f?kea-Vj{g(Hj_j(Rpq8E*Ph477hzGxw5$;v4VoA_; zc6)whbo?zO_ij7`BzQL38Cqepujz#TAZXMqbp6ZwU^>ce9}ClrRKqeIf|=%3rZHRg z)8N9Dn^7cjJuufU@Pb^_c9AG@n4{Tv1Ny?iM^Db| zY(xloD02!VWja@M09Q?)ee9>fnS~331uAl|QD&kJFx)f(DvQw;(=0SlKiR`WHHjSK zevfHWSMFRs?0}>13Oil=AgB61zY~)-8Hs7J)LJ(NC%KE&(w5qimNpj}SXi2>Ols0d zDW5i#RO9Rp2BECdx?$`eRS8~3W5$s~sZ01^4hw15N!p*7np^epDf1t7I>c;~q4Z97 zE>AJkh@E~5!QELS($U1kDx*7|=x`t7JDIzvyUqEjUinQ0dq%^!`T@;Nj~U$zpr!RE zi@ZwV7~TnZr5-vYV`|H2akwf{n`YcwqKnk-nCc?Mhja?lzq>ccABVCnljw`#EXp`L zaQNO;+!(stIWTs-Q{hc6j@@P}bQ&z7ZFct0f@Nc8==$D_aP*Yy<_6K58U8BM3~X?m zyEE~uGwjx%1!KAJ_nn^w=N#4=PQeK1Vm=6VC9Ak3<(laD;#E{fFuA&o$ zM7NUVQlcA&k!ysC@gpDpo;yvKzk_fN+ep9XQ~JX>DkDCH>y&PXcZccy;!~!>sPwx^ z`%^}xpCa0Yjt+^r({yzq6mitCX}=w9OiRZkIFo+QnE+&!sC5Z)<>74m)n5b)vr|9$ zbZ~Uc%?37$co#pPAyC6^o+!}b+>gsyGe9Gwx=7!i*S9IzX3c$V{359?TX;HXA2UTu zKH4>)iJ7gl;`yTB^X>gl2S?R)!%NVeE@(r%$Nur@pz8=7bqBnD zUk4hTvRaoxs#&sbd%`b+o`Knne72iYG1c_k-Sg~bPJkFVi)v;H*Vs>*}v_p5$<@1`V>}9_WW_AZ5qJq!D zc6Zu;`*pCeYCOsP{I1c~@55Yd(nKvdz9adPo-1s9mXdiSoGrg*;ityC36!p#WY2yk zn2U-`4o%R8!+wb&lkMCHhU|f7f@xK$*fGw&@J!H?ZL)RG1`C?y55(V3aIOb->UOPN z{%kPeqVfJr_`2}e^%4Elt!&!prM$bSPVq_Z;h#y47;tXL*NpdIbZ)Vbat`V7U?HVr z`@*w0UD-^D)a{O(U6#QW`Xlg3(6fb}I|*mmnZF56KM|C<;XAkeb8tN7$3t9{F2q4w zKel%`7j+$JaSY&PHUnS|^{+GoKo4*v&Hc7tNCV8SdsYp~O*)h)r$5#yg^GC{} z@%Ek11xHQ4Z$y>t2sbmk8($RwdAn&j{N9E4u(tm2>zCVe~`GJ1ZH zhAo?|6Wyn4G>Yu97lRM>TrD%6Q%_~J2623ltIFqSJ;z&NoRfe_Prn!}#fwMQ9ke&N z$EF%!qjOg?F#T|0_B6`3hWLbvs{Z zZ-w#Y?z$EtMl=&Vv$CO!A=<$PpMsw z^U%a`$4@=biLdjM7jm1pksN2_-Qb6dj(MsphU*_^9!LXRG{t0UO6D|eED`dCT6&AN zCb2s67WC%o@&xBd3$w65}A%S7k0H`#vbc1;D>}Kt{**ARw#CBBhcP>O6FOoa1xbR{LZ(5xKSvT}kyh8svWoNi8;NEda=bYj3@ zAU~T*xc~2AI&B)5{s7aZ##5MPYy}6@-2&<^57f~xpl%1KyZ#bTZ-MFysQ>d*U#chq zwP2cs*XSc)nlgfEF6a-zG*D-M5|}Q-wSW84{-D|BjA<=&IAC(x7<$<64!N}sld!;N zX%gUV&`=!kvaG{^m!?JlZz48y4+CDP#Sri|2za|4@ZvJQLC{N64tg5|z1Q?I=Rj2+CIqTXjR#c})D5hNBUhna`f@N2 zX6X8tgX!6|cE`&>7hJ#-F9#=-kT9M9HUJs9wKNHWpPeo@7&ya|F!Te5Bw-zY4GG(f zS*ce0;}3p(h!Gx1!YK4VO2P_bM8Z5H%s8fcpwNf_3g}F(grZ@({YHvLheiO!=20Uk z+H5GA%L9TAJ0Y8&K=q<4CVIM}6Y@bmMi@t=$2^u9T z<)u-@`f|fT9z$#*?vlj=lXBh<=DRyEk?%Ggn1~MQjGCG3AJqVlfWt>5T{06%R~MXBLoM$zP|$?-%s&L3JtfLXS{tUEtdAeF*Sr=q+0XtV z7${oDpa7&Aw1s*Ab7T(!cZ}nDAYF>@K~U_0ci&taznaA@fEFqg3eOKiPv=8by@(V9 zW&z)H$!%tEs*6SQiJs`9Xmn==OZY0Ij8Lfe9u>E%U%_!4=Es*xWmFH1Bidh_4gtE&hKF#L50)%q?l6Ou$TwRc)P^KEiqs%bfF3zP(>{Vy;T@4nr z^DutRwC?Ni54%Xx8bt@pf>{Q8bQGM~sL?FoyB`JK{M{%p<;(gD3tTSi56%?@37wHi zBc_JroXy1q&D=n6E=zze8{#0b$q$^Ho}KDQI%cWa!*sOHSxWkPjgnE>0 z3y-qrzZ$gll$a9AwL_^XVJ?2yZhAGC-%p=r$5iez(JcF( z*Mq~`yURZ0_i1hEJF4Pusc>!XX8W(N2bYc?aAuiBLN1g@#2{Dr2P#~jyZP1=%-~Vo zxoafk%YPl}49CiLGFH({C+&=6W#8_ra;$8t9hEh`_~#g(HAiMYXwS=k6offn30#&-h}UilaxyAjlxXDI>!j; zw$xIm4r?lFUBCje{_Q{*&ZyRj2?+6|_V90mftp?crBikF+JnCh^5yc88rq4*EG~5E za4WrAT1$^vm)WUyT`){~>QEYr(njZnUKwFAM5T>?ifO25$+cEtDQS4M^S?Xh&_6Vki{i}Ue48kl=NrYubIn#RqMI{e zD({M?6&swBbfQrQF;HmdzXQadoY`057&QBp3e!9woh3#r?r217hJzD!k;>G#iJ^M; zluh<`6=qfTIJzA&H#3lL-EhIt=w%2=%j0gy9Xn|QSfc| zqpK!7MQT4VoG+V{^Y){4WrGUCx4SPpv-;6`eX5K%7j#8q8wpf|tD?ijnT~dDR>%Q) z@<(XXnivxFMKuaSZle%1bC5}piy{SOQK$d{=Oy)Efs(ac1SF`;&st4aF9V`Ub-{rU zwCAU;j(27+D*)9bC!ww-=+F1Ajw{|P?S87C33KRa>RG9{GU^sTQy+inSC3|w)yXy7 zVEGyV6-Tj3JV3USa?t_^t;6nIRn(1~$E4$cN?TcFc(~UW;Mxb^rkjPnaF!fHPXr;H z50x!Yd@f+cd-1g*pA6g^N0Moe2RYZ3Zw7J158+sgM z_LdRHl@~urQl5?_-Uny(p}%|FSf`I?~Fnjrt%`I?~FnxHhz1kKh2rD@uhg?lkg+p>KxrrBy{ z$Ax0s^TXN6Ck@-rP@eeE%t@XMkSe2g@_UQ_s{H2{Ia18(GbdO ztxp4)qmf-v4??jM6-W)t$q@sPfvkc|1WNh;h0V2G^d+dw=T_6iWggSJxa>=2d%o4- ze-YK2Jc>J!r73>mb58_9@uqmt6j8iMe&d~X)?QbKKB~{_bNZYS5xCqC=Fkj^r5Bx` zkx3kGgVuSb2M?M^4V;y5cGasiq24!;WYro*@;_X|ENj_w>54V6cn%t)FciP;;GGl4 zy;4cVdkY@?ou(J?55s%{KcT=SKLjq~oJkkKMXWdJBDnM-Rk?P?IGV0LbfKM+R@J~oV@d^zdZ2H)C?yom9#?-JpBf((T5EvBZC1614z+efdq^M$2 zFj$g+!T1CWNINhv2^h=^&4|rJ&flXN?H?Fw=n(f1VBvv68%M!I#pY2(sBl}wn?Z$h zcHb0#`Nfwr%j(7e6?-bzU^xomS7JNaUcDLCgAL3TgdY<*?Y1C@hZ2%vXWYRCjx7O~ zT$nEbm;U%RD1JAGx{qR>(?E#F4tBTxcwZ+vSi;SuClKx;J%^A(%4mRaE9r^K$Gv_h z;d9i}TLi5>fmVOKZM1m29COSfjtL8x%q6f@&jGXsBtqMya%oCK+b~AS`RHup)&aXIIKHKi)(`jPiK3$b=cpM%i zEZk=D{GPz+E_uC_+gOsfmArS7x1l6&3wcd;Q?ohdq!>P`9A>E+Mor=LnZb<;(J*dM zh&}syg-3HWRtIu(!6=1q#oAE$oEHxg#`i)sA{(}tj_lcXZi_kLIJnkt>kBLce6!!3 zE8{;>&Z%D5)o~t8%Bg5v>Nlj`slm9^FGw}nM_SCdLcTky7w&O@(gf}`NxqCT=rVHO z#4wpO+%>8e*w`io3&IUQtp6ztf}Ar}bMemQt18X5*Ga0r?`Gpnt_A8!{CfzNBD56Rj5s&D;N!eF zo|)>S8;JJ%=pa#4pDKJ1DG=8!itZUfc8gACoZ?!!Z`$vNf8zUH z_zUBEJSq1Viaq_;6+4g=gT~7b!nR2nVzAR{Mb-b7Xr0vkk(61ZID%wYMMW zG}lY5+-gtlHDe3sB!f698I(I|W02#OX3$dihi8{#aik~G4K&$-Ei8|Y?uJ}o!n+o9*W&<8AdTeP(?DXZq!9qNbF_14A|O zF4&J6-K4-O2VPu=0b9;vsCw<%bLcR;7^dYeQaL7|#%cJfiX}JV`9lx}FYil=JO)t#-9Oe~qd}k9U@^c)zz}CygF{X3+yU3g%(`|xd>%+= z_Rkl4cv!KGn4vM+Vr_K8*Y-HY#yS%1s(aINPyg8d?{VhS)nE~nK;5w~O*e`BaN=!M}2><3`MiTyHNQ zFn3X~b%Oa+cAnif!89M~TiSuwTbT^&m`h5cDq_y9TGR>&ZK8^8EOyGRbZ!5`1hX{D zrI_`3z~@6Mz8eOy=|H`VRGI2pV=exDhmXSW$n`!$Uz*$QlBG z(h_?qF(ZLJU!u&_fwIDbXIs399uJC15q$bG<$Pql=vtctA>X4S2)UBJM-{~$=_{3S zdv5Bmx<-vC@h~NJXWC23h#9J8r<5GfWG|m$nwP&3YSyP4np@SWCyxE6`-G?LJ*s;g|a*KBF6w{GXtdhnK4EyO0!EQ_jYF`84y%>~wq05vGOxsC&BU7#hRS9N4APUHL})?DW#|_6TTL zx!`~EP$kdKFn5*V^`Xn&awLVPIdG@)V`iEb-nyFPC}NX6Z>DJ{_q{XC;V0=}QgqV2 zUkKR8fwUAHyz~hTYQPF7scvBDC2ODr#vs(W+rdQFN$D;hWw5-DzBJRcW!GECMVL71&s1{Le_zRy>RF8GI z?aL0#G2Mls{-@x1=U)fK52VH9sXX3#JuodhTCc~Jv-pR+d|Da*Z>)&=;_JLB7G1yW znGW zM(f3@t$a%4s$U$GT(w0RrGnLFWguAl>sdTm9 z1W*YbbF?{$qb=TAw8E#Wh#U~mk{o1?0$H0&vc%W5#d!ToWau`>yi#3B4)l^TsMLyO zB+9kPjaHf?-*5ZD)!jPWrBh8Y3=w=pHOl?TnFaZ3CVQ#+S%{Ix`7N%u;Of{{vSHig z%vNn7`mB1kx_VNaZ_~+z2j@+x7*h`iDXVQ)?J0GKQ(sA~4$4bwmWn2=xj+{WQT3fk z)wPp3n4X;3S$R7NQ_9b_oO^c4G*QHo6alu?3A2+a-1>&$p@+{)`!qx$ z)yqBP){h+GZZfM&2Il-+Y|-Yc5zdGs}U>UfdfMkkfn$w=mEH>BX4B> zmR_5>M^O(!f+oS5hsIsyUPtB@k22_(Qu3>{BL|9K%mIkVf#Mf)03nTke@PY)L?IMS z#X`UjIZ&r!4&Xx$6hF<;utWd^2a_{f3`pYiPz<7UV=fMVzZoL9`wJ!^=IgnW;h0jc z=~KV<1L}2T0a8I*s{Av?6ST>h&#DBF zx$ICkl|{U$!tKX0jvNUeEkn-IH7E6o^qw;IFUmbt7R^-p>9VL*-}jY8Xq|EBTo$cR zc%Upg%3gP@S<*XR$(>cOsTe}QCC*A#8}^CRB>U{KW^$ohm%Dhz)S2)t<;<#hOnFL@ zDOe zMVNbR+sy^{WvV!BtiURvyhhim9iz!R-`lbQ$4j8(kd4Ve=9vmT)$OzudI{Wrv*#aY z{@1acbwP4r311uDXJY9qHdUy66;CdI=}-nsLU6E31l+S;qY6=Iukg3E-}IG@#N# zEO5V$U*TX}?x%}x*`0|T2T|NRbtf;NWdQ?bD84SkbftNyMe&1QO;+DfmVD^GW(<|Ol`K69JI7^ZG=^48hUaD7DNENthFjd6O*V1X=cjS5Tvh(x`}R1( zPF-Rej!EAC6se#Mp@nAU8~()@%?bRgpA)@h$U!XgI#<1O4|g`%4=pj%cx!mu5{|MO z?QViDraqQ6Vf-vozki2P<^M>k$<{12|5*dh$@n|$&8@J%SZYq>qV3cZ%=poH|2o7s&LZw*G@HOWm`hZ^ISxcHsb3e z-)2{xZWc$7Ztj_nibS8yj!~k|j+ji*Pbk)BzjwMBpY5`*oNgvAcU1e{%II8`y0bDm zPOWdL#CC!3w#tYDOXnN&D21CUMXm7zU+Jm$hDuM(2P-{w|I;W>?GKFd)PMh|sL6i# zEhgX6N#pp}E*?JW5^tmZ>RZfP4?_)*P85%AG8vl%UKr6aT=l#v8gGw0!$kdTX~`~x z-MdVOeTqV6XN5!tzhU&*51nDgF}}ObFlz^J#o%_lN*yT|FYf9$x~Q8Ak6qaBoH9(Y z@mtO0;I>!nq&bJZGK$Z83c#Y+$$IK&HLadM8N5Bq%gg6#nr4Zbx z2rfDCls%RshX8mOD-G2ag_!a49`o361@5#I9)(q*!gu02OCjcsI}{$L+c1Qjo zUN9@TikVx_05KFO-nJMUo-!-*y2_kAH*;e^_;z%Ngz;-CIXQ!KcufUw%+BZG6;~M- z^0>kO5f%CvT9Zo7b`$h)<)+Td+@x=xaNnj6`_>QU=-c1x+vLmyS9$zdQW(l7vtBumDsT*H%mwLSY**RuXf>h(}%(t5(3y<&LiGd_OgPbEFs!1@*J$%ix*IWk> z9MsohPyJ8T z9ctcC9^zPOb-qJta>lv7+>djxJU?9?y{P^+n&B3M?4qKYa2j`7E(lvV#_Zvi4(GUs zMJ@8%<@%AYkE;B*r_se0$C9?#OU^Tg9mbYk$CzU*IXY-^)G3|y71_X)l~>Sk9sSE~ zw)dT9wqg)-<~vN^xE8Y@Y|$ijP)iG!xQO$+C@~Lg5la=d*e&lcCmrs}I#*2DKmQw~ zOMcUNzB#EatsljKbN|rd+&{F~!ujU*1=C=f^DWYSX2NOksmRz#9CP)KB<_Q{_))NH zn#c|vtcxEf)nd=Pz#NusvDaK+n!Bc@1)m#M@PI3L{{`lyf}{d`Yk9k&X~p$jU+$fg zi+5&ay-aWz({X%wm^X^NLaqqpOa>SWm&qhi=Y$lmzap+(EMGLs=)GnU)=`OCCHU@4 zESF)vp-OQJ=2y2x7nY8<(=Ig4y*K``=vtsohxzMCCwp66{HIssv#Zs9;6l?@Xp!Tt zmbhhZrjlUyD<0okJiZ;OyCBK`OLuWN71rb)kWaef!wZg^TZiouh?vZf+~hcX&3lu$ z7I~KIATR8=FKoF_{#!VT3|ujL;zed*DYsZRLMCw|FOf;=B$MD*YhJMX75mCXur-Y~ zf3eBW=@nu>5#TMNmw+c35TS&6YcTk!$~PWYS6vflq8@C5Ax8Z;J%I}0s|Tybt1mW( z|IJPdB6w$Ake5U5ipY+;3&{x>(d zcZHY4y5DI+Ap7WdnrRI!v5{Cc8tIht;}-j&cbcw~F~rin$l9<)PaP)`AcU{cVR@+p z_*c#LzIU2=J&DjFN-3IFr)7k8*K*x#G}snjVwy%{G+9AMW3PIg){&a6a^KCWH3=Hv5)K&GfDFs7g>+t5|>3BZ>GD zCSw10bHue*9Jh)ZkApo`mvxU&<7*%%r~OfMgK1B)Bc#q~X}%Thhi+`tV`D5zQq51I zYS#ew-mP`}YCCN0^`)O*`G}ZfQaxeIe zE=WS@$^8Qu>QK4bMf-yVXiI=aJoVQrT#}CzY|NtNX$+%urlPP>Gljl{k2f-R^rxOt zxgD9TMJ}f`$zh!unM?h%M2o7WDZI(r=v_3wH1)C@OB-wr>x)Y>H1;=HAz7Bgi?9H- zkeT%at!X8d+0>`TM*pAE1 zl!7qZp{U3b^ih%q=ojyU-^;Z^FR9)qWOUGST-r|6vMU|esn%U5w7&TkP6J`BXpio= zjHPvzzN_k7_9RF5JlG7#3KL?M*NZRk+fJp+8$98}rTNGqbSocf(YOj^8r|b|?gB!O z0~^1|Y2Szvh+%?z&^IOc+r(IV@uxY2&XBIKsgnjkXNHRRp|S-_Kw{9jZwQ^ci|E`W zP#HpJ`BoG<x!#6?8k51$FD#*V_$PXLrYj*EY?3jh_4;21qh`iukxZ> zeK=89KI%i>sFv+6${~j{4kN3V3cFY+A=JS?+WK|enE0aYUuW|0)63SG_hftQL+i{K zf~VGT_W>@gI_z;5pF3a+xcMWZ2r_ku4z0@15Z=`hH|z4O(XpQ{*R)-TBpa)_9YK}$vdC(ejvyJaD z&4vCrb0Y38;Al>)D+kTtRd^{f*#Png=p=O=hhDIhTG-+Sp!<|ingIoGWLzk^7@lN^ zaY^^#z9R5J{Ly|C!aiJdr^Mg+;c_tNw_&#<`^{N9@(ALOPgIA(eVkGEd0L!PbTzN? zL=L|Jxg1l(xkm}eDW}op6p-WSfMAY)QkWY8H4kzrph1rkkSihnIqd^#5MShu_!Bsg zOSq&2!{OFT7^QHoCU$OEm)E?g361qPvEyMA(B6Xb=;sY<4AN$>e3}H)DWv=&LF=H|I3U4 z0f#Mey7w7TT|+_s-9 z;t|$G9on7=SQh(%_nW>RAwD#$ANf@(zX@9Uj&R0F;AkD!vL1ZDnY;)E%_s_E@l97HTeUkw}1l%?%e<@hBl!#fANAY%Na&qf+2V5tH8y4eX`@@*LOA7725u&3&d>b%ok}}Dqv$Ql zJBc4fv^O4ubuWaUQ&n#g>p*5*)ZuAH9r2FUVO=K`yWN?fbl31JC}pn%ZSBt}L>N&S zY-^kS_y@V;+Z`L31y;Eg<@}_MNh1Lu&k8^QpT*xq(En_r7eHAnuUN6gn~9b#;uBMS*Pi*TzP z)#Q`>5FNyAe2=mFBmT?gGc`jvGyvK8HLHwE$k< zSiFFO?U3awG+xk&X6cu^((=Y%^X-s7)}50>GzI}1S-jvn2T77283cl4r}VPDaE)Vv zb>!Dd;BmA^$`7{LdOTqSnzWqesD*M&8ncq+H>jE%`>Y9|sGAMMT+l<*NkWV)aa>a; z^r+?4;&oAAWWmAA-73o4OJ@a~` zPFojo6fxA>+M(V;GZC67Gt^t0g!Q#$<|DYK=xjN*9cX?WnF3qW- ziNFq|q&@6Vhe3|8BWkse4N8Fzbq>}!dRH|idqXylbIMaN-YE~^0V9a1s5x_!l<9J1 zY9(_*tixz^o)-ad<5d-xCm}Q0f^ZDJ^5X`=+<$O?8YkysTYK>Y%o$k2>cw?!eC~>S zTyemc;@T!q)#9f^y7G;hMtj{gFvBeZcT<9yU+p^`pfq3Q`b{50EM5k#HTU%7Gd1zf z$57{vkM|MCEj47DSAjG`t@gjKHT~QR>%7j)t00SO;r86?%;|;Jn)pWU2f&B29WU@vAXT7-yqJyh_r)AbJvNg4%Wqrt?o%)MMZF=m~>&^Lp@g#|tJ=}T9 zZ?5Ms<4d;b!v?bZiaqMX<{W9*KlNb~0wZWJge$0xhhulg5WYC!qmJ5HTStX&bHu z$7b;f_NI=N*J1B#A2G9z=NNTYz=p$ll{>|fuf*i$h&#l6{8fN&KmW*t)E`bWCJuNX z4}`#4Q+{bbxWVNA<|Cf;V5{n*rjxsc&?fv=Zo>E$&TfpI@lnuFCrdiMbtg;yj4L@^ zVwbGa85bwgg3*v}uvd2xVA;^lJdcTY1?s3Cl)8iQ4Leh>(<)Z$5czRzSrm5ZFug!| zPT5;@9710EN$Lsee$E@+c?h%T&h={nb9d+sv?o(QDN!3JK}dgr+Vq6G%O7yJV-Ga$ z27Ika)HCbi{kRn!bw$1pKtNLirt;)Cgx?K@2i3+%ufJ7WSWfII?!s2%6R*ti8q!uz zb2_PMk=W*G>=1lhr$v1RLM0XB?=9N#I`AoM$|O9-lCKBp;{<8Ru46;;X|aBH5G>rjUa+Be7hy_WMa)9Jgk~M3^h1EvWwDB*BtE9&8Da~1!lZqnk1j)`|;{niQRuK~ER8DU40hQbjr zOng;wR2p)UsAdsPK&QBX&Kcw`2fH{mVj|Rx)PoKx9YDL3Ofm9+x(=dxKPNrlbA``cjv@&x;~IZjk87gMQUss>M8 z9n%Ac&+3AKR09DML?J!hfp6D`L5^NRC^tc-N3d+L|1`khu5*4F0@C-)&dZ*z0`)MQ?N}Mw$oSV z>z0+_XoDjk_Ynv~*|G*@i#snoUD_75{S#5QGsbR@vJL=2JNPpXkD{%P4eUxF5=hi+ z@5Ww9RE20e8XN*Fm}&vXYX0wWk{m=~%oI%NDJe{3=2DRCayfcy!sT$Ji7vJWyy>yo zjd;t#54AK*kZPS}w1gd+f3U>Sgm8@%P0TtXE<8IYKg^dKTb+o78yCZ8T!uy)*_R!n zIdbn%Ohc+Gj>cil@X;(O4yVfvr^^q=mHf-2Vf|TyE=JR3pWSHYJ2trUy>adev4TBL zTT8z~rnRUGlXPCxlCjc5`m*qGe=3UuXO42xK5*D2bg9cgnU3pw>pYuIO}-fUXWlFn0m1;Xe#agLndIr$C@4$8q8d>vS1HjEQlM&1N$uJV6eVRGigJviL<=-WSuaYxAy*qLtAa zpY9j|R()hCUTPNW`Zb*eofp z*9Nzm0hVLZt){C5!&o2^w$vS75YxV5&%4$1i1!!se9f(9*%UV5Rry62lBCOlv{FH7 zq?9i^X+_mIexQ~4Yqfv8)yzB59|$_4&@qK_2y;$Eaa#aj;(n`y07`QzGhI>qv^Ns| zq!%L3Ui(QiaE#OEB(JviNM6xtdXMB45fd0yHRB{6F5hVVTvH&D>qM)4_LJtk3y>>3 z+Y+iK73*U=ssw#Z)fnPl4v^u{0id2MmN(KjXC9;V9oW%~+ z%jmA%Je@2-%y5w~703-2i5>I1qMIKbI(O*a5Co}IL}%)7dFw^=F2rxqU(ttRnO;A= z64874*tt4iXmqpdWOPT~!yfr9c+2Sj%6z_dyA-v3Q>Hi;^XNDKh()1z2OvzGQSMB2p zYQu4|vlf*S)ufnU3iyZCrko+P&XH5`fN)x+GCiklj=~}_{{u(vj`#vdVumE+xr)1- zskJs^YIfTnQv^t1_z4p{COm$aEhuJ+iNPN8SH3EKSk`-~!QPPg4MP$SlfUClzuGwo zGw*osuM=HRb?N2Cct6=O4i@+Mudj@{W$Kt_d)TMVK=Ad~?8%=tlg@qUHHwsl0awc6 zT<$m)JBa(|W*S2LZ>66aR>h6;4UM$yNh&Uqnn98 zeVzTmr%g}81FB{e)$)VJ+;J9XAak-mv`5`;y0Sm84{b3`_FcD|X~Dm~X1CmKW@M+> zCvG=OvrqX?)9)}%!9B0p#dnw~T}h8zTl6SSs?Nks)O+(CW)}5+>kiZ7>ut5K6Axa0 z&Gv2?`mq~nGe5rmb^29Kzgqb5sx4+#_``e12^dP30G;h{G}ovcB9Lg0U1!7WgZAfJ z%q0z(lk=I*FcW5Tb0a*e*+)NP#@zDxM;EkixYyMZg!r@=%&v-=(bc(mcGqXjdB+|4 z8@~rAGBW!=NEh5O#PC#^YQmyDQ*&)~#$NtebE4h0KU-lZ&%_zjzkin5yY)JI!Chud z#q}&foV#V(v#r5_Ywgs3BL6}^L0Mo|4)>AfXHadnFFsSk-N#c zMme`7pDL__8TFfM0%zTA&a?0QoEdv|JeUKH;54&gc8&fa11r@4#4_wi@dH{E{gd07 z&1a!E1gF z!MwEsBZ^FD?0NT@KKsWnnAZwtWwQluf@}Kt5<>1Z285z4_?v93hA3&Tuwa+OAlrSZ zq$GB;Jj2enTrIT3cuUz#%}QVI)YbVi>MaX7g(@R(yGfa7556kNFK}m7_PazEqF1QZ zMCmlHOqqS8%BWCWtzvpjr(ZFt*ve=!404>PoHKF=ZqxefMu9b$E&;viFm`9lvIYOY zpQVb#P#HfvtUi1cOU|F@N4`?GYet2DLpW+_rvnSwF0;n#B4w8^4cT?7;Gru4i3*MM zfFSqrSdoXg!e=C18!Ij;@#GjV;`t9rkv%K(+K{w?MV@mEWvxQGEBc z?A#y>?D7|RIlb+R**?4JZP_-mxV`%Q!|;&jO~5*o2D$A!t5{NC35SWT;>e>@E6UpE*GW59{#HMn{w&9 z-?d`OyWVr<%C#%)w6B?I_JS{&@n@gUr%&?N!rz{Wxy(6)pUe86r0MUFZY=r!OVafv z>63#@rkeC2o2am39x&ZU?h0}lwRJOpYU}Q)zOBzDZKdgNCT+R2UGspk!EVF*l%}^pe4T!JBR@Qs z0{=?m>Rg(B(F{-5m-+M?DjL6Tr6%{?7gJe#c`kDSm1LNei3B4H`)=3x!W$@juC3f| zCUzVk8d84Um8|m0%Pt95UUvC=)@3qX_QdVnHJ@SEP%3+d-Mrl#!It#sb~7dWfc=xc zJ#D*on9tUXSYg*t!*REd%4N=@-f{eu^OvUa+Lh@pNyo&sis|=Lh<9eYb4_t=ZS zVwSu${j{A=eI-R-BwqT{-;GFDF|Vaf^^l&B{@!N)#q__i?;rdZgLbDXmwB7L>0ivm zB>L5VF-MgDpedKRc>Bx$VkVoBzdLJewl0@hPTd#r*U4X+ekbv8dAbtud^*8T^@T25^C&~`sXHLnAQAD;QDeg5Cgyy{(i3d!2nY^Q(I zoPJtQOD@wvdKRG&X(?eX;adp-F$XShR_1+_VY)JN!%XIZm=iDlyUjlFO>;`{la}q{ z{=?K~8=q^gj=Y+xRUry{f+>ml?xv7uu7)Wu}Z?NwS9IIzmqnM%S|!U|Vx^0(n87TMFjZDx(WoLnui0I`d| z%eQ~(+vXq3v(fgWzH2TqxLmmFar6D`JM8=y1rVIgKI6Ym{*js0k$(NR^lMW3xAvtyro(>nN9I~8>Uh%3D^^rVQ9=C@yW&Z6M6iCq z5nsFQNmCC&v)_Eu961bcF8hgDVo(3E>8iVMf*)oX;XCXHe{3$wF0udhW3xIqFu|Vq z6zm1z`lrk(!+xJ`MQwJ%Pt4?|y?cPByCx1Jnl!Ot#^aN6nO^#J0U=Z|^Avx`M~=h_ zCTC!J9N{C^v6e}?b2C>`@Mx@pbI3)$GS!$8Zy?MXj39bLgRm@YbUG5Ddu3@hf)<2ye$ z3(7C0*mB$S3p0LB&vZ}7!0&Y+d-%?kE7k~lQu;oX?}Dt0-h0uz-ZQegw?txg-b#@R zDe@2lnoO9ccadIbuXxgo5B5y=s5{|la}`*A_tWN>B9?z7>FW&p`=`yUD5VpRLsm6i zY5GjCO7wb$J>{2Xaq-vpvT13gOYGghgh68l7yQax*|*}zT&A7us|mH;UqGnZhK=B7 zzcP{V^#MLk-ahKrW?8l+9ru)3IdM&yl^0%=S!6HSXTCnVY_{JbSFBx^$tiA^{mM+V z>z*`y!J^qk+Ei;l_l%i@c>lv^%&fY!zBzN5B-{C{x%Z8$JL)&4ZE4CRKbC++<(ypR zWIAy2C{J97W|ps8C#oE-dv|#8%J;6kctv>O`@;{cxboe@hUM1zpMPT-T3n<0A(JF# z(s$xVzcEvS2j|-A=gi#T;9PtBbL`JE?E9aCqMvK;dkzlg#(B9+pI^Ei^ZdxVo;UZx z@;&mrIi-5rd`|!$n{V?kaAzW8k9)x^&MvSwyA9q}W@-9p(%QJw^diy%#g8MXnC-#SN82&`&6U~Xw%@cL!fyBex0zJSK;5#z zB5br%e``))9X|M5Gv;k4{t(p9bn&nLd+`Y$E}*^Och&s!9ex>{MGNLXVc(bYJ-8v8 zHLt%dlldJh5_~?J%>>&hS@vaQhh<+P>+9rvHEXkHmNk{XydaZVv%TT~3b*V{cI?Zh zKfBDH{IXfs{WwJ{`b;)sJ~}CrajEL^Y?845^)lxX=YEUH+DdC%W@j>Kf({1XQ>Shr zX4bJI{2r23*WO9&$B;*cBxB`Yi0Of`R1tN$; zNCpx~CNYyp5MjUr5m7;b21Nl6#03FG4c@G~DvGSA>#?q`fVv{;itetu9{Bs#JCYX; zb@}iAeLhLL`lza|uCA``uJ^JGxnYlCSRQCcMjrZ!$JPWv+CIe$woODv*iS1M`{Zfp zaU05QFTni@TYwee?Wd3h?zJXgV|v;?lEvt6?P23?heW#n84|g@9ya6vfDggY@D#?* zSb`3|e+`^6RE&v|LE=#$v-iqm?5{72P2bwx$)49>J(l1r7f*wlJ#8H{jdCh%lKs6l ztlZ+wZ|#-A`iJ49FO($y_#&9H-2hHVp^d2gIi>3*yA-zjKJ3%sXG;&*CH`O?a1d~{ zWS_B+vHg^e0^2nZ`c!ID3uIobO}hyAK;o0JjyWm)Ntb+PzhEsqnjPQ4lpKGk{q^tg z<>l1P7m#4|$9azw6>*a#BeX`6BS(p_?=gpu?GmHEN2qh2IPyI{VSYz@*$?(dBx!Q{ zH$Rd-ULm4SVrSM(^gjt@BYvZnpx^VM$73PqsH0%`{tY_!QKKQ}QtZ4JgUP5YMY6MH zn1zr_)MwC~%mIwuNC<96$;KGQwo$c5xn$o0VLw16A33^~FxwLtZ`;r42S1#E$$3Hh zXD971{pRj@)_%z|6BZWhZWVq@`cxkG7GqbzLVx+D=qvH_(`m{gkQ0Nyf)sTR#yM`w z8tjGMz~su4;(mbSqr{yOuMDP8)*CT@fN?sEYRXs9&KfdA#-n_dA9Dz0-%;7u(ay`Q zWnbsVe3S%D2W91_;m*a@vY+yu@1vdhC_hEJzCvA`_%KD0hO0N~m9w-8$jMiaTdONl zwCg1552N~a$*qQpJ8gVy@B8m(jLee~gWfUQ5)_9Oz%X9+C*+NQP9_XzY_$lt^ZmBt z$>ME0&z{y?XmWu%6_L(mxDzuU<-#7Gfv^{u7#GFYBP1*x%O}#QS(55`Ese2S0G=pS z`2cD2@vN14sMAFy=lMirIR}y8uMimtB8Lnj9uPTUC4&Fsd^RGg%u}Q+kt6e=Qkj@1 z^P1p5U1%CN(lpuacn$hyKJG%*@w^2_uz}+QSt{d+zFu~WvcNqqRF#DmIIJ{Wb^Oti z(JedDF~|aU4_>8)D5;E9lI$DQo{rf7%qO&eA4ht!2{9HrbC~L}z|Ld#D3mI1TQG+C z)o6#q!gKeF5*$Y@aF24gb5BR7Rji!XLp7W$#aj+OVBqDJn$mGOO1adM5%Ho;RRfj* zk7%$#*l?4?2!*Ev7o+ZSbVOouR0uWP41gNBsL@^m&_LO4McHUebZ#l=>|?~UZUU5v z78Q`@M2oI@G9DC?UL8>Tl?#C>zMT3vUST z8Qf|``#YS{Y_Md+PO&+eGBmrBJ86%csuWo=;z!%_mF>_|^NDxaV-&w-q!T~1sgCX7-41* zefLV2<5@F<;M^!D>8<&M_wMFAU?~qTii-IVaD*MM)u8S%Um~4J>9Ta^+tzyd5kR5t zu?UoiGb=mzC%;!pY9~Kv@Eh3CNDebd5-a8dc1~rLij+#rh)H|pNM)0ep~*k~2}unZ zkDOURnE>19*^2>f0}%h7%QXT9Z57gq(-7%s&DYpyK#DCheRTC*uA)Ih*ei=aZ*2?5eRNBhEgiME}2E zSq+VC0y`ZVy9$7*vD+wZXzV_uts46dH8M3eB&c)_elgPK;MXGEIr!_fTIS%NqO$)_ zegpOYG{?c{-hNQ+=-x2^X7}h2+3cPNX>0ci0C#k6764PVS0QcoZXeQm@1&KY(Z$ox z(KiSe2VA`8Ao2~0Rf}&>382F_Xd~dvzJW-I;CV@ojf~w%uElaiiv4c0KCz@(*b;m& z8XIKM5W!2M$K8wT52CRjUM{n3LDK%H*cZVwCP;#@0JZmGV&JcSS3`f9@`auLZ2EH< zV};mLOQmWz%Am(A`y_|4f4!=esmFVY+DJa$_R*W-u}FSiFab$W7?LjQf-eB1@Hs{9#)WIpc8 zZLW}HU|9kOwfCJ;tg9A6T@OH8qLrsCeD>ZV_44dT#%>g6Bj&viMh}@ezOdG~)a`uJsL@d&&7waUwHeOOEn|S~7wgJZt^1UWtO)FDEi`&R zCS|L&K?_n{h8rxs@7*pXDDDiMRr>Avw;b-|mW<3e%n_!pw2s=9HrPvtFT1l@dV0F@ zy=5HZE4ixB?CV&CdXKcJ%3v#df3T}cft9-hA&HJt`mFrNDg#U*rPb2HvkXphwPh$g znNo&2)smqGb4>qxtEJnC2SU1^q1z(cjiA^n%ebUp6{Gx2mubcmITc&YH1U|5=Sp$n zLpL9qGYuM*iSw@XYkGOgU5pK*beyXAwGENpPIAdn&Vz+wLJZ#^Z5N-!@Igs_kg24) z{iPV?dP`-Zq3jexJ-m;UAsRi{`%V+9J>1j%Oeml7@O0PFdl|bBF69IdSIN{+r zm&SWANF(~1tYcO_;86-JaMrE1Y&F%Iai;?vV990^{spsb$^aVPG!IsA7f@`I_^p`v z@9q=}VtK+8&)qnbr~DV>E{(pdHtN6ZAU4cLI)MFvh2WVv631hCdeXZ)f%Z>MB5yv@ ztLUS4k`sHC$dBXs!6Vkv%TzUlwnKX%RjFQ$@z7T25CCjDJhTfsLFwG681fn;uvnaJ z_mfFw#_nerrOVPCU3N{wlJpI~Ho3vwZ`|>BMN%ApE~f@vwqe0=Z6BtTTV>jy#;7wb z8Cg4}zRG3Rj9pTm+Q+i9$yy_Mm@?m5}w@ez0aCaAVor4A;)1lDj`^@MNRY^0MG;=p-lBWi}Z3zzvJ@Iu5B}=cSLBLw!KK3 zo3=wp!M@J$bKS ziy9g7F}hR$Oj4^F7xFxS(EzfSVyF2XfLR1&OM@MrekML-{65y{w8AU238QvV;49Q< zja$~w5crQ8?f3&^cSjbPadr7DgY3ll58=-2~Cnq+xc#z!m-BAcOL*# z81Eo`W??9Ut%xQ_Jr!X{BZRt?P=!zGR~7SU@pb}tiHJnbZAU!9oyd#2&d3$2+Eq7i zRHOcX>83U+nHR%YOQ50+%P;^P!%|7WIStDyH5}t4@OjFr#OaT~IekW!=nhePOjq+S zI3r&uHYM@sAYDnQ`*Dc=b3nhJ_-}v@FVinL)GKlD{>crvrP*U(OP&O87v!~)QOj_5 z^e3a_u}#@Vm{rz@2oX)IL%tp&|HYDv`jx03{f8=RGLfg~SA0Z`^PxV;cy-^-{9EKkj<+ zb{IvU4rT1a?Rrt@tlJ?(@kR>I!$0zrdT}@YZ>OghzuHj%Y$x`J&3NHRx=I}B#RuX~ zY$~6N|NFT#l~1rew?iCC<%Q=B!9AMeC9Hee9hQ3t-C@Cmx>U*jYk(+8j8W(#c&GIE%h5%2A8Gr04Y&??0Oq)LfTxeZrBXh!LyE%1G}5soQnwuz*;5;KP=C+(wqIl6u0 zYA?oKe^vaH#(Uzu`1o|*R(cZHi69LzT z;{q?Z0!5Dis=;o1__a78AixFSo@&M(g}@R|fTQPoBPf*`-3$y}fG5|%TY3~Fg!@N< zJ4^KJbYRJB#tsrcFN8>;kHH^2_#%M$>LozF5vJ&2=x*YR(AJAF(H=4=*(TYb3;&o5 zH-L&?K?t9{FLZ1Rl@mSJ2wVq4akO4`>9Pih?=yIuR3)M_`FRQ7-3Kd)LCl?Z5!i z=xS?_sKIX))l~kh$Ttq{p$$1hln+Rsb;w&CB%X-Fs#*nyI0LhJ5!>>*ELYoqXN&K0+u-ivDrC)%_DKgSpzVPPqNmEl^ zLrm{9&ktFcFJVYt9O}T?dB+p}K2DD;u(lz0_Q>w2z`{Ski_TW~a4)AKcpjf^`}R&z zn}!}bx*B$s!Y{`FR2X1b_ zAAqGXA5Egxo3!9BUZqhpVb7VzG|G%Opg zp|DZ*kDwMB^fDYED_{*w$}M4R)K0|lATY_^1!!($Y^_;h1+*Y8jWZlL$eWVxgOD)I zBfoRFE@5J0A&;({yPYC~z*xVj5QGUVya~4P0i)!Vt?-k6rvosKTF}Qx36|=wYZ<#6 z_@Z9Lx^f5x=nVzH-_eIdTZCG~R}c96Jy?}t-eOi`378C|3YGu@Y;;#0 z00P)P1r`kuhl+S?a2&*g7o{b8ceoNI1Y885{(gjKVJ(Y|2boX2jC}@-v6%4oFQJJK zLkh77{|^0W5Ul-60Pe7(=xIRdAQAg-EYcz$G3PG>;3#Rom9e>CkN|qCRDWpeA!_^b zq;B0`fqRw-#^UO+;E1(-d69IF*x#4;Dp(tX9d!?Eil&Aj$iXrV(tT3Aa=Y6^2Epm) zPI3MC#VHgFm9@kng?jsecPtE98yygb=78w&QEU3~k|;B7DvRQ#G(a5f$A?R|i1hw^ zdeSM~4-fVM1@p-(!G-Y@i=D_D=Tb&lV0a3u zGTWMQt=dZoSWPCKvp-~+6m(d9uLU;kNQ;gosL@WmR*g}HSvUcqREOJQ2lK|2JM9*@ zPZ6xI`gk9cb(jG(0pz(Fh)=lC(Td)#AUF$&Fipe^3xT}Js)KZ)+C-2yO;wLt#wY40 zDUNyGErc5Y1Sk!efaVA@WwBfAJ)Lu{xTfWT?s_OjIaQs}O^f2FMOqL2pqPk?;CAB9 zcnXvh$1N5yCV5olN=vzBjSpC)Mu82_SYShn%rgS5mVgprTxP*QM`A;hO?q%eHTL^(O!9(lJZlo#LsLMg`Z(e3RbWXQlLwCyYTJX0;Z>6Z&-oaO!^DLrR^#Zq2&^!Lnd&*6_#J}|m=AC;4SNLCuOKc{!(NQ^uOiPra}<*E zRP4@w7mB@ucw&qmR+sb^V~;yJBV;q%zZk?-Nh)psb}+w9>hp$W%=r!Wv^;kyc!gO} zj>Jc7>Sgi`0|4`q`>-e(%7=>|O89ly2rnJNQ!`UB=A%)cud-+6t7trn8pAl{XJYcS z0ic!GVHxyMvuz=KEmJu_poltzZ zL5s3oeF2Xz|0fo(s811e%LGOb2;aV?wk!}GVeG3twPIxWivoSmgo9@uD{GtfGYyrwXK^cN>8^`hjm!3Q5 z4IKMG`^rW6IPMmI9LsZ!l4E~H9CT+%^tg_aMwDDFA`U-f1 zzS&*(!uWxRv{aI81#f5}_iEeyL5-- z_yWemd>rd=y9Ub>k)YH30XT}0Hfi39Avs^iG2}lh@jfY}NjS>j`;#k$)0Lra^!YFNQ1s zy7-}z#}ywoFbUKTNi=Ax&j2a_MBF@tG@XN(+^oHav0Dymm?}raVpH{+E|$)rM!Q|A z`VQJP5f1Qq0BH&$)o`d^1KMX`{}}1RW(oM51GDcxAj+q6w|sLB1%1DDF&}FC_!TXy z*W_~SS~0IM4j~6RbPGhDQkgws7!F~@r0Ed$pI#UB^!KuZI;t0IFh=6u>HKl+Xr1F| zo#&?M$K)J`;k@`#{ha;ul7B4KZ!ENu-52OL7FwHgDJc$~mC)H)IG2*MF&C8h*@}Lt zqzelRqZoVc0Bp}7ev0I}lyspp=^`9LKLTs4H9BL_ub}I~!fzbB)7u1z?{gjT)6xAp zwo}?*HKMnDewhUr*w1DB(k>GYW4)sbI2o5jya|W9)LzE*5kHrb;JGyVAGQge%6Il% zx-!X!PA&P3ef<)4d;2Sfd{$irH;(|4k8_)T@uo>Gt4h0g3+hFa;nNiyG#cH7)uM!e za~0~@Svakv?iaZ`JKw*?_URoGLuMf;Fj%awRamSMZWs>x<$%2C(Q~>_W$YR~!^l~O zoMByZ-a<}Amz)f&Yh82NPRn@-IsQDjCiLjU2Euvi+H5&;ILwLmY;8}g;(g=Vy$ktO zvC^vc_ttQEt#E(ChqTvb%N4_=>)M~FmoJSN{Ty~!Hg+@C3hLgHY`7Hljg8IT>e{Mi z@9f4_e@#{M0yZNVF99~!wfIUXzm(M$GS*PFV7AZO>aX+HwXh5^YmuCsOrZgDeJ#kX zuJu*V^VP6-lTfp@#oN@<%n;QmD zDrM)1X^Z9lVV8i?WM81QzJ)z2)-RR|q^;tm#d305&H$XQ5R0r3OJ=jLs>XXoV|`T% zzEfk;bBOSuTpmLq+<~V0x)yI$2P-&r5Z__v$^3xrox$aRMQp1m zULyAnQs^VZQqnun(hLUm{`?gli(a#^coCxj8(*Nhs!0=RHGMyUVqEG1r;Nlmz~8L* zXdOB}2XdrO>@a%wpc=!~!WM9N3+NAhjA3qbRXuygmdZrZojhSS1!_RtC9TD!j1JiS zzWG{u5LBzCZZ5|0B+jPhG(eJ&;jR=yn2LXa+7~Tg2w2a�Iue>D(C`LYXX6di z7njP3A@p_;M(rx`#ZuWb{&k!WHh|WgM!awS?+)4;^9=wekS>EER@OYXwZZ3anOew>8vU+W;7_h@Z1xQvjJksdkMzxM zomb1GWOEXAgZwt-2rt$Ll{&d)fx!r7; zKNNRMlh4~w)lyx{&g%!6&Z}$k27Fb`fDuY6j#S85A+2DAcoW0$Nx=T}j-PHOXuRpo z22I;^eV+-18v`or5e%)~y8(rZ*lzI=zM!r?NmCv+;&S1-PL7G7`yh=aLWHA3@otSA zH|hyp4cN=zu8H1nZ}zhcbVL8rlrb6@azChqMPs~Sn*z1$B$zZ*w-6K;ip!VDWn%F# z*&+T0VxIocK|`610i}TEsv1#PBgaP2p`mVvzzVJzMd3ys-TO*t*@a{l#u(o`Vf1oO zO`}(vgxaJH;3gN8YCYe9#Xx->hD$=7mPSJ(XN#|v$?k$YV4|+F>(M(XF7tQDr$2Pz zHjvkuA6T%6;h0z4RVBOaxJ(qQE96Aszg`aQ_kbIF64KR+TAS*9jE;&Mukba)l+QQk z)z%I?K=VHM5KE@ZF#{N#?2;Ymw4ITd_oldt7BLFlg&7L8&VlY>J~O=h*gkZcybrH2 zr!n!`OgUN%s*xkbhd0P0><@dG2pcKqhGGDri}+T|=jmeB3UC}=z{G+Pa#0VuZwt_O zWMFQMNk2tAyF#9mKml*MrqeRR;C#@O^|lV^$3}><8|AX3MU<#FjRJ7hDZ*`L&TDsWLYFnmv78s zb>ilyW*(Qivo4C5q zLXqr2-{r7<p9|L$Sd?7N8V#5iH{z;L&;EMQOKe`-Lh=%Xq7A+|})HS8wnIK;;&p0*xt2+auk zNJoItB~FcxJORV*V+uAfy2!D5MGG-J+LU1wKi|m{^^hWIwJ&9r=u28-lh|A#dn(_< zG}HT12wOm}hiMpRXba_e$^B-QGP z=F4I?gLihtv*WPf-Lz4TkT#0DHp;!E7V-Oy^2~(!AYV@I@)$Bcx1jHhx)|^t3w5WD zsJm0nnHqti(ky~8TC9M~reB-q8pq95^Vv?Qi1vXtERpc%7`P zXHSZSg4VTE)%gQnf1|%DP+eEY=nZv$ORcC_BPT`DYZ?}o*-UZdF4+?`8P=`5s-{d| zpBcUgK?}+tdwMSRAL|hje#s`eTP(e%rHhr(RlMdQl7(#~A0S5Dz6nY@L)^bf9uV~m zCNRVV#SYX}2N*sP(b3PGur%~i`ywEFr>~f@SuS$Zg}Pz1N?9(HgEpO|;?B+Tls>OP zf>zgTA_St%C(V%DjcHuo7-#|aWPw>gq-=r9f;b@E$vRXnhY$MFoVQig)o}Ct#?+;Y zd&5q{FwKRc#RIo4WY`3JBZHVO>_)Eyf;)Ya=I`)96KW_*$DDkaO7sF+L!IB8F-w^4 zG8VHql9pvt#g?L`PNxWt;+LIP9Xro4;K8sR0d_J~?7l;e8_ekCE#Kv>RkQ=pSHYn$ zBs2t_?q43ZH&xBe^g$mye+$tLJKDd#TRswk|7!c^?ectSofyAE9&USUs#vu{9wcRm sTXx84;vYNY7+iO0>Fft(xAanb*@LnoNdv{0o$_F5dHc0H@WM=7nTGF1Q5a|Bv$%6EuzWi|KBPy(1`x`KqUR#>ckpS7*d;*Eoa6*)T{;~P`)9fi0 z<(z3OR}V0qYe9`M`27i8*Wz5~e$Mq!P!IC39*c&3j5DU;&tJ%N<_i_%Mf4!UKV6F; z0^$CA^5P%Yb;gnzWY@V4C>+pP0A7xtzW;Q11A18JK8~^}9*8jR(~D3NVM7TBI_Dw$ zhkrgF{x9~af>?n0BlrVRb2OiyUCxd;!ml$eH&CKytkoH+z^p$~LjbSmZ~4 zN<76TF_x*;Owp3o`e{Nl%bQI(a|;Xd%?quC{HWNvpLT1bc}#E<0Wb2(_-mhaYvy2c z^PX-kS#A0Ci{gW|s9-hRSEnVDgRO7s_4@1asCD=GMb_4czeua$p>)J#(@cP`;mNd) z?=tHpeHzakW%c(x0B7x=d@r-82%6nY{Y+o`cwiGxP>woHq;;X2`qZ_{!gCqriADd; zjZ&V=7&b1yXcRc7#ts1Xo;BN*k4yb*BP+BRt|C|Vczn{J&OTb3&lPG}OL z_?vD;;vWXOeuldAvx14U0^Rm!b0Tfsm?#OFA;HYhjJ2y1H!`Dp(Zi?!Q3K#L!9bH( zGsyC=W-h}|@)7Hdh|p1Em}wL4j!>QW$B zT8|?+QiYmP22Va})#nZm*pb$Sx$V4r(b{)&1B@fvM|t(+9h`qA=yLSh?-X32v(470 z#iRW*(W=}Uof^ra^19Wmko|FOCKY3To&nm{q*SqWPf0$DS{q74@mX08;NSpGjf6V- zx`40QT;0|!yIy4l@`ihvkoVoovSg7@-^&`U3rl}ixcpO4kZDoiu8rd0Ucr*% z*A6Q4`KS?@$y(BbQ1YPZYbR8#E-v1^5Xp_&f_2t7?4SkK~0ui7QPcq6ja@&0en@zLQ4eJO!Fl0*_Bp3P&>$QgC%exml z*!fMcy7CnRS8A@U*70Lb%gHA2sRY)q#(bQu<`7hK$n>?3jC~Y&+mR)OC z&isCy?O1!n8K;DNn-;Jt^pc6zN9R{&0=2r{9hE}x0m|Z(=Zs5)791oSd;7@v}ybAtYFO@sF zqO#4b4&Q<`6xWVvPUne??j|?uo74Him-W^(rwfGN^%5&~`xPAs=PN1- zyt#$bfn2)zgiA5M$gMrrkpomeB9`6c`zZ&N261~!L#5&GEfZZA{sQa1iwaNZtzK`v z_FtnmeeXs;K#d5lb<4%ZWTK=(vU5LK(GT^$NOr*QMY0|K0xNTI;R*keHM5+#zaG#_ zh+gOlZf5^<-XEE}_gjMvdULi(x!INV5nrv!`XGbQVEVpod!&<@8h3w%w(X}o_p)*? z6Bk?4o69o2M1n{|`}X!?h&1^7^yRiJ|LnJ7+d%r>lEC-y_m&4~3xA(Z)2&;7YHl2W zdiRy)y+C@i7hBIX7hdqiY2UdI8C)6R$RWs_G7%H5c!*~uL&CbNrLe|LO+%lFEJ$3I zOg4zZ+~?e8TjzA9pSSi9}>h0|A6;{VHV z4pIAyw7!Q*Jvt4ohXy5D@9!l6JXC5^tD-|x+7Fe!2)}(*X*?~0De576zrx4)`B@}CCB z!)e`zRw4fmr+F!PsLvjVk{&9x9h!|Fh)Q3Tl<-g+dz6Pp#ot=1NXQ)j_o^BKb-ROHTTn^y2he0t?7g>;k-EkAH7nGaJI&j^fn(ojbk`5$qpa1|-0dxR)l?t%#yt|xkI%cTdiwL%8&UyDq$Wh^CnjWsZ&`W5~P?XF&skO^?{ zLh%cRFLH6&BB?I17y(mVP|-xFnew~vaK%PtbJH$a+pQt3KRTM13v2ZcFQ&T>+|*le zPX`s4ZdLy%T^vLRZdK^Gn-nBKjYkq*x^?=Gj=)tF>#iRyWjn0lEB?ket+VdDM_6O; zIiIaVfMGp&PsQQwzi@6~(A^OYn{Ga$hiP9uf`_O@7jRHRI|{de>YMS2D<~&fsUHt0 z-feYj&2=a^IXBJCoEfdaXM5dr>zE&ZlP{6hlRrL(oWU!r$Qi!6AaKj~QRG1D>nm$x z?cb$OLf71FeShWG3s$H&6|ht{7uqRt>4dNN>DuF0O=Iqzs3qS+#%k-wKN-Smt>=I8 zO5r+SL3dd+vkEtA;M{@$t6U!tHdzwarhe#);08#66}#79HP+C3uOw{lx%XmnlB@T| zZN}>3*>3C3)u&9$BE-xrLO_q19lE*9!TvN(*Z0iVaKB9b;8qVP!Ek{_GkYko=LT!S z8WGuqZ^W5sm9MXv7{BGex;3-L+-+Co{xuc(bfJmv-|!~Zpe};la-;RinxT37gzvPB zpAFkL`KBA>?R9MEz7ZWaSQ}s2Pxxy~tWWnBzG;ay@ztT!1q$yoteakqkIlz*BeT{f z?eZIm)>`yUrYdgVX>)EgsS9j$M=fPOhGb?$PFtc6&EWQyKUr^RxE00gv{V`$u!a|; z^(k?!6vW)aS`A!X5{$e=BcyBuQ?O}tMXOFLK{~pirkTFhINfvW!BPljo`KC)?I?% z#$VK&yX$@aLl z-%MsZ*S7oy7g0CAXSF`Kiv8RweaOJ?gon<;?;Q^f#P1^y)fB2bg1F#ghEd06bD+>$ zk`t^w4?V{Yx1Q_12*2YW&fxdg4===T<0E%l#jD1S-0&r?xV`(xXtv3!c@(nhUaRNP zBI{d^p3mFpb1Luri&e8}6B}>6wdoYrW!?W+fq#Ob#dPc4P5JUWH|dk?z>_GDgMZC7 zW_E(RlM_k&_VM}PBQDmWw+2Eacf9|h({OYZHA)0q=yIFzO}9!P9}zH9^UTzh*38Ez z@a0|B_a0wPV?*r|=hJP*8=u(513T$D=0t1GlN}f?K7H~WW?5%FHDoX(DmWz#meA5+ z^u8&+M&gf@sW|=4Z9X%~~6V*PAO5&LlM zV_O2um|{Ry;4+^g&gpw!we;Sz+`Y!WXBj7{f|}W|t({N*rC%Q)|LVS`3&(o@cZcz1 z4_md*wDRQ-Tg#uRO)YyEqar|?d-m+nS`9R+4$P6}PtTN44}28U0{qOd*w#wE>=Db@ zS{!)k2{ge#YvR_jeESoP?=ywDvs;DF*?N6y&#yqo;&8(38hl(P#Tr z_Fyc>fYS8^TNTd@D1k@@QR_4<&1@z~ z`%LW^2x9A`=O(%1?N-WHZQcD`Ejz+`>bZ)2hotS+$Iq1^vgrBSab>vc`EhKf^~Up! zx!K#nA>t9b2Rz}0-;Nqj_g9&@6-;OHr0-u1B@01KGdDYwg#ULBC8c{0CFQ6M^@^MJ zSR;PlKXNh{5ki$EPhNZO?|%@5h}+Xs#$Ki0z;l~Xm!ujMkyVlNswKmMouvWeCg1$UiX5Bxt$m*yR*7`ToIiRt9CAC&x z#*EUn|9JCeTumksVqN?8d{(k{_uD6NR%#vpZW_N=zT1>8yEa`eLMN`lot~h`IeE=B z*EUVMJ{=UeKYH?-YY|pr{r9^fRU(2XuDK>1YWi&bM|S~2bHs(Kp6b>x{ZGf~G9p;L+3a#c5_I-J!_3s&;DqGZA!C}&ZYLzELIg55 zA}KDo3MUutz6vM#HhdY||DQ`GVEzjZL4Sw^uDP3Xo`wO7qTlZ~5CD_pZJ3&{vE;`qcRWcp0t6E~0P6Wmg( z&o+{#&(P_bvS>~-Bc?ZU{D(oDwfY)Cy66;iW9{gFi;3mi%UzyT3yo)TvwT&HQm8vmgfiC9V> zz%t3e7!f5HF(W`jBrFZI;Lg9w7{mIDPmRNTfDy57{ID)TGe=-xn-rLC{r1D7Pl*Y? z5fPy{ivk{E4JIOhNy9Brpue!td$&jF|QH zU3D3SaEOWwnZeepK&WJep+mrexrLTIOKA4D*~yZdnc)|>65NE#@N1g(um#tqg9{q; zs}U9y34C3yzH;z&k@`yFYqt8zUEtw>2K`hOn75#5mWMwZ^jRu6e}R_~8uTMoXu$%9 z7pMzRfrWiRj!}U{*6fc8GsSyBs8MmLUI;=3l=Ov=Qh}ulyqwvf<3=9}E87bqh9HEN z_d;~YgrVP;EI)@FqM%K+QnZk+80z_=YDdJC$YcC4pTXu^Q$PMK1Wodj!T6o<$%KKW z;9k`MsmtqT7p@i4WqsBC?4VT$D!1&Gi!@>~2jcjkT3#41|>1?;N!{}ywb+-RC=!1Z)>Vpk>8xyoJ z0;DW9SaW85<<^EzD~Ie=Fkx?Za2I{MkCU(5+Wl$8U|mUF7g0hH0?#Rckh29IrLVRo ze3q&ph`lL@AiXKdt=7*{r7lEbbr&u!U0!VzTk^A0S&_Blvx&!*kjg8St~wC|2rk`= zs%x)FCxnj%5MR@EZEbCrFGyF2ToFMy?rfv(=skOlEJfF(i$sMG7?IKyR%TB{yy)aL z0zn8sl=YK6B8U)t_p`R{xjs_d#1;#G)1>x=*6`CySn;jjVn?vltq-wtS+O)2GtR(G zOu``GVt^a)A_@$OakH2(NQpon98!dIz9=RP@*DJ3lqS%$sO$Qt+phay`N#$7{-WR- zT%1pcl>7k-7*lzTC=mTIlqN(0NHRW2k;wyrAXpd~8HrKWa@Fy}`g%WPd{1iwgBA zKDQ zuzYiuJ=Bw@z}(U6fkw*p(Acl09xe!4YWN_9g)Scourru0%Yy7oI9CUmF&g6xwRhwi zsNBM6M4@|l;#wH_&=J_g)FdevER3-VN#!#^HX#W1K;!E&FT_Rw-jooV2IqzltAev8 z#0FMkMocM$j~KT>hvtWHLtFg3Qb6k{J}$JD?M%qsXY-K6KpnJt#)RcD%UK}VMM^-d zbeLduT2(qNr-j)QEGBazY+f>k-s9oRn6x6S4v`N>SmUf7mlDK>B&nk2Hg6CKs<^q; z8?@Q=w3EPt@dNy#b?^%t1M;LOy9&U!MA=L@r7`x9Uq#CoW9;*T zAUhD0<4)P^AhHpWbmw(;&?-4E&O|W?;E1Xm5p}+jB7R$vmC48Btm;4s#;d##~k?pGdMCxh=^q zv;(S6R%B3h-wY*jrMc?*EU%)|A*jmi^ahcQ4`D9z26eceF0j23$6rF;kHT0&K9S3& zg5ZffRtl#!kDVWkEfT&30_~m0F08ga1&a)ImA^yBkd&+9jYUQ-<{wPNWko)l1yGmf zgFg~-c|N;<#zVBNj25tR)T5?=J?J#&>ji8S3aTh%<-=kPdI#AHa=1Wo1b6{;EfuaM zf}*Uojkug$$cp12H>^Uj87=83YO%189Sd+T6tbFPfb*~x069n$7qJDYY$9wV`cpy5iqoZt^_~Hi^1O0Z&Ek&O_sJFI>?l~vg37!idG7jMMAJ?_UT0+7{A* z?+a)17Ax~=g?wfx+v0zoVRJ}!+{-GIwXS;8hlcUZD8t$#Fb_)r1{-K?$?2v(&+v0h zxv{3eG*YSfF}V%QlTXz{cul;Q9Vq{Kjx*=}Tfc*taN_bzv4+ zUq6D)W7QjfM;fxLkqBE%bKuz91G6wRVcTFb0=CH~MzYa9%$;NMvyp6q?@}ytBs+{9 zmY~63$Jl9VBAmQTwjIX$2PxgLzDD^L5(E&%l9$WB9L8p_%jB?8?B;%b3S3S@znL_nfddICFi9}|BG{6=RGvMW9m87Xn$hf{qWvVeJf_RpquFFRr-6l#?#c$Xl=Z9+ zj$tc!)6T8kq|#dcELexOqVzcIyg3^utDQizixQ16UxP!5%h*7pV-o_^ z8#7DrtQxlcA(kefE3(QZl_z$3Q9E6 z+y-YqUVjm5Vpqr~FJi-Z_XqNmi`Wmq47Xp*zQ)d% zpIyw#hwlD!w^pNRqcwC{h!Jxq{I#0ap#0E+zUlS9CXZ}p6ZnR|$;Hj=1y(N4Xkq;V z^FWVvAFf~2g1JXd`GT~*5}TNMlW1RQRLDQhVVg-Q1KT=s%_Zzu2nu{6pkK4OoYtx& zCbnYGtdxmM+0InOXucY~-&}>UAG}9$@F|G?GFf^V8_g=^ahI{d6+ODUE7Zne>-6vc zKhdn>4AB1U)7kVf-5=Z-f~MLRHS^FD!f7@|?d*d5Taka5rj2*Wzu60~o8S@o*8>#x zF1kNhKlM!ZA!84$zwAWz4Tehp{UkPo1OW6_N)tfT?eY`7L1U;1U}s2~uR)md(L_sy zDHVdUI+Z+6&ODh7XFW1=GREh5>mNLiZBT_>{WUg-vwP)TC$o)2fwN9wbL3N}z_1`- zY{G!P`xG|J$IWQ79C9iv*B1r@@|nw7N}hf;8zsja$9`M6{k6XJ>w!PZZrfzl@pk>r zI-X5o)M|g44V^sPJ+s*=dHC7v(=P}?0>bsOVm5nlZz${zx$0at9lhecbJ@b_z=<9W;z2~-$PFwC}&M)r=y5_ zr$b!c`B(YcbheH!`K!F+C`|h|uK(~THdgV_h@;ud)QovE*-j>3zKr!7*7=EtLg0vH z@MkH64%y-Lw|^psU(T9{C9b@jjphG}AGgf4yKm<#RwApfVC6&|^9nYo@{2K^eBuf= z%E?nK3+J)?tjCsz=u?Bg@x9n6tO7)FzvFF(O-(JNoU{xJ|=v(vedHoCiAZK39##6ug?$vA@q4D_D ztaj+Pk*H47#;Fbj@_p>>Kp@|HUjIMr4tm%(*boAA-Z$9L^zUJ2cCQUDp21KC-8*cT z(q^)}@LJa5?piEEc^Coyd@W|kMid7mpkY#5*a89x(#1LEU@ljKfUL-{59ox2k5qY> z-3koCMjM-3zGM;LkP*!QCS#`Zk(o+T@hzA4VO`3W$k*Gj$Xg;yuVbgCH~fu8Xxe=P ze?m$MzNSemZqgSvP^ntJn;wYI)T>r(H37iXtH^v37{$R^s+Q2Z=ef!=bt}dyAQfxaTgmo@#*V5 zy(f>WvCf;GCGSkT>rn**x zjH*jC(-H%GuVHIE#wf{gM}l=tm{Hwtny5j21bHyFgP+le=oQ)R>OQ{b<)wGC{_F;M z%iS2KZj>{B$Sih&eC3C%uE6xo?G^2~(T!!vjZ*xG-GKzZ{t;yHjWV@@;X`g*!CG=Q zqW!b7cHJv-^gXtOx#Awy6xxJHGdH@j?ToOK+U z>ck)FlcoL>_9%@>n}5PEp3ADAviXGmT|Z^x9J|0ipaLHI=%;MmK9+(PTXPb^YgO4= zooJM0^_T_pSz+8$&pN_WsT$bs5pwB2SyBGYnBc*VH1<}=#j>y2v1|Oil&hgUT`E6X z%~sgvjVVV#AxrKj57|4`I}WRoxg#(Y4g8FqcF?4`@n>xOwAH(8QHoUuL}@HY8%^k3 zE$J``SVq*>sK>#QFXH)pp<}|Frkx^_8LUa|AB9C!Chubh-#FcWVdIb63j;bo14F3M z7Q1|?jOm=0h~iWi=84!pvZsu|OdyW-d+4klPVjYOvV=V!;PU7DSas!Ounx{ZZNT&r z<3laq$kNFstQOmLR4quyr08N}pv^d~3-k8f^6oB-U_0c-F0A1;yS~`{>>R%OU3tm< z?1`dyH8z!7(gAcyXbOl#rZ1V6NBx{tT^z!)6E=iH>6j`Ybd9ZPJ{dHkO%-h31?se* z0cV)zhhieO68mz5cN_gobELot#8FLQ{$}a)@CqCRPZ^G-2U0e7q5qQRhuO zcq`TT=ORofqRpqpYZSJV_lxah8{5YJ2y8Fv#dgkqv7KXMdr>d8KBBFhy^d9n$&wO5 z2SZBv(_gTPi=${{oac#MY9v5yknVDvMMDGOYo178FExf^IndXQhF3N48z3l;3l2Pn1&SeKo&G%*p+LZm$?0_D*FaHvn z7?l+B*5Z31_Ivln6zq#&RlPP)(P7}?QC|vOdCUW>^t4#tu601vNQLS2t0nu#48aPs z_Fl}&SxCP8083}uzk_KG9n;0mHTpeX6v#op&w+}-2x11wnhosG8~!HEh}mKx(odsq zrMi}y-TI)3XXuf7BhrWI!2lSio>oR;Gpw|<;TdHR00}cZ16+Y^itHI>v|c7Cpz{l1 z^Aye1tx=?bjC>B$02XJE$7)H35VA4x6L6je~=B}lrU3dFe} zdUy!RrW4v=Ob@bt2U)PFMgX!Pb%F@VrfBC^+swU@K14=E1k~-PPUnR(RBzBGdwM*C z02J<=9>?`4Fb+uIyhbELClpXZ)FA8d3F^0D!v`@-2PuS(x~KhS<|qW~iWg8VI$*0# z-$8xO%t@^V)wK#?aL%Eo2|^nwqH_^|mlD7~B(#y1BP|BCz`VK00Zb7i{2B9dss_<7 zh6n{WXy(m>vU7*lY72ii+g!|!>2(MfNf4PvYk;BIvl<$}L*SPv3c#!bFzZ`DCi|d< z&1V7B+^3oj=LkWAT$|AVC}M5~;gSPy`xQItLbvb5aftqa<5`F5cu-c?@LN>J(_|v0Q!mw@bYPb8BJ;WiM>WLj73CW22f5po4LQxvp?87apxkK{LzrsQ=B#VE|s!xe1 z?xPb7>Qo2^z35a8l@*~1sxC2u?g@a#*nz8`4l0kGF&80pMy@xte~6|-P!lhM407qO zS!E_4&P;1Po>r63vEml+S@U zXaV|LHZqY0>6f7uoZ$xgWIy#AX1BFI`Hp1Qp_NlN)|{+|RLWeCanVb_ww|rb2Yb zVz)Zana0crB9X)^F)wCtZH%O*bGVh83z=FIM$Ks7WbK}0lp7G2*h$0Y_!)7Wj5p*StfD%IlTr!^jJZH!9Ya5hUkM3= zqfI!Z1ThhmiHBHCe%M5V5ZA@#KtF|+zyxCYL(Bl67Q`pwtFOJLscq@g8((MFV%~Z8 zL)cr4$@LGhv4!!~B#2Ld7{+mzWIA?D8X{TxyP3#1Gs$b!g!1we9fCkNPc_5T!e{>a zy}`{;qWF#k=ji5J*xB@%_y03=83+ic8DjxXcO6*<-hYeJw1V7ALFlNt@x#!JI4v-O z<`N09h;f8A-)SJiO1lLW?TG3mv%)F`-6@%E-zfpFM9pV*B6|>#dJB$np={`{b@Ls5 zED(VO7b^V{lwEZt$f1KVLy%8cQsdy>3`$|T_gRqBH;=?uayn>P{V+Q`7Rff65tVZu zW>@8cG&Q_~&UC@W3Cpm2|6z7#4AXr~>dh5U;6~&R9$_;}Fyw5e%`tFzx7s8d%~z2d zllr5qUpQpO=ZPR>sjPpLom?0k&37Qy>`@wE=z*J9a*+J~qu7lDE7owVD$wph`N*Se z7#i}uN70|c@~};;I*R!iqp=Ocg*0FyP<@t}X>aihnkM+h&`iwY63=qIK1$!hA z4STa&ZZqvTK*AfjHhaY7Lyxhl4C>fJb=;u(9@R9yIys2SqBRi^6p-`Vk%G1wUL)6& z!=3kFVq=pD+!eC#GzcG-glKpgu<6?e5vYlH%jzT+2uf6A#*s=pEZo-SyH*_lYHPdt zM6ARj7_~91M(D)3E@H_66cVB}TG(t`^@m>uj|FN7ilGG5mekJ}K4>k7fsI-{V$vlO zI#&4X%5KfKafW~!NwmI>~jMNy#TvxG!ex8DRUFadmdBJ8jqye;J-&|?xSGAZa z^cKxbwa}6qgTXYKHJXW5Gd8UZ^r7{qiXmcY@=<`RT+@`AzcLLCU${9<7ZX5Itp!+x zl96ci_1k{S&P`#2yh<>PcXi5{Z zr;@>DY|yK{HX5|m4Ftbj`XtLw;2=N51(X2E$FWG3KYtRsb6vjoB%4@Bu-|YW`$>C zv_T#kflIDRV@`^8F+(j#i$LO56-4yPT%lTub_^l#xa;@9pVgmHdSr6_Ac@$*4x44` zX;VULQWwy0$KXj)zl6}Z&@wK23M`~3Eusw+NYacN9w;l`4=52p7k*f?YI^y@Eo?-_ zFBo_r9jwzdvkl80`p+SKtc58N6jAd81Befn(@U$t?Aty9cod-=?cOQ-l+P$6H- z6^J!Mn12FEmv$%sk{jHE8sO}-&(j9r98@@WGw`tb4@p={ zf-_vavy;&7Ctv`}7`aG_@IAOm(+31YI8CnXC%m#*a3k+uBLEB`udXR7#<1yajBd&4^Nn6TK1-V?f1;!OWu*eKj84iBk*8P)Dw2b@%QcU=!?O<>%D#q_z!vg(6K+^ z^^0cn8Lz*8v-!df+4W!0Wp4cgOCJAiZzO~p0KU=ze$0Qd zdV0RLDH|1S_}3&CFHjRWFcUO!-O@cIF}+v^AH z&0arXKW)Q4Y8zUk^BsA~HgpxV%1zr?-SIEJ-M1dEdi^NtA6`Gode7@eSs#1-C~ME# zb_)#pZ?wP~ZzL-63$Gu*f8+H7_+wr_fIsc^1Nd_e@Kpr7%^L~e?Os2CcX<5(zRc?f z@J_EE!0&N@4|oy4pMR?_S^ntt1NdLOegJ>N>j&`ny?y}S^_ESR6$Jdp-bi5nGp`@O zAMpAC{6ViDz#sSe0sJW&_<5Mm)VaEC8e~3tvu`D?^X5Z2-|_lU&Qh-*<=pP|qntaP zN_74aOuA(+;4gUn0REEK58$tP{Q&-^*AL+DO7;>?_1w(Xx4*=8v3}UEfb?y|ssaw) zLi3qjZ**%lnmTDidI@vg8}dglvjN!cc=Ba-IRC{PGW=(@liWRjW{;KJN?LnN;OO4s zi5MF&`ptr`>wWpB9-QWZn|Ou&yc9Dt%+0>1p7BI99WC7aemBk-?vyXS!b%6fiBjo# zMO>d8&5wq&TLl~mi0T+L-F@VkPnsLwlvS^?He7ad;kO~$FRw(V?r$?l&Mp&J!l>!GKl#sXQFy(dTg zoeeG7e8A{y{?1Nw_E^g0`+sMHsIsJ z4|d-{*GC-BisV~w;CK?+so+ge29-MQO;xGe-efBdRH^niQ7L)q+ia45ya7#VcGq}}Jm(!2DBAzFu#CLJ&NyQKae#t32*>pA02{>^ z+no*N=%;ekJFFTu9C$`P^$wd9(gbe()x#Iw!3O%VIGY|_KYj;Z-}Te==BWJmU3S$$ zleh1tE=c^+d+h9khC+Lk)IL9cpDo}od@Kj=#DO;S0r>leh{wPWD`eMBw*IT!FGhiV z_lpZY_qy}8ckS!W{u#8@E8G9cst&x(`;z?KKiNePd<`G4AAg~3-hcV5ty{kD*IjIa zIwSJw8kWexBh>iM{)H>Q_aTnpt@@A+l;_ODWcj%dS@~G3ylHL>8SgoI6@uvV9=!_D z;>2_GDrDm5)xcdWoxyw>i)u`x!)XVS+Qbi;?xrp_^z9`~fHaKBd7~Ma&D6&;G9f~T zgy2L})KzN^XBw&em=L6e!sZcHa`pr@msYLVE5lq|he5=adVu2P(S1{*T8R}2U^sb8lFGc<25eN+K4^-+nXg5HW-+H+~Q zyA3D%D7u1F_gFt%rP7p}2|F=X6B#q7)ht443@;0v)TPjBA+^s=T|yds0+IsgNv&o9 z5<}h6?j$b8tI0?luoD-V5j$}ql>N|*jpj?7bOi(=-^Dl2#W$1Ni!hdo9Zu?GAB1yU zgcC^Y;ENZWZ*daG``{aM@s0N43uW6fCvl_?zTu3Ea7ZBxZOw8gVUQ3GxCr}QgnhmE zqJ2A^#5ic}AuuO=X{EM;wk)wR&8A_w8pIN?Rm8Aa(kjBOBGO7RPQmJybkNU2 zLtc7SGabD~Ox27s`NxmhHw)djeQ~-IYkNH68NfnWHh;nnZzA3>1|V!3!eaLB7Y;MR z6K%p8iWYvG;R(B*Zg?WDX9GMT`S(v)nq4GgyIFOPYfp)r-B2?Hq_6+xtOGxH8 zscYSPHMmF^BeOV)X=&FLXwZA05jESPX$i?kcC!OP>(oyA9=958@MP<>OM!jxQ&v_5U>(8c_aC(bIvhLQg-|EB&)Z@h>L9(p$9zNb*%Z&0XMM&(z4&!|@!R0V zugi5R>DzvCIIXVnVH*y$Nara& zAmaDL(zt-pNo(m{z#}FUD6|_qc<@=JGq=MN777?QNB6)Nv2O)}$>0eI2Oc18rM;2^ z(stNZY?fT~#i@F053dgOX`TNB2pQA@^g=s;O>GYOw_5;o!%<`?EVC`p2~V~Kmci58 z0v+&WTc8~tZws_x*Yk5)pe=k*Eg%Q;eVX7u0z?K_eASHr(t)Ev&uk-Xg(uqxo8jqg zgl_n(taG4Gdf7N3qHLspjX!E#M8k7P(e zfvPF}fW7(y@!%&dfxQODQUZR`)Y$8AC}H5IX0+Gh(8IvL-}Shv?y{(da2@bu3D+KN zp4LmbHbi9!w`(uL?TBFi=wDE*|F9hgIeJ6cQ5g7qGL2FcLLK^02-WCAA=IP~g;1A1 z6hd|OPoW*e5SzUe>h@A-L$rBnFNM0i6j}w3m$f>h|B|KpXF8(?HNDxFtvaK^rkA|| z*bcQ>j;os^9a_>=U6BGU;d)4W=7UFWEam;lwjS`nFS;r^sj9zy)Z0puD*+H~3FrS1+QXqExDBP!c-d%V=$wik7`MsdMF>4Tga zCgf7w?w6kx@zAiX4pm*#FF3jbY;FA&;caWLTn$`E;%;+c&i|p{GN>#yprbURD!WP} zsus)xr>R2wZzd2IjR+nIxN5sjc=BDf-74)}tu+O8cQzejD?7CUe= z^>2$2^9K~GQIq6nn+aCSoMsBjb?-*- z26#fw)S8?w+H}TIyqC!BQXFefbq+Yi?2lAmY${Z6q`4XU>H*ZgVUA)t;^JZSeQ= zY?b%G-`}&9z8!wLIdz?Zo#i*{cu|I~x6_%Cu=&|%dnV1HV~7i5E$C^ul!L2FChUmo zDs%-xabv3JDjZA*%u@+ED8b7+oNR5BErinrbI7J5C~nx1XM&PMFi~}0Tmup;VYaBw@`+litww5RTNQRXYi}=mBRp%mbbV8k}z$pq998z}1%gh>kK?DOjqKuAJ^{45o z@}!zYzpMvmBCE3=Z0@FA4`~IugUCTgcrsrhJkYdTy8@Sgw+64kRg6};46xT>5V~R3 zR(XXw5>kabeNp+@FkW62?$fO8bn{a+W5lkw99z#T$H#1{;F<)^hS=x*9YQ)0Ifz(p zN7#hy??jZ#o9lVw44mO`2wJV+7Nb-#&`yJ81a9l8Gb{aDTSdQCQEekRRTQgCQ*oQu zNs7zIvSK(t47X3|{7NhCv4j8+mruZ&b_iW8MMzF>NRAy+-W!6q1t<-AgqsVTKp~aA zA(eIronUbw(@7hoDaIM_3=Z71;^b7?iGrO1^m*9-!1V}q0*eH}I-E8U#jsXKF3vNp zA{S4ItRNTXmO9BT$8jKXb8vo$TpW_RgL3@#g_e+ulS^nETtaOX158~WJAxL?;%YR}rz)S4kal~~I$fU&mQX1O1Y{^OpnNw0B&O4KDY^sEstRtjA0jx~ zn;jAr&^*~8KUKG_!fR>}<%m|bcC$l@DlutCL%af*CwF)*h?m8!6VZxJZte&~ZHD^r zT!<|sL#sFxoI3>r0f{r;{^VGMq=842O`Qu_M#mV{eN%HQZb;Dpe1lKjE5mvStcoz| zptpL$dJtAFyGQeZuvyE^HJ+#s5fzfVNAs!-?8nw=3w=JG?sexc_o8L;}nYGSM7q1a{ zuq?OGFig7iOKkxHiDG&37(SS7m){=4kIfWMfklL3+k92Ax%yo?3hxue>g21bM?ot0 zsH^a=5yfPx4u&w*T^6>zbj zrt{Zj2&_8>7i#Mguu(R52E}BJt!$Qw_eytVtI&)h}My zq_-=ZF-WZAsgG{D=nMdh({Z|6zY72wR3`y?I^TzWDxW;h*cs{&1X$Mo~%j{2p6?yWb@m$%thV^ss*s16RFhWa` zq*ia!n|6RO_&15(MHn}Mm(7VcjckXleCjQoNC%Tcz4W6zJGdv0C-E{QhV`PpiMJmh zaR(BUA&Q4lVnHtukwW23`STI8V*)QE(-FA1ZZ6x5i?OH3THuBfk8aZ&Fc2Un3m68_h=w*YcKaaCgR#vLh*x1M-z2Jx(?Cu} z#WC58s%GG#K%n^)2Z7Uc^S)-$4%n-85#|*MNhYT8$^*}b=A*`do$7? zh>V_8*}frqQ_;|P09Yh5>>dQMWdG9_pp^dGXn|u_g3v$7i2P?GoKXnd907#Q9Z!%5 z$kqW7r~`u^Un-KKpmU%kG%bg5E{qC~5;4BBfT}>R+@##22NjZ06d5 z^e`3$s-{)+Hvb}#$x|tuqujLD2-_#w6g@~`CvY|hX>|a*J=p}?vk6k!nF*aBr_wiP z@LI zP!ZS~1HDzN>_JCS)F^DwXe9X%enq|_+y4&yir{&+|8@9P`=lD`DFe|-ow(QjOlBNZ zGwAl7T)^Iq3krv_art%=R37#$%${_0ebBAZyvv=G(Gs)w!jU}%Q`Ul&QL@sN#yCs~ z>S3~dJlq<%6X22w_rpcWN|^eoXiCCU4mSjsiHem*E}nqVL?tfOwVJ`Jk`?mSBY3e7 zq$?!~mz1N0RJ(ve3-D*pWa-VLl+y-P>b!$X>qVk!!6J82m?!ewMCuTP3(HQVwxCga zf`s-eJ`pqC5~nmfg+rJUyaZM1j;EeRvlC&m3kfM~(iYpK&9_NgfCrWyn9NTco;}mi zjrD)fUG;8&C%XhzngJ&UNROYw2WO}cE`vcL62RZ_tUfvsMvIlDp0HI2qgBf7p0N86 zMsKRP>xms$Pe2=j&Sm%4UGT&HOxE9q^!D8i6_4340$@s5zBPqcT|ff^rjj&j7={q! zqy&vDurz@sP@50$@~I)iC;}dv0XxupDnVK2htVV?x67vIb6}Sy>;E(Su;?jUr*b1x zqS)4F!0668$nQg@Fuid*e+4|CAFiibx{7h;J2^6sPt6g zGS;B&p^<|YN>RCB8Xp3i{#}4Pd7tsN2FV1iuF!01_6WtHT3e9^6rAQ18Egxw!(6$Cmd(x?Jv@%NzgTP-y~UxU``EC`(`s;4-x5(8mNid23`YaPy-im2Fr`5^Top(#%Ze< zyraiyKOuM2IPIt8=8n_uB^R2t)#R3s)7Frikm4v_l{;>n_H*)%9jC1&cZ{5M6d!n8 za-8-{@|KO$enswKFh}>c0v~F^Tj?*3{w{D#F z2)RS#W`H$P4ZM@Qg$*#c1-G;T2DspsH1KwE<1#sekIc&%r`5w}pxP682w%nWq#1lT zUCVBt!H*e;l`O-;)V&Y-uDvSG-Uqese&VLur!#on z#%Nf>!dV5!wI;dg!@s@r*secamyu0J^Dk*{9QS%*Z~PkveG6F`kw)h3nS31G65lg>#C|5@$MH@g_ZE zbRV!`O|RwX+1GN~*mPXt^gNdXbu8XRgPL9UJeTADz=xLrqWB)6CZ>l%;EisWi+|EsjP#Zw%NM zl+~L9wxv}+ERLxxzFj;C?dj;PzAYc;Xbw5XS=zD9lCSEjb9vrpzL*{Ueym3m86>AA}tw zp_9cc+Kyl?=;HKN8qDe`^I3=y;l1)^(!p6c!Uh41Ly}k?1o4QS5e7m&69*YQ7Qdp7 z#V<4!S^Q$X7Qdp7#V=*TI%*b>`7gv16$Lh*7684FDuQhNIToSPm@DIg5StQ?Lqkrc zKbahUGJoW7Xxd3JN95WWfO#xb#WwzfnxSX~TQ`k4Hh8=k7$h57KDe+AB3r^9j~%SXV+Zq@tUrqv2f_DXw(0WdvtVq^ zdD_8w65-CX_+sB3FZ$--{{7(+_wOTe{@FYrFaHqM9_O9Sp%ov)LUH?X4PtuS%a?1< z=KXbC(wBcen^#NUIlQQ<4D63*g(vcIINC&X%`$T{Olp;ZM9Dju%MzXy#l%do{SWkI}DX3ha6)n{;q}(^_FIPFyBhh-=6K1zu-`IW}zk z`0;9yn9WNvWzdbI@k{J5phB|{>e2LP*@b;Jw}C40ojXVK6Bd5ee;z%sV3dqKM7&R*x4LZXOuMq{d5?6=G$vDN;g7Q&gqSIDKiitECNKQs? zS1m=nsM4falU#T%)B$B;VuNbpBT4$A!5er#n?=G4sjxVi zfR-sUmtfTk!40s~H0_iY7wQ+n#TQwMZlfR!vM&MSCf)%2;J3}R0BMw8X14i}ZX1QK z0<>*V5#tlQmW*NOjwR5TRD^wFQX@!A`nA1 z_GR*}^LP;qQ#^1UPYYLhUP6`|G3Brskr+B1CJM-1=kY#fZ@vgO$Y+n&l@SrJKw*X? zKUe@l+2P1AM5k(gkP;RF(p8wG`Q0%_RFQ27Vr-`uE`#FXHfA>~3?(w7>Hu+ipDZho zrB2(-$+(>tR5qNN3$!;j*VUuIAVBR^ zAX=CTw5_dEQF^wh-+diBT^;YqJ1^veR@Hha`W4$EUCf6ZqUko%GkZQ?@u2}6Hz ztgrA!dCJ8+#iq*3FXp9saq@@qhKqR_wuHhcLveJ1>_6%tI6j<*M!-|@w-g`upXG9s==`SA7#bFsTcl{9rq@!? ztavR2PGmHX!O0`*s$nr;X6w?Ll+H@=JMLe=q_~u=+}&>^v(9?j$Mq_N9DKW`9gg zwn`LJL#`;>Mm|`jjzS{{fIi3=jMF6FvGG)3rmiAewkZ=^E(K=jDtcWpg=SS#I_yDH zu9%%m4awO!J4-~$%lHsSxDAr;T*gOa+)+9^j@ZSCc=i=G*vGCmy8~pXnZZP#I|~Zf zvmmT1CSsOIf*2Dl)i=@KyfYX~NcNg>xKo%HKJFd|JolM$K-pwZUZRdYDu4%mC{Tw& zL-h@$kS|`&D~a)YrvmoW&Be%<#RyeUQ?@L!dNvA&B>G12Y!uBD2x2xD%SNGn)Eo$7 zr*RZfB#va4f)YkG%@gJKTY33GGm}T|7s0MPl>v%kzKza6b8x^tW-hPH3~AB~643B$lnMFvV zVRn?36JNv|zFV%~i6dOu7Eygf^rM*(;Ha5VKYP;D&jf`uL}8wUsO&r`2R1VnOUxkJK zowD&NUVyF3ldj?eDt-t(UJc(4u zo5Yu$O_{a_0@t;{O#M`>)*wA9Ah(KT;%b~5%#)L^=5?7A`U&jQ(fkMp@ql8O&o?*U z@Gz_=YETe0abtkF4PLCbbi8~{Ul>C)l|+3tQ5RH6NO?3sCBYZH_lIr2X7ne@E(Xv5 zg_B^xW?MZHGI!&^L3b{UVG`~kH6jI&Te5HP!ZAp`BY;{)R27XFRVifd!tu^L&>>`k z&63dM;&Bfhi^jQW*u87UWG*F-`35h~2&m?{k3?h%fV3obp=n56 zU8Jdv!)Rmt;c=4++F#g?BTLxtpo2>&0yf1_A8^d>Kzck`98bX9c}aVEYSSKVL6d7- zoXt0Mbm+9jdfH4X4&~&*Gp-!$hit(t?q}FsXado<0 z_WveM8V*4C08-G^i~1(Ec!5`gHwzZi!48$Ea=NnFP&$eM-voXGppFv*7GOGD0e00; zimB59SU|T8@efAD(aPT1!YH>ZYeWUQxos>Q8zW+{Z6bUc3M&-INiWkq$>Qo6H%~0@ z6U(WgA|g$5@3d|5+mn{{NsE*my9h(S#Rp~zL3>iSAZ)xSCvqf+9NucDi5_Oih=4Q^ zk$@Hf-4CrEL5}c&a*!$!Yy`Tu1(9xmZO%PGBuPieoIBbb2J`(HM2?sL{Vhzupkwkd znBC-d!=)#Q^44_i=Ob3TFJk-4p>&*Cglc;c3X;M`12Sa5e;E2E>eno$t~B>(t?4jL zdnQr6TUrr}>I)S#9<3cg8_Uht@JzmlQ9CER?JQ5fmQTaO%|E=BFV1||m3c=~y|xF^ zqo|i1jqyE}l#aZ$<#;ybM4O06I|)fu|AUy%Mh~DNzsZ4i0@}U_sV0dgXysu@6O}j` zb0Y+eqAqrh6E?st5c4%}yc0dqjaKsC#%Dezy~ar*G+YBFT9fa165lm63XIPpHD3lY zeOH@bXuLc(V?W+XqfQ*74r#d+H01qGq%VM=%W=nwb0|F`WWR zp<|}bYlcv@WhhQoKnhksm}P^uA}EO8W!0b$s$ckNP>^?xnvx_uG7A<_u69{k?x*ii3;7KuA%&KSo}9A7+1K@4r8? zLv(yD$sw^7$%(w@r+SG=Brp0OCwc$AB=1ioACA3gB5}TA^c|t}8R}uX6r}Q0IddU5 z!c*0j^RFc>8;z3a#c-ed(Jl+78c4aUc(r|6ExkjOf z{x`_uuII+M2_9}ViH!$}3B= zp6y1%i3#%A>-mHll#6pV5#sL#5|Bm2;>wdl1`%A&&y%CtdAdrdC$R*bMpyGdQuw1@ zTO&p#kQMT(c0P~^d1pJHQeH^|ZzXMgRSH29!g)|J>ol7%gxu54iw>HmlfRRl>J|@m z*Yd^@px9PedH4>Cm(us%;w<=g1Lby|BcJxCpVk1FhR zT!$bclFUNvcY?-X)TiRR{G9%SX&AxVL{Lc#vOnL)SqM7LPJ{}p!|dhq?(g!N95IZY z4P{!?Ha=V=w;^z9kr}|W1HF@$N`8YDS_#Yn@wf*z8HP@Y_Y*@&44x{63YDL3XJoEG zLyl)>Q+;sNV+e4W`aP`ThGR->KQg202I-g{gP>lqrIc1sSKy4489Ns1Yl4PpH*P58 zO^c5w)sTU^J#Ylhe+xvS<|Lmuu%Od{ILczC=QJaGdJgtIN(n0j5quvJ8@^=%!~8;lmiSes7TjdEycG##>=J0m{g zXp)LIq7azhY-3Fcp-Bg;*j!#N(9#An50cxL@XGw6z3@mv=$G7pixYwfg|Q()gffbG zBTj4C7~zqnEI!4(_!QgtV4QOBL7az`nm&wQDvCNw#tu@=F2EG58LU8(z-68Cz0mVE zhl`$StsNn|wX($cB8@Xn{^CZ=Se$aHgJ7T@BnG;SX12pWSTtt+9q^~+WlMRX?0*xl z$n+P3vvFj|w@wUl{nRYGd{|9|(5Rw@pNi!wrqP;lrrm$f6x_gS0$iIkigY9~U(%X~ zF|1-}iXtNEFbRNRt^g2tKoaW~NJ0N}d*vW1)0v3r$HIX_1%QYk#3SH~UKt@A8BrO9 z6(?6l4Cs{+stCMMs$@ht^}-=&9^y#q>flIPJgYeidQcyEVM~e(u@`l&UQtm_q5`V~ zTT~=kMESJ%;d_Y+T0iV1Dq?mIKrhOQ3hETZFXhB9kQAhx-AhtT>lG6F7Z2q`!x3OB zJlHmZFpG*|^o$Wssu5aeQp}m?i-D!Q;V@h=JQXvm5yxT`fI|>s#B@5+HiGt$qarP8 zoR;n72XKmP1uq;~E<_ociU=1$Lr_;v(0E039Hvm10qKH5DGIb1Au53bRh7D2LgbOK zNMNe>yQSE^>@WYilvhQEIihlysFXQ3^O6&7z)1yIy#!q;h8>%Z(Gmg#SD=%qo)>h| z2Avc|==Tur5DB?d4TuC1{-VFU=4M{YD&>!F#)zQMIB?o*MvU!swud@urD`SUv1tH< z;Z%uXTHp!*QWJU@vlVjquzP=4472;Y8{JP-kaA!ciMGT{>^-QuK(EsbJNoGu z%|Ahk3oSl2x5&hMr?@+9QgP;dF-+cX@!Lav=$KKrj!{!=40#X+&|ZXxDTE=NQ6?ts zBgj#&{uVhH7oiadb84|H=5C;kE^;ZcMV(f}W`cQC^A?@uATEG76-twqmw;~qojpP8 zM5G?i&P1kR#CQqlvA{JkTi>b+=+4!;W_%J%z>4_$Z}F{_<9C9N#O&_A9@Xpen~m?FcIoMrR;-rqILHEvI7) zx9@p_55~wLxA9BZnevX?_|(B_Tt_=Z%pI5%s8L<{=;q%4;_Xeq>?+Fq|9ek$clvhs z?Y`aVeLd%rr8|2_$PVOmfFy)nHkBT{qed~SSdh0E_rrXqY zX|@rg>d5PuFiBuA$Zg)<@!Ks4R(9D2QxA8 zA@QPCP~L9xq_!*a!tLQMvcwq{d0~jOUFvqS8R$NxCO?{rC#H$0os{-)f1`Bx#YK}5 zwizXr@39%S>Vk+Svt#St-AH4Y8a1hLMQ)kjjxIABT%mAxHsCNed|ozKt#EHPxJu#v zY+%a`)CLzRy{;A+hH!mtaIV5_+2A~d+q1z^g*&pr=?ZsdgEI&VW=tdVko#+M^T}pJli=KBw z9u8!k66f~+@guiBf5+XMKhgR@K3I}st5pRS^uO}Cmw)rd>!1AIrwX>jD}}-IOtVX6 zF6zI2{mla#-@NDHT`60pB*`@kDzdB}haYcpoP}?=7?Ow{TvhVP;SJcX0;`8Npit7F znP!{HT=|v_*rFm=9Har8co6P@{ht_aaJ$?VXLIN4=Faq$xh}t*GeEUO5TUKzgy6UU z&WOPH@eMgKrdEgkihPqWW)er$2azD$0d!(Vns9{e;ZB<|2<;pn5J$Kz>wwq+bZ%Ku z(cPUiNBhi2mo1pDW(_o>GL4(5e5h`_-#sVAtajV|Xl@~;Lci8#AssDb>8}d?6SGEy zkHde7g zG%)ZQhq)22>f|xu%@J1yClT$rI+)|vpsHwqfuaz|TXzG(CK@%t(Znx!V+zy#Inn^L zgt5S3@nBn5(k>Jzf$L1tm2eArxad@Oi+P*KYa)*UDdw?NiZZD^q~XOp?4t8^wWLS|B$m+@3(7AmY&Q@e}}A&`byeTG-ombVP5jYnIs49{wkb>DZEwhtIHdSNWsE zY@%v+t9gbjV8tmn+x@Y&iCS^tSz*Q5V`XV?@AGbI1}W=#+bzneRP?R87z9?fnWj)S%7ccfN&>S7ntm^Z5-pe;g0@CJmLp(FGJw=GYGz+rP5C&J^Z>EwP(2E>e zO$Nmw{|7kHsEf!YdHU>0GKD%(r4?ShC@9n(259j$JaFGHHJ(lcsh`qDHkWJA8Q;r(ZiM zBBofJ8d)qe`6}Up&9dZ)rHDo+SrE8MaiYC{EyOrC(|dvmRVy{I;RZ`X;l*N%nn)gu z7^#>CqZPXw#Yxc8nlK0&j3(2uV}M`o8Y?Df5zV6N1@>yxmnU)yFaXLHgi9Kf(A zg$j#i)6$yUCjoo(+BpV5xqZA87%bSrfmgPt!nQ)VXCKpXh?@@SV2cAXO9PS&#Vid) zG6=Io;*;*r0vZOpZzae))3ysj7X~YWhMJVDrYvv|ezy44I$!{~K_JnhV$nli@kZ55 z@Qo}Jm_1H%z5EsL7lj!>S1^NKZN%8Y630QZxB)Z#ydT74ixfc`*{qnxg*YU#shHk_ zKDVi;5{34Y?k+mIg^4CzH@I4azcecfki?lWR5)@s0&-Wz6f;}{#CIFTC#PSln45lD z@h)3B?{c@JITB6XfRyRhnj*1*btemw;|PxE4Qp?g-&@3AE}g}+VO+8KQ?xUZDE@2% zc8rv_7p6If#)qIr1J};H1mh4nVXu0zS&?Wjo0akYxY%Z8{NJ)!sgWQjMM`+%)3E1?gwm0l=AhQc{#wW8h#>BgyF)@ntK z5PGMi52jMltNc)_l?ebuFF;7+19dK_0YG|@A#MAlx1O%WSR5CC^V_d^U9(X%k>Q^N zm5^EE9H@lMJARK&M-x>h6wZXNkhI`oQ?HW>q9gD2Iz$ZMqipVN2ChY8Zd0EjHMmdm zB~sRBr=H1{a*LtXd&%`<7|De9kOCf_wt8#q)0EWwC(BI6pEoD9znu5}f;D5iM7XfZ zt~n4pe}44qyD^WTJ-r-HkYBbZFtF10t1|$6nv`XAptYIsIZ|DA4%8mL>d#HH7H7ii z#Y0R?<=v>X_og~#q&}(#w^-()XK*zabja9rvOfdUkZS)Zjn|Ayea$Ka+L=lB1?Xt1 zy8pUy)hty<#cBlBCxl|n4LYRu=-)PbiwlB78Af7_#Rr{cz#4kkmn!|E`c*qKicZ8C zR}uwyEcfo1PUv554MotZU`}JrYIc}>GgW#)swS&e5CRp}SSL6EJ&SZL=kq)368_Zl z8_jPbSgd6Fl(uWCgwmJo(5m_x{AgF)d; zB3#cGv=gqcwPiy+qs&nXGb@4#gg8|RPA0^eJ_?wTata?zBHVz;LG=OA7vTA(!iogd zB;@=3>~Pi!Y?{svr>sEBaW-xiQTE^e-`m+U==Xx-Eq64U%>(3 zP|AxwdapOPTAA@7h3H53dU=~s?zoBQje9XtQ;Edzza}>PPuZi&b*E#H#DYFjp8H;_ zpoL|KXG$|*x7LzoAlDNTTPJ35ql;sFhB2v&@2n%fL1pPRe;KaaKX~)~krPwf@Y#1v zS@g9n&;$SWVsgh&Kk>PfjpG3+)3|!%#8B=EIU3k8Q=ZS-KalT~fvPXG{aJATv z;TXp;5L7&}$TQT?U}khicvvQ2S^@adJTQXG?@E~4;f`N2wY7n_}epqMXUu-8>%Fh&D8q><4#*NL(~I7ZNp zOnBiG;YU>sR@E$C%yd7r=B8?u?q#y<2@jDO(ZMN~2IKsyAOq~dt@`;cXHG*r(Ebg? zl$yaIY3?XqR2+8<73!`z|Oca zh%=ZY!t-Mo0kqv@=GMititzyJ`;?XWah!Yes;k41*w9ihJJ6{Aoj|#Xb5}@=mxewbZ-I)oh^!8&JM1H~@89*M-gzoek` zbbJT~-kwWNNF^hkaxagDov%w)DrEV5U zq8kRI>yzlDaMKR}2z&`yMr)GbWs7nOWWn~Jp#$T?ZE+#va_H3dILCfwp<~k)YF6w^ zRzqY9TiuZ@)a2Sx=K|jaXEWDHthE@+t>`bRB^&&DCF^H+I1mMyj<)s*IHGcLqLYJPK&)U7qyN4? z+;;DyPdvZv1roVO`Dj$L&1)M$9V+n2aL>r}$Y}mHuN45_i{616-$!kj2&v^%+-EMi zCNEfF_lzu``;$+-=Nk9qO!s8>fln5>C%C8_e23Pk5${$9?Aj*&BvvAh3=1sB`r6*C z{_eMb2Uwi7tKi;r?%s_HCK$(x@X6affFUCjq85@=$>u2-1aKo zlEn6yLdf43n!d&j@ z_gtGl-9Pu*{Pg}ew*KYKdk22|%b%pK$xorE*23gFI||wSC8~IPHqQiG4c}6QTe5>6 zx~TL+TYxQgT84b+41ejhc{AigDL#ZOGLrqH>8IY~pU(etPrc{bYx7g2tD=?510r_*VDz`yv~ z{vCgP<*~27_}f4KSLzyD^`47IP{ZA(xj?lzmS9(4%;M0OiG(H zlKyXEYDASzW}{1xXOp{>)Yca{T46wik{m5aAW579mp9WtlQzZQoKM1HyJn$_1d7^( z?RVk^gFG3I+)K@5EymDra6At>l#Wp>ZTC#J8u%U+@cw)fMfGqOiK1e7q(8@&vELgU zu)(paIgcT&Xc2&TnAY3mE+yk(+F)~bm*i|DC;G#`qNhdDrWIPLBANa?}#l;o7tn7T|3gX*=T$I;SM~O3J0Q)JRs! zwFJT@KsienYyylNCuks1lxZb5slDrK3xFx~r91&;$+H}eErJi<+gM(dXPpi_>5kCf z>A+*@yeQG$%6Wg-Z;ote4&wgofxkwMb;Khl9XJIFNLlvK6f^_@Yq~bx2A+@-yr_+) z*GRrl$fWC;(yohN_mi%RSub|osvwJ9FUqA(Lg@$wtt<-ps@i&Zp7CVq5=+57}uRLa+@A~ z>tSzHTbDmQ(ZF{xmcBu-L$bzL@xLDSj!Ab$bH3wUQJ8MeAn*iPCULivRLnXqFLD`4 z>NzRt7}UM022lx(>-cb!Lrd^cEVVL=SqNQe4z@69x2C1brcMNx2B*yKL?3Lm2{l0N z6gJW)eOLaB+=0vJ*zaN(3li!aeJAD6uSrRbC2y}b#H^CKQ(QMn>P}zB>N}Z9d$h8X z5(1zGpO{rr<9`8xAAFaRT9{3VVM^+$aO9mp35}w;8&Jhm`+daF+S!i_oGgvCFk(BK zbQAq`RQK-?1}hag6^T{<&G1i>?0JHQY(3&Y%0I!8JoLx57b(q{=$W)N4$3VF7+lVa z3zg-19lsrG3*dU;#H1GEFHgc#920IgYM@O4T6na(41c&2ahQ*kSBIESp+MGy`t!eg6VJgKtDKE=>lvWC2!P#IgCOw~&o!>nGD}v%>#3wj zW)Lj@ipUeo@b)8rDowu_UH3h2(ipx`=DR8n_*6AU4#!vf$78qofp==&4fS3-|*10ep2@NJfN>hxIEc+yE zPmLaW#2d-E+9x0Jf+cn#!C3NtQ?dMk9p1c&EvRQW3l9UMZCAHO#kP1gy(~U+q%fNk zR_M1ldSQn*p@4V$aCf~<;e$5b$U(kE$^U44Ulm z)I;fc=(H6~J3dOGex=$9={Dc;G`i7o^v_>qnxnfP^;%}fr)Y)(gLFhKGGSerpH$K_ zZcSxNPLgpw6zA1Pz4l`!g*{j?4>m|}_&;or+Vk%3dlPfR;WFCreQ&aSi9Gs!?}UPb zs03;s{D~!-;-}TnpxDq(b~ocTT3qT3+0aFi20K+B-VAbehHZ%kie7L}0O~(3Du8QC zmE~C^za1xP*hxg6iR*v)=j|*^#@diaaE-YD;!-co68n+$`bs{rVW|&)NN$(=RE#6_ zdTF2R8kaTzYwzn>BgR<6M2041o*RN!ms!|x;PcXQ-F#y7q~21fX)g)|X@ zgk&;aS{2l&E|`E0*v0T%p!^Ape-c9{6X#VXvFNPFyz>i>p?c!rWUiG@VzL=l=UQn+ zeTce=V_HQ9v>q1-$|y3SY`4<152a3owoLGS>f)S~RZi>S zLk(dn*G3nJKin`UI{b&;{K5qc2kHF{`A#W{nF2c%K-of&hQ^?Js1JA57sVl0r+tt+ zn@8JN>usJ*?PsKHUXsGjO@?Edc(tWFME|)=U))%uFJk9uKe?d}nBbF@r8Oob+f`^Q z67LcYK!T;m!;&?~2>R-=@yMYjCVVhw$B+i2>x~WWns|_UBs&A%i?R}j#Rkx;%!+~n zIu|dvEdq_$XrntEZ?&nm07fnt$*9?l`AJT7PDQ)wExf2>Gn36rHzK|9mNgfDSh9C$ z_+|;+qvK`t7aT3+TeR4gQVlJtr9Lb=g`{nddwE!ld5?Ro6B4$CwHkVW`R*h*mh(fq zA`2l0N{C`K@VK{R^i)wRuIreb-cTol=Be(?(p2npET1kec51Zuac^|GD{9{9z3Y$$ ze+q^|=j*nVYL3H2#>=DUj6S*3JEPHOB)MZDW?AdQW>7GCV<+nI8ONZa zuBS7>JjbPsWhs%-f&gFwXB(ZHi1|`E)>y`)6Xy_+bdhyp-ME#!RQMS_`k^1&E%%_t zoA1wO^+DVbGc(qouQFn0I4jv(WM;;SnK=$_W~{AtY{JcqrHB@`k*p++k*MA#eYL|iUmnQu;t%oXv2AqX#bq(FF*DUAJ+}1K?8gU zM!5S!Pz{-cu!)P~7KMXYxF$O9Ctl0>+8cIDKQLhx-#QUe#l&^niPJ{XM7>TxJ6JjS zG@b6AV7+0A$WPcS7_%)?&gMo<03TQZ70%nYkP7I83hoYBM)iIv8~ zO3Ya+Wj@Eb%tcRdNf8IsbZRRG5aO~=i`G8j&FXn)kgBPx##w{7V&Az{1lZ()15?4i z=oe3T-BXi2wo2ixjKGUEv17}oRdS02tO)5AzbhK|BrHr*bk>vJ;h@YNPvSPTCHmHr z-gJQbcTajfWLE#w>(7j#WqScf?)&+v*V9p~2!6-x!L`SLj-5+u+!U?nCxQNuRUy5G7d$!5g<7fEAp z6z+Y2X`T{p)SIMhCPo{7=FLH^CZ8mz(2*%%-kGh@;dkNwGe$Y1R_1PuUi+ChHIv95 zqoWzS@aNkcowm!H-zdi)v8{m^w(UAj8+~<`H*Ve-w{d)fzhJGFRUMz^8#vo?r^-3h z(Pytq>a)1dmLgA6I0S(6e|J>(l-FEuy%@w^3i^3^WHjR`FBlt9BJrUk=a*&hif&)L z4|3J-b_e{!q7OXfoq~x1Xz`K%xbvIdF&(Asj83dUDu1whdrHQEpp50OzC$`*-7N?F zKxz6LyCdAOO>F6tC$>Jvu=Ew#?7rx{pL@qtY<%FuWx<$e^UuA*5B+`V+tA%tO4HEg z*Gtn)E5hr46?Oc=8`FHhq~uiiDFQ!(bDNd_(@Bf=ew zsm8hmNB91iBT|2{A(s-&OTFo${<_0cub25_qV&_8IGY!>KJ6Wno*&J7+MAfZE?V)l zH}zeDS2=%heps%v$KY}PaHtM3fzK*AwkPT?UF14YrIUfxMhO#Sbpu0Wg^ENLYLBsT=>0z+UHq!LRuyYEqsCv zcdZi~lu{wB6I~KyA%9lr?Ghnmnc%Pq)oQEW3ca!$5~y<{M7&I>5k-GO+Qp~Duw|kR z1nP1^FWu@53ib=?VbJW?O-xmWIB_fYeGLFsK4p+}ONB8_tiTYVSq-$i`tqdbV;tE^ zjQ+kockmK zGE_QOY?K}kZIlq)cfR#}Iqjgaw)fS{)OKSfLhHMu)5izNOa>c30uZb1pb4}9o`Qv~ z!&H@pNZjW-V5tL&nwjeX+<$g6+1>8eHre6iYjfA9W8%;~F?Eud0qHteg-&H%=$76A z$ySCfm89T)8mX*-9L*>>V(MpjzG4NYYzF=VmAPNg+Db(C!*y$#|52uEC!GYj87Q~M zaC3Y{2RYdT%(prQRA20mO;;zl&-GG5_tAAT(H4x<{P`>SQ`A%Qy*`thX$rHbB}kAU z`7}uIkBpZ-o*)68vq;b_B#7KoWl z!B-K8`M@wBj(0P=hX66;V-Sez1jOAIh!G3w1jT&HLUEmdShKH>)Hxt_pG<%_X_Ouh zZImDn)X@REItRpcF%WaDiw6)F;dW9HZhZiEINZtw1ynZxd4lHXnqPazq1brz*WRJ& zYophGZRuUxbKbEf^iKQ8b$~_gT0Q}0otaEE=s4ZcJJbvZrFZRbhu&?#5>-?Eu?Igk z$QBQ$cNF?RrFR`e=-re-dN&p35bhiQ6P- z=r&u*edV$9hEf8=^0nC_s;48)B5o zR>sgBS8b&(cT%;6Ye*?*{OFDsypEm{#U)=GqPVOLQCvRp0@j?IKB?d1HI5(lJx|*@ z?t9XvxbJzk#rV4Bg(ZP-a2{qtmTCv0+DDOMX6E|=5q0V$8=FV$I2%K$JHuPVLtPab z2$kNi($T$pa0rK0`z2Dg)Iu=R&f8gZM&Qh%oxoy`?ObqK z)+Njz+N~QIW!W39(`d71yJ@lJhBBLkXCYXG`y7I!v}HkmbtIp$(lCt=w0Z~@`W!4+ zfPvJ@&d^Su_EP7{&Tww@`QLdh-6dv)a^h2JR+tN4i+=h$Z(d8H%He){p*C9zHi-IO z^12QcZn(V$zm6%XWl>3N3!CiRh^#_o+?#AK2hsa@Z&Jygfk3@<&%ok$$(})N_+s>( zm%IfNtI|>;rDy)PlM#6Mp1Z6t2nL621O=?~?Sm=%PP(-Icd1vzE-8{@@K9-kq}o zx17SY#vElQs3GEF#gQWIjTuV3p4x?33}r*K_>Tzo-O=(t;?VQG(dYi?{j$)6(j>r> zqZLE)mify|40$iKBoBMZm^S$NnElEFl|hSRzo6D7G6=r5WA+PfPO7Sd$siSL?2?S_ zPHwT~yqe3J=K~kaXDcv+o2WHfjsd2q@;~9cESOmpR7*7PKfQdp{E-IHWiNXR3tjS3 zlAH*wp~XzVtVC}vs3bjQFbyqeqVd9)ltgLBK@)$9Y3N_cSyZ7NX@`5ZJb+ws7B%nZ z2E~mo5GCk}7W~PZ0@JhRPf*_$Es2^FG5N!%L=I@3gbFwBj~kp&Nn}n)n5_5ZmGNNj zA2;OvUfwUhSMT0C^58ET%CXun%r{A!?chSZG^b*NR=ZYPFyxZ3)BelrE65A>P=eFS zh_Jm{LX=veSg4mu2p3dLg9@RsOE`bZkWjP-Bhk;36A7SA+(=i^i9}b?iG;*Ps{@&l zDx^f;MFM@hQX=na#n#jbms>UEQgi7Pq>{{dPD#0&CCP+q-OAgha6y{l=n=WNkOKj} zg@UdSMR;29(8ZPuKF^7@it4Ecc|5DziX_U!L^t`AB&2u{lFx~qjYlE=WEkLv(^cLV zSKbv)Eq-uz+=y0fnjmY)b_3|fmls}!nf=P>)qP%LA4o0(;&?*?^&V%;*iBaT2>;?g)^BOj>`nVlKi zr{sx8csJQ<{1a6>_|B7CmGiD6bR2^U{dd_XG=xWM^{6u35bgT2mo1DIpIE;#KO4$p zp*IhTEDFX8Q%12PN#JwBgrG5ih0@6{cw7K5V4V^&SvQ&%&`H*Cc8{Pk-?x$`t`el{ zjuqDAUavKO*hU=uVBFlX``)nJ~wEp)6?8sO7?MJpWjF z2_BwvNX?f5uBk;faX;Bs@4|+xkef}DjBZlr&Q4zAw2@>mOo_$Dj@CG*JVao>Po9{^ zgB8KVfoKC3cHLxv&yD3+r=e$3` zNNaQ0F_7riV^%ZqJVtYK7#+5v#SDvCgT!=(P^&hag29}lo->48Vl0bksHwQhVj60i zcq;*?W{8?txyQj&e|B7acNvfK z3vC7bRjM7I0^2dhCF$ytKVxn3sH-cueSLy+2n7;pPZE`XQ9&_5O`fPGJ8pOatu2s2 zn_UJFkc?NT@tULYa*sld*BmWANt)rBqv1-@4A&eDSCZ!2)~GH?^KENam!ugkOzRl` zI0M@{7gI;ozgT}TRlOKH*o&#^g*JBL;`V@pHgJ-3(P!KYG!JnlOJI6~C2U&T$^l|V zQXb|Y0!$i9opi>#@i^7zck9R>!0Rs&@KyuXU4AvsE|tv6N==5(@*WcvypfSn=jWw3 zhsabja~tc?F~Gf}){oB|tb5+6^}%pZLPSS9#3DMO{6bl(LujeaB(^b8rX zI#Qi%pA)p^8yt-g8IZPBXZs-oRxD`_pZwaBp2$FRIADropc!xO(cQ1&a=fT*L1+@D z=X@U}9!y%Rs0h_huoi(qz;NOJ5{^5sBg`N>X+r2q&5lu2o1Efe24Y2_}fvQ+d(~ z?%)uYDxY%XGY%G#4F16UcC}9=OidO~;X6>OV|n_;Yu@xio+%m*8XKPfTcF`{Hp4;& zp@ocD95F(7l0e3+7&6AhkU`o)hKV7g-nZw*1*vY;i9YtaS5e4o!NGDvd$RF@;~=G` z1X8p#)a31YW8q}O2*C-Noo<2oJ8<%?KfaP$lI;Lac2&l3qQq9RqkCS(Z%N+i+R$GhURm1MFeMswhgo%&}PH?Gq zBJ0|fjNg;=YQa)Zb^%5c5>Oc@pc_93bmIhc;{|k>DT@yqC#cgp7(?l};z{UX3rRA+4J*QA=G-x@Qf5Ht(d z*zIVLSSc|yOrej;eJ{-xT!WNHS_XKe0Kg0ft{*^b+=*}T{?DwtZ{YTi{O-|dGYtmq z{rCO&g^$1b?YsW#4N79>+5fp+e|hTRkN^DoPje)-m-LT5c*h_A`uMFM{t?f8(l7sh z-~PgvzViB4zC-~)yZ`RJKiPNhA0FH~kIR}#KJXu3`|_@xzx&1mJgy|Y=@b8b+nbO5 zW$lZuLAqgZH_gz^OjaP?&0+nr+*+ivcp<^*@-D6y)h11Y+rvHJje>i{kBds%)ewI9 zcdtMGfdh&TFX{17rtF|i7_84wiU~ewDUXf7A#LR1az0;VXd1%xHlxVVH0XA5O7tdh z$#mF^B2Uwx?IcPRSCIx85s*gmCi* zA^x)Ku*G&Eh**h?=U**yGtY z<{XWO-$25RUT~hm4PLNR;d(DPUEw+pzk!4UUU0F({psKmg?rPu4V3+*gu@m(b7PhH z`Y3G&w!otXqlLe=XM)|KJv+DBOr&QwSDT~ao&k>ZOkP6{+Or1Rv)TsRGqBP2ELvA% z4jUOmah*^khT>>vjky_Z(m7c(y711p&Bw=mI6@(P8l#XtPEkmoJ%x+`18v7ddX4MV z@`CgHhQz&1kuO_^NDmO`f`K$(lXfo z@=Jr}WDNj{s>2E zh@i~oMq5U5o9n>}n4~hX*|tLWap@h0me^0s5`>L1VFITIn9#^>lP!qxp;(>(@pn zH<{a8>S(tc6uHE4gINWMd85g+4OTq34n*Uc&1ZQ%6NPy*G1}j3mfJcID|p-0;f8x{ z^u8AJC78ADR`aQ2Yl}~9K@Aw6vGcUeXzWKI&RD|+$d^nSL_07ZMl!30eh#uNU0c&_ zW^o!jx+U%A8|jvV72Ya=Ok9cp&^<2av7p2OplgnUmjjzqQT&)4pzu~s z&`n`B=60Dj+`E65B<%EH?Se3_r4$>TZ$2g~1!D|b>+yC03?2vp-+gAgTgfeGZPx+O z%wREk>Hy^lKyVNORjmGia?wC{DUcL_Y~SC4zO~Op_1$LV+yh&6>)UJ9U9N&T2Uc+Z zkP5aLx>a7%El7{WtbNf>x`AIrlxR&4;IGMXQzD>ECTeXI_@zA?9F^#J-@e`w{lR~W z*FPOVzPh3#^LW4gTc7wt-h8R73+;Ur9b#^uE+}&7GXz1PboiteGzm>K-_rd~aD+2j zI?A-NT>ir-GwCqd@Q9qo7Yj6po>U$fu#6HO>E2H^?5&;|F3!sJFi+7lqfAR1o8+jM zG1is*E<~a&0ATKhC_CEZ`M{LXc3D1Ww3$6fRG_$SP}e>i-96e&8+^jM?#ZpsMt>S@ zB9QeXW6UTv#k8K$7q@OUX-$I;ZXBVtsYzOVQXAqFYa6Ncaf)PJ#IdHUP=uZ0tUfmccK#+x>`RJ4CQ{*p3FB-v7%Xpl zAubO&S+_F$DuzUs%L{LC+F!O9WWXGz3PBYd;ra?ZrXrGMD8*=TwTpcfe@-yZAsit4 zhh>9AFGG4;*$OACH7VGl-B%~9Z7=tm`lGwPy*_fn+9n>w(%P^0OIq8gj6_^3NM75Z zoKk^py)q=RB?8+zWt2#414@^QZ2P&7Zivjb*8r!rDC=mL zsKqDx#6oI5rYaAm7Tbm* zu^n6MiCO%Uhb6UIh%c?#Dt_L&>gVFBYuecU=70@X`%vjyjA?PWBpv>pA;rVw|J#_t z(nx+x$<#c&bwr%_M>pugV29%LtlaOgY zJsE;bdzDd&Ona0OBU797AIRcEv1u0>(SM9*mDh01&sPnDP{5APT8@e57DC}{1;;05 zyJ)|4d{nb70ulsRS_oke{OM!R)=X6G7jmR@<7KS-BTV2ne@*z8c$Z9H#R)Ezkid z-=Jt9v}g0X6Fgj(5`DE8SiLpL?%0%ZXcD4;_Vm5$=t5EyILt5yYJFo|1h0C;RMY|v}z{&08U||#}74r zp^xvyy&8oMLocNKQR1G<;*&@2vTPVTZfE@`&dS)Ut?gKFR3XELWJRFEbvkbC*vGou zy=ku?wML1lN?+=?6}n&B>ML}cw|f??nr7a+pfl@*?e@Sv&NZC-#M0y78C1fh2Rp6q zaQ&;r<4YMG?ChOx=6aj=N6V(0%W=|xt+4F_il1Ht3G9g=mg*Z9l#0RO&hh(LO5A_H zk2{J}M*LMKJaP%+qvNy>aT9<&v{8q43ufQG9sU)M&+uj5qQXd+0k}GZZ zV7mHJ9P?(c66dwC55PI{T3JV5hMr=7TjPjhkNzX$n|-8{rg~w72IOIPD~yH%Ce#gw zt|erPmLZkB76%yZrqazbjBXkIw;85~V@%HybTaB8n}?O*i$D?#PF(&~Qq58OO!LF) zi+H#nLfVqMDEiY(b6nbwj+|x2)*mUf9LNM1wQ+TFC6|8U6+^BAz*;9IS_-&1hAgp-j z5!@mJAoAJOoAQJH1Y(3YrSRf+O-@apn`U|dyl{p)6aQibaicigFatc38|SHJN`R49 zZ)yV95fk(V?@%xbqa1=xuK1>8(8w^5%ms%j(47x?6#z9dhPwCyKF5Z9z2BOf|+wKRUv6^?a3Kw1<$m)=LsPwpH#N z93YnQ`?h9}G-srn0$$=Kh#~>x*lNBydjEX0Fn~UDLVQFZB5bye5D~T%V~Sp(SX10qN21qN zI+8t5=}7o%BOFQJH^Pzly(2hia$9KfO3Od&KrW_yJ=e77nzMK|}GG4${Lesfh{@lsAu zaqxt#-r2;Z7CC=Ge=invwf>aoqy=W8H}GmySYQD+dS-zc-D^oQJ%0&WHk>STZa<-> z_eSH6GF|B{(W0Zwsu5#CuJH8=(E~@BncSiKhoem}vJ4t+sv*;F`<+8r%D6!U3Q+tC z10&NzVVWi9gfTf!)`k2S&6Cyz91XP74w#!!>g@oWPb<-PYQNRyaP(jnvCAX{jq)#* z1Z4A1l7uu0^{FoXU039vM#<_Drs1Or+matL&1$fU= zI2UhP3NOS{mcmv}bt*h&ruU#iHqyVVkehTKQz&n{3R#yCmqz)=;h0k)8}ZL6?B{wj zB&|_0XMaiIJ1|dC$d3Cyg+4p*3R%Z?C}fQ1&-AWgD$K-K=M~ z*k^|hdDaUC>De86HZf%{10EHAffVL_@u=6%O>b10&yl+TTh4iQE*2b-`ZT%zsoH}d z<(~t{tvE4$jC{T4U24DW;7-JD*QO+Z43um;He!K(864`bfk()~#bq%nW4ni?UY;Hrx$5ZBAnP|qlaDoLryHZ2k2Mn-`NF#Lpizw& zC=a+OR0V!`Eb_TpQf!BwuPfKb8m-j_9+Xz*+qCaWFV>d3UJR7yC&*_PW$&a;jyPBo z;PE==a*p4K;bf2Qi>~kw2^!^?%YHLo8)RIcrlBTmwCtyv8ZBF5{6qXKeG1uCZeu}j z3r8&OsI5qQrmVbzPiJXiE{ZlSF`F=sIr%sA}~+Nh|JGg-uWFNnG4M$YK3 z2pXeLA7_rAWXsxv{pq)U2g#9lnvXZfwf7swvd&3B{bdy#J4;fzBrBk(Mzi?Hc?VasDL z16ghH?-^v%v*GSnqq9#!NNQB^#;{4pj~m1LDI&J1(V^YJS`K#nzqDKppWu(r-7lAP zi~M=#9G$Hs$y?ioUP;1n7ipkH$z&nLK!ILN?|C zj07GVEbS;2Bj3(MIhiv0&8g-v36pBr_!`cz)dQV1oH0>5s{9&_5p63R2c?4=GZ5^C z*CWWBb()!4-Rw*-^R2m~q7A2+DcAr!dYW05el41Ox@k}Ei;g|r9Gc#dq&A#x&XG{E z|8#TM?B;v)@`u(29O$wxCBwb@QHIFR+NSi1@Fte1aP42?{Y?CJK^^28DEHauqBBf) z0VfRgEZ%;P^BFW{h2N7~&$*3WZwh(~f?4H$50@gW@$nMSC^m(G6E`FDV{GM4N~N?6 z^vv+EmD4KFpjDv0&%;@rtx6!tNa$dDk7zW+GpppNfRf)H^ z6!SP_8Dq8etA;IOHS*2U7~YMAbZRm83)?G#bS8o`xHTR74l4#WxnPFn$Y#-s(Uz~5~(42B;nxfF1Dg{;6 z(21jaGW`Jcq>l6#`VAe}qkE`Mpcy?ls{n3~L8qZt6sx%kt^_Au9YUavd@D_{C*P(*jyB(Qg#D!(g$lb6hQ8&U={cf*MN* z^t5WtW3*{oHBm~?K$}U5Eit$VZ6iRu8vPt@&|vLGTbc`A#DmdI-SVDW95P%ZEXlXT zW5e3G64_j=NO6b?M#E4XBfkZ(%fJcYi-dP+rrX5pi3bNGwK!JmhN(fr#MYjMNwk-l zM(pXy-2n<)T(7dxUT;217hsVK0csU4%KHk|7t$z-a8Z#A6-N^^REX0&y@|o2%36A5 zS9P+EvN;WL4)er0;f4CMNV5vj|65a|o7LRZ)XIo5`Cg&HuPx5aVBbrpsAPt8tO<^W z38;ZMwT1;y#vb&rqnODz#`B|wjMiKLs15OC==eg(+@Ra^YnJ2-$>h*Q*$(PBU~zJW z!LMM#Z^?Sd#SVDmsql-F(r^{5cEN`Y;A?JD{I{Nzf*ojP#m>$mxmJ|BR%SURm5@ircvd?>SIbymEqK5tv0`OPL zGOhNGyr4y_jRhOG3~KWI7-AUJ9<*68(C993l31u%ss}n(F>%cud_X((G(=H=9CSxHc zHG&j-K(i^PsJQwO9c9DFfrsuN9z|-&irIkBw@TNo11TQ$0x9s$BCVP>781$4 z5LVGpAj4j{7Z60F&ojLRxv8o_n}rFzMb;XPw1$YP9=+S zK|IW@nr1LyaF~_Dxuay50}V5Oh9TssjLpy$jHbuqTtWd_9R+Bm18$f_bdwcq2tqR2 ztVgF0d$_d&bFuwf;CUqu3-$hQf^>#e_N*Gpn#|s22GYUOg=&vu+Q2 zy-WQz$EfW=LDrVLFX}np$kPjwIvpfPbTeOE#mAAynD;6;ki!8;7Q(v>aySVjHWi_>z$%dgD4RMM zd1@x}H@zl37 z$yrd2L#?9v>>`c4*o;S>J@R7HGjgV=i=PeJw2TulRne6fn^D~&0}#2fh{>oXb(#zf zoj>(>a5T&Luy0)KbU-7ZOzc-Pfj7M#&ovXDYlua!c}{Mb2fCwcuqu#R7UZm0bukT= zo(WpO5C{V3K;#mT-^7kI@gcn3z_EpqlsaNMU9C8pS!BkCfD+nq5+>!7V$*V|aj+Z-{; zD!S6oc2KWf)(I9CN){Y9w^A`nnAgf~u$q-qgias+JjUf5BU?n|?$F5SFk|PQeV(kS zE~Hx(XyjDX_6X{Yt(QiuHIbV%JUz%(!|AOLbt#XR{^+mEF-}15;M)a0mKGZ{hksur z$%zuX#humC&#e>>2Iz>TAy^nR46-y%SZuJ+U28|6HW`6T^r;nQ(WxUCx;DQ_D{-4VUz8CvM$fM>lln$e#twTzSsBI3 zkp-pzEU0J?KtgE)8w7PF84I)Jpi*Hz#l<&cTOpRDHHJ*8bTOTiU;|&Pa&b~~@S)ie z+EzbE@rr{NuMqj+XcnKPW%ihiUY0IG?N2^de(<+-hUH)%!~5I8+6L{BiyI__cu91^ zzEFp~h&ANbNCL9`cVm}h?40nTL26{w__4|1rPI8GD#|j_f@+4;js}7+fG=u8QM=$L z%Ws%lDct}XjWkS4>MJVg0fqXsrE!deRTN;r+u;a7BkBmp`Bbe8YgVy-qFIZ6f0^lC z)&oYoU`=VQ1?X~jXvn!tfYL00S6^<%G;#NfN+b#+TFz8N_g!up&rsK-xK4q4xH3lq z!FWIn8b%0--7Oc_dmcv< zJT_dv6Y4`6>RV;84cAP+xx(~vFtGMYGq*y*0$#mNywaRfXsBikt=b++D`diih#?|W zJB6zG95evN(##JZbR}68Dp|DAa6+tDIP4pQdc#(*jtcy&l39Sihz$q`c1E&cwH68l zfw0WZ->B8*klA3s8kE^-&VAd8{KmOyTai^kTg_L7g&t_;9_K6bJ=D+;UUMZK8CY#v zE6^pc%=bi}U2QHvA%vHHtDx2!>FSz#{QRlYRx!q_9a+y~9T2%uA$&r-i&2$*B!_)IC1{!czJJEk2l@=oUh z{>6x!e8YahvRFKVfyn{{+9Q7XJ!a-HY%uQdq_ysFTV(Pj7#};a+sE7901ltvk4{** zO>g>yO63Vph^gu2&1lutCjUR)A3Er+S#wib>q7^qEq|Q1V&4lZ*Ab0)hX%7ao4=(7HR*zk@P6A9UX*g03@MSKHBK7p_8=5Yz~lYlEW|P(d{2 z{g9~7L?^!AOzJoq^nmblW$x8LyY!wkgh6iMB<+UxL+z)*fr5F{s12@CDZ8Vt5ml~B zso%(n!Y8oP5(ofN-^2CJpn_5Ueyz4qiV$rfvR+RU3vhIpks`yTG&p`vN(!1&uyF>m!ER?X=N|z`N8{m4 z88*p%xUvT$Fkr`OO3OKmo4sp7Oy4y5(ps#!S?o^(IgSpUN=ndB9L$skdr&_gjX(Va*Emgi14l*ncItmd4DQjY00vobHoZ4f*d z8vrhAwP$c;Do_<`&v>A08R6MiVNN7tNxmLZgB#E+qN@smbBmD~-G5k<#RZX#K(N}z z$f8^8wCybeYt?M&)7nr@^5AIKk$knJ;4J>=z_M0kQ~_$6Q@Wi(LQ1zrK#EnS#y^FE z)zZ10vNE65N7x*)&z#0>xt}S3U<~6aDBF0DUW8_z9(S_^Y{3(92okQu{iTIY(NxO_ zcE#`sTx#rhVh$y0LbMe<9sveYH9%(-|9h&>$)Z+Fl6FyF#^era(i&vr)kcMwhpn{!iWaLuTgvm<6M+Dt zo64Dgfena#O01+oeGCid2U5}$+I+!|!{w~MSV+O>SvIsolWuJ?Sam=cH<;kL1gUC& zF^`MdoWRdYo8#w&OX%4FG0SGKcvNXml%{OPc!}w?jpBQJ2USf$a*@^DG}y(?>TYZf zVi8{lXV82KjsrHcfDBm2-$2N~f|7f))y5Q0iAP?iNTmf-V{ z0mGONJ`J4J4(jr1h`n}9g3g#!unVR-wYO~viCq}20)d(=58B}uPLVZ5M~H_xmZ1)R zF$Uq9N22g4Hj6jP@~)Wnx@+>bjG~pna=^4yre$nU|C9*1$a@SSjDyq^7TE{N<(dMv z3%ti(Cc56tLjh)cLz{{Zqz0fF@_b2NOZ3Y1rZ3$S_1s`On=sM^Qejl>UI#HPIMGQr zm~IIMVyD;KV3t&}GOx%VP3n;wOjniV2pSluhUgDBn7PNf{X$C!+9s#J#n!kJRO})E z*EW|tkO&eE1VO+sBhhE2iE*Emk$^Nk7t6)5$O7D;zYqa7k`OSkE8 z5=5y+JrU?;>?7RsLR&(NS`fTNc10tKWm?<_OE_=gVdu(xq16tsmChMxK|H{N_C_qI z#P2n+%qs_ha578*PJxlm#8c(7TQgZAMnDU(Pmr9{jls6!2bmE82-Gzh8@{n(hpd}}K7H(!EJiaWn?ox1`^1o&G>7T>5N-RDlIC1=S6h~U?s=+pqBWSZ#g8~%6R9J!% zEV5)%Odu-sq4GRZlclAN(Tg|YkU^9JBGV=gNCKU$xzm|%vm8^kT>u#-01Asz_}~tI z(GVk3j2L5+KgO_pMY!oNB_@A^qBp}E4w1dX=bqo*j>%oPWsg${h1-dQz2P34j(+v& z^gE+1AK^IE7vGFt{)m}y)|cMod71BVNG;6ej$wYiuy;Ln(*BeR4=W9~ob!(&df7Eo>ZBR8MsFoKRa>qnp{-~Lq zelyzrQPY`zBbxeoGcwBEWTtwbdox;albN2L620#xv)Eg^Ir`2`X0&(9o6(CmnaQ1T zYwUMuO&C|33P)1sVK;Nk<<01_n@x98=jU!Vf%oe-qla%EeA)K0k(Ym~HkH$+CSJ~d z%*?EK^KNpy=;Jq=#tLo`X$|A1z4v1#n|?0(;K$6xb%kT|DPY@A=jI0ZYKtzv#k5CF zx0w0qEuXya-y75OqLsH`a&HsiMK;ulUpT zNk!&`>mQ1)xYe|I&s-lpPMM0GdX}{{e=pAZb}=h`Pjt@5O-IF@%CVm}AGzLs&b)Tr zL$^~;`uXUeK4Ch%pI>J`X&%4M)nD<*b-ZHx;r;7%wi)Id@iVh2DfDp_YOx>xpsT?9 z%Ydusg#p*CX9tpc%#Q}_C%u1nMZ9kf*paCCqKb@EEoQ@j`+Ry=wCYwG{!u-$y)y&J zBeZ!uO8+7H>#gSO=+O=4>a922ZsvK>8Mm3yX95DO7HL0yX&&#zIbVOUL-jxj!@?2n z0HW=m+{5X78u~##43Ov^Z>|Z%D6zsSo-YqK4y1kicS-cbZRYsulDDG?x0^FEWdKig zSt?9LM|{eh=cP~By75!Sq`mZ_=u3B)(-gjThxw9MI5zs?r%4x%-TK#0n?r1Yp3j)m zDll$H;i%`+W>oaq&zLvHSHHG4T?teTY)@4`nOl0*Rm)bdS$_E{|DvVKFI{%w9RJc~ ztNbJUDXCOy?bh#n!Hi4;%Rl>~`EYq^PA+w3birL_iEpEn*u^iyZ}Mr>a@FLERxiEa zf~A)&T@_vTEi*NG^)55^tP6O2JHIA=H4}2Fa|pkdcCRJr{~+B^^87cXYfI86c&Sts z>4Sgf?k|}`4tvbYrSz#!@YAPWnBqS5h4@oR`g`%GY&tSuHj%f>L`$DE&AEfsz971G zlNmARilwWsS~h9*RjE{CSuS-x_5Xn1SbjVTZrNnU&#tM+ zrF=@SS4t1k2}zbJWrT=ZYOp1w5d z`Kptk$6Wc{MCA3`GM$#ubD-sC6Bi9XjDnj zKN2r}_0Pl78Rn(*Q{AM;CGSUT?>4;$@5f*7Hret~nOy4ZsAjVnA4lhHHq*-gJTjNM zWb2yEW}-RRyNjnSU$ti0>QzfGy~=`CS2mZ@s0oZa_$BF!h>uRvTYI;d6Dkf?-Mf7I zJqrFSL-@C_azQj_tEq3gqS1lzeH7P#_f49V`b~7+Rx`J1FOPh(a!t{fwwhB;KBg&` zY9oCvp^)@S!WzPB34vV;)PJkYM#?absX2ZswWuF8KmWcPE!t*I^vavJerlVkO*b^Q z(By8OH?~CI{V9-aBm zW_)H3$!3yoL^u7jxv}Dx_W>ExqTm6JtW0^pR7A5MFfAkQ{5rW$1<@G~aF(yLE&AXC z=FrSZZMjrCZ?1^8KVT-;f1G4B$vX*E;l|q^G~?SIBPBHXIU$Ie`X!;hzA-xLK{Gc< zUdpxSEG*9;tmN?#gcY{f#s{Izmqy=x(9EpgK&~cMVA;uU!`7OAF<&fCFW$Q4VRNxb zFOFKjXMU3YQ1sIGSdO+um5-RQ=_jHo3SNuOe8e2$jT#+Y`-nN6Mc|1?%)MQ+#<=F4 z`rc(rR}RC*lcJ3~OvhoDQlycOEnao?(o2_L=wG(%s;icQt?S6IE6HEI?1Ia$Ubg!E z{^FzP-CkZzKN!_KYC6hyQL-W0`#saBuI(dLA8r4k8Q-T_&W+7kv{`o96>HuP@o)g{ zue$ugWvQchHj-D?Mt4365m_7UeAFx;X#75dv36Q6^=>@`9J992S>Lx$HGDoIokCDGtJxFX9;_=rcaZlUmq*q6U_cC<CMriADWe3@A&AOKQuEG{{DyN!~^O`7QdG0mj5sl zTQ)q!_nzsPJe&;jsdck*6LYB^+O>hD@qL6(^Q-44imu^Rn>ph-qm9+ex>TW^|Mmh+g}V`GMDZSoG+RP5;566yC`~ zX+?ed00_JCC+1lHn5i&*wBkaz4E5xIQWjx)o-p&vw@`Fhbm0?b?BOd8b#xEBUjq^F zFIv9rQh`sh23*5)fz}0AUvSxb4p!wm+O#Boo?=@49->zh32i!>@}!yU^-dFRe*y}y z?@4oHkpg@;ZsywPsGpjdK|)lXfcR>dlJx1Im8b%g=%;33@$Jp5XX@nUXx~p^3>mqb ze`Z!s{^nu1R4ds(BovFVg;2E(Z}{Xz%Pw8M^nKAGyG&4CHY1mswe`4NW=Xm!>Gp?b z=ESX~mY;t?>dt8MQ|905&zj{{(PdYyNnN7&?LRZ)qp$qjjPmZBWyx3c>z|uh>Ag|s zFU-trQq!mAFf!43zc8EYhLaUbeHJ}!T24&3^v7eMIB#|?bpoG1VT5Q*IPeN|>9=)D z*Q^n-_SanQU%340L8o$9?$o4O|Zd1?{^AZFGzr|}sk#-AG< z{fwF8oi#VQ?irTx4blD2KEqMG>bmwlf)mwCAblR`X>hxE)?)Vk-K3eje;cWbKrnQFt+8Ioz z!hO-o=ghI=AA2fYJ?X+_=fCITbC<8W=<<(H4VCe)EPeaWw60&oU;Uf?(`ff|C>!pM z-h2-KHn&ARzcE4jTU*25keU8jbl&sqZQmB%_`F%u)qQ^|RWZuoTV4Z_7W&fOhsdq@ zRyLLTujueSrsw~w?MmRID6)R_O!ahnk}yd~GLvg^k&`4OAwYl-LPS9hxx%4{VMrzr zNiuOJfp9q}E}|j?T`g2R@K{t-@C>-R;^(3$2<(Tt>bipHuCTg_c&zT~`u$(`bkgAv zKYzRZ`!Uq3<6ZUY)vKdBaV5Wladtp-%gdk|u>?*&{dsZ4F}$Vv5k&6$21LHSiLteN z0elE5)#;3#u>?Z>c@=y!Dmr6iAbc8w)|!!vXc}3u4n+Kx-6YhblwVNB*t|c$Ev&?6 z1fB=K)|OjCIa4f>^}P=Cd%K7_Zk-i;<_X56Ws<~qu3&8Ob^s@&@SUhV7~!{7oTC&bwhNUjGkAWQZ(EL5regTj@gxTgh0uD=jDrTk{N`Krd zpIJL^fto))4)0L*L|5eBtWGKOjSHaC&||+(iivvBlo4Jp$?UU%xg_L#3`4701BdDf)5og~>?j!UlUlcicCS%`H z*~vKj1Lm@mMNU^5Oc|7wAFAz7n9F`BvPYwx@hCsUIkHd}yWmVwq-ymAy>gyb0Xan~ z?&x(t$cS^4qJ9VnKJs;B-`RmGR01gw}0FXtC;()O~o>Dmx%t>I(Wzf zrk2v*ino?3slb3GI1royBK@|b=N=;iODfx)bJhv)W|`Nb`XFD z%3(7~pT%u|v$#jbs2Ac1P_8x^g0vX#gI%50Q^|R&HC|nB&Jb3W^;6cH8+FL(>Lzm? zYV?q({?3|_`7^{6*M4U$4!&!~_^=$IzHZ7$m~OE%C6M34nDnHasWg}}lEzw#l*1;| zPjX-n;a*cGyFaw3ws%Y;Kh+bh?hC>AD?~gTiv;CXlhJ5I)|fIPmPo4df=R0p7fL^? z=akLeh5+*$Cq>#8n=<0pI&5D827UdBFNm>|u^LZgb|3qXrUsFvF-|9hkFp{n8rs*H ziF7WdM`hW=O-hL@iUJDtW0NY0IP1;MQc2a>G3Xoo8062ujz+R7M3PuB9r-@P2Y{sa9FE2P80$pZJyTMd{XCO2Nyid^ zT>Z+Fr6FHsQkJw^&OFuGxcZ^OgFqRV0moxl4apxjEJuAN4T;d>ULTptdi$m^asOf8 z6mm_w@DW( zxZa3#57#@iY6jP?eZYB;DvXO!945b(`g=J^l}s6qy>pL!&9(~luO?@q2DK@Gxzf>|?;Q z@x)C|uvJi5=!v@|CTKru;yW%VGv_q4$feeGiO#X8J(<$=Nwyku%Z7OS=chF6=~SSP z8y)qt`m|gAb5ExslWhe%G@EJ`+X=w1*as;cve;cnn=STjYGibNn9^gh!Eyjb=Py9I zr}H;z)r`(J*({0op9&VbG-TO3&asR$ANAjgGKyiiVn5iiUF&>b&y z5Maa$eGGey(gFr?sl*FYCvRfxPKr0yC{jZ8W_{pEbFn%&JQn9X;z5a5#@%v1)@(#% z55iz(*^H$1NAWL-=ZqXL5YbcnS_~XZGveWHGRohrMDgyg5cFHIT$L(Sb&$aW{s&ws z3N1WYS|-L>c#^&HpkB%C7CsA~U@3Y_Ja6H{gVQ17F(SW}r#NeApxL2;qG%T8L5T~1 zv=b!@QBrJ_tc*eoO(p-s6P0xUj7Rz-k5dY>4G=Z{&f}F;{dzLoh^%dx8jL5%T*o6- zZI?Kd74jI4&A>aChyVraP33K#@c*N7b|UjYrK6k5Ff;3iA>8^?S66 zHU?-q3F)gm(Q${E#reSCp*%xmbaz^e@}volFP2p$44Waw6Mu!p5mo~%D-phC-zg+p4xJIL;m zl9h2L#vEVEk@j*^Ms6a;2vc{Ndu^!&j@5Qseh+(DSy@U^K@XfXi>pc!WoYu_|LoBg zRcSG^_mWjr>doBk4RhO0ky$A;RiMSG@{l=WHKJ)@>Izq`l%rm6%20!%;-A*A%y&&P zyZ46mJ%h}mM8S=~TTG^rwcM!;?4Clffi(lG&{^Vng%?N{iN7g)Wd9prPPy3T%377- znhXzGMd?IUxmQ~&>H3fya*SOn6*Xb}HtB2eSr{LhdN0V#qPl}5r}A4+R?@z$;Wq(Mp%3JVvDF`6J{!#}14u(obwIVPm zCJ05){zu(iaNr`n3+!|v^hoY_@sT*DCIBVj58n>8ig#mpp=&vM$?~lY>zVGy@~y0T zMQUGuwQVVo4|_^%>C3AyuN~{lhv19+xKch}nk3f5@`;v+N5vmwc}eivE=0+n`- z6+Z&AHn)MI zAOQ)fV8Rv90~M^s=~y#k%?}|xn9f^#HiQJTV{u86a8H-`%ZnL%7U|Sk5}$rE&W6y8 z(ns5HG(;$-qt)G{>a{64<`VY#bU2JHZ+!a$3#Xv@2LD7 zG+b{=Jbw{m6~OEIpm+L}4Y*kIs9q;^6cX%@63L(1@fHil@siIe(ijA^pjCENv`6C$n^y)!e!S2?lL1Op4-I6Rbf6m6j~ z&0%eWN&f@vr{9l!~2v>ROz2WxS!1BPCJEHLRkjS|9r!A6{ELbB6=mGiKH1r@qq4wJ$ehJX{A zb}v>h1@cX+adi}i?tTTPecwHd{fpQGCCemqzV)-IjNMDc`*4D59SGC=#E`{t9!`s4 zE1HzOMAiTyCGbQXz$Yj0;jZf6!0GOT5ll7u@FuW6Y@hHY@RVR7toir5vE8DbWX}NJ zuOUOIfetVqa(@LWvUg!W!Z_r99e#J}3&<#>Zef;_ zDt;1dF!6h1q30)Y(d%-ppp`Qbp2(x(X8sR+WY1Z6{}KeSzZb=c-09l-Fm`+sLwdm=zLWz%pkNkRvmPWOY!tt{fBzT$^t@nw$fT?iTb7<67c(ikxdMgL?hW7dk<$-Jh|{TN3{J7MZ?Z^mU-@q9ABChH+s zQF4{U!|rD6HI!5OWq6?Fp|qAS8dCVA84Le_1;|X-fP6Ud2qZ5Kw~aDIFOh%FQp-%R z7R}kmqPFow9?r1WnBgOMu}_4%`8><0yM)ip(}P<~#Yqiu%BoDGF<4g+FYPo{2(Oe} z>ST1D@n9D@4Qbcl{s@Gl(PDnA>j)aGL>h5i7#(0dNa=o|7;X;%IJ>aX*#(}U_KQGH zr2`D(Idd$BQavapLd)W`V47~!8eyOd=`*)99xG4UoHMZo>**$nP9iHomO&qcE*x)bHy;@vi zB4(3PnX4L|v4aE*_R)gsgGiq>s6Ix8T2QU+n!tf~s}t!ntCKoneI7JQwLyf|q_dDd z!x`AA=Um#6+)C6n)}>ON*=|N|~wtJva0+v<| zz?yz#I&bZB2%g>X6~ddBk{CNA{+-UVlUv{>93$bmtRJDjHRv@&LmD8eGPpB$!(-U^ z!0jVzK?P&ofZxa1VWUI?L@)zO_(pMS2G7WSd@CB&fK=HG#%3D8vK!&I9txGjZ^7Y& zIFi9*le2Dx>&KXmIe^I}@m?^e0Qf8TNW51>W%3Qd;`=dG!uL2gz&B3?nkn$j1Xzxc zr=%@mYX_vFTR|DefyE3!8J28dv<$_Mbtyu^TOc)F?v$*3=QFmNfEobLY+-B^bd-<; z${*J!eeLQp zph5b)r_yHP1cWke<4mroaQtNZ5EDG06b?}R`+y$i zsR88*6(MCvAKLo@Gx-~dWw~F+e;=j zrn*$+F;lsA1>r}N*60etNfT^rTDTUY6`f8H#%E0!XcsGxX3#?%8N^kU22)1pB+AuF z?wt_WlU?NMw_^p2gGmpzr2~w$}D&=uX)YoB8vu&e#r`)KVNDf6kj3`n2I% z0_`6mx~>El@$fCuCAsP^+LcR2ryCD&Fb#PW?5#L4S3_QobT8zUsQxqe4eh65x963L z7xKB=sb4T{e2cN2HoEwxvrydiRX$fG$=~(w{`_WXfMObK{5osD!f`|#?n3z>De6#$ z!8b-cc9aYLDs~m}S0t}kQ^YfKXQF?{qAKsS<`#`WySda3{-)?sVC?`vjiOq`p&~v@ z@(WN*FOKR(af7u;quBYA6wed82Ef(%#g_wkdM}o3hZI#q@Ze7(sP6hzF@M_<^W%&j zffD~_QTv}1zJWYn8YgZa$kU}6;<X2B4wQQ4DN7vw7vB(;2F$B(unP>3A|+7Ym8H(O82$nV*(DjLh!2o%N~$BkcR zy%~=2f1gDR@iAx?tidV`C^cXsmX|jfC0P>~TLMi)YWHX9&Y=Y~vzYa3Z%em9G19P?YQE_=WzXEU3yjRZq&nknY#uGawy8qY_ z286ZAwAjM$P#%UUFV-7dv>)Ru7wH7tNG!Y=z*qqAIZ>ODHXb_kiB2A<-$)D$=;lum`1v<}!p}p9aa7a~;|0NK z&|lgvtY&-&#C8&J8?76|&W98PU5*~uITe$AmtGGjwkyyUyH2m3iQoky2Jw$;qViIG z-2eC%Zf2tQzdxj9Yd^X4m#|HbW7E%U(&KKlG=`@k#A(|Ym! z*sB0)=_)MVDFLI@AH!C(((5GKd}MD-M9u4^Xyp8<(W_imI_ZULJvteL`#)!$+i0Y-yV|CgPPSoMj*_>ob`CV#h4VR67p7s?$_^^h~^jwilRYOQ&_# zH_Ol&ZwY=OuL$m-_Ko8s2B7^BqrExc&T0h;%X11|O5B<5K(UXU4o68d)Dw zLl&&lcx+o#wH0_~5l&F6Lwco#lT@i*5AeYdxfMw7F_CMZ$k@95XD0W=l?dR;E}`|C z&E&SK>ixiTEJRKq9pw&*p#Ba}IUo|QZfg!wN>A2UjTxFHhjQt=`hOMeo79>+&Y1H}6A+$m3;3(V0|(z2c|f(zQlsEIJ3eUM&2~&O5zL5X1Zz_xS1P&W-JqHW*}d{&E%; z#P=6*mu8|ej>UK@c3r8qjB_A<1U{DD`T4Jx1pj-4+V0XVM?Uxy0>MPjF6-xT^k2jF zG%a*ubW89*nto~?)DPFRl01)oxTckCx(zcjRU@6=g-K+!(dLYFg6B~8*;zPy$=R7Z zy`&5d{2Y9rE|C~L1-JH|EZ4msg{Sxv3Gh}n=!+gbXUq)7R_Pg`oY#<3)hox2$#6=q zoY|-4JaSsj56EdLM8HCiPHZ7Un%;<>Lk@@Y(4NP-`cLHp6GdemFBTVF%+u(nT(~dc z3q(;3ukU)enHRgHcf0=dD=yz6m5CQ$;TgBy$)lr&dg?tTgX$VQ-XRTTr6r{zs-IjT zc74xnUCBxE)N1LbF5evavZ&|-S)_(_m`q49PvtWqt>JO~)~8|d;%{v8R@5!<_$%3_ z5}f3Dmdx|kwzo94G`6wX#Y`+OlilfbC1Ad{4f%Boymbq`^{fxtHn+Fcwzm11RLaC{ zWpZ9Dy$I4>rPfnl&)k_}?yu!U@o0;jd>*}05@_|-)wXy$+A7)ujZ3|iY;`GP%Nm|OEqRyjLJ_ZFdritIp}9}MaIJK)LmnkD5c7~Q1t;>#wj znhxve%LjB1qcKoRDz2U9Yj3Id_?NJJ;P>kiyaquVpf&ms52H5>>QJeTEkOw1>Z!+z zrET8%eoqrSX~|^5y^bf>(tQjlx1znglF>eSi+8b>9tyLoZ=8>g!@JY0p&6Qk&OU@n zTRkm}bqn#y7xpgtKxcO#)J<jVNCW((k%x|d;^#V~# zKx`6H%a9f9%^ZXT9iF+OJ5 zw$t^`51>ss6TF8X6h{ zZ;JJ8@}z;Z5r`q{H;oFq8cV|0;_E$i3z)AAs#@6CS{v|s{D3_ox?S!sJtRi7%Q=JS z4LyC(Q-^*D4m4BJjd=sqNFB+Jp|kXk9$d15eI#ycmoth#r}2yN{y;{^qH60KF`_(6 zLY6~^I2vn&8QY6u{d76e^%<-Lz0Cd%AJR%MocmkYY+w*ly4*LJRmGtP;cWs7SSd^+ zWSb?Rx=l=2BwrTWh@ld)(NG8Fq>A4yl2f8+=T3Jepob$rQG6qh%Y6)%GL6Rhg`p3S zFnTMe-dC%QD{UMG7%p&%i%R6#!AqGoergw$umYe9G&Q1^20_p^U&zGht(jII`lzv` zt~Su-@xyz7o(D)tSoT}U2g_vlcB^a<`u-KP(q$P^yn@kY@G|sm03TP%ELl-8u(Tj; zNb!(LCJh!xHpppW&L}y&^w05F*U{VLHSMiUUPk-N^cj|hCf{OXEWO)}7FeV6#M}-! zv5fYD$p`fK$@5K`EM39q&MjO}puM4?u><3oeVZc=FO;*z!Ey4st}SEbNJ&~R9$zmz z#b-5gwAi>*KF|7B7ZWd*$p!t;tFThPm(e>a9`a=EEeiv>|1L-?t7M~~%%U>6G@cID z0`vtbPn*Fo&IiP8ljM2}u2sFaOisWy);G)KQR&B_HB4Wv&|rYk!AZBvXl`#}c%4Xe zj*v%-w5#O}R=ngTMvanltT<^E_5t!Zm0sf1x>J-4mlH^gaFI7&9wcm6%6%-&IpT|Y zIaO@GLAG@bzfyi*ilM8B__!IHi2unG;}Z=1SHh(iLQ(VJFcX2Wp|Oejb3BsX7B~<{ z;bo7Y=bC)Jh3&2E4b;Uqt1tlXNHTRe@6yo@eG zlOMsyv_cc?F<{VKaV`0*$^v{5M>Duuuix+UYyOATi4RxGr9&SEHr;;8FsTVEyJ?K6-(tT zvG*!j?L+SnphI<6x=sAySMuxmmF!L!GI^ZZ1Qs;02gEy9%ele9 zhBIJafSs8XPOvRd8s@Cl1>RjNYmw%?H?SxIZfY;*{x{)5?y#m9L!h=eD25RjpIR#($dl>%(>EbyE2Wl51 zoCtNm3K%H*h~=U0LY`&?I|}7ubl~e}ZEZ`~=YaH2mV6IAO!C*W8)5&@2ct`|ORraQz1HF8Ss9_YQvSBGJYm}CxYPGeuQ zS~|rhw%9IAWn#lCJYF2RNFoCMIR$C-TgfhlmJyI@!PH*J>S_Zo-ei&oi&dE3-e5qN5M+gb0>`uAou=Mj8_d z)8$JneM<3NR(VF=UU*RWEOrq8hcIE_)0>7y^rlbnZKCxC*%d}#0PtSHmXL<>`qT9j zO+vN@{Wqyq>veijn&u2&v)524UicO7SI8-;^vbjr&!QFP%K#gL-ksOp*i=v6d^tr| zgRK)0C|!OXPl=;@E1J#HFxA;eOcgh*mD7UHL2%!(Qq{>UudPfvzo)B0P;Pf!Y>diziUm*vRnaxwx`a&KyQ> zTbQ_H^r8U7IK@vxMXTWfCwuBg>9aHIn?*B5ytv~cxgS0U{P*>8Rsy|Xr7M=v>Ae;~ z4lJ!?_lf?UawcMlNuBcG=#v<%&=D*-&{!8>9i?K!I=MznoGL33vmmPesmO>j@k1wq z2ReNZxwc9+7nVy)OTRdOgFHQ&KI&i&->w#0H^?cjEqaq4g_2*aWYy$T3oV)X!f}%v zmqrKqAukO#GaqI(2};$KiD532?o06J_|{?cg8_Z{buR^-A zB~rl4hSG~x-YeQYv<}c`x&ZtbDZMA+^OPdT@2Ai1;CVkx+xO_Y{bu=tFlmffzDZsz zy(YfeBv(sz@z|YM{LH>nj>o%0TKd*IWhV~IkRB%e&~^MyS&^iL;)}ZgKG#)nx16B- E9|e1-kpKVy diff --git a/wasm_for_tests/vp_always_true.wasm b/wasm_for_tests/vp_always_true.wasm index c80fe1f8d6be0ed0217e60b3fedb80a22fbb733d..1c4afdcff358ecbc157406345ebb0086f859e60e 100755 GIT binary patch delta 116944 zcmeEv34B+@wf~*DzkNx5*_U6Ixt9dOzNiEONNyBRQE|Z?7u@{?l^_qb0>2=j(V~t0 zp@WKTY^g*6jp8%3r5dbNRMe=nV)be4!ZwX+P*Y^G2dnNfKb7$_% znKNh3oH;Xd=G-5>TlkwTg&}R`6_+wi)7URr$t|JsxGRny?Hm?o@pIhtY)2M45v1zz z_(JU*O*=TZ7A~ZW9(tk&dWB4EAzOHkjuhmgcldGgJCA-8w0|_@TZp#{&+#Rb zP}Z&9+kjC-(_dyUvLNS7V}bzRHiI`?p{dvz}l=-I)5i*d#@{COfw zXD(k(u3z^u{OFn=FOcrZqbPp3t}~X;;|RG9ARNFMck2Mh-H7)2K`Olp=q~O;E<`xj z7n=B&%?>-vt+Rw=5r6Yd zo3vnDvzikfY<&Derdcf5P&=_cZmW?G_MFZ zf7PW~gRSidB4%PIzc@Td3kp`lT|>1g=@zq3SEyNSpU514HmSznD7cOf@$kJJx}l~p$*jp@@qo3^eO9a@XujX z=F7mJ+0?|Jf)j@v9cIF9u{jr6e4gdKB4~Jo+wcadR!aw@=H@4~Ml&D+mOd?ULTE(} zQnQ@Yh=@??7okr*E%&1>{Sv#vXL?rHk7s0=-0~$BWRDLUlf>wB*FG%pptS($WtKHta?=rUikvb_fUa z3KpqbKRfDjWdR6lGzXyrnHV%}{k4^Ai|TeQLH0th%s{J|#gIcK6luYTsud02{s3}p zVd;bpJ0T&)Je&oR0VD@VwC9+_AI6+ew&6MlqT2#%su;j2>TXUPH}<;<>)jEJ06J;IT)GnWdscJ#Rpk0Md3L%}9c>j_yD*FVt={Gm7%)zvOC)i13xDF#3yS)i)hO47@?qftJ^_)eEGUxjL!w{e ziwjE6@1FvK)}y8YPMjLN8S$FTn(J$IYOzRy<<<%oGjp9R)Pz>V^PD&#--394V$p>K zr=}}PSFtbPRvU^$ohm?8#Ii&w$QKaliqcgm;04L{xl-+OqkZC{BA+?X29=n0Q9(Q; z_Uu4K$Ru2sNWyiC{LFSYGrQqdGmn{NDcsr+s}fO#8@DzfR**r84#W#HNYT#YF|#NW zY#UcS=@CMN72Ax7nb|^5X0b-Z%!qKMqBSuyN4S$w;)E_&tu8Wuo!oXnc^9N}St`)WJ?%bvWWfoH%3}Do8fG3ympG^?SCHCnzG$Q%K}wmJ<$( zCh6f4NlGN%za%?RdPyj54o!gr`T(B>ijjbL8Z^cL;(aRUu}P@9~^_V#< zRp-QXb?J)sT}6_vE|H|DTT~``FDV{n4o|_uPyqZiPz(jc)1WaF5YI`}#|ox=SAKI? z7ag32n}(=hC0@J4m`r@6BvBWkkb%+$J(lVzZgr$$#1ZYOnCj^^J+bwYnsXgc)hQs! z#s^c44~fdmuI>Sgs8Iw8pqJ8Z0|gLIk0?+8@%%(rBM28sRe<>wSf{H2S0bLIG-0|K z@ybMG*0BQopA=slk<(8dv#y52 zqUrMdV?Cl2(@0bpsT^i1N58$%I0tBuu#Ad9sRDyj1%@P=E-Q$lQV6VXtn&Z*`)?=u z|8)xdz3o#JBv${I<)MEF1xdObpkPwj*+cMs8-*1Q{!3F(Q6TcK%tML-IseKO__k5< z_r6yYCcWu^ocep@RQ3T1elrIt9vgaqLf=9m+EzL+3jd3v@b8s_|LpQ=3=BWe!~Z-E zKOklP**yF|9lqj#fd|IeKY5JN-qV43>Ko=M#e>xcD0F~BQoFNz?0YBLz$0Xg)YD0F~_{>Dk-fl+pVhrW#h-}e519QsZa zIv|JsPs<@S3dH|rjqlrAkm&u{xUor#Mhym-DWgV>Oly;Ndoa>W8Xs9bu2_{8)HN2o zDbaM#D6*z;Pib1e=ALRc{MsMDwzSOJjUZx@!82a!FwR-oiiCbn0-3xmQwfQ$SC?^E zOqaJ0(bxI(#Ify%$6=ielVlznqG{G+@uR~iv+mdLyJqAVb& zH!72Apg%3qa<5rHtzeA?;smJv2*QgeHr;y|Oo|hp`)=Wz*C(3Sh{VtD`wd%}h_6|~ zm)Qx`Ys)9}JnY!slEJWsRi=xGVcACFh!CmB`WJSr_AA(HkBH$B2E)fD;%oaydJlWSGc?YWQpV+!? zU})D3^oXi-t0&>V|HAwwccv1MbGw~$$TQ>k?t9mF-9LpTt=M6X^EiT-(@ivx6gdAjtxkxeDL20wY-NepiJ!lX4P2RzfTR)5;Gqz%OgYd(Y#pj23D&q_;=rwnD+3n+`ZCYPyFEF z;d>{qO>{pzd~YbbmdU1CHf-+~Tb3mTzB`P#{)vA#65o5bV(%==mnGJ`TRtccrry>- zo4`wMCW>ppTxI@kxvhY;4oQ(0SnxWoB_4dOAQ9>;;=686Se>Q(0~N5h6Eixa?2SZ6 zXVmjMa{hA6E_^pUtN>9VZ$iak=VFl4tpUn z{O2|L4LP}Fy=^3}`T4Kc*Z<-c3^&|kxR~xLOm0fN@ymL2Tjj<}@&BfcBiQo9qZ@~Y zM@|a+F>Hs#VD&aCv1{Wog;m0XYYmga9t!i(S6@$TiX4Lo&+tfrIprkN9AV79*Q{zr_rU85n{KJyMnU=vVpHK?ZEGtG zS%2x{0~p`9ByroXx8(HRf(|INc3==iM#IFvCUM0RhqJBgH#~v6I=vqy9{o)_yD@Rm zlLr3Z@Z_2J|EDJh;D7d0H3jM(g@$JOL4}w#KANGT8w9NnpuR0EamO>GA+lbiaerST>)EUMHVO(8KYkYXCjO9k@!2Olb%qvl z1?9}U^%DwP)&g4?uL@U;?k3P31FBMg?&QPv{R(9{E-;y zl`j{A5Unp8L4PXmmc+)FqorUw+@Pt`iVz$evpP@~qY=}EFY(dK#~y6@M)MKEN7G&8 zaNO{4WA3z8(y+4QI#R%mt{kS(+ZsN?dSU(CtsXY;a06|`w5uBL+$h<)4$XO%sjDKy ztqN-5!{7dA^c! z!YoRiX1J_hW8%bs`nCI*#MEsS$MkNpr;wPb3)eAW7IKqO`>);B!%qPvnmT|DhKjW*B8I?3lL$DwdR)yx|sFuMud;(kz*1ky;@bV;olLf zv05HM&<4}!4G16Lkz=xhxPJ3mLcaQV)(T`%_h7JNYpqCJ^!xs~!aY>eOx8vGajXmX zbKHr0em^leSG7|CGx3+-4`dCAve(M@7T8^hsjo%x@}k#n#|0_&dL27DG5+;>y7iR2 z3*{3&y7hF^>rW4@qi(<@7qA#Q)0Gs!ek;d%otg;&T!sw9R%^aWob^U^;3SX?B8f## zT7T0UcLhCLHlt{+JpQM8ky||`W_A{O?xV*DJ+c*v_&X=lgi!phnfUCTa(X-sk9F_v z1N&8pOWxhbEY!08zIO-u+3AU`JGbgLM#|_Wj*;m4IE%#-uYNoz2lrwauE)@In1T8V zW<=NLe)2=cN)q>ex{#Hwui14HXVJvM&rSTl`|}2~Y=K!WeACy#g4-*yPhK~FLBquB zp@IcO>B;LBAgwgv{z4R&3h(rF^G#pFSDl}I{Xp}d-*{PDZ03pb#Dp&fv9iR)Un~Z^ z_r4IUG!fo?9#WcipX4%`C`oMDeYj3x^h-O8eL2JbMWBRkT(P*UIDrjGeEH?ri<+P9 z(x7>oA-Zt^W{FuXPD+@r4rx%6tX3x_%T5^+vsO4M*(yb#a6Pi(UX5@{;mVD0vTws< zcH*wDE{I1!BD_F#4Ilsbqqo2MGV$c?@ufx%l2MeBuWh}lx$Wz9k8jhK8o7cE)ax6* z`ugjyE0-F1$TUXf`S8Z&f4pPO+8-7#HS&=-VlQ|FNUhlmT%m{qo=CzgBIv~p8NiAu zCDI2o^yxs|*9WUoO45>0qNoa&o{(aM@Q8~}^ccn?Za2{*E_e;p&oQzsm!Ti6_|{}b zP_O6s#k9tnT=gJbU=qfIcJv$)ig=Bg08)cb4&!c?0ij1%&6p7gX+bTByP{MyaO-ib zu)uFYv!)cl@CZKy55t>lSk*YZA+_Bjd@%!pHAs+gFOj4Pq;VrI1gb(o-0VUWdZpHW6?qrQ%YopAO)h0;ld!a z4VMVX9L@@k3NNnJXCvwrS$MiwJ!RwR9Q71goY9xH`l%{0XK`kC*6PQp)Lc0m@Zx!k zon9p7C9wH@L5xv}1$%%Pp;8MMXEH^tUZYZr(h!A8DDDffSS6O&5RY@%zgEXxC>rm3 zfP_3oYBUWJM8SOMikyZ24tYvJa$T{-PYe4y66r)wQ5-j*>5qQZaJQf2a-BWRqVg;k z8;sya7aKpI3_PiZ6%AzF>cG@fjwCgx?4))iRmk^UtRjG$Qo6dvR@j;2X2-E8l`bYe zr>s;il8jWy`5sng6pKPE?{#&Zt_oaaj};p*U8$>;LUbpOB0Dk{-muJp3va;hSog{nwrsgN%sOI8va9P?delt|sjPGv=MhL0V3JO}}iW17<; z-(4aq%mwpt1<*zE-PN$Dxw(1vVzW}@hyc?2spdmT*hBCa%{PlgrRawV!>p9=`dE3m z=;U<*X-L64S>VU*Wh5aoK#upb>jNbX>^k9YnAox;G5m}YRwDo8XNR#;IXJ*(B6d@N zVGPR$0<7q;5}PKtoa54nS^y}@lRE8M03Cyiz{%@O{{nO>v~O|qTZG{zxBN1~jL|CZd^0fLHeBsMUmz2ty-G2T@J9KFWZ7xCMXMzpYD$Ra!b1r*|Wy#h&HaCJHmsw|) z%+JRJlqDPTS^e}4Nzzdd(3VP&A>EbCmM`VA zd8pvX0(J}nSpeS$VR=R&JH4JvsC8=xP!)pos7 zO6^9{72Qf}MP?FRNfuUXW>QNs(!niZgzy)pA!K#i6>xtnQUNU_h{hcoWs}QrgCA&c zBZqP|Xd=f}29A%Ei!9j@W#@UbmI~KmL`uupMg8nZ{!%D(DA%fE*2z)t4d|~Z&Oq)} zWtbDg^5ZhFQ&{#ZXBPx;joM}gQ{G(8%0QDf<#3!~%F8O)A%{aj?%A6Y$H5YN#E7^c7Ndb3d56D{9}| z56LsCSu~Cz%4uW7BxeFtK}r)M&?z0rkBC}NP{-I)1WhXGO48slo*Xt;myhe!EDnYw zs@pew!73m(T~;lXUskh9mQC|ffj_0M$=d$xsDSd2z#`?5b2@M6&n{(AtgBWqNK9z& zi%sGUJK|dm`I1w^@^g@318o}FOvOdE99_dYa&RL#6geB|5vvD+DspD$mo@Cn@ZNi( zJVb3Sdr0uN?uPKSUvg+llkHE`^MO*1E+N;4v4V!C{ zw+ZN2M@F$7qg$?H{vEO9$L?CVH-NT>)9&au%)|;wv=`vP^T(- z7`|nkUH>I7+KE;RWg2B|bTV1Je^P~3?4b9Zj~vFvdn%SAvvmtLeqM44ZNLL|vbL*u zXXGgMBi_*U0@4OpI}oVact&lzoJ4CksBRr(btL0`YwJClmAS)xfZuinJDtsyXIz0! zd1<>`eg%7&cWswP&4t>tymR$jHkR?`cjO=bjlF@e^YM9XCzHpV#-hA?yNsR2mauc> zho?c5waY=Lvy*xEC-U0U*?D~V%kt}MSjlZ?V6ZR5_8o)^ht856?3{WDi?WK9+q<;k znl?@&zQqRSa>NH}TCIvNlOJ5l#`7I-%ORJs>*bM+>`?;jpM{+Ogo9_nlhXa2*Uw~E zGgQycV?)M3sBlOod#Q{I^;UUhGj= z#W;-en~b(QNfKN4|2CsNXx}}V@gK=e*RVXNP!QGIF@u0q$pmz+eRLPXhP0f}1j0Rf9(zQgH17g7hUu^2DaU!fYh?dnx zg}nD(5|yg`nz%g7U{>#XL%L-&2t;)XMhMS zV0jL_W$a41b^#V7SIQv^*+FQkJbNK|ta4+b>5MOg)3eU27qbgk#p{`DjXr!OGya0j z=GjZxKpHW1OIaONFc0xz&m-edO{+^X!;_f>0qD`p_#>(DGJ*i*UdM*XN8{{ZcCGwt z96JG5$&;Gd=Ln8l#A<2!!-S}2N*~RXI8n#17c(V7YQXA6>?KzAlJ2D?SNfON(^fsa zrMcqr-0N6l^3-5EP=VW4K6g{QfIG%gLiNw(QLbP+o>i1S<1myh)DmQe7?qvP>kxt9Iad`gi z;8DF(U8_?)x(W)nnrMPQg{(*tz4gxoPtIS(PSS4<=GvOdio4j!jBnc}dw;}+8n)6f z8>@QEh38{H?tGP0m~-%i^|rP9Re9)-*=OuBIi`*MnO)Xt-VN7%)h&>)!BmHB?{U!H zOcSwTy@vqJklm};o%=nO7oEk&QJ1}XCLbfS?_o8l2D=2*+1P?^xQ8tw-UJIO-t-c0 z`jr;vPit}ic@NvY-xW_g6Q0%9ox}ZdXgkY4^f%XM$edzGsmwTJ(uT}9##%>ayj{lI zv6IpxpJ->xSe-onUUqu&SQlKMJl554tSdL&%TD{IcO&j&1x2YNU8oc31Kt5_!-4tO5e3^U3?!HH_UV8#>q+5g@c}ivd!%(YG+s__`*n(5z%95I^*%X#eXA7$kY0pS_0 z(IHfDwX$^4MF<8o@Cq8nW)4hfgdYcRA+QaO?&#bMPWE5|Cqfp~yGW4eBYPiXMa(OY zK`_QUL}SXH4xL99cF>7v^0GKiwUgNJdd!b|BJRW_ld~iHX`-^ zbVM#rAu?;9h|IDPxwsD^zAn};O%ZhvxgQkCd7Kr-VctaD6F~RCxFmF$kphx2>&|QWr~~U5v0?GtT-~dluUzOi7X^( z=^UKSQ?Y`hejY)}CfT$3j{zKr1`&i=Mnj-Mi#!dH2q?@E{!XF_Ox;OL2~8AF z!P5yI#F8SITb=%Cm=T&ogk2+pQ{30t1I zA}|LDa9oTsPF{|@n4LUx=b!{IwV}Kc6vqn%H*Dq3#g1XG)?|Yo#)7E8i%BmGh5?yuV+fneaQ@{mnJ0tE00)Sz1>qw7<@(>S zqb>^CXP+mB-855q0dOC)QL7qroaDoDHRfyq1UZCGCaf?4Tkkoe%ATN*PYuG>j6v8R zj6gPB@JUTV%r+rW{qD7Xt2Jiz=WhHkdH0i81^@e#EFS~*nI~DlxZfT^R8gbpwK!=V zBt9{y{M2+cQl^rj_)v|*UUkHgp#RvZbCBY-^o-o@A({>;P6HAIf;dGEm!(X=Gl3$T z3-wSoFDe4VfbdX)P_tc$8-4Cg*6hJJ!qzYSYUql(X>f~a4L=53`POI~H`lbN!j@WX zcUB5xY#NiXJ8JZxzN6?7eGJ^dx>naMN>IsnpJK5=UMnz5gk#V#RC~u`8l;p!Dy6k- z7ThW~3ZINal`+v-E6{JwB!#V6Hw|=jK}fhqbHqb((`MFxW}97!J&=Jj$6rS}4SFg@ z1pZo6Y19w+>&Z+Y5_KmNk}P2tkw-ku%v|y`+5u$*x^gTs!s0OSVbF=>51wYBJQ@zr zE9o3$x3Xb0{qp{&S@FecoN#LiQ_HM?6`TrMYlfQ!0i1}pFp|8mbfWS|M)_rSR-ZL) zDh&F#wOqx~GsxZwcj8fW?D!*HaZ4lVGagfkHO}zL5znxJI4wK<8CGKfjiFixXxBs% z6nNp7gzT@_3E1`(VOTdQ6fk)M=EDr5HT-XoQ>>8;kRWWr^49>9=d;j9G@plN;S>s; zBfRpfXPAL%#IvlXDztXN{D$URp4<2*w!jP?9|i=pAjfK6{_g8w3O7k|IqO+Awji{Y zMEi*l?7$;p2Iu26B`AOKEECXXUwc;Rw!5BXmw~zg zYvZTBsbM#dgA0?_2(Xdp167fN!4<|)u|4IaGwkc0fd);*dSNGkc_C=@Mi^RwMGCM3 zsaqfNX<*|p7y`YXa*Jp|YHfSztMun8fY-#a~L1f=mCo0p@A z+e#DxkzvT_lkLy3ABCXZQ?IV#Xr(;id7NbXWZUyle}i(v^Q_A6wZd~n7y?$l{X9Fl zAUvA)Ito5YLAh^Wk%98K7g#ieUao=8Z=f5)^2!(3aP-US7trfI`R5l{zaaE9M)C~< zQ5JQxW;Q~u=w<_$Uv_n~f*RsQG!rm3aI$y9!$uAf0k%`A+qn@2H+Ut!*-+?K{3c6Z zWc|i^t*;a4;Hl_u3FB zlOc7UAIFeNDX<<~-KAO8*2Z>7IheyIcemEwi#Xks!GtrKKLOGXgwc+Hbu+#%K#?wX zDBx(A=&)1;?gTY=HFkP_^AeWlTRxWTWj3xp+~-mWF}Ahi20+GEx7Ch&EvUI0fvS1g zF2rj=RY)jn1GcNIw$J4JmsvGFN%51Hu>qmUw_j#PpIBk@{0 zVve%dbi?oweW3%E-*kr=D5}}}RWJw#1K^M8=x#vOl?5mOc@=0tk+1)jmHTz57=VED z1O7WU;&8H^#DotcOvnq&tzaK}%f?=hfF+O^c?zcy9xINy)zZ`p;imf+I8c0&VI@58 zci3`v$wz+24lSmuUD=qB0%$Sl7P*71HvLUF__1DLd6f|DK0=Y&8Us5~JAn?AAK`W~ z6-VKnU(ua5RFKM)#;p`Nmz?%0E9j4z0%agBtde;qlva)4G$Jo;fDso{l1o1D3if$^ z{Ti#1@4v!^3424bSZK5%83N%}JVW~hKGoq??=y1THddOCjbp6#)d;4oK`>A8EZMk? zjjBhlpjS<`YHP-V1y!JE{0d~CrceY82AdcWUXLbvKn3XSNeZ}S&o)+BqS_ACk`A_< zw!@gI35HD|P)6G!1t@iowwF$FWA6wW{1td>`Jf-7UYnpqE8Kqn;mT#!=55nQ_$flgv2kSuJ}2_JrTQ-<^aF)j{yGs9>9N>83*t;GUEXL$ILi@e~=jm@V~xiljUOq zes|_ekmcUYIDkKx83*v6XT|}1Q)V2%pR|FWiv>w)Y?1ih`EFkd?aqv&o+X)ZP|vc= zIOag z^@qs$@jKw_x8IR(yu)@S#pC!@!I%uaL8C?@wyskqcz+de{XwS5Nd z*Y586Mv%Ns7JS0ap~hbMiQU-eKVc8-H>w|AiqoXWKZPX>sQBkkfj98&zf0lUxQney z;=AD;S@T&MUqmneEQN2W-2NFmB)<>R-QZ=?%ZGjr1+q?F`Z=5NO$U~5e$LKFUQ&hT zm3h!3h^d;3#<$^C*Ke_4)Hc`I!1?wNw1k`@IdJ{OZvuHQIF6V)! z0S_CTtPy8*q{M5M2RgL;z|Bif1N3?8CNPaQ znV1l?FM@?AGWxjlC7LMveBF2Kh0 z9MasEIr;JlkGv$lxk-F;()eOavDL{O>4R@}65lKbUu=Jy*`xV#CwI6H!l5L>xDl8{ z7-woLoXmkf_+tOl#@DCtg=3GEPChT;3#PDNyOa332;Z4DfzhLFPHvdVW)PSY!Y24; z6)ov-F$iC3#3ol0tr)YLL{^gsHi=LZy{VisXA-R`tQWm8t8`w>dJJh(o8&1z{=I^v zvlVPRVQGr5H+ir-EC1-@6B_*B8lykbwjnLs-oSuG8zNZ^SWVFCXEP$$^Rpveh=h`n z4TyN;1%7U_i)4$R_sdRxo{n2x*dX!BU-MTpi#u z;;B}(Vv&|=RSP1iRy8A%YSk_R>@<;rUce$PWF@`}Y4zeIZ4tC0%!^zEf~!3r-Olf| z1D_q>*bYRK6AF3GNIy0f@_=P_%I;oWn^086K8DjmoWMFy9=RpRqlct$@5;n|LniJW znYgzj;*n(`9xaC{f8ZMtlp_l{_Gqy+nnubjr7pJ7GNbftBW9{AWHxGMF{R^UoSWK>8fa9}>fJi>toNZr^O z+$Uib-iOc@Z8wTg2Yh#eE(!B~zGM$6K*RlL{{MFn=!QJp^-1!^-`x!$9&A;y376`I zHbhe0umX`(H?$&>?uHh`GP|J}_u#&z8=6sM|7iGH_TSwN{}d4a=xzYuNzB2DGH&*aBf>|;b80Fmax1GG3m zivwdMwUjy_FVf4(e+n%y<-nfsAEyMi(h_?I=7p5*+=EC;3EYNAYRc(Gq&}rPZ$>OV z<=|u41gQlUi)GdjBOiy@-@KryKB_Aa+n;$TeWl)B1FPEdX^$>nPVUdEvYf@Hv#%kq z>dy~}(?$harnUj>r5+Z;h|?{3d)19q1LCwLV6VKf)=(zhW7QL8k)FossRbmG?QvwnN-^lj7?5cts|3a?TBP@TU+qkx$NAw;NQt*Z9#`< z_K8UgHq~$uMePEs9k#H1|1e%6?;pqq#_=6>ZlOl}hn|HIs#+=n#7)j74H_%L8Qh2V zDkscI?n8@hx;vXSXtYiDz4m|TxNcV_9ea<+B;*c6Qp2_fkrelBLnM=q-DK0JS`@ao z(){wHp}gueYD_osy45hHM%Y_cgucCDmGmARO6{}3bj^9|Y*k@~0LOtb zNYVy7R@y#F_bMzuQ#}gHWO{aX@m%!m?BXezvu7`YIM_Z?8?z3Bv>}q0+{IggNMUjp zZ+I(WzT_@m3nK1}UA$&kQ|+S{2U9OLBX2%>5$+wG-oYN2^Hiu1rvCKFO~ZI)_P(H0 z=ZFL4576%b{~Q=Uw(LJ3KmJ?gC!}~jhy_b zH#C6MY3yc1Qm3&vI!>R)Za~B%gA+Jh;Z~?A{~NS{itnaPbwo{s*F?V9>l9NPOWz!9)>ruO$YNl`PBqo827@MmP|2%)>mJBJ#iMKFo9da|qB12hhY> zP?h1&T|-8f%AvunUiG4lUij=1UX|`s>8qRq%c+1*y>P1+?ewD1 zF5p(_*(!alQ(z?($W|}1=tZ1&(3?EFM3#CHoTJPVV4t827htNOl0C|!bD33RuQ_UA zx>82gLDi)DsR*3bSEV8_!0VTaz;N44Mrg&|@INCQAX-{S!J+Kx3Aaw|=t}#AO~`8JMZR1Gqn@e61!M^Vzk~{I4r&zx>^xA#afv}0hEz8-i7Mo=k(`XX zTvTXslWj***@T5yNDjC7!Et3+g1e-IZ|OYTT>xv6K}i#ER4KGG;H2P6CgsUbr{voy zbbjyTA&VyDsYs_(*eMt_$vof|%9Df37%^%KERsy8IBs>|?h9@M;0}Ymq=o6l!!YX* zIqVZLO;T?=Ep z-Cuw06*;(9OolG)cEZQqXly3fy!z@I$}@`G@gy^|HuC z^6JC5k>2_tVTKE1hw+*axrVj2fxy_Fm(2rteoadLks(Y<{*e_-${K}CU_5eBB`>2* zHb`mPe#YJ^vTbARrJZdzV=wK%Bp4{RfrcyPBQwKPqa!3Dt4UaF4MAu@9EB9G(xn@x zkLV?YQHtB5tKmPShHnXp0#)z{EZ)__#*ipd4-bbh)=8*Q-_pe+`tOJp^_*mMO1Gtf zGc@Qe0f%%{Jwl=nIF;ErZF~!qCx@(~C-Hs&sF;(ow*b8|GMEYjfD%v;y6C%WCh@Yv zy+WUZ`%bCMkczl2go^Bb17>p|!cwnW@mk@Ud<{vMC10DwqjAVSHRLLrsL~b4s2Y;Y z2nnb}oc$W25m(PMstL{kJyO-Q;k9advUJE1lXD<(kPE}wp$VCDDM>WbwJ#YIqLG{- zh9=K}93s;`bx+XhhTFSraNA9;*g&|`BRd^y8`8)Ra^{}0$*`_|)Qn+T_tZqVX(=lvuJ#~o-+fN*~S8)a3TJ#Kg5P)>%yLDp_g*$a) zFoo~w#t=Dk3NMNm0O}7gTZS+P+rIv=MT@v6W6p&SQc*!X3~L}l9=;kyKE;!kMTnCf zS#ryiyhW0oS#r|~zNyuQ2l;YOZe75%m`a0>$?{v+kZB^>?51kYQ5KdaMmX75=2|AX zLqJ|QlFCpPUO_CBM4MpL&^_6OO(bDi(;PErohWD!*DWy&i|%Yvmw+51r%uCm2tK1V zn#*}da07mXRvf{n73ECA?b;kmS%T(RTYJe`-6eA5S4Z%oQ*zc3BwQ_2P8o1HE@V~W zO047N21cZ07WiK6l`$&Ksbpnolu?k2>$(VXjA{xZMim9w zMn4L&43h$3R8UZ9^rxW07(hX}QA0tQF_eO+F^qyzV>ksRMtlT?#l}bqii}Yd6dDIn zP+%NPLB4Sa1-dbs0&dh&z>F~zgpIKjgp4|oJ5X~O<*M(DRNonP--+^u;uf>CVHbv0 z_F}}ww3tz%si@0RekvMj@H}>%SuF233I=fzx@fu!Imf8CqSnsjL#EO8W8}IeON7eS z+*`Rg`Pkeu5znO!Bd%MboEV@w9p7P4-e(V4XBLo43~_K8*g+{^VwC+@j4DP(?PtPo zgnBef6oSLonGt#XR9;*hAxI6r>qN;CF)R&as9ZFalmFnSrt*T$OF&!h2MdGJqfFVOry0K_i)rA;7&w+hp0)S-|-oYwY z#jr~Y0Ps3%+4hJ1Q;4Gkh5-gibfHIVqZFJvUp7sb4Fc#xKz0FR(txUcxQg4gYTlmr-F3pb@B;Qu5x0UKG->29U z1u9DcvY-XHpAABz8}LCxJ!X@ksFw6I>r=#)R#a@qKSW$JPl&67HK!Fw_Ya`bqT;$g~)LHXG@p1iyAaNpTW)mnI3mR8%f@SP6~9ubrt< zov|4TMbn`wmO9nhSse0|;#&mKeau*PCa8(fnT1W@9Gk#-Hh~L8iM;P5 zK7C}$SfC5jm2-mtGc_WqwV+xS+Xtc$G;;XKd@ws%o^djdHj-k0ONRE|4mTU5(chkt z_5jkdaT=P`xqG2=W7UsgWQ*-xh+~FJ#hZ~oeIo&LBjWJuCf_=lSH?+#L+v8*ZWuyP zP$FmwFfe9^YRxYAZ&I=yUp#`o4bYgF@Y=4b8*%s_OU3C+9`N*)ioc0?5UlH?^?Z5GXFAQpQi=-Z*?sHCC`Di%2US9fBx{rGt~A4t={X4a_XYpf-$jO&C=L zYAlA5!Vr95kEl>IS$9z~B{_D`V|IP@Ng-*JV%@a2#s_XfjSvU$XB5O9l_mz&@)`t# zYq>x$M4ov%zi#H3I<1`{T3e^xOX27`?LG=~>$Ei#Vh?F8g;-~;qtIWc-B00!I_&`p z$JfEj62ftH+Jh7htkWK%u&z#9PvKa3_!)e_@!>k{=M*ig(|$qWA$8g>DLfcAjwn2+ zPWu&wqw2IrDI8g+Jx1Y(I<1Ss;dR<33WwEck5f2QZUk7Pq?Wf)w5S&Dg%Cz-;U5ZN zX)SM|Fd|(u_(8dWI&B1E2Iz4p_u(l=9yx=Lq-(PCXYgYOU_r>B^g6c+lWWP;twKjE zd*nwmc)#Jzf2F0ShEGj4?E2BWKmOzI-fWTaU`T7Y;}1`K{`4=mtngCg{_nr}+b6%e z^{rvD{!Bh}zi);r*TuLL3fIL~|Cb9tdKRyvi@Tqn#V`3U19{p6TL z`iN8a zWx?}?@<~EIs^E0v$_se=9=@rET*yy~myx?($3>3GT=jGrmOwM|#q(Jpm~tG z?>SESsl=8RTo%E94zl8%<4VS}M=8^D988I`|s&;A>h&2&iC3I_=pg;CME| zcXp>>?=I!pDByTDQm&u^RzA5o!h0ee=}v@`7vRYgB$jRDCkL*2$mtP$6p?Qn)`-4= z)1%ui=8rO6K6WuLs&=924375*l*=+^+0gLmGC0K|w=k{vcCaqLxR{TgMC(t-)7ukp z-^kE-W>qyt9%1&K-ERVgDN%vyhW(%}=Uu`_qw;l^@cv$`bK&w&m%qP+-&q2O$25iF zA}J6~J{Mymmp8|_f!os^F&;fhO=H9{b`B>abSYKV4!HFmHrdpijGXXuj+pvtmWu60 zOihj|wghf{Tv9ah;rZm30l6cJC&;%b+Aq&*i2VCU{eK8NkW>EwmI zv-?s$kX2w<5Mf<&;gtt%0h!^j3chJHQYWX$qRZis%5e$rxNyJZa(q zRu=r>QUo8+x{Sgt+?YdQ8TRXNO&gcK;jPbut!i8sU-m`nYYt($F1`eDN4p4HyFZs_ zHu0hH0?J-s70?~=0;?N0%1OH}AeYOUrDKx4Y?45=F9j?KB@&a;d$c)Ji zYFD>Fme1iunPOBmC*xxjsyP|A-@|nKz3n%=P+mQUD>n=?Z_tb53cV6hkgnM;3b4gb zD59}&s!dHNqD`i6p-;w8Eg+}bejr>h8Z;)vvC(SpkLsJ@(vRK^1l5BETBZ7FvZyD` zA#xa+t9pqTSLKPDOnH>U4xXv0CviNz6ZN&KvnP+H&K7XDRRvg}dk1I;?^}91LP$fy z50Kh%BoC1bOPTTLqf%>`8{A8K#y}1i+bJ*3qLvW-OVQ1^Mo&qYhU2fJ0S&ws=pw;( z|BW_Kwq`f-ZKLFNK-&o?33vq8MM9X|Ls{rnszSj;9SR_}Z52GAtKiwr!v7Y$S_Lp1 zCTQJI>OngCP!2|&jeY4rv=mDgc)n8Z3c$HOToo>rv*vP>ygIbb<@o_(Zsj4jRNgPPHv3`%6BXpzfVjvR zkt?t(C7IpL@Hoeeif?$Q_bKu-6d9^*=I{`??{w-g5X;Dwo_XBxmRhLDtdzs%@sj-> z27~^co3i&Bo^?KXpj28~IOY2xtj0BVR`hYqg+QHs=8`7}&~ol*236LA{W2mce-M?5 zTr_B0`2J}n$xMtH5KR;Pu-hBBl8@YP9Aj7Vp`#tiqcAK;Vn`iCH9}9I8sRW9fOrAC z4JRvn?n*v*zbhPY6(2eR6=EfBlO$asx|b@{)uujGrKhS|auq*>Z}>EDNvZRTGhggp1QGM?DTY5_MmcFUX{Pfqd&~K4ib!*GKjdr;kh$ z(w}OV^1Cex25JlkC;hB#@DNyu2}bm*;oDqhp?vInym(*%mFs8qDANFZ`x`H}(}EY_ z>$KKRmb>q^Yk2kPlMu(&JpOOTN%cniyG+R}KLuz4Wcm*QuaI%DQ5f~+X8#$%}O&$ z(mRVi^MZ13pnt#`Y8)bp0lUpbBTjM4@9{|8M*j4Pa&W-sP%eV;OG5?zO38I|WT@uH zDwy)*X5`6@jm9^U_(5=?Bi~xUXOOf7(fTaC$XjZr%Ng>3n(1=JPm}JQ3;DRoN!)Qq zm#W^2iZp{cWF$Y>1%x$d88Em)tkh;vSr+G|^dWM^Q)zC>C&#zP#`*B!lW5-b3#bvr zG*#0_A<$>|CrO>gEyt1+ZW zIqEB^QaVx)j6;&}o@PD>ht7Mo9#ej@@c@F;TgwGUyEyt3P`WaNFr14PSjF?mA7!rP zrigFeI2D0K5_*?(BPtcLOQ}gT7k!cx=_nGA(-!d|jy#%Clw6yqq`ocgW$z*$y};2` zJhrZaH9!?)DzLw_?!$e<9R#(c0 zZYk`rY!x6q?bA0@otX-Zn4YO^O)H6-D}@@I6e|rYg|(h~1*?F*uXySeP2>bIRf?rv zky=1H3J^1<()@lZvNeGuMn9+lh_7_RScDu!JE!8%Rlx0goQi*!p})79nn#s`Hc*hk z(n^mmm7XQMB0j1?Gw6d)@Xw81W-AQmqJCOyEC05@049nM%}RA7HINuVscaa9N@d#$ zmEs&M3~lA28o5LGi{LL0e}#aqAvEw;Ni`wtj}WUkgX6j3Lj0S6$Y_Ko;?KhKB>WwX zzrzqe72$M*0^u}-Cm}o?;RX1+5YJ~IoPobH@q9V{W@E+E8ib?NDy;XDzu=MJQeK%k zFtM`kBX|OP2%Z259;i9v1}6niz#aqDq{yJOQe>)a9jDr&p(Cp0V@t8XuBGV($3mpV zWI0+4Hi${{8)Eyks^<%poA#n1SzeFZrE_=@CU4a{06IJJGiLj6A9Ef5>3*XexZa+f ztUGyuoN+zRt~?chzx0spjBy4ok@5FD#xx|s=MXS5E|51~&j)2!5(i+L5oc8hrpaGl z&qo(x-nR2IA|#nz?(kp^&u-!U%Q5k)IPKFpnM5Qx3(6B(_$czs3Xjz|*P_)g0mkZA zw(vOz=h~Y)l~|PGI1~r8lfqGpgp^VUOiY{f*)Pyr1g3%;iEWRO6K>$e@jM9mN()wU z#SrRw*zO?lTq$zZSdS=mgw{C4G+|MB_yCJVV!)kLN}3^Zl|De7MYV|tNjwY?jNEAmKZx_EQkEKQNDB4ADC=fc80j zR;w zV^NI>*^FI4VNzdHkHJYXOm1)vwF6k-K!ZLEfhypM8O;MVcYDkUng*i)|;6VM{7$;>5Q zH;2nNzt8){2Oxa_xX!yDLji!*ohj)RM7a6~zpY?j9iwbd}-d%$l1HtEEG#ZY0( z&?cZrF33eLJ~7;+9FYf!R{|MLg`eY;tSQ67&+)!oBrXct?vAIRCfH#^7ZpX3XINN$ zbE*jTbIKp{6l`~7zU9x5gW2MV*4tg_C8Yx4M`*9wXpGmGZE#i zdW?FlkCEmK1hD2_*s_zfKOqS z^4uTr>*6;hrQXrhD2T;c8ed0a)(=Hiun;OtO7m1s@Tim5Le{-r4*hk*MqC?a@Q?f#49Zh^%iMlfRl?+S{#E6 zwFM;^Ocf$2_3?P0)s3VGl6>i0*qp+g6s05_%qj_cJ(B!L%2r9>2cIZYP&eaS9wMsp z?Sf-`_#~G<v(Q#2G|WpV7ZBUBMxKV4-N+A5)n69M?XG+kcoFMsl1mBZ<2s>S_mVFQSzLTanZH29NEnC5h zLj&l;U-lR{Q+BMt!mvnoui!<4id=D!^{?RLI)pnB9*Ph`(qeOrDoEs{RA`ksw{xTJ z&mt;!t_Z z?RV=g-fDZBNNbaeFc@@bfa~s=6!G|OIk8kH@rJ=sbV~`!T((!079_{K580A3L zkfZP51DGMtx`R(DE2qh{oDR9lg+a<5&Oa~|Aw&&BK6VE$+;5h5-I-Eli`W2p+MT>C z$F|b2TfY=mfKhqfoqWKELe>6jF;0hSEnai5vf&OYJaky`6S-(o#M>LZ2GZq!+{w?b z4wBIK!yqRn2HTH+g++8Zjd}Hg47H9US0Hh6p5=kkfKf~riV?Y&@?Qe2^!gbG>XD0VdRDXTi6&O5HK__@DeR2#gYRH*gax) z4_W3cm|x((`amkMLK4xZ(2DDG=}@AK3KDiTVq71AI#wKHa=veQ!fn=Ovr;p zO^Rn)5D$v6sdy{mrWhzk{uC#oZ@0ndy?@H?3TqrN-%v3)nVULh7cPAC`p~TgHeayp zgeI&RXW1k8EWr(|2EexwBaft;^|}@t&aetmi{F6hClN3#DFO_9!4B&VNIdtEw(>8^ z&3M3aL-(iUJYIMq3IX3pO9{h~66Hbo@kvUF{%I+ps>pJrM3lziD0C3Edllr7Wn2W)rWDE5*9Qph-0F} zF_0Beu*Dv-VoF*}>{~!YiH3u~Sg;))M3@D|F#51dK!gabv*^uP7>!T@bwe3s+%IP-uK} zUTVK8h%cb)gFTDZddNMF%|M}H#CTcQA2PB~onlr4ij8Tg<)&&Yc503N8^=zx^gYHV8B@12(!m4(px7!rB%>ebvhAuYxB zDZiAUqTLloSP8#r%QJ=ED4V0u`v(g}aXgKa@2%#Sva{sr?R@fJHM4_p0#+~70yVFz z7^ypIR#&k_EC#ju*>)ZsGJ@D(gzyu>Bk1dKr$Ut?MQ|HwAUNa4@>1Z*gjKN2y%##+ zs?NH5`3o#?IixNYbH?nT%sEBRmjl-DexnY7P#lB5hHJ53+5FDUK0?oBMu8D&4N+oO)aNQhgmy>GD%DdO&br52-Ji(F zI$jd5f<}S)j

PF{jc%Bk3;YDCxG7#@VjC7 zEiyJ77xOJ}yg>0K@mLTS%+8Q}iTUq9Vgq=A<%_O5tur|j-q-!D-s5_d^jAoKL206Q z7JQxN-a#wPq=OV3so>;2H7Vf#{7=TXf}Aw{d{tPq&~LP$P?auH&`6JUjQU$$qgH4v zO~CkRA{R{#f=)F3&rCEu*#J31S6anSnb`F;YXREOq97;u3TGI%sR@nNkFU+B;{siw zmPA*mC)O2efQd{bqw8EM zo{nDTQYx6Hvs2V&MT2K-);aNP(cR(vCz8#h^5_)oY?AReg329G``8?V7_o^vwnd609mIw-`_f& zE@r)+<6cfnHtno(cJ~^8Z{BOy>AfoG)NXORbafg>aCfJ10e5$LF)btfSqYTu)cr>T z03A?TAr~g9nJjSlpQ*X*YdEYxO*8kTyVcFuZthA^*+vg0bJANV6D<1{qO19$pIar< z1GqDV)KDH>RJAXZ%@zg}linA3guqouQx!3X!Bmm|(Be9G!B?A8!Mn z0~=@pfsvnU4A`v*kx@*C#qddH6eS0(4*uq%8@j%ZA^b(j(F&(b>TVKz%2BE2_D zA0$1Kr4Nuk4&Zn;4d=+9#9Bl9Nn^QofUyJktVhuw2Yg5U4*2sqPauxI$7J8r5GHAd zT=!UDlMvG3Bn=ufYdJsQiHCTT#nR- z*ukM=z%mtwEzkOd@C5n=$3|)IoAk^=+VqphE8xsblQzPROxhfj_EsLZluyTs#4H86}AR> z5TpZ`Cn{|3c<8NP$edE03Y)=lQPA$xH`)r@N%~rvxssof!qx@MLnbIYlq3xTR^8F* z6P2WQ94$JXD@o}c-_J1M$+V=Oo|2SGwR>_c=^dXhdfllc8L0omN)r0#Q_zyic$HbO zvZGv`bXS4FCg*laH^O-H8cr>{ z6XKuI%0VFjaVh3vQF@u{lT({c?i0L7eG>mMrjy*s2^E{VnuC#bD0j`|A!@J~+dsOBye#8bJJk^ZR@8B-W2fG3s$=JO1yD#0)SZgE z(;bmuc|*Ahf$j)Y3%#9BL39HDDeLX&|DV7I_Uu33Wz4V8$n$~z=P2Z;0o;pzBznamXBuYDsmb{qs4u%~HIqFs$xS6(9te}{c32ppT)vZh+ zx9uiQAs7FVCzv;*z0y7P`cJ~PLjHm9r>c;P|8!~O=EX4svr8ezH^Kqv>YUVa3BEGw z_y%)C5{O~WA17+g$0;+19qbwS1lqCM$38zQONi@0;bRW_sHm?GMCR%pq1< zotwb3*{6tYFXsCX3c|sqxJD{JVGq2NmzlM}BEDG~95_pqItq%#{gt)2ZNAWR4OW}FVM&^v@7>|iOWS#$O1s1uetb6?M_7(3L-++<(bu~BwdD;lLOY8qHn zn`m$}fb+Z$qt9&d{(|4l-d~1O&1{rxG^8KcPtMnejN>B$60K^V1F&StnR=9J6AI3SYH;8VAFP5>9{A03~;l1+G=;^)$rJ5 zF&e3YG|-5>WY2A=H_BPi8NUSk00j%I43a;vDYt7U|UMMGH%90_$mehl4JGY71h zoMJhD>_{Gqb1X-W8cv((Ne$bh3b=_P{@*bF9 z_a^V8JB{S8n6r#Bdlh(6IH3r+mtWhtAdIZ=E9GQ0Cj{Sr&!Y>+?)sN^egMM{7gKLK z^(P;_^}$1jzVyS-LRk2ix(N#(Q#WDZWa=g?oJ?JWg^$SytDvi+D(H;DJ5t2jfyyZm z+=1lkMqq=wbHs7q5Ev|T_;?-za`#p$J|Zw_*Zf@FOh-p6Ii_O-457k9Ue}H` z6jej3U3^4kP-_<}QJI`m9o$4k&}*0XO=Tc==Xxzg`|kMEvb11$U2wsVl~0v8U_vlw zYv_c>ljA)ExaR9rw&tt%EK%Cu#SvAyNNMAw;Vclf+^U^4B0N{Ez7xlEgu>E^qw4B3 zeyk>(h@%AM`I_<)zl$>PeVZ68pbWjYhcGv)E6hwj2hvF^4CJrXTp?mx~j7jx?yu9EE=ykZq zDU>!{RBCJB5EuKWn2ADhH~vmZ+u>^|CZ0=KnfR+X~b+hzIIf_WW_%O>8(fJS!Gc<=+#Iu?Ch z@Q8+?ExsY=nsPlB5}ZTi2koV7T~F?7H)sd1Ur5;l9c2gEb#XCehwVfW+f<=u?&&;h zo%8`;rFWP9KlaW9&aR?L_^;}|*W2svyretnPA|!QFA3=^orUZRNp1rPS=f<9L4txn z5*ZQ@L4nsX3A7L(VX-1aZIqFS2?RwtDx(cX4T=yoYQ{kc8pa@7BXJ0_eg9K;dAGAb zP@M0Z`I7$S-CK35PF0;cb?VfqI;XPd%k$P`_G5Cw|DhyipgbQ-B6@Lvd#m7>@b)BA zug+8FBQ+S(9H-QuaPMP`3qEHv1@bXW4N_-dU zAErM0F5cJi3qC||F3n~IU}q`E^E3wa3*0H;o&$yl1V@Arr5@s`J#-X)q~BWsKnBJP zuNVh`_>(^-@CQ3IZm=~ch|CZfS`eNZ#L2Dkk!6?8%QXpDRp5j=_&$X1k#S5*5IKR! z$V|)&A~76hLw=EOMx)&k<7pu^oFblL-HHt-AVwU9tW|-Vf8XCMf`s@K7!*06O=C6I zBzPG?S6IY2xS(d`2dsqKC}s%Bz$dkAjQG-cD${8@8g~a4ovdj&H|Q0Ck5pPb=7z~0 z#-)~?0b|2dAh3iV!lE}D_%?A<8TLF2-@IR893eEnlx@zyV%@jeAuetz{}z-Gv!-9e zAalZw^S(eR8DnsxF4a#|8qxFYL!4AffcwneJ;c#Fdj}U%vs}anQsxnZEo*x_{yumSTeei z2F8ZeAWSj33(J}g=|!t zA?h}w>WP|UqZ07A;1V2b0>d3O+@{6$Y9%&;g>LQv^%t)D$xk8M-*=5!k9X|n9zm~m z?{0cqPV`f?BrF+Z=2<>j*LH$SR#HAL|H39pPZ720W0?&&LxtH;c+oMuif~0BkjQs5 z`ye{vfZi^`rE&g9I2juxzHbP*V9uUegzC8bftzg5uv?btdli@KQatP;wfASXqI*WQ zd7V&XljAL8HE#K`t?BiIY&~u07D85RvY|T&**e;g=wO^7`Cy<+4Z2FWJ)(#jFs(i( zPna-vbBR}Y7t1PtDFM9{Ua_MJV&xxht?u&wag8okQLJxL8$60L&JA4-DECwcBc zz$(jpa!GKnGt{oV?=UwDM>8pZ#DQ(8YFlcGlIczu%x%1j3vNca?BMi5`-; zK9!v#CEk)^0R|Od*h`I`r2~IUEu?I|RY&sqO zCuehqRDJejH(4P~IhnH#JUvX5cUwB})`^!yi&VU|JxQHgFq3?Yr?(u|<-2Jo02;?@ zWN06QRx^{~^t<5(}YN4;wYNMK|-l0tk65X!JVNNu%+9IN-)X9z1P4MW050_z0ZM=;z1(_u8lSPsLiz;MErN*97YI^jb7A4ydyL`7 zT@2CiLnQe<29dw89X68TH$`pD2Z7%bcszLjHSa$Odi57%_)!qQmH5rU`{O#tNPUEm zw~4+?777_j-YM!q`SJi?wD)LX!WG`|W4?aFLTLtaesbb_0m#jKcu3v>{2jZQz?6P0 zf?IS!cW~Xu`zZjirQmkE0eD{tsJj4^X-c@+BSSGmzQu(MH#0IZHkc$b-guuS|5*-D zIr7c|gPC;#OcF4oV7{4rQ(`#GmtYgTm&ZU@FOLQoE=jB5w}djVTOQ?bTRSs1NI43| z$YZ*&S`jz@jPs@tm%DJHWN``0{SKTHbx`KgC0R=rn^{!}dC+fiRlUzS?y;=ec3Gh- zn}Ihuac|H+579sCgZJ<7-pSHUhVFyBKN7rugZFhdO*8o>C$0(7uP1&v@7{6>uZT?v zi1z@3DFVRz43(&j6D;!t8CxZz>_DV4!V~R98NEI6fh!e=iK8PVMuxgf(!%_#n8lD6 zabb^?5HQA&!QCb7t-u?6{@gCbkx+Pd$S-#BE^$)6WZnwvh(G+rbL$ZTx?w;OZ_2wWr@G^Z<8A&sB~+IuNruxSaby_E{d}` zN#9j0x&*4jQqYd4n=RpB&CkrZN{H!O3!%90wlJ#k=|H|U0%H{FHCdE_ZL4~) zI*k6^ibluKD)kk8h8unnH_&)rj8##txVwO4qO)LcE||PUWr1$W4@pbbkStVLi1mr@ z6hppjMImCxb2H70?lxNsx`MmS$_gzIML=}7S+ocdUbKevP>l?Rn}IS^UPpp(x!X*8 zjc76>T;oOrdw>5(g~hU{VKQz@OB2`cmARwt=Dt{NVbC5oI9bmH+Phy|ds_jEK_~9k zMBEgAZQhPIMIIEkLjW#U2Kz@U(1)S)q*IzyqyFX5<40zSt{X`y|72*cx5B#4EX!>mLP#xX87`9V}VT zw=%5JL;3|sAfL`=38Cj;k;~m>VR6XcexY&d2_(0qTrN}*q)yw~iSEq`gz@C!8icLe zBzhkw?TeQFED}RNUb6pRL_ogipN{|s1n9Ic6NU)zySG9y|Jl~lp+Wa|GTNnQa#>*` zqC4Xz36#q1vW-|M;)BA*Tp-4DjqdrfDc9(JOoq6PKVgD1AEI=nZ|I?* zZyK?BBLWsegf*#ZwEI$)$3$s%!8)wYGl~(OV;M{o^HX{q)yQQc!ZfnFOgyT19z9-P z&Vw<~Cc)eUy;7c>Md6yRM(iIoysuF_G9kF<8&Q?(yCmZ5@p`>Hr{gP8p7Y1+2jq$K zerD`O{DyE@15aF=Y?fyOOHFyAoZ2Q&)SBDnc??&*%X7wfjSX?5dnAlOo($dR<%tWG zE_vbsWjFY5lq**DNCcX#E%F4%GbZRO!Q667*0^`@c>~|(Oh~*fX;=#*0W`X&+SBlj zGS5htPV-R#&zp+d9t*5Vj~$=5mbBjWRve$`Pe_6>lI2HDN!%bGZ?Yc`@Ak2YX+pl; zF5j9HLoIOc%Y^WD5nvrLqg~Q`iP#q0gH5%4(K0P@2eIyP6PzX;$KT2QFVw z+KLZM_@8ZVWjf+~8SrK6F&$k`S~gy^_~d)zPwWmgd3VXR znOu|UrtCI{5;bR+!|{RAnH*k9d7X6RdER>8I~?DbW~$`g=9CzBW-B>9;x5ef%!{q8 zTP3!zVd7>PPtXKer{jUpfVn%B#Tfk8b$0Eq(msOS@Olw^eTI- zjOF3NT%DZd0;PDRvMAkCl znU#9Qv+FC1ly;J-A}}JFWG?RH`K0wWl`91><^eW9>hQ+Tq-?-Shxy-k?)!ar{5!rB zBy~3c55*I(XXJm6_I121@$Z}W{r(0QL9CUUg8#54r5Lm=%9aty^g*O-kYuYsN4_dd zQWadoTIyv3&zMzW1(&UYN69s;W!A4@(py=83c{EfENp*kD(S5)d@W_;+b{E0zf9_C z%d85nn3baQ7E${vd#j|TY`J4LEPG7>I%P8p*)qbi*A>2&%5~PZy(p2Sy=-XuWlNwn z>EU-6s_4R(s4YY;#SRO(D*z+cDN#zhloDQ!jT|c)B#Sfgrp|X1Ljq02B9u2Ukt9j1 zLlFP8Wsp8d1(o`#(3MCkZWfnh60%@}WNZ+TY>Q&#p}v(p0$J83c+a472;cGJ9Vqnb z{ivk(7+lZ#+k&W)-Znzkjk66wxxRkRh9FJ!b8;$zU;sXVyf@J2y8(rOV?f{N26Q>o zjNJx+d%-hexPl-HxKgsu6tzEnG+a$~W-MA6S8~y4f!-TJbW0*@3fJxPK>Im>5uBtA zU|``K(#F^bcoI(l6aO-Gao;zJ z3fDxzW&8riQiG%1u|gPW_rq4gR%E2rGsQWpoQ?BDMP8X-?3K@|r~Na7_QTyJU z7QjrLYx!`k-%u!4-JLy3X6h7;iFB?hZ=7I1WW%_74EKpr+`Ah2`nH_(Gw@y6C(Ofl zTVb%W-v`@80Bw@Dk!t=F<;q)g;vcFKEG)a^u#i)KGzEZ&x~j!nDuHlBIA1a&6w!Tj$;M912KhK>sxE;QB2Onm_$KJ1m?rhH1`^MZ&w z*nx8b**E3RL=d&e*7T z0RLscgSl^5H~cAgA!4y)gRVEyx^~{ZZ;~Y3%}jZlmAWr^#T@pUx!@4(s z1&sB;=oENIOi27dl58hQVc`9eP`LX^nKG&pKb7RoEMVk0k_C-C`?BeibeoT^D2Ced z&AyP*A9$V2R{eOcL{K@LCtN*SE%AtN$tBqdysl$Z@j8^(&CJpRd7@LuHhFuNKAae$ z6WLK-SMb*0?F!zec{_r)oZB~B$NWE(i34Uh)GOh9o*M?3WW<=*eqfT3lY{o%T@b!C zMQ@{`w~4_U^+eJYinKzwU)Rn4neHY|-n$d{$(2({f!ZW2Y=Sq6^2?&ACW&fd&p(P` z?K=UUE1s`|Z0MaPFdyX3jbh+<6Btjq^P(7bq$bGpc~}_ZP6*z_uR31qMoAS0N~W1L zvJjO3=`*rOxLx6`ye%gOHy5yP9X%>S?Kj27J;B}Hv)lF0W3gR2304Ac5_Kn*-O=! zR4>)byxLP$=2Mb|#hd`N{Z}Wf%CAtSQbtv$;-oz$A~ODEl~osf>{0kQvx43qmv|Wb zj7j{E-_eO5tI(Dsdqg=Z5U#V|9}4ot=-(JM(tf$Pk124W=S1xRCCR8gF_hq8Ge8H2 zQ&`SK=%|Y423z~j?R(r52zg2K_ki#{aqe9S=s?Z@*v@qirY^7{XP>6j(a8|hdjixA zjzvhFFI5Q5fnP)#8KidQBderfuwt6XpsDkn+vS{0rqHS40!Cb!%SpGa1M{F&>q>IN zs97#8!W};)xeDk{x=S>R0qLzP$qut0Mc+uGgj*pqq$S;O9x7!#ihO6@?5UdY&q8ME zSK?MNm1GmHEyPNY{FLMd1j8iB3NRQVwh;N49RP7jAp;h;_SmLPo{s^HY*bF7&FT;@sL^iC4x78!J0~73Padjn2z$?$s8IBfc3OD+kv35~&@>G8Qbe z1AW#%)!yjC+waj8rGF>>dJA^ExJ3uWme zy@z0Bw^Z#%i*`$HLTm?<9Ty{e{ zdwd5D2=BtLqR#>04*|${k`0zILWo5%l0CezkC%|%9B+*2th1Hju5Qd zYjY8yS*3*XHwyzHEHps5P^z{%<8gs}4)|bpu+zdjl)rDeRUD)aS68;^DsN9Lf`U;C z#UdEoRo<>BqEK{z3gH1DEnzF+leygHO41NqX*&yizO%hZa+XSi_$jofG-!`R%5=-qwkOuD?8nj|+pj^@3%wde z#2RKn+J+6I{Drw;!SrgCF;I|Szpzo8S!QzK;XbWmvY=JNXjdNXO`K+jgbC2kibHh;+d0dqYD*4_WEN|gfDz1%Pk}vc^1-wn8Rkh7k{;J5*kYtizNuK#n$0$$exR)ERuI(03PV{84r=Le0K1O_6vIsPi7P zJDVsXtqTEmMauJN2kT@jhT3Ka@ak`VW-nw1&st0+=-h<@i{yXTs6FSL8G zO#b%RR?7Vs&i=}G;@PXU@m8rMiv&9)Grgtx2;U*&@rP&%{D zRh&u*c%}?jxvo44TS7(g7JjEOa&u-7wxBH?h$@@Khpj?^SP=m18eJrLS$tShWn@|s zb%I3)F0N$Gk|hjFlm)o~Zdx?QR%v_SDcsW1dhiC>`uIV-K!wbD8!Nf;UVJ}r=5i#f z=yQHQt8Qea==**?W8);04i!N#bP*DfFX7^fLbw&Y%A#2??Du`2v>g;wHcB}vNSWU^ zsih2#bFn5Km`i4>*rn@abEm>u@G}!3WaLJpnyiw8GJ?!(DP+N{=`kUZ6uA_dCB(IF zRU+3)WebeFa-sKqCF7k--@VSfj6KWCk%}x2uVlbSH!j&Ua=y64l>+T#W7}Y5a`4Tx zF1xGEk+y@;A~R}MHWyj`4Y9QzEma)?Pt0;%mDISF&_to>{17-HjU592qt-(pKS+8A z(*$e9Ay9<9uwH~$PB{hYQekIZQAqZLc15A#gmy$BnN3hS2Ki*p-WrAEaPJo6|KQ^g zg6I>25R*IDvtucd8!V%WpTeSjaIk2XNLg$0w2Sq5?hpZhxHiuYrYIotQWRhqh1Md2 z^U(@kM&x@cYIraXZBh2-K!2RhlR4Jj92kZvaCl+d+REE5O}eA=`}FA$CXVi1DlAL7 z)RN?=oC97QOVBxcj}hWSK%!~e*%jUBQo0TziRdyAn?kzG7YGxzPe_@~&cGA$pwH$uexs1Z|xM)a{8i)nFR2ibKSXuZ0%LX;ZZniXpWZr!rU zuod)Oz;~lY)G`8XBP{2o5tie2YlI}pN395M#EZ3JKclP29|t627uE9zvqXS#Z++Pv5-K-CHCNQI|Ty4_GjS3McV2IcIL)8Il> ze5g5q)>_U$6c=&WFu~sg9MH^g{v=d_vsH(Z4$Mbb4K)`WImt7T$TxpGJUKprR=B0L zjSVx-rdW9bRc$B6qu}+g#d1-YUl;C(0Lu`;79=crcA_bVPg|i4z8GjIQ0##&kxy(p zL_!B@j*ZQ>yJ=10?!0Wxa_+}(w-wc>4Y-$+w0pI`BvXT^QYTyZem$U#y5Pi=1A4tp zHp1VUvk@C@M1>U_B!c94Y=ickb@m=L{EYg#RDWk*Pz0rB`qF2z+?BRMdj6qayV~^& z31Vb#kP^c0v<w-$(yh6LlE+} zml#>-5!h9NlLe!Oov*^EL6RO|N6wqEk;CDW*vJtf?C%^pQd9QegPQ7iS0D-5O|N|x z1hP#Lpa)gx=)}5wNlvvSPm=L8F7Y)GKPIt(-_eQtO2W+`+<75kQ%?r2@F-j3kWVFC zwLGiJ%Pp7Ntg>{OG)V9(gUH9OJ&?d~SJqhGUsuS192P*L1d$CYZ@qu)%nn;2$U2Wx zK6a=k+;N0BwiJY9BynEJhW6qRj5Vo_xUoPdhLs|a&TXB2KDTyO6H_S3f#?&;d?P@8{G zF-qV{*Y1Yeh%Oej_y`CA_89-a*)*-a(jp%iH!M_i*mY_P2xWEz+4N zz~HYTkeAlj3#fYspYWI&tZ0Ei|FWdPtfyGgRoj_as%ry)u)?ejAV-AfsH%ncgh#BZ z;kv%{wM{lEkcMqw;sQ5sFG3__d4?bg3kG2f9HESiw9(QEmk1fXSOocJs00^Mz3rTZ z+$5W=oA}ZX0pvG|fl?>O7;|-Qf41e=Sb>#5b8;NAm`?#K;dCQ`@Gu&SZJGrVdQ(Tv16A5n82m7 zjLztgQp-_qt7VLT_R8d=oWcD&4hxypvz$Kf>4;E^1Jhu)$EjfH%!ov{fr~_N7M#_S zfyk!&dYK7qp9F+W90$--VvHiZ&3mX-hO2M^0?c`s5O0-u;X4*7nKEe=M`V*5;Bbtg z3|%J8j27^|xMdX?X;IE74`+^CZc-=0NLso9Yk%7f@LQ1%{m(2((q8eg%z7y~RBUTn zij9vwRKetB&l}h}$(aKZ?OpY*mt$48Tj_sS&7~SN!S&Bv<+~qfdmj#6*)vz}NV=x` zaVxPjADHNo7M@v9cpy&fb&fzb6bs+?`TQ`Dp&jrxLj~4FA!h?r%iH|x zu`D_Z)*0P0VhO@O3$|8raKR*=1s}%KrMFGO_NY2%vKb-Xt~WR_=K3_`J&zj_uT8{i zxs&pM6D6~xOy~QXCaX3eV&E1tZGnG7fDeiT-%gAVT=oIOX;#jGNavzm+Tb2h$iIfF zcpNCVs{&g&EAvyi!QPJ7_$J4XHWn@L*8GjYZhi!Irjg2*GQZ){(Jbk&=4ZFJJ)E%9 zU^!ffnH;l%@&(?`YeC5a{A8Xmuk=?J z)nU?>vxll(ja$Ac{A3fI2nyIivy&*cN4No8|ExK8m>Sb~>pLcSV3_)_+G_4T)@d}O zhO2M%DMpP^3{|j!ijH%lcpt+l?JW=Ytvb7yUdp9l3Ya{WpJK=Bqh!V%uV=vdT{C8cn)=VE#060$ z7VWnZi~LGlU@P(B2zAUqPzm_!-kp#u_sTEI7J)+$agpHU$}N6Dw=Ks_vXD!+g1LBG zOaSP(R%PxQrDh!c?xa&xAl`1I6RU(7GFqLgicIThRh=&yo>1QR^K;-6MM9GN-}+e? z>m-;eRJ|Zu2Kz!D&vF}(b$yHNQaJ-iu4&Dt(C=le9$jeH(yvf;&hZo~6$dZSUa389 z?iS6e7a;^a5W}u?ZoMcxc_z;m(Wq>}a5&WW5lO5<0bPS#DZ9yqzH2QRmN1y95Y4@Q0->X7jFuuNlR^u@FkJqi^CVwdo0sKkr_W$ z4djA0_~HTqOjQ@UMGMlfxFY%sRpLUgj4Q8^_=|naK#m+rkxpVpk2(ne816_5#afIj zHml){#eNr%P7nt4Fv9kUj7DozRy@oyW1kH|%McxTeP-;_Gm>dI@=bD_no!hf$<=t{ z)Ck&e@i?`J$M43eW*+JBsy~lmB(G~C9`5Q7_<(%qa23Jv(a`GH78Qplv^fk z6p|`}JkUAH*2`p$(8_bd~3fs=zF?_QY4v-z{89}}Yiw{M*_bIBhjg>r*qzBJl?ES!oXhHzO z=?=nK2#y7LNDgIQ*&;V(j-Q~Kz{TYg)bO;W_FzuBZvyJtVoRc~WqBbVXbmeWv`rJm zwsBPRtQb%K=?8rRaANlC= z9JcM!|3WQHuZTV#24*rr7n?6nR5*RW^9GTDjhB_JR;gTheJGJ^0Rj>=aRnA3Xg;?%Aaor~uWsA(#Nve8yu~)umfw!t*5sh9T zF1_6z3$i`LinOFforD&Rw@j|YE#AP~keRGb2l$1P)j%FMPF6QEt2y2a`XbBX9(uK8 zZbix}%eI-$gxKbya444JaF$bID`$^%;qTQ86D%b60w}=Cd#fGq$vc99CDe7tJ_OFPEQpF=w4P@}u zBh(-sj~$`T;xTlZsvm9LXY4@SMBw!}cm%2;N4jFCbtYhS;xa#Z6`#na;GMoc}ho3Ll2IJXCjR!LcG9+;tqH^9IAMmmtcyeAp+448%MGgHkQGt`LFi`tS*I@@W90c8c>liWGyYcOnUJ1>pEExdF#GLAI?og z%d)lin9q1>v3~MZ^H)#JnEY7S$3hNX=XHLMdn@TfazDVs<_^|CxNJeYzWG@`?l%9@ zq7FOxD_SYhxF}g&XuDSxxLmUBR%}Fh?^Tk&|69H={^GZsspc<_>)r;Ru19s;)uK*e z+hm^k+mXtA_?mP&wqCiKclpmBf&qd;3PC|MM*R9FCglzRYAANF37PBSf70pbkrw6@ zOhaM1U|u>(^%-3T=4qSu&ll!O;0gq+ZJs}=1%aI0C&7>?GHSSk(`lDUi{gq;Aia}S zyvnxeLWZS67BK8uB4V}VSG2S){mk;VRqdUv`DM9c{F*Y$p$%V{Wt_LDtFon3 z0qSszKXtd~T(;1w<;XzVt6eDk2+IJGa+FsY8h)j{qcZ*{A)B;UEddhPMK9xb8EmD* zmQe%XP=jbw##hit%GH)NP9yvldUz=}CD#o+Y5;~gG5;TmWEodjQ zomHOL&Wao-wzEm|)=V{CCC#{5s{fz^lRWnal?Lt^hKC>QN!03yh(%rj(cEMyd$ zPU~mYW)z!FWd_VvBde{BN>i}KxAZ7qJ~ms83cvXFmqKjv<(}D4X}NiuhhDYbd|;0H zfLT9B^({uPLbuN~KbfOus4F|tbJgmUb8k_#tcsh=k55or>IA1~3w)~~IZ2ke9Ar-9 z(XsACwcK&;KYpMj?``gD)5RTgKcHAzyFI+T+oL{*?8NX-&NZJtRUKOnQB;TYVt#(A zS}rjaQOt)=Q&Y?xr%APv5F5F^eVRIsYL%R>hUm&|e_`B@5D^OK+d8IyP+g!acS@xA z>n}xQ+G)OZrW)x|J*#$Q;!~IASeNowHoHD2VEI4SB+nACru|w~+p+L0b+;;3$B;=K zWBQ)0-kPvmV2Z3(L*(*Tf>M)|Y6DVQFF>YH(xQQ2PE1j@I;P{e`D&@sx4vS2evVq8 z?|P-rldXtRJj$;|nv`d*c+u2@lf;I!)I< zWG|<4WkAa6&k4LqXJJ?-ccmJx(&o4;VNZ8-JbtD6fGu>u)lvy_%+;#DvnJ_lN6h@! zP|w_WwHl!xdf9yMYIS7quE#nPZbAqnc!MlXHhuDH-ryn##`kx@c(6`U@N6Z@l0dJ{ zT#DAH#C$fd8aSJUrrM4iJWC*Xltt=yJH|PK6uBAporpNCGH)@PS=TA8?Ej61IVd^& z=);k+Muh>P?qD~Ty2z+=Cphz=m7vratUyA2|Ne_&>dH}KxANdzoWRl^rFWd!s(PoK z?SUT2w5?EQtIwHTE0kL+{Ckv$Q;M;m>ZKQ`4&>)vqndk(3`CzWJTgdm6rMI`UPB*# z&iwHjbsUqoY5c23LXe{|%$kYv}Tj+EKW%xzP{u-;OhYd7Hq*Y?On`b~3Pn;KV1 z0m9!LPki7z=BI6H3?2JcoBC+3b17Os8A;3l9Q~4z5H(XR_ts13B(voc%F#Bn$ku<6#B{usFa-0jB8Mu;RwQ6a{ zRV&q3b*9~-3dAAuCA^JG&6u0i`R`fFX9bImZ6@2LYfR%R)o0pmZ80SxqROgx7^P!d zJWLf^*_5)#IjewK`h;%$%~C9hLU==7!a3?w}v23J%*^BK`h) z8o;o^vkr1x&qP92q$$Y!2jbxL}2g0E;{BQg~S*b(l`}^kGc!H~Y*oPp;)O`pn zIArPp!Yenk(0al&+@dDyo!6SvZ&3ryiqEP^`mT+p^Rue11~xKYH<6nC6c^m1DW)G2 zX5}raUUGcx7Ijkj8~2Fbvz+pYDZW+pD+zS^J5BSg>f;sD=?9VbniHAKbaVHuYLxS* zinL5jeN5M_s~re(b`{>qEdBHwE|1#AqIp5QldBvBck%Lp& zVw5u)af^+J%?aW5vQ~Cxn;XBN*5HjMh{lVic+#A0CVWx7TJ*-f%oLN%*e|KGidNod z-`nP>+`dz_>D#V$r;fFJAakc0!*;mPyBwYtKIec>^i%2o=s4p}?hKOIE-L0Kw=}f2EJp5(lnz{GsNKWWbUs0dMC=U)O zdaa?9I&^l&n!DBdnwrfksIV}VA;K6*U?cijPNKKJOILsV`|1G!OXn=i$%@38Yesge zlN#Et4WP0M!sd*GgjLB7#2WL3PIX4#D1~KaVT$L5j50kRRv$L4SF4m+^02z0GOYD* zYCU{*N5vy*TUGs@YXbO997?3pHcScM&Ga&AksjFL{#sp8qBeEh{(E&=NpkZt)nDLF zGbj8>^^@D{g2{_hpRyYvqG046P^Q~lib&+@k}#!AW8~?S#Oz;dk0YTBlhM3U0i7Ak zi$A%|SZD*A03u!waaWkD4WW1&i7o3#wAlIgjiT2PEpDS^imfx5U8?$|u#U(+K{E7s zVTO2Zq}R09PIYXhs8XpL7FgznWw5<%;SRR0%&-ghF(A$1jC-yjDg$4Egd5HMRAR#3 zaJdPCIiNg9Tt;S@{lmK^qy3IR`E1RBTq)G*s-N<+Y zaI0)T3eH-Q>ANF`kROL^BlWkd!(5NBG{T&=%4LGEpo+s*In~p6e^yl>TIeYtE&9?D z7M4f73W5~k5vN%~u}32y<*H5VpH=k`OJ?A`cdW23!hK?e?IK)b+FwvLALT?(E_9h` z`=4;=YE#Ib16;Yt!KSpjGi-I|KCLbd?g&EL;Gbhgcuc$1X7qEax_8)kZY&6Dl5%@~ z?giERydC=i#83et_P_@~gtpim%h|)x7N}L`lR>_0G5N)6j6d27JJ-XUe0o%3eiK0;e9kl_icqA>ClK%giCHgx;->GB-q-8-vV^ z0r%-7_lzjBKNx2$G~P>U(nmv(?RI4G4VYZJ5fsiAN$|$G!o4fh9#u$?XLF*r2~wfa z4cc6nl78YbDI|`|dlJ<>~azbnGLgp9h9kKb*ZoepV|0pxZI`+=oDN8v-Hx7tVDETkj*e zAHuuq&xZ#^RWWSsaAama#2wxN1u<;3TFLhiw%!3-2StGIU!qv&AQc}XWzqcnchF}H z@12KOh^z$uVZ6ssy5o@S@V=lltNmq~)r|gk42$=Vu^oc+K8lA2h0NIaYd^%&-k0%r zPX7vK|L~A!6YkI|^?emF*(^DP()U4X|KH6Z z-@n$ySZ(K_)y@0DYU3z9G@9QHr7>)^9un{04O{=5fq#ff-xs{cNcj*ieSa21ag-jS z()XQ8525rBO4Wb2LhqpFu=v0~B$pnd(*O6V^nWWajU8EX|Cj0}4z7t$=k)RQ;kj^Y zT@)UT5kWt=D$7q-N)7xr*ASB4$y~VtiieP)U5iBI|t}NYWyE= z!q}m~+aX>s#9bA~7Om+`o-U=Sr|Y!R$mwUPJ&;;w|N1 zY>}(P5rZPI&@GuI_rr*Vc6DIw!<$(Bh!vIf4uu;ZYY3EZF)|{5=n4dD7n?0iyB&W+ zq)p+_ufa9n9i%T5TMO-Rc?9-CF4irFAEt9P#YAD<;bgHl7f>^f5l@Vcx$rQ3q*zej zdzfC0LF0_U`akumZy9f}cFi$O`lG5<;ML@8{ zCq7{EYrVGEn>mx%6Qw_31`pAP)r779yv&lE_Ik`+OucqB&o*Zb(VtS=%=RIAK95O5 zwOexl7X;?wp?XC5*1#?uQ} z*RB!zG_~2x7^zR28C6Ev%7j(J9vZ8;pz3F6F;4g1SoLti{)llZoh066ygd@R$NX-j z&XzsNJKykDF)U*ji*8`%oNkbOW!%M?A<{%P121%C|5k}0%~9c>O*|fYun7HCh2|_ z)*X|4SS4XqfEB&ZGQXUpN5DMvWPO&H=bbZIkCBQ~!LZ_YPS*cI%X&`Hm-D!83h6hS zZ%@%Tk!|)=eWql)d#awKW|^m_>K$rx$0JAR!Ad{WW}cg-AM3gGX6T{8dyIiA-X}6g zm@U)wQR<947`%tLr>;qiMg1NIr|3aN;E^{IKUBw?n`Y_L`n(a?d%9Trv7bPtv{dDdS^Cs*`~Q}; z*GMNw?LANC%t^v$3!#o{j{&Zfm+Yf?GFIymehO98O)ML&DG7#EBz{xrXesW#mR{9`<^P1wLXQd=L-wtA@jw# zy1q_)P|AH~H9W>H@Y;bO?nm&VYJNFaAJ=cZ9>o_XVuo+#bVjNKJinmK>1NtI-A8@D z;{)?_k;!CF}AMUiiemtB$B^GCKRU}T9cA7=U>pH{(?*}RSGuNy>UiTyK z#^ZIZT4x?TUb~g+w$Lf!&LHWzUZ;8Wc)harr{9H`hk9Rnh({;>l>Uw9gHGlgbH@pK zV88Vb5^VC`kVm`r)_$AkeL8ba4_qjv|6qQ1f_|jONX<)It_CeEpl5SR~nGc<$8++|DeQY$>odl~p&3yGF z{dtURXPgX8&o`ewS$^dp5S>t(sREAi~n6JXeElKM)lUQ1fG z{OzxgedEv1{?{vsWdQr=5;$7D`SB9{0X#9;+PRnLqtcy(8|uM4#!bPS=^G z8K<{N?b7}Ih97yU9(R7%PqDRk61j>Aj_X47I2s6ID#f1*UiILRkNIhO_%Rc)zzIS(mg>{4E+j-5Hr!Lj!9nljs^M&$S{`#etpLy%eTYmKU{0eTPQIc1p zKeOtF*7mnIJhDBp0=MS)tC-R97DK-Oifpx6`Uzc~?@e0k-l7`>+#UFSF1i806FUws zLIAZAs%?4cx?BJA_OGwMxpqaiuf$}!0m9phxb=!xqh6vC0hD~Ufub-)CJIk_O3aVt zwUSqCj^!0v8sf~)W~@&+q57Q4mOC>ve@w<|adHO8N*~SOP0U*&(?D8$4`s{6jUbE} zua8~{UWSyc6;w8DT?lHtIg!2eXmitLx_58ugHhsuB>oS@nN_Lz_GNnD7(U5r20vA} zW$~1_dUkP4B@W~;z+Z@`4V$Z+wGc(&`zBjvGN07Vm98~CCxVhoW}CU6)bma&cT?Fi z921F)KdQbktK2f6T31$hfAqv_l2d_SE5#jN)J1Ona#b#!E6U=g5_fpir1WSQbVn2`y=vAt~pai{~I27u2(f&K{dixkWFoO$_5T{V6{G|cQL zJJK)&#s3B$$ZKII;Kr=-qELf*VukKwPPj^!a5q-B0;Sf4qNLjXcoygl=IX0-<1iMB zFtD(OQa2b{P)?4J&(9>o0Q0@8bnWp1vyen!7Lo`qd`{=rA7so3kl~jiv>%p*-P`5a z8q@b`eL6lomRzl;_3SIgaJ9Mqr!cu-wD0ye4_~dTN*mptZrN2==3Mi;t94C9pDS~H z+(y@BKEnS1zH%BX`kcDKC5{-rnSZ$w_iZ!sI$caWF$2v>d3`OHum3jB+*V`W%g>dRZjsDjMJ-Elfqbz{UU(Oz$Qr-;Sg#g!o7`%zgi5D zx1agOQa#NP1iZ48n%8!G`ZGMzvFs+bTp!WLuO7OVg>lg4gys71=7G`&3F&dSkN*k> zcN^`i>k9wCA&^{hm42kST82*6Y+bG^l{QZ;*Pou{2=%$}M;S5@LV=ECzz|U&CGjyp zgg-$Dd~{IE5i;d!bLR^9kncQ#881e+mQ=u+5?>Gj#o3JFJYfbfw2UAvg3D-b4cy|- z$pW5xu?m4|Z&M+ly^Uc&W^5rkfz!jnkflNr&WcXTyGDOVIpz!3=reiz?HZjOPiF{0 zm0gKatCUW2omhuog|OlfiEBKOfOcfHe<7@RS9zEK0 ze;y;-^x%F7vy#G13PB1x)KWQc|BZewn!W5IUFE>IaP6d<Idi4{;88Y>$@Gv;L`Zz8+5%n<_6vW zfGK+1q)#d3vP;|CO0(!DJ=&Dts7E*zZl!trCOy=Qyh)GUJH@YV)Xndd;_UF_d8)#U zU8M(_53bVZ=Rsl*a`c36jK(4>HD;_16 zi@4s7c+~bVu#q8Ay9NFUx-3aJ3$Y=?0{M3!45&Fs~BCcUz1wXWmw_0{^L#pNs9 zBFe=$)D3l@L2shbQI<0W&7wj(dgl7(j~$b{Z&9D*vBanCuuk&3swcv zZwZ<3pzMwZ*XSt)XxyQbX z#;xYE^?KyK@eh~|;`i0ku6f*QY4^?WhMBZse;IcC>;OvIEk8Sel6L*itde%$vRD1g zsE_DI^Pzk7i-GH@Br^aW@GiU${&J=1xKAH#wrM%t`#4ZWgS1gUtg*QE`LJwLzhD|W#jcC9y_lUJ@yGOcJ$as5z+pH z`RF(GxFo`4x%vDz_1B8!4G+`i`Umt_v-O)OVe=2@rw4TdwN{wq{$Q>6mOkHy^}@IG zqyAgRgZeLOUB^8g`Yt`~p`Uk(-eo5byOH5ka1BSGYu_D2dV98#=x}d+7=GhVHq%|Z zI#zy1&nW7<{1=2*BM$!3hvKc{1zpd!j%PacSM;=vl7Fzborf(%#|qgMM0(GI8*hx) z9)=5r{OAYlJoO4;24#GsveE4MlO7{#g`^q1OJCx2F2Bj#y-TlUxqj5M`dsx1^R;Jn z!^pKeIulKa#P|fP1sU19mGF>6aAAgMeE!NxO61e6Ofvn< zcV5uDRinA@Mf864=0`8;@v5JB^F=+(-19iPnTO1e9@i_V_`D}jE8qHzX?p?<$X$EP zQ%|6U*lmv7swd~9G5}<^&A%n*obIYZ4~LnPU(%C$w{Po2>=B2V!JBoM`ND7XD=2)P z-mY(*1j0&CY;xCLEi3~$RIYuqIJ)`Q-EnkXJp7qo``0pU{&zMGcbh-|yFM=ls@Gni zTFE*xk(gr954!jKDh9giSJ4em0jP$K9lzD(N;1Sc3HmSo5y;hBF=_HY_M^5X=|M_WiB3w zW+AU&ct>#$v+;gi+p*#g`VPN319s{mT7BKj{I!0_5?kswdZAhJ8{KT`exsR_exsZE zX)nFN`=0k2%K*XC&+vzZ8!9a8UR{b{Uzy9aCp4(9w4p3HO6FZDX}%C*I8#b z>_Y`H3?F~{uw1apS99DSbVJ9y|I~M?q6c09pS9+dE&3L=kFI@Ge@m}_#!UMO!tCyj zJAT3*hsD@$eyV?GySC#o$k4p{L2iV(?j=3&J-e{H_*Ca`)1ymgz>wRecd53H(wFr~ zs{gxD?x+4@NB4t&VeMy2Wzy=%>{U(>Ji_{HD!L{t8{p9P1) zbIs7#Wjg7&{B^cwIxhb&eV*#q86SxZr|-tY-!!kkYKdmz9z8`W_K7`u$jEPyq&dOG z8vz5nC!PfmUyg@w4+i%i_aK+-HksG-$lR}-V%fAU{Gl2X&;RV@;ii%e$U0N|rd}8- z+c$P~tbCI_fyOlZ?kGy^9;gf2xYR4W7HjRZ<|l9Izsat>^}=sw(z)grZ|fxu+g3nJ z;?}VU&4Fh_)>kYkciuMNnV`1aw#GS}-=8|C57^D1kpf^`vh%_~+tA`()8Z`0ZL8cN zc4t59S$cp>!>PM3S5D>J2X+NxKjkskg@K-4xwkn8gtdI&&!l93-&-#;-YD-$9yZ+m zpM+iSdGmuF&KFAyQ`LfubG>t1u<39YTh{NGdoO5fgD`VB(w@)UZ%p?RhoUu7{US54 zkFx}Ky{3=zd$rMAR^yCt{aOEFSBnI3E(3GxZv`Rqd1E!_ZSUAz@qzbaz5xhg@fVMO{&`H250zt9-rc37V`OggLAg+N7*-R zMak(jI@g%%8y!0=ERBDy(GiE+dm5c;#b#kef9HeYW+Ai@ZWbnFv+%0^&KWJiX5n7n z(pe!SbUCtn#%1=f+t{{Yx6m&Qe!m8BrOPxI4alA5RoA&iU1qM&I?dHya!JVU5O@*q zRpw6fldN+)3C_zoXOjGpobwfRk@?sFXJO@&pflpw+ssn~9O2jv1DzwYJNe}G%!ugi z>B)<JApzUU?!+v<`GWt*ZVi>3YS}C5<=G>1zfbhMt!pJpM4}^!JQ@*rTBTwZoi$ z{pVdxu=^R){b~ZUVX*T9;rmOPoKq?O(I)3=X~OIw&ctp`T@6lMasDon^@T3jcxRz{ z#zsAd#3EFe94!uLITB&oKgiC?$Ifu74&aFP7iQs5=N$DF^RuDOUE+>*@i1p-;EtBQ ze`pvYcd>bUn3F}bn@t#ZxYI{-ugUn~&hQDs+d?1l+%k{|ZW+KWw%jt1D%>(4moLfw za>|~hVBp^4(u|`OUv%C_n+e`n+{Cv1`PAxp|GEPQu0TOX`?$1JgX6ex>i;{liWNNcGI5d;I|$sR$LgmO25aA$$Z zz6XG~_Z84UG0!$TjipI$69R&pGCf8(qmGiRUF0U961l{NdwsZr!zXBav$X776PekD z2wUXkIljeC1Tqaf!3#J{!lj|VWY57Y8{textMr2r&a`?y;k>UL&%>}{dr3p)YuwIT zx;U3K&y8~WRVTS(-nNfwuzGS+@}+UKQ%lwoi*!c4#43CG%U-$Um=i`jy+6#}26?z4 z)$4m%j+>oiJ}X&BS|X*YHcF6}zr@}SA)+j5dmV<8q9QLy{({FS41Qi7>5M(W=clz? zs%TFZ;Q*Lrxi9xHRqh}l=en3@(i7w?mE1Z4$AN;yl{BZrxZ_#&g4nz9uaq#CjB*Yi zoMMj#*R))llz=hG(H#!G1=V5)hR%aVyjt_kG+&iM!BEUHq1s=A}OHwuQh@DjeVw*<|IgzKXPD6FTT3MzxD zL+(Wl(Fu_*B!~2_pwrw(w@{reqn#sEvZLs5XM<8H^S~Hq9uGCv8Ll2SBgQ&6RLS9_ z1Uv6sGEm}a_L%f+^Y~b2gc&u?X{fROwYds_F`+W|E4wpB0rq23KAL04Iiq92FO1^? zdG^k7R7gh7?2bQ=bGTAOBo1!fkYc#HiT;zOKtWIEkL$VtzBp>1S@479=hyKt<@`P#rkp>%WXm!Ar#P9DR>waA{PXcJ!0(KQ0sggk7~uEE z!vKHK0v~?T&KF~i1@80lFmQLr!@zwb9tQ66z2JT}sC4~QC##+`N62G~IeV%z;>6Fz zvr^O-<6(;WYCKF)_r}8%^2qA(T9D-5upB|cwXwgIUWY~WAQMsx5dN2eli{g_R}x;{rai^zbXC^;3ggh_?O~g zfZrVt1N^>t7~l^Cz&|k!;D30&J68T24+H!!@i4&u77qjbGkXEQ)(8F(cA+E67q0uQ zcs?5OZ}BkY{4gG-oS((Rl=G{2m`41TLuE3;RID6fgl+M3l(Q-xrkpkLFy*wz!<4fw z9;TcPww(Ho7pFUy>YmRd^EdhGBDvJX>oS+LID-)+?r4DvzHI)z#rZ3bCysP}r`PW` z_a5bpl;=}NIh(5f_3cjX{DotQa(44>0^7_zM?3XH*F7hbh%3Boyq?EnlZ`mWs#D>M za!yRMx5Vk!myULpa_rK}^0H;j4t#0-TMQbANXu0BKX8`BEk%iZu>`X5~P`P-*e{h>f%qH<}72@c>Ofz zG$BBOgUl7DJKw8*$5WASbor+uQ&2CL)yzJ_8FhfQ!GD;W&TuYZNbfnr`RZP4gLl2E zA}l8P)%Bc|k8YvrXSE~#-D@262>$$o9~}NL-Un2bIsZe>5dV6JTR!9*7ROJK)l9!0 zF>1Lq9<%JMUf>;S#sZoH5_IN@uPxzn$+~+!U3$ ziZxPHW}7US!ZKS4l$j;xI87oxavNLbQ|8W#9QIFcbBdZ#1hdZZzbfs z?uQKC%Geun1{KQejF-77Ugkzy=5ZhK%cO*OnVdFgN22atX0BGy<(Hc4EAM`}x!&^5 zDKP4gv*#qZ-9_GeS@gbVwJ1KJ>|pMUIv zz9UfN2JpRHG}C$b_qUpFi2@IQ_pqA|6j-^CpRoE!DuGzzke8)TLqWvSaL_W zY$p)mat(n1myPryp$M041R`9PoBVkwJA}?aW^3s|BlE5E3dsC#%my+kji$e^$ZVBs zJ+_0F2$@?6M9ADiAV4PRItfL{+(e*&O!lD7;swsQ5T6?l9-sGKP{8NklMTd>jq1rr ze&48Uqadb@>$4-ov=WFAv!{RCcF}vc3F`bJFBX-BZBjw@+1*9Cx24`^X!hnZOTG$^U0m0?VV%VR!M&nse+PkR+TTtfV0kP~ zwi1e1-WCE8%R}Vv6i99Cq11cBIeOH2l{0Nn=Rq+)ne9r+-YE7Tji%?Po%8co_|>v2 zAYX!};WS^?LH>#aF^;gThWu5DtzK+p!eWo_`(2&bs;)`L8oi1@)abVKf)Qb>5kmPC zHG0opjqWN#p%%8b>tI{kSQ2mSS*GkNr_8pPI(2I?)#}z_YSyjA)U8{Ksowru+$Ei} zCEnuBc#Ais7c>`IyfNP5bp+zw+@21*w7Z-H#Q`+J(mE5a&Sjn5BU~KI zBD>Oj{Ay>My&;aq`Fu`@fy&#{u^}-~e|v5=BnFybZxsxQfkybKw98Z$P=qb0bTCGFSB2K6-a|VX<`qs2B353`7rK5Ad zrO|zeWoBfn)4#|65HPSL8U^~oC7LuXT<^-E=1_M~8I97iSf}`|?Dx?ffJVB@>RSTc zK|6t}PCeb6mkue84qaD89fEa=S$eHA z_`n@;DElGQLDZu5Iz%>tg+oK;5W5hC`@U1(KgcQvSw_v2;LE*-*!BM`o)dMSudVzi zcHanA1PB1pCd^g>(I(6m0?{T+CxJ54vC3%@(?VYiIH1S{`EGnSL7~B%wA!f|T^7s; zRD`o_gJRo7I|yfD+ey0!_rRJ=ea5LW8&*60=9hYJz4i9=MRHtedHF(wswVva8%`b` z4;&kN7qPiAug_(HOg4e89Vo1KOE zYECJUDrG-c*pKUrITS{swUWKUen{I78|6clpCN7IOPARFHg@FXi2+%ujV`jD3+lZl zUn1vtvs=%J0bj1Nf!YP`Ai+)EhB*2*P(n7qe#w9^Ap1w1#l(xRfP;Z|kl@%d>`YvO z;oO9+#g}j-Z?Rjv*e&5O*W#)m)v}- zbBa^X_F4UH&S{O%4tFRc2l;E7i11`hlQ#3O#$Q#L*>IaPByUM5NPY!dm}0T%%f>j5 zlXtrn#(tu_+XV|fChvB!Qn)xB6N_+%ZDYAxez(+bTRA?2po%iv^Kz)NDCi|VLKAe- zFu%9@+Vqc=yS?no=5n`>eR-(d?QLHk;AES*{Bus@^nechUbnwxepkz{lnOO^Z@m4s zkg*hA?HB&g(|F{;O@P_|XZTHO`nk zhCt1UigBF=1Q!lLQYsQorG32g`G}lWmgc^G zyrO9DCU(-xAQ%6bC^Sd7D8^$rVs3AXm;G0%AU5dknTXr_z4STRti>-W+o3lV-f(c~m=xj-0Nqx`fo#CU(!V1aPCb!OOy?Qaf;C)H1ZiCwk zn}_9D*Hd7UR=ahk>~?3E+HS_*?i`=5JGvr8;_AtX5*}MQ$S7wAaNvL_;W=sApYX&y zLyog?ubMxXS5Pj#aJYv_fFm_0FdkiZ~W4#}~@( zaGIN_OHo+#+SfWQ6YANVsLEEmJbGp8<nF=yYwkRB?b#_TY8G-QX%qdwa#kGkv# zdGyVWlt*oLlssy(qvg>jd$>G$XUE8+Iy+V#PIjC;Fsqk`%1)3+MRuY*%CnQ)o);fnldhEf*W}7uApAY#pI|e&KC2CD*t$%iZPzjNt_s!wN8lnsHy?P)8qg-WQz1a+$)1djC~WQtiKj6t|bz z@CB!SAVUW8a!5%u8dyxqh#%s#IxSs(yQbsd!n{bzUga%g+VD#tgA7XyxInq7{~~NH z7QcpgTsIZ4EwFa!7_3<0RU}q>C*Gl3o_N!iOu>c3ZViRER9ziUPF>n?rjo}Ig4BHn zt=O@J@Er%^w|9J#jCby(DcS*{R*Y? zwnilK0R^hDw8VyhkTnVM#XH=ukPSz%euZotGN-pQVH063MW!8Lv`w_d_KDb0`G5qi z6=+F)n^QE67n+uKY>Dd31?|ps1P~wZe8)4K1!sZ!c;;KIWWQ?xZR7lW6dvH&uZ&M< zQ9cU`V#u72@o_F9a_ghwD#o(O-GiL_X1h~-Y_D+aWDChe)VT^8_s1Cw#1gq3VReO6 zGIa^CLaK6e;+;;_=`x2MAUj?Ig5d%u^8Y!@0K8qsg5MNr#QE8>{Z!jz?{TXCS+%yV zG5(%f`&~?}y-mRw4;sP;8iEOhP=WpEQ;qG$T`YN0EtsXaWlJ)`kMrU;tCzo9P%d0z zYib3KFjnOH`HZcL3>Ch3U_Wn9t#EtU1c8ho%KEe~PuK)Iq6C4)UBuv0UyQX0c0~zr z#FIG1=S^~4OiJGpeMIElK~zAn5MJg2PM_6KcO*I%Ck})juST8^PC&G6B zs%F9{$sl;L$LkVLiFuOk2glca7yDZ85?4@5%5wANyPT##yz-@~_E$R-B2b0Wme>c0 zujN3zva*#mP8I3O?R$52GNQpK+z3qJ*~D`yPjM@7gj;(zj_n#+G6WiVmhe<=!`<1Q zxY`kiL#=ADSA1D!jG6tfPL1kiKKvD@CTlle=p@@U;Ak2DeaX{tYxwg!slVOO3$eYz z?YC@2xFG7~_6ic29sla2>Oxq4BD+$e{T7mR*=d%laxix0NSDlM$%ECRXjY*{NAVBqA`YOWhg{J;)r+$d64pzrj2kUTN zQ%cyPffeHMm(QBI~;S$uy?TA# z|9n2M_u9i+Yp=cb+Vj40XMfc<%#U>Wi|01UY4NyAM));NUI`(ndOi&nfbF0cxLHod zoG*JbPW48LlFf2ntTtWHbX3ELCl@bo!2tojN{dT`NKbB-!&OadFiq06Bp}*Ikc<$j z?QTY+$3Xc?jS}Nu*Wx(WC1Ox9++T)S2r$e7r0c^TVPFr{Lk#i}K4Jq5TM_5P{O0oE z7%B+l&?<0y&L2G@b0ub5lHep|TU>CGv++PUJu27naoOw5&Sje^%xxI7Qc5-{=vM0&S&1E~t?U~DVlADvuc99#O z%bp^)V=jA|+=N`#1lQ|wb@T=opPU31y9WzUnF zk;`5nH$9i_BezQ~dy(9v=4m$^BZazE&cx-3UR&i!xYTgZ zR=HQqJGk6}nGxQ~?EBSI$2b1``j&7-Oldw{FVXaruB;P=k9WzrbiX6wDY^VFBf-V!d;aUprt3RlO=^=I zE8!9C@q~D7w>%EQ@32QM`xCe1^RIkg!dZ+dXATzctAfF?W%Fgech!U8e< zV>xx=iwj=Fl~)+q=o@jzeS94Cvq`xu2-Uychzhz9*V@O&Nd@gNrnPinxEPCQT+iBi zD_-{3FIdrUR1`c4*k|@w!Tq<2A3v63f_4!a8_*&{TgQ0O=@Yr9bWlwGM2?mYhyA3l)>Nw15($8l2#&mZ4-TuzVn@u5)w9|b;y40DU|;;G|!MdG0N z@;DaD4v2sg!19|YI3Z^k*|d^{BHN-y@IiNH!;tOv6LPlo;Ff*p&PDJExt&#eYyW^a za{|}jwg}~_^`7%5^ogFQ9*MQl9kJUO*S~ zPRZ-!HMhPf&SzO7tcxH=-;2)HZx_QKtiSI3si-=QTgC5bj=X>Ii(52vLDL=2LpmyQZL5#U~(U z-AzF{efe*Tbc&X>i-Z@LEDIKibWpK;zfh<8#X^mOe0rf?63d4RH8w0q^a#s--L-CA z2&~~mJ6!VRU8B2lBl1$QdxYb0d2Latz)~NlU=iMuPgv?ZClgFtXisjE?WX1De$dfJ7uqM|(Wm&RF14Y7@G9JqwC~o>v zew2P#ApR@B-oZ_CzLMj$1RKuFW2_Q>0R^u@&i@i8W@0YLzXtl>y+pDxtjJ>AdVkZX zZ)Lo+lq%l%PQF#SDc8XY#rW^#Aui3!7(0eP6@Tet>O8e|>7}*q;$nBDyGrc)UM`$i zjF`Le7mB~sR0q2O?qiak!+_re9Ab+96mT08Jj}wF9q?roYW^jU?%qyzFrsn?{)mb# zQ>XHfkBR{w@KMoV@#DW_A-TlrA7p1xYsNGFEe{sI{vg{kCUOUx0-T5Omxwbh z_`8gweLvzY_MG&6?#E@{Ifi8 zj1kp@sE($j=ixVJ{k|2fLV9LWF@RHy{9?{8a?)k|qZay!3%|&LN`>lRlZ540xwFrk z^{d=f$!q6eWlcB!DyPX;$~#DWA@Mc|oR4DFM`KXaA07d}Z z2$x#U9=HK;Ux5qd(zNdP(wsvYh>+#F7(3I?0Uu)lVWN+nkCa9<-C^fJQt;$(hgRfz za2$j9$zq2;?<%bqpZW8C$wv`OR8PU}0Jqj%HPz$tRF$~Ot4chxT)%!WF9n_)X%00M2Dr@R)D}@?6`w9Z-~moy2v4JXIbeLAi$waT z#oj=k9{e#te}L!U61sEZ-+{bygfr5i8H-ME(T-ROTq1r>GMr^KE!`@EL?s)sj9&JtHf&&d>ejPYEC4N>b|y#0*c~wnAy{p1gHMPEM7vkChw-HzAsjG8O^{}Jj zHDt{_hv>GZ=sHhvt*72K%`=-Zp4bv?O~aIm@?t&Cl_+APcqHzu=0>4<3q@5Fj|yFm z9E2^utAkB|`*(;nR$S=BL&eT09wOz715vym9D6j+jxD3*`7EEjsHfnu7Ar-kqS0Q$E%I=yvQ zupy0)62H25_rQ!a2a5&$-RQ(-iTpSoeHl7R>`kwqRbB$)&t@URG-SxZA2oaf9^48Z z(h45h3U&h~!5aC=9GPM28!o|K*r^r)jFkg6GeAt4;aggzU(gC}Yy~fB1>b6hL7y!# z2f%+@D|k6zGXuA`f>*SHHv)#TG)rJpEBG-J?4{CeF$EC3wH3S@u$h59t>6Q#;8$D0 zueX9DU{8GvcF^iP@=|~XN10)9ES?|6Z>T-ek@veyhcKiW7*R_ z>GEKVQwSzXFeadaT$U8=VW9Xaf#)knk<=sdJMqM;muBg<&s9}j4^!eJE70)QHY=FDJr-M;v_aeAYGhnf$6UR@#WKoL@6W?^=H=&i*B=SDZ4f|FfQ~6?F zBF~O98umLdwEK4N+jzZ-J$OXhXy)4MyE6T!E^ zrO`kIT*B76g2e<|t512-L z2Am0)`cVVU1x%$e;5@+8)fsSqz*G|kd^KR|an)XWjW{4A^asJ06<@#xZN5yYpg37# zQ5PN-W(?YoXbM?y;X9bUH6dbK7an<8E=`A`(|J&+L6*iAR4fg~@B8s|^kFsWyccHL zThd{u{d$X&>5y7RZxNcoQ}A<1`5C;kWqEI1TN*R?7E97qA~Tbhs_U=P4eE2^flTfU zYh`(jdVD#P_qVd2k+z2j$l{$9-P=2hC;Gf)@Y?t1>0<>{+dWxe;D|f@5b9-osq9kC>B!5@n^tf_rgu^ zGWcnnXvF8jPnD>_VtzNi(sFe{Q*d{Fqea}A&D)8hZ0-z%NHu+wSi!0#Vple&Ws6_4 zd8Fg+_h}GPivRTWd`~3h@EE)`J0OS0Vk9y(hsQ}zH{C{o()(glPo9L(w|esW_A$E{ zvv!mvCf}9{#UseBNIpPJ$>rU>e|rsDyGfGd2XYuQ0Fbf#9Sm9Sgio#?!dSr*jQJ(` zNzC#A!ez7>_6e+|-)LEqi?UEQ1Hey4vX>}v2ad#Kn`C;~?CPxu%Ju^p3`Jh_0U#j4 zdhxav*H+QF7mw?CaUWwrQ&r$}BiZz)0QJO6cFeK@wPQ&HGQO-9_J22Hj%3NQ7@+}A zL$u$BMZI`zXy)^%!&xY<<$yn8W~2X!dk#OU$78%_P|UXPpqTeoFt+?KfYYGTHx5Sc zN-^O2<3^0KkuW?&0m7$XW8@A=7}X)DT@dCXmRDfMEL$l&wHsqo-hvgIZV(JNg;I-_} zWGw+gx~MVcy3=PDI+W#rDK-)hz`g=N3`org!O|` zM)zgxBe7}#KcvLOfj7}+5Dypi1G!_|-!ctukoLM@`(7OUGCtd=cFt*8;13mK^R)BJ zPhe~y{PD?>>Qn%f0dR(>wzlvapL31WPF*2B8pyj?V&a7VAf7UA=N}N61tLd%M1}&_ z1v3#x!0!@SJ_yFMPCPM)_mP%~uLkiF?=)R_8aL87CCo~%UK(FmrfU5WifDYHF4fx6 z4Bx6)=aQlFeeFi6wrwUjATLn0{$L7dlM-yLG{J4XH>rNTP-lfy2bF4j>)oab^zWW- z9q%%bGv+@jUma?K?dRkW#M=B#ECp1lq1IeeK-;5cYe)s3bKcT`#+Y^HV-2Zr!C?+&D0f`Uj)3Gt(h4*;M{ zL6m7X0mxuGZbn&ciLt+%ajA@s7s3dzEikEoG!s9gxxjTPQTW|*n7Yy&AY_$ww5>E} znx(|4>&$5=(`K=BDDM#eCyFVK4CNW#56u`KQv%dCOaT#NEOutAOS#0Dv{Q+pobly^aTXneA?q&S*52m;wT3ORDV! zlTZT&Nq?&1Y!5c;0km(h6lk4k3J6;sWc`LT!5H{7K|5NU-~xWD?lL0Tk+4lO%~aYstR4urZm!SCD``1DIjXEr25TILHQ>GWi;%f^Pi~6SpcH`5fn7DBV()Jj~OSa z%-+vbny7OTK(2mk3eu1_nuI02qQtkhHO`Jmun8zbJHYNVD-FsYnwzLT?IM=-FJ@(6 zENf|+288~PO_R?u?UO;DOp}vL(+O|~v*aYR^aJ@P_>LoR*3v8uXb8N97;T{eND%Akt;0q8CnMI){K} zrUB?ogZ?Z42G?uhzr^)!Et$c!^L5}nN(lys*n&)EEq1>WrAnrNpu=2Ywzi%0($fFf zSCkl=)f5oZ)*52_CWZwyr-`Yw_EtX=tFPnjye_0XN0f%Awm=i}jhv#kMuUNE1}UEv z=|@M|2Y^fC3mp+*?M-pMFZB8l`zaHnp!`>Mco{j0wQ;fwUoTVKmPRUGoU0Wkll;hP2G`AiVN#qTd*v=p7<3I;Z?a zXczd!D}6svQxw~mGPxVR#QX1pW^3$tDIf+TX)F2#yP}rpW9|6UT?EH6~cADr=}Y$D}a(&psMnLX_A}Vb(Zv ziW|c0s%g-nB`8EujW4k*(e5(An2WKNT$P2j3P^S^o7*g(r==8io!5j!m*E?xX+ftZ zDcUyJ#8}XGN}xT*6p$2&X2H~b=31L$fnBxUlVUK7icLtcbxFMhM-_6_7DWMC`Qdiz zZ&7VkX7*l|Ra=RfyTg9?wOmsG%|Fx3Fj|dmvpHZfM$f`j70x6nQC(^ZP`zT<_1rK1 zLsOY!4*RveLYc+v>v1-7jY%~UZVI<`HO31lLhx8jItzs|0b8KMMAih}r}HwXQxX=s z5*j-=N1*r5Cx4`Bdq|ra>GF_*ln}d=CDu*g_ekG~9us-b*oQ!-kkWRS!fnr(5)%#E zDY0TAmTo7A=OtC#0^n_2z(W zQkq(9GAYS-Nls>a+?-?wmYu3wCt!ISYt?`Us*(qtQ=UyW8D@xfx?_1FbZbn}p$ z2$Veo7^Ma^((gxbi(q)55ep(YWfycl866Vu(O#}?W;Lk+dVHe zm=^q)30Z86# zSFpS_`0Ypns9>LV1Aj+-F$Fak5o8dAC`kWIa}^vJrdNTTHiP~O@GYcC8%TEAKx*o* z6VxV5X`Hujxtc*u_UqY?=FP3=D%{aLQWaMAho3Sp? zF7h`n19E5(`6mp33*N~;bUI3?hs*z{WUO*C!h6teNwpRI{)BcoK1ry@Nc_M|#`eM= zTPX1{cVfc_*~ovY9%I;xgkub{Z6GNw$K;DB$L^zTINDs}Y6n=Rqhe?|Z!et_HxaockW8Xf5pkRrc{m)K-Nq<%kH~aTgF?Pfk zexfa7Gju=m; z*RVO;pq2ju|1qbqBiC>XrY=td7=^;z+l{gJ30N2?$s4^(5d}fVlwfw)}hdfm=(a#0#F3Ht)~z}xUX4@9Z8hza$x=xEImPlF$er4e+I1% zhooa>Vs?XFJ=m?Ep}R2$p{)<2q5VYcfs$o7CgSo}BN=;$qF=}UtK1bj_JvWFpnllv zMcdO#*`mm1fLR7up+Lh@7$Z+&Cn@pqzX6Ls{Gy_Qca81~@aos0AR~?Xy#vL{rH}}b z-it>Rra`Uuzk-DnZJ&B9=zfPn2VkGtKe+)TMzn^2fmC<<#xr)3pjoIO^A`5Ei!n{v zhouN(3;PY2){!E$l1B`=VJD79--z_gQUj}N`5bIoh5-)9pUK#`mtg%aZE$za#2|D( zE#qj_U2LS~PYz=2aj~kBhbGw{r==Z=2coC#__rK>o(1NLpcSq(i5I{mekb0j@T1Zzo8bxmIBE# zKOpheVbgINLZ$)9<0BaRg@7SM4TbPNAbEZQxG6LWo?{6(I4P631PEfm!W+h~f8Dh;(Y#v7Tzx($P+J*EWyd6H8dfeJQ0EEc=qclPLv;cqZbxAq*xTZo z2HweA1sf972c}3qgWA`i*Dx|N1Lk8#sl6Fn{REanaO_BK0n}DAcHD?D1KKewjUOx> zC>zQjg=o;4gMY9FO=RMW1|C}YB|)5%kxvV&SXN0gMFF>$d0sE;<4g zEji1xAQ3uP)Zuer(Et%UlSg>hKs*un5plUK1{sS97!BZQFjD|wMnr+k*&@ceZv}vM zEz`!z&yYd{&ZYbR(i2D6UIlQUjHu@TRf9ytcj&%F7f6Hvj+mx9aIF9g5Gue0J(l6Fc82cvK<>hvt$JH$9Jl}rbwG(cp| z=6$6PMEz`jee4Oam6j)2JSuv^n-ftU3K#j%I-!krXrl~% z{iKIjHJ4x4_c(Fq`!Z~C)|X9UjCHEECrt6$VTRvKQlrBR7qxYKuX836()I=+*=x!~ zi(qvwg9JvlIObAnf_&Kl}8D8D`0Ngc>`_-Qq_#p3Oi`wws=`d`l-UMDTb9nz5e ziB+`AnXn!;l|CaZ?9U(Npz^8t`UZ@1He`N+xNt^j9Q^=)W|I0R*YyTRqjrrC*_ejU z0maIQOw#b>!2jp*wQY^ho}49YH}jZq{bXGEd$?<4rIT?w(Tt|vH*-~zYMZX{@;jw; zo2eb~+vJXEgY%FOCc<_y%JFswgHtqSn2OZT#Ii=-R}8*|FT!;9=q)@xX&h>EAkuR@ z5!vM`WS&f!VQ9Kc1nOA;D5s-FL@nTjQmp{lxC}=Nva97T8rfBUK-SiXD=)R8S0j&W z!MF8oo2nt$^t%X}z;~n^^4r);1&aJXi`x03Xj;ru;s=}BbmRa_kXnz%YQPEXQxV&|frRR^fkU2)dA`#Lr83&DC^DRHaj* zqwj)%2v{KT{u%ngQuTr{SU-rW52x6VwTXxth@QxSe{xD2WD`%`#@pu~-+{h)y~Oz2 zndn;yn1<|{m5%O=E(6(vo}_tQA9Mh)y+QXFvrc*Y4LIv6+AQTB91pEu%11=AotTrP z*wAG%%lCHt)0QIAu(XkISe#hOqefhX{wU;goDgR6oiK%e-;1%xS)fl{f@vdiYrtB} zckeJ_5{5E18&U|=j^ol{qxSV$`?1u6fU%TsqI}g0dfG}+u)h_Kv#QUM+Yv*a#!(;Jq;m{q_!RGTQM@zE^s!>G+)-E5AW&#i2DgApL_lyN0I)I#DT>H&m5&FET{j z-TVt&NDCDFI%#rKk>HuKbXcsumq!H)EC`g^z;==(&)I-O)Z#e=OFP5|_wuV^Xv}CG zH6Y+1x`+qhSM)AKZdib|$2Y|D_wq37G!*cvcf^SMcx22WU;2|sf5DXg!VJdxzp16q zdmTkpnBSTNK)TBrFFaOA^p(EJCazfz}p)sM>=y9I(H90eGt z8K3oDRqX&AWj-9C@UJrA=s%9Je~9#TXuYBV;=m6j2M`PhHDvH z@utXF2LbN58RPD^#o+sSWX?GsCV}*h7=xSV0p$Q9Zk~j{(1-!1;}0|T!5bQ;!hR8$ zD7>YM1uI_7pLLt69z(uagadpYhW`Nrhk6mvAs_Zn;Xh}@0DCEn_wK`D!+JC**Lu#S zk3_Q`mTeP=dZ-%cz>7B@3Zh%Vt$V~uf%^Z>OaM_ zU}0_uh9j@TqV(jgB-bLQ1)brev1|-e>(w${G3pP{wP4{-R^H`#yobT}U#;=W(fu*D zR(a6J&~D0qW&viic@vGFK*vRwb`)%T@fs|-Qho*dJbx+1`@co^Ut`0vcT2wO*Xzop z9Bm!*C(C*sCi**Qqb8qKH^K0{;giqkP5S1U7L)AOHqX4e=+@nXUX;?2P}ielSZrka zuS*2Bf1icRV#@y`cRF)d;`@)-zPvzU`6D=iw=+jK!03W8f-HdjvO-?;=^+EgGS;XE z_(EPoNPdeDJ36fET7*ovEM&`NA-^D`Dh&e;`gCH`FeGV-=vjntm=o=LqN(mt-ZfIJ z-^_Ew^IN$~410`ch`eX{eDT#rKD6oLKAz!}K5UBrfGcaHZeo6<(m}l1O$iR_=`L|+ zb}ueoT#A=bk;qQ`AsZUR7StCWuKdDy}%%=b!4SM`&@Gr+AvDgtbA=%7*%) zn)+HMW#M&$Tb1N?bd94qNs+sxgvG>*k6u)I^rO2Vbv2&iqAJg<`rZw7<#RlFY;hKj zRhC!QRK#a>pNF@Zv26`r${0=kt4mAkJoUYk8gf!y4LQkqtXhnIN$DQ23M58)>KZEQ zaSL$GOG<|Hp*Zl8;_@%*fh7@Q4!54aeo1l4ICn12H!8zI>41;jE3Jf(AfD5ZUX8n|ym(r9 z)l~K&#I5tz6_TZE2n|&;YuzG4R0htUln z)Y^71T3)3ERumOii`P>WwF|R?;Y#pVT8*3O$(IljGY^8$wWO3|be$Y6(hn$M{#oD= zrDr%iD=sWl;zQO!90j#g8!A0j^Qurl8)I4H#nnEz|dtEi^dQ(8WY9YK{}Q#HJ**u(C_@XOGtJhlQ@K(tP8P_!CT zTv1)+(QBU$oz~VDQA^P{e76IXq;i!qv^%ZDq4n09+6f(drAAg%>8>v>W7XZD{L{*7 zit0S>TEIBTDLz@FB&(ajipHC8FH&N=@6>9OECGoecMf#j(^Y*n)Q{?&okJb!deI|u z9y=pW;#GS8^VGIs1|IFe#Mg_I&^EM7qj5#3_KM6`mFV!VAXQW_`xTa?#$8YK&n6(l z_TxM>DnAre342pl#GP-tCQVn%%sH)c*Y#m!e zl*=Mhfv^q_r+8R*6tlj%NNfEJH(|u~hm}ye3PD9>&!BvD4VCPnW(jqNIvhl9y)@Yw z^B66|&PBb};l-Hv%z3@LW~I8aGqUp-_V|SRE+x6!$6=VFd8RZ>9o5 zTUt>)(`aQMHlqYaJXFubEw3x#Js2&*l8LxvF|Rg>GHV{AbF?r=bq%G^L^O7WYl%(0 z?^E_kG7e&kxJ^nYapDc7pZuAV33Z8*+7Z3svFQAclGW=mVAGYQ8*In@Pz90$I|U;` zRvt|(kG&rbMI{9(swWLWcRG_{d0t4H(e8$Z7zj|^ZgJv6rL(A9q^Kg`4#h6x_&rrJ z-g;Z5sM-YpRH?4b+Y&|lCgm!ztx@q88E+_ylG**xV=_2(jLwVtdN!Rhj0>X;j=Hi6 z_Na(^Pf7CjfJM?u$-V}I<7h;|HbRV3QGhZ}rKbpaAQMIxBdIp(*eftU)irf2*vP4O zk)&kQhmQo&N>6=Rbx9rjTeAqut4gcc+mNx=#0gOzqf_Q>Jkw#eXrPeCu*V}#tW=`B zv~^>2XgGUA*1Vr_m{Ma8y?`6C(~^UCo0QqdHkKs@Z3Q=CM<#2Q(WP@cR1M z>^va-`IMg_py9P8>~`og1jXn8Y=y^NLi$g=#(+9TJ1r$18VmUBKN?>pqumzsC}vs9Cn^lizLH^x5x=fc)ItYZlFnBq+CE+3((p{sDzTTV+oU{3TS(NaG>~o4 z!t>be%_1f%t!*9YgE+0%#ywJT{~{$tOg^TxkzN(m$IxX~ie<-?E>e-$drXOy4v7zs zDUo*CiY9_+k-q}A%x$GL3+>G$M~Dj_C=t?i!v3KWIgIv<(H1p3%;?BGs%t_+&ZJZq z{m*0LAt#JCwL#NiFzc?G&35{T6_r;NSD+v>i~-uSWS0ksIp_==4bTi~8oq*#A(hW@ zEP!g&q9%N`Iz=RXq(sZOli2jrI%S&_91MZ`6rtxlwglF$Xo-^MrQ-_PJ7!uBc*7;0 zib~w|Zbm1mwT`5;ycXTGHYRun1@mE=$LOAXN%;)c6FJe9!)({Hq41a1R#z731GWex z@l{|pI=4PO&}wQi7;;ak@K|u)9%79UZ+@%<$nU^fxi=}-TiRrao}VaVJ0FHwgoR>9 z;rdXZk$;AM+R$;mtHxHO*#4*z=114^J=582l8K92wvt8~MMp@qQnEu(*x@x=he%hN zscWdNME9@bIV)0*E3vV3wVB!(s-Xm(U>zF(8#AS$yrP6`^-VOg>L})|Q9`6^#Rg=E zro$(i0#b9-QAHXJQS3wcu6hAI)*##!u=_4 zYp0#vVi<&>33&fh8J(NQCV}!udMj^`q05YpV55-i6%+ zG=Ots8sM(2rMGeLed4FBU^JcjOgZT%4G{a!D>J3nMDGhqUyFU5n14a(X_=t8@#2#U kO1Nc<4~YC)36p+l%KTcfNzydY^P-X~J=;`&QHikqKVEtcng9R* diff --git a/wasm_for_tests/tx_mint_tokens.wasm b/wasm_for_tests/tx_mint_tokens.wasm index 52a43fcfe95b7a720dbc97399111459754bdb123..bbc1361638ae927511a18362ed5b839f4cb9490c 100755 GIT binary patch delta 158507 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 delta 162999 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