From 2255db30d7aa256b60b2edddc39aef9af6957a67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 30 Apr 2024 18:24:28 +0200 Subject: [PATCH 01/29] core: add arith mod with a custom `trait CheckedAdd` --- crates/core/src/arith.rs | 21 +++++++++++++++++++++ crates/core/src/lib.rs | 1 + 2 files changed, 22 insertions(+) create mode 100644 crates/core/src/arith.rs diff --git a/crates/core/src/arith.rs b/crates/core/src/arith.rs new file mode 100644 index 0000000000..83d9c87fb0 --- /dev/null +++ b/crates/core/src/arith.rs @@ -0,0 +1,21 @@ +//! Arithmetics helpers + +/// Performs addition that returns `None` instead of wrapping around on +/// overflow. +pub trait CheckedAdd: Sized + Copy { + /// Adds two numbers, checking for overflow. If overflow happens, `None` is + /// returned. + fn checked_add(&self, rhs: Self) -> Option; +} + +/// Helpers for testing. +#[cfg(feature = "testing")] +pub mod testing { + use super::*; + + impl CheckedAdd for u64 { + fn checked_add(&self, rhs: Self) -> Option { + u64::checked_add(*self, rhs) + } + } +} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 0af456c454..c567f01e7c 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -6,6 +6,7 @@ #![deny(rustdoc::broken_intra_doc_links)] #![deny(rustdoc::private_intra_doc_links)] +pub mod arith; pub mod bytes; pub mod event; pub mod hints; From 83016cf83926d1d82a5e7e4ce5fbd00ce7fefba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Mon, 29 Apr 2024 15:35:10 +0200 Subject: [PATCH 02/29] deps: add smooth-operator and re-export from core/arith --- Cargo.lock | 19 +++++++++++++++++++ Cargo.toml | 1 + crates/core/Cargo.toml | 1 + crates/core/src/arith.rs | 2 ++ wasm/Cargo.lock | 19 +++++++++++++++++++ wasm_for_tests/Cargo.lock | 19 +++++++++++++++++++ 6 files changed, 61 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index d1669b6096..cc9d4af01b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4546,6 +4546,7 @@ dependencies = [ "serde 1.0.193", "serde_json", "sha2 0.9.9", + "smooth-operator", "sparse-merkle-tree", "tendermint", "tendermint-proto", @@ -7161,6 +7162,24 @@ version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +[[package]] +name = "smooth-operator" +version = "0.6.0" +source = "git+https://github.com/heliaxdev/smooth-operator?tag=v0.6.0#1e9e2382dd6c053f54418db836f7f03143fcd2f3" +dependencies = [ + "smooth-operator-impl", +] + +[[package]] +name = "smooth-operator-impl" +version = "0.6.0" +source = "git+https://github.com/heliaxdev/smooth-operator?tag=v0.6.0#1e9e2382dd6c053f54418db836f7f03143fcd2f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "snafu" version = "0.7.5" diff --git a/Cargo.toml b/Cargo.toml index 24b09c5dab..aa38ffabb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -154,6 +154,7 @@ sha2 = "0.9.3" sha2-const = "0.1.2" signal-hook = "0.3.9" slip10_ed25519 = "0.1.3" +smooth-operator = {git = "https://github.com/heliaxdev/smooth-operator", tag = "v0.6.0"} # sysinfo with disabled multithread feature sysinfo = {version = "0.27.8", default-features = false} tar = "0.4.37" diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 14f29989d8..9e1af1aef8 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -66,6 +66,7 @@ rayon = {version = "=1.5.3", optional = true} serde.workspace = true serde_json.workspace = true sha2.workspace = true +smooth-operator.workspace = true tendermint = {workspace = true} tendermint-proto = {workspace = true} thiserror.workspace = true diff --git a/crates/core/src/arith.rs b/crates/core/src/arith.rs index 83d9c87fb0..83e694e9be 100644 --- a/crates/core/src/arith.rs +++ b/crates/core/src/arith.rs @@ -1,5 +1,7 @@ //! Arithmetics helpers +pub use smooth_operator::{checked, Error}; + /// Performs addition that returns `None` instead of wrapping around on /// overflow. pub trait CheckedAdd: Sized + Copy { diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index a59b5ba551..d2c2ef6ae7 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -3629,6 +3629,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.9.9", + "smooth-operator", "sparse-merkle-tree", "tendermint", "tendermint-proto", @@ -5818,6 +5819,24 @@ version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +[[package]] +name = "smooth-operator" +version = "0.6.0" +source = "git+https://github.com/heliaxdev/smooth-operator?tag=v0.6.0#1e9e2382dd6c053f54418db836f7f03143fcd2f3" +dependencies = [ + "smooth-operator-impl", +] + +[[package]] +name = "smooth-operator-impl" +version = "0.6.0" +source = "git+https://github.com/heliaxdev/smooth-operator?tag=v0.6.0#1e9e2382dd6c053f54418db836f7f03143fcd2f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "socket2" version = "0.4.10" diff --git a/wasm_for_tests/Cargo.lock b/wasm_for_tests/Cargo.lock index 630b6bcca2..67ec32df07 100644 --- a/wasm_for_tests/Cargo.lock +++ b/wasm_for_tests/Cargo.lock @@ -3603,6 +3603,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.9.9", + "smooth-operator", "sparse-merkle-tree", "tendermint", "tendermint-proto", @@ -5756,6 +5757,24 @@ version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +[[package]] +name = "smooth-operator" +version = "0.6.0" +source = "git+https://github.com/heliaxdev/smooth-operator?tag=v0.6.0#1e9e2382dd6c053f54418db836f7f03143fcd2f3" +dependencies = [ + "smooth-operator-impl", +] + +[[package]] +name = "smooth-operator-impl" +version = "0.6.0" +source = "git+https://github.com/heliaxdev/smooth-operator?tag=v0.6.0#1e9e2382dd6c053f54418db836f7f03143fcd2f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "socket2" version = "0.4.10" From 3956994aa56942f1ccee97ebae91792efa60a0cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Fri, 12 Apr 2024 13:12:08 +0100 Subject: [PATCH 03/29] core: strict clippy arith --- crates/core/src/lib.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index c567f01e7c..bf749639af 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -2,9 +2,17 @@ #![doc(html_favicon_url = "https://dev.namada.net/master/favicon.png")] #![doc(html_logo_url = "https://dev.namada.net/master/rustdoc-logo.png")] -#![warn(missing_docs)] #![deny(rustdoc::broken_intra_doc_links)] #![deny(rustdoc::private_intra_doc_links)] +#![warn( + missing_docs, + rust_2018_idioms, + clippy::cast_sign_loss, + clippy::cast_possible_truncation, + clippy::cast_possible_wrap, + clippy::cast_lossless, + clippy::arithmetic_side_effects +)] pub mod arith; pub mod bytes; From 6081bf528fc1a314a64096698ee3ec3e354548da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 16 Apr 2024 17:57:58 +0200 Subject: [PATCH 04/29] core/voting_power: sanitize panicking code --- crates/core/src/voting_power.rs | 102 ++++++++++++++------------------ 1 file changed, 45 insertions(+), 57 deletions(-) diff --git a/crates/core/src/voting_power.rs b/crates/core/src/voting_power.rs index abe7cb49ed..0696c8afd8 100644 --- a/crates/core/src/voting_power.rs +++ b/crates/core/src/voting_power.rs @@ -13,7 +13,6 @@ use namada_macros::BorshDeserializer; #[cfg(feature = "migrations")] use namada_migrations::*; use num_rational::Ratio; -use num_traits::ops::checked::CheckedAdd; use serde::de::Visitor; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; @@ -49,7 +48,7 @@ impl EthBridgeVotingPower { impl From for EthBridgeVotingPower { #[inline] fn from(val: u64) -> Self { - Self(val as u128) + Self(u128::from(val)) } } @@ -66,21 +65,22 @@ impl TryFrom for EthBridgeVotingPower { } } -impl From<&FractionalVotingPower> for EthBridgeVotingPower { - fn from(FractionalVotingPower(ratio): &FractionalVotingPower) -> Self { +impl TryFrom for EthBridgeVotingPower { + type Error = (); + + fn try_from(ratio: FractionalVotingPower) -> Result { + if ratio > FractionalVotingPower::WHOLE { + return Err(()); + } let max_bridge_voting_power = Uint::from(EthBridgeVotingPower::MAX.0); + let FractionalVotingPower(ratio) = ratio; + // Allowed because we're checking that ratio is at most 1 + #[allow(clippy::arithmetic_side_effects)] let voting_power = ratio * max_bridge_voting_power; let voting_power = voting_power.round().to_integer().low_u128(); - Self(voting_power) - } -} - -impl From for EthBridgeVotingPower { - #[inline] - fn from(ratio: FractionalVotingPower) -> Self { - (&ratio).into() + Ok(Self(voting_power)) } } @@ -148,6 +148,24 @@ impl FractionalVotingPower { pub fn new_u64(numer: u64, denom: u64) -> Result { Self::new(Uint::from_u64(numer), Uint::from_u64(denom)) } + + /// Multiple with overflow checks. + pub fn checked_mul(&self, v: &Self) -> Option { + use num_traits::CheckedMul; + Some(Self(self.0.checked_mul(&v.0)?)) + } + + /// Multiply by `token::Amount` with overflow checks + pub fn checked_mul_amount(&self, v: Amount) -> Option { + if self > &Self::WHOLE { + return None; + } + let whole: Uint = v.into(); + // Allowed because we're checking that ratio is at most 1 + #[allow(clippy::arithmetic_side_effects)] + let fraction = (self.0 * whole).to_integer(); + Amount::from_uint(fraction, 0u8).ok() + } } impl Default for FractionalVotingPower { @@ -157,12 +175,6 @@ impl Default for FractionalVotingPower { } } -impl From<&FractionalVotingPower> for (Uint, Uint) { - fn from(ratio: &FractionalVotingPower) -> Self { - (ratio.0.numer().to_owned(), ratio.0.denom().to_owned()) - } -} - impl Sum for FractionalVotingPower { fn sum>(iter: I) -> Self { iter.fold(Self::default(), Add::add) @@ -173,33 +185,11 @@ impl Mul for FractionalVotingPower { type Output = Self; fn mul(self, rhs: FractionalVotingPower) -> Self::Output { - self * &rhs - } -} - -impl Mul<&FractionalVotingPower> for FractionalVotingPower { - type Output = Self; - - fn mul(self, rhs: &FractionalVotingPower) -> Self::Output { - Self(self.0 * rhs.0) - } -} - -impl Mul for FractionalVotingPower { - type Output = Amount; - - fn mul(self, rhs: Amount) -> Self::Output { - self * &rhs - } -} - -impl Mul<&Amount> for FractionalVotingPower { - type Output = Amount; - - fn mul(self, &rhs: &Amount) -> Self::Output { - let whole: Uint = rhs.into(); - let fraction = (self.0 * whole).to_integer(); - Amount::from_uint(fraction, 0u8).unwrap() + // Allowed because the ratios are capped at 1 + #[allow(clippy::arithmetic_side_effects)] + { + Self(self.0 * rhs.0) + } } } @@ -207,14 +197,8 @@ impl Add for FractionalVotingPower { type Output = Self; fn add(self, rhs: FractionalVotingPower) -> Self::Output { - self + &rhs - } -} - -impl Add<&FractionalVotingPower> for FractionalVotingPower { - type Output = Self; + use num_traits::CheckedAdd; - fn add(self, rhs: &FractionalVotingPower) -> Self::Output { self.0 .checked_add(&rhs.0) .map(Self) @@ -228,13 +212,17 @@ impl Add<&FractionalVotingPower> for FractionalVotingPower { impl AddAssign for FractionalVotingPower { fn add_assign(&mut self, rhs: FractionalVotingPower) { - *self = *self + rhs + // Allowed because the ratios are capped at 1 + #[allow(clippy::arithmetic_side_effects)] + { + *self = *self + rhs + } } } -impl AddAssign<&FractionalVotingPower> for FractionalVotingPower { - fn add_assign(&mut self, rhs: &FractionalVotingPower) { - *self = *self + rhs +impl From<&FractionalVotingPower> for (Uint, Uint) { + fn from(ratio: &FractionalVotingPower) -> Self { + (ratio.0.numer().to_owned(), ratio.0.denom().to_owned()) } } @@ -303,7 +291,7 @@ struct VPVisitor; impl<'de> Visitor<'de> for VPVisitor { type Value = FractionalVotingPower; - fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + fn expecting(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { formatter.write_str( "A '/' separated pair of numbers, the second of which is non-zero.", ) From 689fe21e2c68ace03e8ce01fd7d1eab85a4cb7ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 16 Apr 2024 19:59:38 +0200 Subject: [PATCH 05/29] core: add `assert_matches` dev-dep --- Cargo.lock | 1 + crates/core/Cargo.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index cc9d4af01b..3431e0340a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4512,6 +4512,7 @@ dependencies = [ name = "namada_core" version = "0.34.0" dependencies = [ + "assert_matches", "bech32 0.8.1", "borsh 1.2.1", "borsh-ext", diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 9e1af1aef8..d78444c3d8 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -76,6 +76,7 @@ uint = "0.9.5" zeroize.workspace = true [dev-dependencies] +assert_matches.workspace = true pretty_assertions.workspace = true proptest.workspace = true rand.workspace = true From b4a06687dba35969728d21212bc6f508ff260d45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 16 Apr 2024 20:00:04 +0200 Subject: [PATCH 06/29] core/uint: sanitize panicking code --- crates/core/src/uint.rs | 449 +++++++++++++++++++++------------------- 1 file changed, 240 insertions(+), 209 deletions(-) diff --git a/crates/core/src/uint.rs b/crates/core/src/uint.rs index 60cfd14952..8c69c2d4b3 100644 --- a/crates/core/src/uint.rs +++ b/crates/core/src/uint.rs @@ -1,9 +1,11 @@ -#![allow(clippy::assign_op_pattern)] //! An unsigned 256 integer type. Used for, among other things, //! the backing type of token amounts. + +// Used in `construct_uint!` +#![allow(clippy::assign_op_pattern)] + use std::cmp::Ordering; use std::fmt; -use std::ops::{Add, AddAssign, BitAnd, Div, Mul, Neg, Rem, Sub, SubAssign}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use impl_num_traits::impl_uint_num_traits; @@ -11,12 +13,12 @@ use namada_macros::BorshDeserializer; #[cfg(feature = "migrations")] use namada_migrations::*; use num_integer::Integer; -use num_traits::{CheckedAdd, CheckedMul, CheckedSub}; use uint::construct_uint; use super::dec::{Dec, POS_DECIMAL_PRECISION}; +use crate::arith::{self, checked, CheckedAdd}; use crate::token; -use crate::token::{Amount, AmountParseError, MaspDigitPos}; +use crate::token::{AmountParseError, MaspDigitPos}; /// The value zero. pub const ZERO: Uint = Uint::from_u64(0); @@ -24,6 +26,10 @@ pub const ZERO: Uint = Uint::from_u64(0); /// The value one. pub const ONE: Uint = Uint::from_u64(1); +// Allowed because the value is a const `64` +#[allow(clippy::cast_possible_truncation)] +const UINT_U32_WORD_BITS: u32 = Uint::WORD_BITS as u32; + impl Uint { const N_WORDS: usize = 4; @@ -34,6 +40,7 @@ impl Uint { /// Return the least number of bits needed to represent the number #[inline] + #[allow(clippy::arithmetic_side_effects)] pub fn bits_512(arr: &[u64; 2 * Self::N_WORDS]) -> usize { for i in 1..arr.len() { if arr[arr.len() - i] > 0 { @@ -57,6 +64,7 @@ impl Uint { (slf, rem.into()) } + #[allow(clippy::arithmetic_side_effects)] fn shr_512( original: [u64; 2 * Self::N_WORDS], shift: u32, @@ -81,24 +89,26 @@ impl Uint { ret } + #[allow(clippy::arithmetic_side_effects)] fn full_shl_512( slf: [u64; 2 * Self::N_WORDS], shift: u32, ) -> [u64; 2 * Self::N_WORDS + 1] { - debug_assert!(shift < Self::WORD_BITS as u32); + debug_assert!(shift < UINT_U32_WORD_BITS); let mut u = [0u64; 2 * Self::N_WORDS + 1]; let u_lo = slf[0] << shift; - let u_hi = Self::shr_512(slf, Self::WORD_BITS as u32 - shift); + let u_hi = Self::shr_512(slf, UINT_U32_WORD_BITS - shift); u[0] = u_lo; u[1..].copy_from_slice(&u_hi[..]); u } + #[allow(clippy::arithmetic_side_effects)] fn full_shr_512( u: [u64; 2 * Self::N_WORDS + 1], shift: u32, ) -> [u64; 2 * Self::N_WORDS] { - debug_assert!(shift < Self::WORD_BITS as u32); + debug_assert!(shift < UINT_U32_WORD_BITS); let mut res = [0; 2 * Self::N_WORDS]; for i in 0..res.len() { res[i] = u[i] >> shift; @@ -106,13 +116,14 @@ impl Uint { // carry if shift > 0 { for i in 1..=res.len() { - res[i - 1] |= u[i] << (Self::WORD_BITS as u32 - shift); + res[i - 1] |= u[i] << (UINT_U32_WORD_BITS - shift); } } res } // See Knuth, TAOCP, Volume 2, section 4.3.1, Algorithm D. + #[allow(clippy::arithmetic_side_effects)] fn div_mod_knuth_512( slf: [u64; 2 * Self::N_WORDS], mut v: Self, @@ -206,14 +217,10 @@ impl Uint { } /// Returns a pair `(self / other, self % other)`. - /// - /// # Panics - /// - /// Panics if `other` is zero. pub fn div_mod_512( slf: [u64; 2 * Self::N_WORDS], other: Self, - ) -> ([u64; 2 * Self::N_WORDS], Self) { + ) -> Option<([u64; 2 * Self::N_WORDS], Self)> { let my_bits = Self::bits_512(&slf); let your_bits = other.bits(); @@ -221,32 +228,28 @@ impl Uint { // Early return in case we are dividing by a larger number than us if my_bits < your_bits { - return ( + return Some(( [0; 2 * Self::N_WORDS], Self(slf[..Self::N_WORDS].try_into().unwrap()), - ); + )); } if your_bits <= Self::WORD_BITS { - return Self::div_mod_small_512(slf, other.low_u64()); + return Some(Self::div_mod_small_512(slf, other.low_u64())); } let (n, m) = { let my_words = Self::words(my_bits); let your_words = Self::words(your_bits); - (your_words, my_words - your_words) + (your_words, my_words.checked_sub(your_words)?) }; - Self::div_mod_knuth_512(slf, other, n, m) + Some(Self::div_mod_knuth_512(slf, other, n, m)) } /// Returns a pair `(Some((self * num) / denom), (self * num) % denom)` if /// the quotient fits into Self. Otherwise `(None, (self * num) % denom)` is /// returned. - /// - /// # Panics - /// - /// Panics if `denom` is zero. pub fn checked_mul_div( &self, num: Self, @@ -256,7 +259,7 @@ impl Uint { None } else { let prod = uint::uint_full_mul_reg!(Uint, 4, self, num); - let (quotient, remainder) = Self::div_mod_512(prod, denom); + let (quotient, remainder) = Self::div_mod_512(prod, denom)?; // The compiler WILL NOT inline this if you remove this annotation. #[inline(always)] fn any_nonzero(arr: &[u64]) -> bool { @@ -281,20 +284,6 @@ impl Uint { } } } - - /// Returns a pair `((self * num) / denom, (self * num) % denom)`. - /// - /// # Panics - /// - /// Panics if `denom` is zero. - pub fn mul_div(&self, num: Self, denom: Self) -> (Self, Self) { - let prod = uint::uint_full_mul_reg!(Uint, 4, self, num); - let (quotient, remainder) = Self::div_mod_512(prod, denom); - ( - Self(quotient[0..Self::N_WORDS].try_into().unwrap()), - remainder, - ) - } } construct_uint! { @@ -348,7 +337,7 @@ impl<'de> serde::Deserialize<'de> for Uint { } if digits.len() > 77 { return Err(D::Error::custom(AmountParseError::ScaleTooLarge( - digits.len() as u32, + digits.len(), 77, ))); } @@ -368,9 +357,12 @@ impl<'de> serde::Deserialize<'de> for Uint { impl_uint_num_traits!(Uint, 4); +// Required for Ratio used in `voting_power::FractionalVotingPower`. +// Use with care as some of the methods may panic. +#[allow(clippy::arithmetic_side_effects)] impl Integer for Uint { fn div_floor(&self, other: &Self) -> Self { - self.div(other) + self.checked_div(*other).unwrap() } fn mod_floor(&self, other: &Self) -> Self { @@ -407,11 +399,14 @@ impl Integer for Uint { } fn lcm(&self, other: &Self) -> Self { - (*self * *other).div(self.gcd(other)) + (*self * *other).checked_div(self.gcd(other)).unwrap() } fn divides(&self, other: &Self) -> bool { - other.rem(self).is_zero() + other + .checked_rem(*self) + .map(|rem| rem.is_zero()) + .unwrap_or_default() } fn is_multiple_of(&self, other: &Self) -> bool { @@ -419,6 +414,7 @@ impl Integer for Uint { } fn is_even(&self) -> bool { + use std::ops::BitAnd; self.bitand(Self::one()) != Self::one() } @@ -448,13 +444,15 @@ impl Uint { .map(|x| x.0) } - /// Compute the two's complement of a number. - fn negate(&self) -> Self { + /// Compute the two's complement of a number. Returns a flag if the negation + /// overflows. + fn negate(&self) -> (Self, bool) { let mut output = self.0; for byte in output.iter_mut() { *byte ^= u64::MAX; } - Self(output).overflowing_add(Uint::from(1u64)).0.canonical() + let (res, overflow) = Self(output).overflowing_add(Uint::from(1u64)); + (res.canonical(), overflow) } /// There are two valid representations of zero: plus and @@ -529,7 +527,7 @@ impl I256 { if self.non_negative() { self.0 } else { - self.0.negate() + self.0.negate().0 } } @@ -580,11 +578,17 @@ impl I256 { let is_negative = value < 0; let value = value.unsigned_abs(); let mut result = [0u64; 4]; - result[denom as usize] = value as u64; + result[denom as usize] = u64::try_from(value) + .map_err(|_e| AmountParseError::PrecisionOverflow)?; let result = Uint(result); if result <= MAX_SIGNED_VALUE { if is_negative { - Ok(Self(result.negate()).canonical()) + let (inner, overflow) = result.negate(); + if overflow { + Err(AmountParseError::InvalidRange) + } else { + Ok(Self(inner)) + } } else { Ok(Self(result).canonical()) } @@ -593,224 +597,212 @@ impl I256 { } } - /// Multiply by a decimal [`Dec`] with the result rounded up. - #[must_use] - pub fn mul_ceil(&self, dec: Dec) -> Self { + /// Multiply by a decimal [`Dec`] with the result rounded up. Checks for + /// overflow. + pub fn mul_ceil(&self, dec: Dec) -> Result { let is_res_negative = self.is_negative() ^ dec.is_negative(); - let tot = self.abs() * dec.0.abs(); - let denom = Uint::from(10u64.pow(POS_DECIMAL_PRECISION as u32)); - let floor_div = tot / denom; - let rem = tot % denom; + let tot = checked!(self.abs() * dec.0.abs())?; + let denom = Uint::from(10u64.pow(u32::from(POS_DECIMAL_PRECISION))); + let floor_div = checked!(tot / denom)?; + let rem = checked!(tot % denom)?; let abs_res = Self(if !rem.is_zero() && !is_res_negative { - floor_div + Uint::from(1_u64) + checked!(floor_div + Uint::from(1_u64))? } else { floor_div }); - if is_res_negative { -abs_res } else { abs_res } - } -} - -impl From for I256 { - fn from(val: u64) -> Self { - I256::try_from(Uint::from(val)) - .expect("A u64 will always fit in this type") - } -} - -impl TryFrom for I256 { - type Error = Box; - - fn try_from(value: Uint) -> Result { - if value <= MAX_SIGNED_VALUE { - Ok(Self(value)) + Ok(if is_res_negative { + checked!(-abs_res)? } else { - Err("The given integer is too large to be represented asa \ - SignedUint" - .into()) - } - } -} - -impl Neg for I256 { - type Output = Self; - - fn neg(self) -> Self::Output { - Self(self.0.negate()) + abs_res + }) } -} -impl PartialOrd for I256 { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) + /// Sum with overflow check + pub fn sum>(mut iter: I) -> Option { + iter.try_fold(I256::zero(), |acc, amt| acc.checked_add(amt)) } -} -impl Ord for I256 { - fn cmp(&self, other: &Self) -> Ordering { - match (self.non_negative(), other.non_negative()) { - (true, false) => Ordering::Greater, - (false, true) => Ordering::Less, + /// Adds two [`I256`]'s if the absolute value does + /// not exceed [`MAX_SIGNED_VALUE`], else returns `None`. + pub fn checked_add(&self, rhs: Self) -> Option { + let result = match (self.non_negative(), rhs.non_negative()) { (true, true) => { - let this = self.abs(); - let that = other.abs(); - this.cmp(&that) + let inner = self.0.checked_add(rhs.0)?; + if inner > MAX_SIGNED_VALUE { + return None; + } + Self(inner) } (false, false) => { - let this = self.abs(); - let that = other.abs(); - that.cmp(&this) + let inner = self.abs().checked_add(rhs.abs())?; + if inner > MAX_SIGNED_VALUE { + return None; + } + Self(inner).checked_neg()? } - } - } -} - -impl Add for I256 { - type Output = Self; - - 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()), (true, false) => { if self.0 >= rhs.abs() { - Self(self.0 - rhs.abs()) + Self(self.0.checked_sub(rhs.abs())?) } else { - -Self(rhs.abs() - self.0) + Self(rhs.abs().checked_sub(self.0)?).checked_neg()? } } (false, true) => { if rhs.0 >= self.abs() { - Self(rhs.0 - self.abs()) + Self(rhs.abs().checked_sub(self.abs())?) } else { - -Self(self.abs() - rhs.0) + Self(self.abs().checked_sub(rhs.0)?).checked_neg()? } } } - .canonical() + .canonical(); + Some(result) } -} -impl AddAssign for I256 { - fn add_assign(&mut self, rhs: Self) { - *self = *self + rhs; + /// 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.checked_neg()?) } -} - -impl Sub for I256 { - type Output = Self; - fn sub(self, rhs: Self) -> Self::Output { - self + (-rhs) + /// Checked negation + pub fn checked_neg(&self) -> Option { + if self.is_zero() { + return Some(*self); + } + let (inner, overflow) = self.0.negate(); + if overflow { None } else { Some(Self(inner)) } } -} -impl SubAssign for I256 { - fn sub_assign(&mut self, rhs: Self) { - *self = *self - rhs; + /// Checked multiplication + pub fn checked_mul(&self, v: Self) -> Option { + let is_negative = self.is_negative() != v.is_negative(); + let unsigned_res = + I256::try_from(self.abs().checked_mul(v.abs())?).ok()?; + Some(if is_negative { + unsigned_res.checked_neg()? + } else { + unsigned_res + }) } -} - -// NOTE: watch the overflow -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) } + /// Checked division + pub fn checked_div(&self, rhs: Self) -> Option { + if rhs.is_zero() { + None + } else { + let quot = self + .abs() + .fixed_precision_div(&rhs.abs(), 0u8) + .unwrap_or_default(); + Some(if self.is_negative() == rhs.is_negative() { + Self(quot) + } else { + Self(quot).checked_neg()? + }) + } } -} -impl CheckedAdd for I256 { - /// Adds two [`I256`]'s if the absolute value does - /// not exceed [`MAX_SIGNED_VALUE`], else returns `None`. - 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 }) - }) + /// Checked division remnant + pub fn checked_rem(&self, rhs: Self) -> Option { + let inner: Uint = self.abs().checked_rem(rhs.abs())?; + if self.is_negative() { + Some(Self(inner).checked_neg()?) } else { - Some(*self + *other) + Some(Self(inner)) } } } -impl CheckedSub for I256 { - /// Subtracts two [`I256`]'s if the absolute value does - /// not exceed [`MAX_SIGNED_VALUE`], else returns `None`. - fn checked_sub(&self, other: &Self) -> Option { - self.checked_add(&other.neg()) +impl CheckedAdd for I256 { + fn checked_add(&self, rhs: Self) -> Option { + self.checked_add(rhs) } } -impl CheckedMul for I256 { - fn checked_mul(&self, v: &Self) -> Option { - let is_negative = self.is_negative() != v.is_negative(); - let unsigned_res = - I256::try_from(self.abs().checked_mul(v.abs())?).ok()?; - Some(if is_negative { - -unsigned_res - } else { - unsigned_res - }) +// NOTE: This is here only because MASP requires it for `ValueSum` addition +impl num_traits::CheckedAdd for I256 { + fn checked_add(&self, rhs: &Self) -> Option { + self.checked_add(*rhs) } } -impl Mul for I256 { +// NOTE: This is here only because num_traits::CheckedAdd requires it +impl std::ops::Add for I256 { type Output = Self; - fn mul(self, rhs: Self) -> Self::Output { - if rhs.is_negative() { - -self * rhs.abs() - } else { - self * rhs.abs() - } + fn add(self, rhs: Self) -> Self::Output { + self.checked_add(rhs).unwrap() } } -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: u64) -> Self { + I256::try_from(Uint::from(val)) + .expect("A u64 will always fit in this type") } } -impl Div for I256 { - type Output = Self; +impl TryFrom for I256 { + type Error = Box; - fn div(self, rhs: I256) -> Self::Output { - if rhs.is_negative() { - -(self / rhs.abs()) + fn try_from(value: Uint) -> Result { + if value <= MAX_SIGNED_VALUE { + Ok(Self(value)) } else { - self / rhs.abs() + Err("The given integer is too large to be represented asa \ + SignedUint" + .into()) } } } -impl Rem for I256 { - type Output = Self; +impl PartialOrd for I256 { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} - fn rem(self, rhs: Self) -> Self::Output { - if self.is_negative() { - -(Self(self.abs() % rhs.abs())) - } else { - Self(self.abs() % rhs.abs()) +impl Ord for I256 { + fn cmp(&self, other: &Self) -> Ordering { + match (self.non_negative(), other.non_negative()) { + (true, false) => Ordering::Greater, + (false, true) => Ordering::Less, + (true, true) => { + let this = self.abs(); + let that = other.abs(); + this.cmp(&that) + } + (false, false) => { + let this = self.abs(); + let that = other.abs(); + that.cmp(&this) + } } } } + impl From for I256 { fn from(val: i128) -> Self { - if val < 0 { - let abs = Self((-val).into()); - -abs + if val == i128::MIN { + Self(170141183460469231731687303715884105728_u128.into()) + .checked_neg() + .expect( + "This cannot panic as the value is greater than \ + `I256::MIN`", + ) + } else if val < 0 { + let abs = Self( + (val.checked_neg().expect( + "This cannot panic as we're checking for `i128::MIN` above", + )) + .into(), + ); + + // + abs.checked_neg().expect( + "This cannot panic as the value is limited to `i128` range", + ) } else { Self(val.into()) } @@ -819,19 +811,13 @@ impl From for I256 { impl From for I256 { fn from(val: i64) -> Self { - Self::from(val as i128) + Self::from(i128::from(val)) } } impl From for I256 { fn from(val: i32) -> Self { - Self::from(val as i128) - } -} - -impl std::iter::Sum for I256 { - fn sum>(iter: I) -> Self { - iter.fold(I256::zero(), |acc, amt| acc + amt) + Self::from(i128::from(val)) } } @@ -839,10 +825,26 @@ impl TryFrom for i128 { type Error = std::io::Error; fn try_from(value: I256) -> Result { + // The negation cannot panic as `i128::MIN` > `I256::MIN`. + #[allow(clippy::arithmetic_side_effects)] + let i128_min = + I256(170141183460469231731687303715884105728_u128.into()) + .checked_neg() + .expect("const value neg in range"); + // Because we're converting abs value, `i128::MIN` would be overflow it + // so we have to check for it first. + if value == i128_min { + return Ok(i128::MIN); + } + let raw = i128::try_from(value.abs().low_u128()).map_err(|err| { + std::io::Error::new(std::io::ErrorKind::InvalidInput, err) + })?; if !value.non_negative() { - Ok(-(u128::try_from(Amount::from_change(value))? as i128)) + // This cannot panic as we're checking for `i128::MIN` + #[allow(clippy::arithmetic_side_effects)] + Ok(-raw) } else { - Ok(u128::try_from(Amount::from_change(value))? as i128) + Ok(raw) } } } @@ -851,6 +853,8 @@ impl TryFrom for i128 { mod test_uint { use std::str::FromStr; + use assert_matches::assert_matches; + use super::*; /// Test that dividing two [`Uint`]s with the specified precision @@ -1064,4 +1068,31 @@ mod test_uint { assert_eq!(e.checked_mul_div(c, b), Some((Uint::zero(), c))); assert_eq!(d.checked_mul_div(a, e), None); } + + #[test] + fn test_i128_try_from_i256() { + for src in [ + I256::from(0), + I256::from(1), + I256::from(-1), + I256::from(i128::MAX), + I256::from(i128::MIN), + ] { + println!("Src val {src}"); + let res = i128::try_from(src); + // Source value is constructed from a valid i128 range + assert_matches!(res, Ok(_)); + } + + for src in [ + I256::maximum(), + I256::maximum() - I256::from(1), + -I256::maximum(), + -(I256::maximum() - I256::from(1)), + ] { + println!("Src val {src}"); + // Out of i128 range, but must not panic! + let _res = i128::try_from(src); + } + } } From 3399415aa9d83e990ca27d44739e8e9b2959b065 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Fri, 19 Apr 2024 18:43:06 +0200 Subject: [PATCH 07/29] core/token: sanitize panicking code --- crates/core/src/token.rs | 297 +++++++++++++++------------------------ 1 file changed, 114 insertions(+), 183 deletions(-) diff --git a/crates/core/src/token.rs b/crates/core/src/token.rs index 9765a017bc..d6b80aaf12 100644 --- a/crates/core/src/token.rs +++ b/crates/core/src/token.rs @@ -2,8 +2,6 @@ use std::cmp::Ordering; use std::fmt::Display; -use std::iter::Sum; -use std::ops::{Add, AddAssign, Div, Mul, Sub, SubAssign}; use std::str::FromStr; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; @@ -16,6 +14,7 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::address::Address; +use crate::arith::{self, checked, CheckedAdd}; use crate::dec::{Dec, POS_DECIMAL_PRECISION}; use crate::hash::Hash; use crate::ibc::apps::transfer::types::Amount as IbcAmount; @@ -100,9 +99,10 @@ impl Amount { /// Create a new amount of native token from whole number of tokens pub fn native_whole(amount: u64) -> Self { - Self { - raw: Uint::from(amount) * NATIVE_SCALE, - } + let raw = Uint::from(amount) + .checked_mul(Uint::from(NATIVE_SCALE)) + .expect("u64 cannot overflow token amount"); + Self { raw } } /// Get the raw [`Uint`] value, which represents namnam @@ -188,9 +188,12 @@ impl Amount { /// Checked multiplication. Returns `None` on overflow. #[must_use] - pub fn checked_mul(&self, amount: Amount) -> Option { + pub fn checked_mul(&self, amount: T) -> Option + where + T: Into, + { self.raw - .checked_mul(amount.raw) + .checked_mul(amount.into().raw) .map(|result| Self { raw: result }) } @@ -236,10 +239,10 @@ impl Amount { val: u128, denom: MaspDigitPos, ) -> Option { - let lo = val as u64; + let lo = u64::try_from(val).ok()?; let hi = (val >> 64) as u64; let lo_pos = denom as usize; - let hi_pos = lo_pos + 1; + let hi_pos = lo_pos.checked_add(1)?; let mut raw = [0u64; 4]; raw[lo_pos] = lo; if hi != 0 && hi_pos >= 4 { @@ -271,29 +274,75 @@ impl Amount { DenominatedAmount::from_str(string).map(|den| den.amount) } - /// Multiply by a decimal [`Dec`] with the result rounded up. - /// - /// # Panics - /// Panics when the `dec` is negative. - #[must_use] - pub fn mul_ceil(&self, dec: Dec) -> Self { - assert!(!dec.is_negative()); - let tot = self.raw * dec.abs(); - let denom = Uint::from(10u64.pow(POS_DECIMAL_PRECISION as u32)); - let floor_div = tot / denom; - let rem = tot % denom; + /// Multiply by a decimal [`Dec`] with the result rounded up. Returns an + /// error if the dec is negative. Checks for overflow. + pub fn mul_ceil(&self, dec: Dec) -> Result { + // Fails if the dec negative + let _ = checked!(Dec(I256::maximum()) - dec)?; + + let tot = checked!(self.raw * dec.abs())?; + let denom = Uint::from(10u64.pow(u32::from(POS_DECIMAL_PRECISION))); + let floor_div = checked!(tot / denom)?; + let rem = checked!(tot % denom)?; // dbg!(tot, denom, floor_div, rem); let raw = if !rem.is_zero() { - floor_div + Self::from(1_u64) + checked!(floor_div + Uint::one())? } else { floor_div }; - Self { raw } + Ok(Self { raw }) + } + + /// Multiply by a decimal [`Dec`] with the result rounded down. Returns an + /// error if the dec is negative. Checks for overflow. + pub fn mul_floor(&self, dec: Dec) -> Result { + // Fails if the dec negative + let _ = checked!(Dec(I256::maximum()) - dec)?; + + let raw = checked!( + (Uint::from(*self) * dec.0.abs()) + / Uint::from(10u64.pow(u32::from(POS_DECIMAL_PRECISION))) + )?; + Ok(Self { raw }) + } + + /// Sum with overflow check + pub fn sum>(mut iter: I) -> Option { + iter.try_fold(Amount::zero(), |acc, amt| acc.checked_add(amt)) + } + + /// Divide by `u64` with zero divisor and overflow check. + pub fn checked_div_u64(self, rhs: u64) -> Option { + if rhs == 0 { + return None; + } + let raw = self.raw.checked_div(Uint::from(rhs))?; + Some(Self { raw }) + } + + /// A combination of Euclidean division and fractions: + /// x*(a,b) = (a*(x//b), x%b). + pub fn u128_eucl_div_rem( + mut self, + (a, b): (u128, u128), + ) -> Option<(Amount, Amount)> { + let a = Uint::from(a); + let b = Uint::from(b); + let raw = (self.raw.checked_div(b))?.checked_mul(a)?; + let amt = Amount { raw }; + self.raw = self.raw.checked_rem(b)?; + Some((amt, self)) + } +} + +impl CheckedAdd for Amount { + fn checked_add(&self, rhs: Self) -> Option { + self.checked_add(rhs) } } impl Display for Amount { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.raw) } } @@ -384,7 +433,10 @@ impl DenominatedAmount { return string; } if string.len() > decimals { - string.insert(string.len() - decimals, '.'); + // Cannot underflow cause `string.len` > `decimals` + #[allow(clippy::arithmetic_side_effects)] + let idx = string.len() - decimals; + string.insert(idx, '.'); } else { for _ in string.len()..decimals { string.insert(0, '0'); @@ -406,7 +458,7 @@ impl DenominatedAmount { let (div, rem) = value.div_mod(ten); if rem == Uint::zero() { value = div; - denom -= 1; + denom = denom.checked_sub(1).unwrap_or_default(); } } Self { @@ -424,8 +476,11 @@ impl DenominatedAmount { if denom.0 < self.denom.0 { return Err(AmountParseError::PrecisionDecrease); } + // Cannot underflow cause `denom` >= `self.denom` + #[allow(clippy::arithmetic_side_effects)] + let denom_diff = denom.0 - self.denom.0; Uint::from(10) - .checked_pow(Uint::from(denom.0 - self.denom.0)) + .checked_pow(Uint::from(denom_diff)) .and_then(|scaling| self.amount.raw.checked_mul(scaling)) .map(|amount| Self { amount: Amount { raw: amount }, @@ -509,7 +564,11 @@ impl FromStr for DenominatedAmount { type Err = AmountParseError; fn from_str(s: &str) -> Result { - let precision = s.find('.').map(|pos| s.len() - pos - 1); + let precision = s.find('.').map(|pos| { + s.len() + .checked_sub(pos.checked_add(1).unwrap_or(pos)) + .unwrap_or_default() + }); let digits = s .chars() .filter_map(|c| { @@ -522,15 +581,13 @@ impl FromStr for DenominatedAmount { .rev() .collect::>(); if digits.len() != s.len() && precision.is_none() - || digits.len() != s.len() - 1 && precision.is_some() + || digits.len() != s.len().checked_sub(1).unwrap_or_default() + && precision.is_some() { return Err(AmountParseError::NotNumeric); } if digits.len() > 77 { - return Err(AmountParseError::ScaleTooLarge( - digits.len() as u32, - 77, - )); + return Err(AmountParseError::ScaleTooLarge(digits.len(), 77)); } let mut value = Uint::default(); let ten = Uint::from(10); @@ -541,7 +598,10 @@ impl FromStr for DenominatedAmount { .and_then(|scaled| value.checked_add(scaled)) .ok_or(AmountParseError::InvalidRange)?; } - let denom = Denomination(precision.unwrap_or_default() as u8); + let denom = Denomination( + u8::try_from(precision.unwrap_or_default()) + .map_err(|_e| AmountParseError::PrecisionOverflow)?, + ); Ok(Self { amount: Amount { raw: value }, denom, @@ -558,13 +618,15 @@ impl PartialOrd for DenominatedAmount { impl Ord for DenominatedAmount { fn cmp(&self, other: &Self) -> Ordering { if self.denom < other.denom { + // Cannot underflow cause `self.denom` < `other.denom` + #[allow(clippy::arithmetic_side_effects)] let diff = other.denom.0 - self.denom.0; let (div, rem) = other.amount.raw.div_mod(Uint::exp10(diff as usize)); let div_ceil = if rem.is_zero() { div } else { - div + Uint::one() + div.checked_add(Uint::one()).unwrap_or(Uint::MAX) }; let ord = self.amount.raw.cmp(&div_ceil); if let Ordering::Equal = ord { @@ -577,13 +639,15 @@ impl Ord for DenominatedAmount { ord } } else { + // Cannot underflow cause `other.denom` >= `self.denom` + #[allow(clippy::arithmetic_side_effects)] let diff = self.denom.0 - other.denom.0; let (div, rem) = self.amount.raw.div_mod(Uint::exp10(diff as usize)); let div_ceil = if rem.is_zero() { div } else { - div + Uint::one() + div.checked_add(Uint::one()).unwrap_or(Uint::MAX) }; let ord = div_ceil.cmp(&other.amount.raw); if let Ordering::Equal = ord { @@ -670,18 +734,17 @@ impl From for U256 { } } -impl From for Amount { - fn from(dec: Dec) -> Amount { - 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" - ) - } +impl TryFrom for Amount { + type Error = arith::Error; + + fn try_from(dec: Dec) -> Result { + // Fails if the dec negative + let _ = checked!(Dec(I256::maximum()) - dec)?; + + // Division cannot panic as divisor is non-zero + #[allow(clippy::arithmetic_side_effects)] + let raw = dec.0.abs() / Uint::exp10(POS_DECIMAL_PRECISION as usize); + Ok(Amount { raw }) } } @@ -702,140 +765,6 @@ impl TryFrom for u128 { } } -impl Add for Amount { - type Output = Amount; - - fn add(mut self, rhs: Self) -> Self::Output { - self.raw += rhs.raw; - self - } -} - -impl Add for Amount { - type Output = Self; - - fn add(self, rhs: u64) -> Self::Output { - Self { - raw: self.raw + Uint::from(rhs), - } - } -} - -impl Mul for Amount { - type Output = Amount; - - fn mul(mut self, rhs: u64) -> Self::Output { - self.raw *= rhs; - self - } -} - -impl Mul for u64 { - type Output = Amount; - - fn mul(self, mut rhs: Amount) -> Self::Output { - rhs.raw *= self; - rhs - } -} - -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<(u128, u128)> for Amount { - type Output = (Amount, Amount); - - fn mul(mut self, rhs: (u128, u128)) -> Self::Output { - let amt = Amount { - raw: (self.raw / rhs.1) * Uint::from(rhs.0), - }; - self.raw %= rhs.1; - (amt, self) - } -} - -/// A combination of Euclidean division and fractions: -/// 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 amt = Amount { - raw: (self.raw / rhs.1) * rhs.0, - }; - self.raw %= rhs.1; - (amt, self) - } -} - -/// A combination of Euclidean division and fractions: -/// x*(a,b) = (a*(x//b), x%b). -impl Mul<(u32, u32)> for Amount { - type Output = (Amount, Amount); - - fn mul(mut self, rhs: (u32, u32)) -> Self::Output { - let amt = Amount { - raw: (self.raw / rhs.1) * rhs.0, - }; - self.raw %= rhs.1; - (amt, self) - } -} - -impl Div for Amount { - type Output = Self; - - fn div(self, rhs: u64) -> Self::Output { - Self { - raw: self.raw / Uint::from(rhs), - } - } -} - -impl AddAssign for Amount { - fn add_assign(&mut self, rhs: Self) { - self.raw += rhs.raw - } -} - -impl Sub for Amount { - type Output = Amount; - - fn sub(mut self, rhs: Self) -> Self::Output { - self.raw -= rhs.raw; - self - } -} - -impl SubAssign for Amount { - fn sub_assign(&mut self, rhs: Self) { - self.raw -= rhs.raw - } -} - -impl Sum for Amount { - fn sum>(iter: I) -> Self { - iter.fold(Amount::default(), |acc, next| acc + next) - } -} - impl KeySeg for Amount { fn parse(string: String) -> super::storage::Result where @@ -870,7 +799,7 @@ pub enum AmountParseError { "Error decoding token amount, too many decimal places: {0}. Maximum \ {1}" )] - ScaleTooLarge(u32, u8), + ScaleTooLarge(usize, u8), #[error( "Error decoding token amount, the value is not within invalid range." )] @@ -961,8 +890,10 @@ impl MaspDigitPos { /// Get the corresponding u64 word from the input uint256. pub fn denominate_i128(&self, amount: &Change) -> i128 { - let val = amount.abs().0[*self as usize] as i128; + let val = i128::from(amount.abs().0[*self as usize]); if Change::is_negative(amount) { + // Cannot panic as the value is limited to `u64` range + #[allow(clippy::arithmetic_side_effects)] -val } else { val From 7c6e8b89182e9342a7a50810e7828b5671608db6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Fri, 19 Apr 2024 18:43:39 +0200 Subject: [PATCH 08/29] core/storage: sanitize panicking code --- crates/core/src/storage.rs | 261 +++++++++++++++++-------------------- 1 file changed, 118 insertions(+), 143 deletions(-) diff --git a/crates/core/src/storage.rs b/crates/core/src/storage.rs index 5023ff9f4a..908f962967 100644 --- a/crates/core/src/storage.rs +++ b/crates/core/src/storage.rs @@ -3,7 +3,7 @@ use std::collections::VecDeque; use std::fmt::Display; use std::io::{Read, Write}; use std::num::ParseIntError; -use std::ops::{Add, AddAssign, Deref, Div, Mul, Rem, Sub}; +use std::ops::Deref; use std::str::FromStr; use arse_merkle_tree::InternalKey; @@ -168,17 +168,25 @@ impl Display for TxIndex { } } -impl Add for TxIndex { - type Output = TxIndex; +impl From for u32 { + fn from(index: TxIndex) -> Self { + index.0 + } +} - fn add(self, rhs: u32) -> Self::Output { - Self(self.0 + rhs) +impl From for TxIndex { + fn from(value: u32) -> Self { + Self(value) } } -impl From for u32 { - fn from(index: TxIndex) -> Self { - index.0 +impl TxIndex { + /// Checked index addition. + #[must_use = "this returns the result of the operation, without modifying \ + the original"] + pub fn checked_add(self, rhs: impl Into) -> Option { + let TxIndex(rhs) = rhs.into(); + Some(Self(self.0.checked_add(rhs)?)) } } @@ -260,34 +268,6 @@ impl Display for BlockHeight { } } -impl Add for BlockHeight { - type Output = BlockHeight; - - fn add(self, rhs: u64) -> Self::Output { - Self(self.0 + rhs) - } -} - -impl Sub for BlockHeight { - type Output = Self; - - fn sub(self, rhs: u64) -> Self::Output { - Self(self.0 - rhs) - } -} - -impl AddAssign for BlockHeight { - fn add_assign(&mut self, other: Self) { - self.0 += other.0; - } -} - -impl AddAssign for BlockHeight { - fn add_assign(&mut self, other: u64) { - self.0 += other; - } -} - impl From for u64 { fn from(height: BlockHeight) -> Self { height.0 @@ -375,17 +355,32 @@ impl BlockHeight { /// Get the height of the next block pub fn next_height(&self) -> BlockHeight { - BlockHeight(self.0 + 1) + BlockHeight( + self.0 + .checked_add(1) + .expect("Block height must not overflow"), + ) } /// Get the height of the previous block - pub fn prev_height(&self) -> BlockHeight { - BlockHeight(self.0 - 1) + pub fn prev_height(&self) -> Option { + Some(BlockHeight(self.0.checked_sub(1)?)) } - /// Get the height of the previous block if it won't underflow - pub fn checked_prev(&self) -> Option { - Some(BlockHeight(self.0.checked_sub(1)?)) + /// Checked block height addition. + #[must_use = "this returns the result of the operation, without modifying \ + the original"] + pub fn checked_add(self, rhs: impl Into) -> Option { + let BlockHeight(rhs) = rhs.into(); + Some(Self(self.0.checked_add(rhs)?)) + } + + /// Checked block height subtraction. + #[must_use = "this returns the result of the operation, without modifying \ + the original"] + pub fn checked_sub(self, rhs: impl Into) -> Option { + let BlockHeight(rhs) = rhs.into(); + Some(Self(self.0.checked_sub(rhs)?)) } } @@ -564,7 +559,11 @@ impl arse_merkle_tree::Key for StringKey { } original[i] = *byte; tree_key[i] = byte.wrapping_add(1); - length += 1; + // There is no way the bytes.len() > u64::max + #[allow(clippy::arithmetic_side_effects)] + { + length += 1; + } } Ok(Self { original, @@ -1159,20 +1158,19 @@ impl FromStr for Epoch { impl Epoch { /// Change to the next epoch pub fn next(&self) -> Self { - Self(self.0 + 1) + Self(self.0.checked_add(1).expect("Epoch shouldn't overflow")) } - /// Change to the previous epoch. This will underflow if the given epoch is - /// `0`. - pub fn prev(&self) -> Self { - Self(self.0 - 1) + /// Change to the previous epoch. + pub fn prev(&self) -> Option { + Some(Self(self.0.checked_sub(1)?)) } /// Iterate a range of consecutive epochs starting from `self` of a given /// length. Work-around for `Step` implementation pending on stabilization of . pub fn iter_range(self, len: u64) -> impl Iterator + Clone { let start_ix: u64 = self.into(); - let end_ix: u64 = start_ix + len; + let end_ix: u64 = start_ix.checked_add(len).unwrap_or(u64::MAX); (start_ix..end_ix).map(Epoch::from) } @@ -1186,17 +1184,57 @@ impl Epoch { (start_ix..=end_ix).map(Epoch::from) } + /// Checked epoch addition. + #[must_use = "this returns the result of the operation, without modifying \ + the original"] + pub fn checked_add(self, rhs: impl Into) -> Option { + let Epoch(rhs) = rhs.into(); + Some(Self(self.0.checked_add(rhs)?)) + } + + /// Unchecked epoch addition. + /// + /// # Panic + /// + /// Panics on overflow. Care must be taken to only use this with trusted + /// values that are known to be in a limited range (e.g. system parameters + /// but not e.g. transaction variables). + pub fn unchecked_add(self, rhs: impl Into) -> Self { + self.checked_add(rhs) + .expect("Epoch addition shouldn't overflow") + } + /// Checked epoch subtraction. Computes self - rhs, returning None if /// overflow occurred. #[must_use = "this returns the result of the operation, without modifying \ the original"] pub fn checked_sub(self, rhs: impl Into) -> Option { let Epoch(rhs) = rhs.into(); - if rhs > self.0 { - None - } else { - Some(Self(self.0 - rhs)) - } + Some(Self(self.0.checked_sub(rhs)?)) + } + + /// Checked epoch division. + #[must_use = "this returns the result of the operation, without modifying \ + the original"] + pub fn checked_div(self, rhs: impl Into) -> Option { + let Epoch(rhs) = rhs.into(); + Some(Self(self.0.checked_div(rhs)?)) + } + + /// Checked epoch multiplication. + #[must_use = "this returns the result of the operation, without modifying \ + the original"] + pub fn checked_mul(self, rhs: impl Into) -> Option { + let Epoch(rhs) = rhs.into(); + Some(Self(self.0.checked_mul(rhs)?)) + } + + /// Checked epoch integral reminder. + #[must_use = "this returns the result of the operation, without modifying \ + the original"] + pub fn checked_rem(self, rhs: impl Into) -> Option { + let Epoch(rhs) = rhs.into(); + Some(Self(self.0.checked_rem(rhs)?)) } /// Checked epoch subtraction. Computes self - rhs, returning default @@ -1220,94 +1258,6 @@ impl From for u64 { } } -// TODO remove this once it's not being used -impl From for usize { - fn from(epoch: Epoch) -> Self { - epoch.0 as usize - } -} - -impl Add for Epoch { - type Output = Epoch; - - fn add(self, rhs: u64) -> Self::Output { - Self(self.0 + rhs) - } -} - -// TODO remove this once it's not being used -impl Add for Epoch { - type Output = Self; - - fn add(self, rhs: usize) -> Self::Output { - Epoch(self.0 + rhs as u64) - } -} - -impl Sub for Epoch { - type Output = Epoch; - - fn sub(self, rhs: u64) -> Self::Output { - Self(self.0 - rhs) - } -} - -impl Sub for Epoch { - type Output = Self; - - fn sub(self, rhs: Epoch) -> Self::Output { - Epoch(self.0 - rhs.0) - } -} - -impl Mul for Epoch { - type Output = Epoch; - - fn mul(self, rhs: u64) -> Self::Output { - Self(self.0 * rhs) - } -} - -impl Mul for u64 { - type Output = Epoch; - - fn mul(self, rhs: Epoch) -> Self::Output { - Epoch(self * rhs.0) - } -} - -impl Div for Epoch { - type Output = Epoch; - - fn div(self, rhs: u64) -> Self::Output { - Self(self.0 / rhs) - } -} - -impl Rem for Epoch { - type Output = u64; - - fn rem(self, rhs: u64) -> Self::Output { - Self(self.0 % rhs).0 - } -} - -impl Add for Epoch { - type Output = Epoch; - - fn add(self, rhs: Self) -> Self::Output { - Self(self.0 + rhs.0) - } -} - -impl Mul for Epoch { - type Output = Epoch; - - fn mul(self, rhs: Self) -> Self::Output { - Self(self.0 * rhs.0) - } -} - /// Predecessor block epochs #[derive( Clone, @@ -1376,7 +1326,8 @@ impl Epochs { if epoch.0 > self.first_block_heights.len() as u64 { return None; } - self.first_block_heights.get(epoch.0 as usize).copied() + let idx = usize::try_from(epoch.0).ok()?; + self.first_block_heights.get(idx).copied() } /// Return all starting block heights for each successive Epoch. @@ -2020,12 +1971,36 @@ pub mod tests { /// Helpers for testing with storage types. #[cfg(any(test, feature = "testing"))] pub mod testing { + use std::ops::Add; + use proptest::collection; use proptest::prelude::*; use super::*; use crate::address::testing::{arb_address, arb_non_internal_address}; + impl Add for BlockHeight + where + T: Into, + { + type Output = BlockHeight; + + fn add(self, rhs: T) -> Self::Output { + self.checked_add(rhs.into()).unwrap() + } + } + + impl Add for Epoch + where + T: Into, + { + type Output = Epoch; + + fn add(self, rhs: T) -> Self::Output { + self.checked_add(rhs.into()).unwrap() + } + } + prop_compose! { /// Generate an arbitrary epoch pub fn arb_epoch()(epoch: u64) -> Epoch { From c44db4c54fc7433b006aa1461c69c0d92f5ee9da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Fri, 19 Apr 2024 18:44:01 +0200 Subject: [PATCH 09/29] core/dec: sanitize panicking code --- crates/core/src/dec.rs | 324 +++++++++++++++++------------------------ 1 file changed, 137 insertions(+), 187 deletions(-) diff --git a/crates/core/src/dec.rs b/crates/core/src/dec.rs index 485d5226e3..be892b8407 100644 --- a/crates/core/src/dec.rs +++ b/crates/core/src/dec.rs @@ -4,8 +4,6 @@ //! precision. use std::fmt::{Debug, Display, Formatter}; -use std::iter::Sum; -use std::ops::{Add, AddAssign, Div, Mul, Neg, Sub}; use std::str::FromStr; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; @@ -13,11 +11,11 @@ use eyre::eyre; use namada_macros::BorshDeserializer; #[cfg(feature = "migrations")] use namada_migrations::*; -use num_traits::CheckedMul; use serde::{Deserialize, Serialize}; use super::token::NATIVE_MAX_DECIMAL_PLACES; -use crate::token::{Amount, Change}; +use crate::arith::{self, checked}; +use crate::token; use crate::uint::{Uint, I256}; /// The number of Dec places for PoS rational calculations @@ -99,7 +97,7 @@ impl Dec { Some(res) => { let res = I256::try_from(res).ok()?; if is_neg { - Some(Self(-res)) + Some(Self(res.checked_neg()?)) } else { Some(Self(res)) } @@ -121,13 +119,17 @@ impl Dec { /// The representation of 1 pub fn one() -> Self { Self(I256( - Uint::one() * Uint::exp10(POS_DECIMAL_PRECISION as usize), + Uint::one() + .checked_mul(Uint::exp10(usize::from(POS_DECIMAL_PRECISION))) + .expect("Cannot overflow"), )) } /// The representation of 2 pub fn two() -> Self { - Self::one() + Self::one() + Self::one() + .checked_add(Self::one()) + .expect("Cannot overflow") } /// Create a new [`Dec`] using a mantissa and a scale. @@ -136,12 +138,15 @@ impl Dec { None } else { let abs = u64::try_from(mantissa.abs()).ok()?; - match Uint::exp10((POS_DECIMAL_PRECISION - scale) as usize) + // Cannot underflow + #[allow(clippy::arithmetic_side_effects)] + let scale_diff = POS_DECIMAL_PRECISION - scale; + match Uint::exp10((scale_diff) as usize) .checked_mul(Uint::from(abs)) { Some(res) => { if mantissa.is_negative() { - Some(Self(-I256(res))) + Some(Self(I256(res).checked_neg()?)) } else { Some(Self(I256(res))) } @@ -152,11 +157,14 @@ impl Dec { } /// Get the non-negative difference between two [`Dec`]s. - pub fn abs_diff(&self, other: &Self) -> Self { - if self > other { - *self - *other + pub fn abs_diff( + &self, + other: Self, + ) -> std::result::Result { + if self > &other { + checked!(self - other) } else { - *other - *self + checked!(other - *self) } } @@ -167,7 +175,9 @@ impl Dec { /// Convert the Dec type into a I256 with truncation pub fn to_i256(&self) -> I256 { - self.0 / Uint::exp10(POS_DECIMAL_PRECISION as usize) + self.0 + .checked_div(I256(Uint::exp10(usize::from(POS_DECIMAL_PRECISION)))) + .expect("Cannot panic as rhs > 0") } /// Convert the Dec type into a Uint with truncation @@ -175,31 +185,45 @@ impl Dec { if self.is_negative() { None } else { - Some(self.0.abs() / Uint::exp10(POS_DECIMAL_PRECISION as usize)) + Some( + self.0.abs().checked_div(Uint::exp10(usize::from( + POS_DECIMAL_PRECISION, + )))?, + ) } } - /// 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 - } + /// Do subtraction of two [`Dec`]s + pub fn checked_sub(&self, rhs: Self) -> Option { + Some(Self(self.0.checked_sub(rhs.0)?)) } /// Do addition of two [`Dec`]s - pub fn add(&self, other: &Self) -> Self { - Dec(self.0 + other.0) + pub fn checked_add(&self, other: Self) -> Option { + Some(Dec(self.0.checked_add(other.0)?)) } - /// Do multiply two [`Dec`]s. Return `None` if overflow. + /// Checked multiplication. Return `None` if overflow. /// This methods will overflow incorrectly if both arguments are greater /// than 128bit. - pub fn checked_mul(&self, other: &Self) -> Option { - let result = self.0.checked_mul(&other.0)?; - Some(Dec(result / Uint::exp10(POS_DECIMAL_PRECISION as usize))) + pub fn checked_mul(&self, other: impl Into) -> Option { + let other: Self = other.into(); + let result = self.0.checked_mul(other.0)?; + let inner = result.checked_div(I256(Uint::exp10(usize::from( + POS_DECIMAL_PRECISION, + ))))?; + Some(Dec(inner)) + } + + /// Checked division + pub fn checked_div(self, rhs: impl Into) -> Option { + let rhs: Self = rhs.into(); + self.trunc_div(&rhs) + } + + /// Checked negation + pub fn checked_neg(&self) -> Option { + Some(Self(self.0.checked_neg()?)) } /// Return if the [`Dec`] is negative @@ -208,15 +232,20 @@ impl Dec { } /// Return the integer value of a [`Dec`] by rounding up. - pub fn ceil(&self) -> I256 { + pub fn ceil(&self) -> Option { if self.0.is_negative() { - self.to_i256() + Some(self.to_i256()) } else { let floor = self.to_i256(); - if (*self - Dec(floor)).is_zero() { - floor + if self + .checked_sub(Dec(floor)) + .as_ref() + .map(Dec::is_zero) + .unwrap_or_default() + { + Some(floor) } else { - floor + I256::one() + floor.checked_add(I256::one()) } } } @@ -250,16 +279,22 @@ impl FromStr for Dec { let trimmed = small .trim_end_matches('0') .chars() - .take(POS_DECIMAL_PRECISION as usize) + .take(usize::from(POS_DECIMAL_PRECISION)) .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()) + // `trimmed.len` <= `POS_DECIMAL_PRECISION` + #[allow(clippy::arithmetic_side_effects)] + let len_diff = usize::from(POS_DECIMAL_PRECISION) - trimmed.len(); + Uint::from_str_radix(&trimmed, 10) + .map_err(|e| { + eyre!("Could not parse .{} as decimals: {}", small, e) + })? + .checked_mul(Uint::exp10(len_diff)) + .ok_or_else(|| eyre!("Decimal part overflow"))? }; - let int_part = Uint::exp10(POS_DECIMAL_PRECISION as usize) + let int_part = Uint::exp10(usize::from(POS_DECIMAL_PRECISION)) .checked_mul(num_large) .ok_or_else(|| { eyre!( @@ -267,10 +302,15 @@ impl FromStr for Dec { num_large ) })?; - let inner = I256::try_from(int_part + decimal_part) + let inner = + I256::try_from(int_part.checked_add(decimal_part).ok_or_else( + || eyre!("Failed to add integral and decimal part"), + )?) .map_err(|e| eyre!("Could not convert Uint to I256: {}", e))?; if is_neg { - Ok(Dec(-inner)) + Ok(Dec(inner + .checked_neg() + .ok_or_else(|| eyre!("Failed to negate"))?)) } else { Ok(Dec(inner)) } @@ -285,17 +325,18 @@ impl TryFrom for Dec { } } -impl From for Dec { - fn from(amt: Amount) -> Self { - 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(amt: token::Amount) -> std::result::Result { + let raw = I256::try_from(amt.raw_amount()) + .map_err(|e| eyre!("Invalid raw amount: {e}"))?; + let denom = I256(Uint::exp10( + (POS_DECIMAL_PRECISION - NATIVE_MAX_DECIMAL_PLACES) as usize, + )); + let inner = checked!(raw * denom).map_err(|e| eyre!("Arith: {e}"))?; + + Ok(Self(inner)) } } @@ -304,14 +345,23 @@ impl TryFrom for Dec { 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))) + .map_err(|e| eyre!("Could not convert Uint to I256: {e}"))?; + let inner = i256 + .checked_mul(I256(Uint::exp10(usize::from(POS_DECIMAL_PRECISION)))) + .ok_or_else(|| eyre!("Overflow"))?; + Ok(Self(inner)) } } impl From for Dec { fn from(num: u64) -> Self { - Self(I256::from(num) * Uint::exp10(POS_DECIMAL_PRECISION as usize)) + Self( + I256::from(num) + .checked_mul(I256(Uint::exp10(usize::from( + POS_DECIMAL_PRECISION, + )))) + .expect("Cannot overflow as the value is in `u64` range"), + ) } } @@ -323,24 +373,31 @@ impl From for Dec { impl From for Dec { fn from(num: i128) -> Self { - Self(I256::from(num) * Uint::exp10(POS_DECIMAL_PRECISION as usize)) + Self( + I256::from(num) + .checked_mul(I256(Uint::exp10(usize::from( + POS_DECIMAL_PRECISION, + )))) + .expect("Cannot overflow as the value is in `i128` range"), + ) } } impl From for Dec { fn from(num: i32) -> Self { - Self::from(num as i128) + Self::from(i128::from(num)) } } impl TryFrom for Dec { - type Error = Box; + type Error = arith::Error; fn try_from(num: u128) -> std::result::Result { - Ok(Self( - I256::try_from(Uint::from(num))? - * Uint::exp10(POS_DECIMAL_PRECISION as usize), - )) + let denom = I256(Uint::exp10(usize::from(POS_DECIMAL_PRECISION))); + let num = + I256::try_from(Uint::from(num)).expect("u128 must fit in a Dec"); + let inner = checked!(num * denom)?; + Ok(Self(inner)) } } @@ -352,10 +409,13 @@ impl TryFrom for i128 { } } -// Is error handling needed for this? -impl From for Dec { - fn from(num: I256) -> Self { - Self(num * Uint::exp10(POS_DECIMAL_PRECISION as usize)) +impl TryFrom for Dec { + type Error = arith::Error; + + fn try_from(num: I256) -> std::result::Result { + let denom = I256(Uint::exp10(usize::from(POS_DECIMAL_PRECISION))); + let inner = checked!(num * denom)?; + Ok(Self(inner)) } } @@ -365,131 +425,21 @@ impl From for String { } } -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 + I256::from(rhs)) - } -} - -impl AddAssign for Dec { - fn add_assign(&mut self, rhs: Dec) { - *self = *self + rhs; - } -} - -impl Sum for Dec { - fn sum>(iter: I) -> Self { - iter.fold(Dec::default(), |acc, next| acc + next) - } -} - -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 = Dec; - - fn mul(self, rhs: u64) -> Self::Output { - Self(self.0 * Uint::from(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 { - if !self.is_negative() { - (rhs * self.0.abs()) / 10u64.pow(POS_DECIMAL_PRECISION as u32) - } else { - panic!("Dec is negative and cannot produce a valid Amount output"); - } - } -} - -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 - } -} - -// TODO: is some checked arithmetic needed here to prevent overflows? -impl Mul for Dec { - type Output = Self; - - fn mul(self, rhs: Dec) -> Self::Output { - let prod = self.0 * rhs.0; - Self(prod / Uint::exp10(POS_DECIMAL_PRECISION as usize)) - } -} - -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.trunc_div(&rhs).unwrap() - } -} - -impl Div for Dec { - type Output = Self; - - fn div(self, rhs: u64) -> Self::Output { - Self(self.0 / Uint::from(rhs)) - } -} - -impl Neg for Dec { - type Output = Self; - - fn neg(self) -> Self::Output { - Self(self.0.neg()) - } -} - impl Display for Dec { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 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; + if string.len() > usize::from(POS_DECIMAL_PRECISION) { + // Cannot underflow as we checked above + #[allow(clippy::arithmetic_side_effects)] + let idx = string.len() - usize::from(POS_DECIMAL_PRECISION); string.insert(idx, '.'); } else { let mut str_pre = "0.".to_string(); - for _ in 0..(POS_DECIMAL_PRECISION as usize - string.len()) { + // Cannot underflow as we checked above + #[allow(clippy::arithmetic_side_effects)] + let end = usize::from(POS_DECIMAL_PRECISION) - string.len(); + for _ in 0..end { str_pre.push('0'); } str_pre.push_str(string.as_str()); @@ -643,17 +593,17 @@ mod test_dec { assert_eq!(dec1 / dec2, exp_quot); } - /// Test the `Dec` and `Amount` interplay + /// Test the `Dec` and `token::Amount` interplay #[test] fn test_dec_and_amount() { - let amt = Amount::from(1018u64); + let amt = token::Amount::from(1018u64); let dec = Dec::from_str("2.76").unwrap(); debug_assert_eq!( Dec::from(amt), Dec::new(1018, 6).expect("Test failed") ); - debug_assert_eq!(dec * amt, Amount::from(2809u64)); + debug_assert_eq!(dec * amt, token::Amount::from(2809u64)); let chg = -amt.change(); debug_assert_eq!(dec * chg, Change::from(-2809i64)); From 0ebb891d219f2d6b897d930c1c65ad0497a5a525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Fri, 19 Apr 2024 18:44:08 +0200 Subject: [PATCH 10/29] core: clippy fixes --- crates/core/src/account.rs | 12 ++++++-- crates/core/src/bytes.rs | 2 +- crates/core/src/chain.rs | 21 ++++++++++++-- crates/core/src/eth_bridge_pool.rs | 2 +- crates/core/src/ethereum_events.rs | 17 ------------ crates/core/src/ethereum_structs.rs | 43 +++++++++++++++++++---------- crates/core/src/keccak.rs | 2 +- crates/core/src/key/common.rs | 2 +- crates/core/src/key/secp256k1.rs | 12 ++++++-- crates/core/src/masp.rs | 2 +- crates/core/src/time.rs | 9 +++++- 11 files changed, 80 insertions(+), 44 deletions(-) diff --git a/crates/core/src/account.rs b/crates/core/src/account.rs index 66fb9094fa..879e9520c1 100644 --- a/crates/core/src/account.rs +++ b/crates/core/src/account.rs @@ -31,6 +31,11 @@ pub struct AccountPublicKeysMap { } impl FromIterator for AccountPublicKeysMap { + /// Creates a value from an iterator. + /// + /// # Panics + /// + /// Panics when given more than 255 keys. fn from_iter>(iter: T) -> Self { let mut pk_to_idx = HashMap::new(); let mut idx_to_pk = HashMap::new(); @@ -41,8 +46,11 @@ impl FromIterator for AccountPublicKeysMap { "Only up to 255 signers are allowed in a multisig account" ); } - pk_to_idx.insert(public_key.to_owned(), index as u8); - idx_to_pk.insert(index as u8, public_key.to_owned()); + #[allow(clippy::cast_possible_truncation)] + let ix = u8::try_from(index).unwrap(); + + pk_to_idx.insert(public_key.to_owned(), ix); + idx_to_pk.insert(ix, public_key.to_owned()); } Self { diff --git a/crates/core/src/bytes.rs b/crates/core/src/bytes.rs index e2d9c27058..e157c80428 100644 --- a/crates/core/src/bytes.rs +++ b/crates/core/src/bytes.rs @@ -8,7 +8,7 @@ pub struct ByteBuf<'a>(pub &'a [u8]); impl<'a> std::fmt::LowerHex for ByteBuf<'a> { fn fmt( &self, - f: &mut std::fmt::Formatter, + f: &mut std::fmt::Formatter<'_>, ) -> std::result::Result<(), std::fmt::Error> { for byte in self.0 { f.write_fmt(format_args!("{:02x}", byte))?; diff --git a/crates/core/src/chain.rs b/crates/core/src/chain.rs index 511c23d785..df8113ba46 100644 --- a/crates/core/src/chain.rs +++ b/crates/core/src/chain.rs @@ -58,7 +58,7 @@ impl<'de> Deserialize<'de> for ProposalBytes { impl<'de> serde::de::Visitor<'de> for Visitor { type Value = ProposalBytes; - fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "a u64 in the range 1 - {}", @@ -84,7 +84,13 @@ impl<'de> Deserialize<'de> for ProposalBytes { where E: serde::de::Error, { - ProposalBytes::new(size as u64).ok_or_else(|| { + let max_bytes = u64::try_from(size).map_err(|_e| { + serde::de::Error::invalid_value( + serde::de::Unexpected::Signed(size), + &self, + ) + })?; + ProposalBytes::new(max_bytes).ok_or_else(|| { serde::de::Error::invalid_value( serde::de::Unexpected::Signed(size), &self, @@ -212,6 +218,8 @@ impl ChainId { let mut hasher = Sha256::new(); hasher.update(genesis_bytes); // less `1` for chain ID prefix separator char + // Cannot underflow as the `prefix.len` is checked + #[allow(clippy::arithmetic_side_effects)] let width = CHAIN_ID_LENGTH - 1 - prefix.len(); // lowercase hex of the first `width` chars of the hash let hash = format!("{:.width$x}", hasher.finalize(), width = width,); @@ -228,9 +236,16 @@ impl ChainId { let mut errors = vec![]; match self.0.rsplit_once(CHAIN_ID_PREFIX_SEP) { Some((prefix, hash)) => { + if prefix.len() > CHAIN_ID_PREFIX_MAX_LEN { + errors.push(ChainIdValidationError::Prefix( + ChainIdPrefixParseError::UnexpectedLen(prefix.len()), + )) + } let mut hasher = Sha256::new(); hasher.update(genesis_bytes); // less `1` for chain ID prefix separator char + // Cannot underflow as the `prefix.len` is checked + #[allow(clippy::arithmetic_side_effects)] let width = CHAIN_ID_LENGTH - 1 - prefix.len(); // lowercase hex of the first `width` chars of the hash let expected_hash = @@ -259,6 +274,8 @@ pub enum ChainIdValidationError { MissingSeparator, #[error("The chain ID hash is not valid, expected {0}, got {1}")] InvalidHash(String, String), + #[error("Invalid prefix {0}")] + Prefix(ChainIdPrefixParseError), } impl Default for ChainId { diff --git a/crates/core/src/eth_bridge_pool.rs b/crates/core/src/eth_bridge_pool.rs index 704e37253f..69cb0291ad 100644 --- a/crates/core/src/eth_bridge_pool.rs +++ b/crates/core/src/eth_bridge_pool.rs @@ -104,7 +104,7 @@ pub enum TransferToEthereumKind { } impl std::fmt::Display for TransferToEthereumKind { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Erc20 => write!(f, "ERC20"), Self::Nut => write!(f, "NUT"), diff --git a/crates/core/src/ethereum_events.rs b/crates/core/src/ethereum_events.rs index ec6984ac42..1a7a7504e4 100644 --- a/crates/core/src/ethereum_events.rs +++ b/crates/core/src/ethereum_events.rs @@ -2,7 +2,6 @@ use std::cmp::Ordering; use std::fmt::{Display, Formatter}; -use std::ops::{Add, Sub}; use std::str::FromStr; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; @@ -110,22 +109,6 @@ impl From for Uint { } } -impl Add for Uint { - type Output = Self; - - fn add(self, rhs: u64) -> Self::Output { - (ethUint(self.0) + rhs).into() - } -} - -impl Sub for Uint { - type Output = Self; - - fn sub(self, rhs: u64) -> Self::Output { - (ethUint(self.0) - rhs).into() - } -} - /// Representation of address on Ethereum. The inner value is the last 20 bytes /// of the public key that controls the account. #[derive( diff --git a/crates/core/src/ethereum_structs.rs b/crates/core/src/ethereum_structs.rs index 3236dfc95d..6c2c952bd4 100644 --- a/crates/core/src/ethereum_structs.rs +++ b/crates/core/src/ethereum_structs.rs @@ -2,7 +2,7 @@ use std::fmt; use std::io::Read; use std::num::NonZeroU64; -use std::ops::{Add, AddAssign, Deref}; +use std::ops::Deref; use borsh::{BorshDeserialize, BorshSerialize}; pub use ethbridge_structs::*; @@ -98,6 +98,33 @@ impl EthBridgeEvent { #[repr(transparent)] pub struct BlockHeight(Uint256); +impl BlockHeight { + /// Get the next block height. + /// + /// # Panic + /// + /// Panics on overflow. + pub fn next(&self) -> Self { + self.unchecked_add(1_u64) + } + + /// Unchecked epoch addition. + /// + /// # Panic + /// + /// Panics on overflow. Care must be taken to only use this with trusted + /// values that are known to be in a limited range (e.g. system parameters + /// but not e.g. transaction variables). + pub fn unchecked_add(&self, rhs: impl Into) -> Self { + use num_traits::CheckedAdd; + Self( + self.0 + .checked_add(&rhs.into()) + .expect("Block height addition shouldn't overflow"), + ) + } +} + impl fmt::Display for BlockHeight { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) @@ -134,20 +161,6 @@ impl<'a> From<&'a BlockHeight> for &'a Uint256 { } } -impl Add for BlockHeight { - type Output = BlockHeight; - - fn add(self, rhs: Self) -> Self::Output { - Self(self.0 + rhs.0) - } -} - -impl AddAssign for BlockHeight { - fn add_assign(&mut self, rhs: Self) { - self.0 += rhs.0; - } -} - impl Deref for BlockHeight { type Target = Uint256; diff --git a/crates/core/src/keccak.rs b/crates/core/src/keccak.rs index 70ee83799b..8ca44cc718 100644 --- a/crates/core/src/keccak.rs +++ b/crates/core/src/keccak.rs @@ -131,7 +131,7 @@ impl<'de> Deserialize<'de> for KeccakHash { impl<'de> de::Visitor<'de> for KeccakVisitor { type Value = KeccakHash; - fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "a string containing a keccak hash") } diff --git a/crates/core/src/key/common.rs b/crates/core/src/key/common.rs index a88132b010..5a758f4b07 100644 --- a/crates/core/src/key/common.rs +++ b/crates/core/src/key/common.rs @@ -281,7 +281,7 @@ impl RefTo for SecretKey { } impl Display for SecretKey { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", HEXLOWER.encode(&self.serialize_to_vec())) } } diff --git a/crates/core/src/key/secp256k1.rs b/crates/core/src/key/secp256k1.rs index fbc878f05a..b853433882 100644 --- a/crates/core/src/key/secp256k1.rs +++ b/crates/core/src/key/secp256k1.rs @@ -327,6 +327,8 @@ impl Serialize for Signature { // TODO: implement the line below, currently cannot support [u8; 64] // serde::Serialize::serialize(&arr, serializer) + // There is no way the bytes len + 1 will overflow + #[allow(clippy::arithmetic_side_effects)] let mut seq = serializer.serialize_tuple(arr.len() + 1)?; for elem in &arr[..] { seq.serialize_element(elem)?; @@ -346,7 +348,10 @@ impl<'de> Deserialize<'de> for Signature { impl<'de> Visitor<'de> for ByteArrayVisitor { type Value = [u8; SIGNATURE_SIZE]; - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + fn expecting( + &self, + formatter: &mut fmt::Formatter<'_>, + ) -> fmt::Result { formatter.write_str(&format!( "an array of length {}", SIGNATURE_SIZE, @@ -472,7 +477,10 @@ impl Signature { (self.0.s(), v) }; let r = self.0.r(); - (r.to_bytes().into(), s.to_bytes().into(), v + Self::V_FIX) + // Cannot overflow as `v` is 0 or 1 + #[allow(clippy::arithmetic_side_effects)] + let v = v + Self::V_FIX; + (r.to_bytes().into(), s.to_bytes().into(), v) } } diff --git a/crates/core/src/masp.rs b/crates/core/src/masp.rs index cc1e2657d2..04ceb10d46 100644 --- a/crates/core/src/masp.rs +++ b/crates/core/src/masp.rs @@ -159,7 +159,7 @@ impl string_encoding::Format for PaymentAddress { fn to_bytes(&self) -> Vec { let mut bytes = Vec::with_capacity(PAYMENT_ADDRESS_SIZE); - bytes.push(self.is_pinned() as u8); + bytes.push(u8::from(self.is_pinned())); bytes.extend_from_slice(self.0.to_bytes().as_slice()); bytes } diff --git a/crates/core/src/time.rs b/crates/core/src/time.rs index 38f07ccaf6..f322553590 100644 --- a/crates/core/src/time.rs +++ b/crates/core/src/time.rs @@ -15,6 +15,7 @@ use namada_migrations::*; use serde::{Deserialize, Serialize}; /// Check if the given `duration` has passed since the given `start. +#[allow(clippy::arithmetic_side_effects)] pub fn duration_passed( current: DateTimeUtc, start: DateTimeUtc, @@ -182,8 +183,9 @@ impl DateTimeUtc { } /// Returns the DateTimeUtc corresponding to one second in the future + #[allow(clippy::arithmetic_side_effects)] pub fn next_second(&self) -> Self { - *self + DurationSecs(0) + *self + DurationSecs(1) } } @@ -198,6 +200,7 @@ impl FromStr for DateTimeUtc { impl Add for DateTimeUtc { type Output = DateTimeUtc; + #[allow(clippy::arithmetic_side_effects)] fn add(self, duration: DurationSecs) -> Self::Output { let duration_std = std::time::Duration::from_secs(duration.0); let duration_chrono = Duration::from_std(duration_std).expect( @@ -211,6 +214,7 @@ impl Add for DateTimeUtc { impl Add for DateTimeUtc { type Output = DateTimeUtc; + #[allow(clippy::arithmetic_side_effects)] fn add(self, rhs: Duration) -> Self::Output { (self.0 + rhs).into() } @@ -219,6 +223,7 @@ impl Add for DateTimeUtc { impl Sub for DateTimeUtc { type Output = DateTimeUtc; + #[allow(clippy::arithmetic_side_effects)] fn sub(self, rhs: Duration) -> Self::Output { (self.0 - rhs).into() } @@ -283,6 +288,8 @@ impl TryFrom for DateTimeUtc { impl From for prost_types::Timestamp { fn from(dt: DateTimeUtc) -> Self { let seconds = dt.0.timestamp(); + // The cast cannot wrap as the value is at most 1_999_999_999 + #[allow(clippy::cast_possible_wrap)] let nanos = dt.0.timestamp_subsec_nanos() as i32; prost_types::Timestamp { seconds, nanos } } From 5ee4ab5eccfbf431f6249b46bd02737d0bc0ce99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Mon, 22 Apr 2024 13:18:51 +0200 Subject: [PATCH 11/29] test/core: fixes for checked arith --- crates/core/src/dec.rs | 91 ++++++++++++++++++++++-- crates/core/src/ethereum_events.rs | 17 +++++ crates/core/src/keccak.rs | 2 +- crates/core/src/storage.rs | 22 +++++- crates/core/src/token.rs | 107 ++++++++++++++++++++++++++++- crates/core/src/uint.rs | 80 ++++++++++++++++----- crates/core/src/voting_power.rs | 18 +++++ 7 files changed, 309 insertions(+), 28 deletions(-) diff --git a/crates/core/src/dec.rs b/crates/core/src/dec.rs index be892b8407..ae7e9deac7 100644 --- a/crates/core/src/dec.rs +++ b/crates/core/src/dec.rs @@ -466,11 +466,94 @@ impl Debug for Dec { /// Helpers for testing. #[cfg(any(test, feature = "testing"))] +#[allow(clippy::arithmetic_side_effects, clippy::cast_lossless)] pub mod testing { use proptest::prelude::*; use super::*; + impl std::ops::Add for Dec { + type Output = Dec; + + fn add(self, rhs: Dec) -> Self::Output { + self.checked_add(rhs).unwrap() + } + } + + impl std::ops::AddAssign for Dec { + fn add_assign(&mut self, rhs: Self) { + *self = self.checked_add(rhs).unwrap(); + } + } + + impl std::ops::Sub for Dec { + type Output = Dec; + + fn sub(self, rhs: Dec) -> Self::Output { + self.checked_sub(rhs).unwrap() + } + } + + impl std::ops::Mul for Dec + where + T: Into, + { + type Output = Dec; + + fn mul(self, rhs: T) -> Self::Output { + self.checked_mul(rhs.into()).unwrap() + } + } + + impl std::ops::Div for Dec + where + T: Into, + { + type Output = Self; + + fn div(self, rhs: T) -> Self::Output { + self.trunc_div(&rhs.into()).unwrap() + } + } + + impl std::ops::Mul for Dec { + type Output = token::Amount; + + fn mul(self, rhs: token::Amount) -> Self::Output { + if !self.is_negative() { + (rhs * self.0.abs()) / 10u64.pow(POS_DECIMAL_PRECISION as u32) + } else { + panic!( + "Dec is negative and cannot produce a valid Amount output" + ); + } + } + } + + impl std::ops::Mul for Dec { + type Output = token::Change; + + fn mul(self, rhs: token::Change) -> Self::Output { + let tot = rhs * self.0; + let denom = Uint::from(10u64.pow(POS_DECIMAL_PRECISION as u32)); + tot.checked_div(I256(denom)).unwrap() + } + } + + impl std::iter::Sum for Dec { + fn sum>(iter: I) -> Self { + iter.fold(Dec::zero(), |a, b| a + b) + } + } + + impl std::ops::Neg for Dec { + type Output = Dec; + + fn neg(self) -> Self::Output { + self.checked_neg().unwrap() + } + } + /// Generate an arbitrary non-negative `Dec` pub fn arb_non_negative_dec() -> impl Strategy { (any::(), 0_u8..POS_DECIMAL_PRECISION).prop_map( @@ -600,13 +683,13 @@ mod test_dec { let dec = Dec::from_str("2.76").unwrap(); debug_assert_eq!( - Dec::from(amt), + Dec::try_from(amt).unwrap(), Dec::new(1018, 6).expect("Test failed") ); debug_assert_eq!(dec * amt, token::Amount::from(2809u64)); let chg = -amt.change(); - debug_assert_eq!(dec * chg, Change::from(-2809i64)); + debug_assert_eq!(dec * chg, token::Change::from(-2809i64)); } #[test] @@ -696,12 +779,12 @@ mod test_dec { fn test_ceiling() { let neg = Dec::from_str("-2.4").expect("Test failed"); assert_eq!( - neg.ceil(), + neg.ceil().unwrap(), Dec::from_str("-2").expect("Test failed").to_i256() ); let pos = Dec::from_str("2.4").expect("Test failed"); assert_eq!( - pos.ceil(), + pos.ceil().unwrap(), Dec::from_str("3").expect("Test failed").to_i256() ); } diff --git a/crates/core/src/ethereum_events.rs b/crates/core/src/ethereum_events.rs index 1a7a7504e4..3fe455494b 100644 --- a/crates/core/src/ethereum_events.rs +++ b/crates/core/src/ethereum_events.rs @@ -435,6 +435,7 @@ pub mod tests { } #[allow(missing_docs)] +#[allow(clippy::arithmetic_side_effects)] /// Test helpers #[cfg(any(test, feature = "testing", feature = "benches"))] pub mod testing { @@ -456,6 +457,22 @@ pub mod testing { 206, 54, 6, 235, 72, ]); + impl std::ops::Add for Uint { + type Output = Self; + + fn add(self, rhs: u64) -> Self::Output { + (ethUint(self.0) + rhs).into() + } + } + + impl std::ops::Sub for Uint { + type Output = Self; + + fn sub(self, rhs: u64) -> Self::Output { + (ethUint(self.0) - rhs).into() + } + } + pub fn arbitrary_eth_address() -> EthAddress { DAI_ERC20_ETH_ADDRESS } diff --git a/crates/core/src/keccak.rs b/crates/core/src/keccak.rs index 8ca44cc718..970ba3f868 100644 --- a/crates/core/src/keccak.rs +++ b/crates/core/src/keccak.rs @@ -175,7 +175,7 @@ mod tests { let mut hash = KeccakHash([0; 32]); for i in 0..32 { - hash.0[i] = i as u8; + hash.0[i] = u8::try_from(i).unwrap(); } let serialized = serde_json::to_string(&hash).unwrap(); diff --git a/crates/core/src/storage.rs b/crates/core/src/storage.rs index 908f962967..7e31c82b8c 100644 --- a/crates/core/src/storage.rs +++ b/crates/core/src/storage.rs @@ -1971,7 +1971,7 @@ pub mod tests { /// Helpers for testing with storage types. #[cfg(any(test, feature = "testing"))] pub mod testing { - use std::ops::Add; + use std::ops::{Add, AddAssign, Sub}; use proptest::collection; use proptest::prelude::*; @@ -1990,6 +1990,15 @@ pub mod testing { } } + impl AddAssign for BlockHeight + where + T: Into, + { + fn add_assign(&mut self, rhs: T) { + *self = self.checked_add(rhs.into()).unwrap() + } + } + impl Add for Epoch where T: Into, @@ -2001,6 +2010,17 @@ pub mod testing { } } + impl Sub for Epoch + where + T: Into, + { + type Output = Epoch; + + fn sub(self, rhs: T) -> Self::Output { + self.checked_sub(rhs.into()).unwrap() + } + } + prop_compose! { /// Generate an arbitrary epoch pub fn arb_epoch()(epoch: u64) -> Epoch { diff --git a/crates/core/src/token.rs b/crates/core/src/token.rs index d6b80aaf12..a48d20b2fd 100644 --- a/crates/core/src/token.rs +++ b/crates/core/src/token.rs @@ -954,6 +954,7 @@ pub enum AmountError { #[cfg(any(test, feature = "testing"))] /// Testing helpers and strategies for tokens +#[allow(clippy::arithmetic_side_effects)] pub mod testing { use proptest::option; use proptest::prelude::*; @@ -963,6 +964,78 @@ pub mod testing { arb_established_address, arb_non_internal_address, }; + impl std::ops::Add for Amount { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + self.checked_add(rhs).unwrap() + } + } + + impl std::ops::AddAssign for Amount { + fn add_assign(&mut self, rhs: Self) { + *self = self.checked_add(rhs).unwrap(); + } + } + + impl std::ops::Sub for Amount { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + self.checked_sub(rhs).unwrap() + } + } + + impl std::ops::SubAssign for Amount { + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } + } + + impl std::ops::Mul for Amount + where + T: Into, + { + type Output = Amount; + + fn mul(self, rhs: T) -> Self::Output { + self.checked_mul(rhs.into()).unwrap() + } + } + + impl std::ops::Mul for u64 { + type Output = Amount; + + fn mul(self, rhs: Amount) -> Self::Output { + rhs * self + } + } + + impl std::ops::Mul for Amount { + type Output = Amount; + + fn mul(mut self, rhs: Uint) -> Self::Output { + self.raw *= rhs; + self + } + } + + impl std::ops::Div for Amount { + type Output = Self; + + fn div(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(), |a, b| a + b) + } + } + prop_compose! { /// Generate an arbitrary denomination pub fn arb_denomination()(denom in 0u8..) -> Denomination { @@ -1021,6 +1094,8 @@ pub mod testing { #[cfg(test)] mod tests { + use assert_matches::assert_matches; + use super::*; #[test] @@ -1202,9 +1277,35 @@ mod tests { let two = Amount::from(2); let three = Amount::from(3); let dec = Dec::from_str("0.34").unwrap(); - assert_eq!(one.mul_ceil(dec), one); - assert_eq!(two.mul_ceil(dec), one); - assert_eq!(three.mul_ceil(dec), two); + assert_eq!(one.mul_ceil(dec).unwrap(), one); + assert_eq!(two.mul_ceil(dec).unwrap(), one); + assert_eq!(three.mul_ceil(dec).unwrap(), two); + + assert_matches!(one.mul_ceil(-dec), Err(_)); + assert_matches!(one.mul_ceil(-Dec::new(1, 12).unwrap()), Err(_)); + assert_matches!( + Amount::native_whole(1).mul_ceil(-Dec::new(1, 12).unwrap()), + Err(_) + ); + } + + #[test] + fn test_token_amount_mul_floor() { + let zero = Amount::zero(); + let one = Amount::from(1); + let two = Amount::from(2); + let three = Amount::from(3); + let dec = Dec::from_str("0.34").unwrap(); + assert_eq!(one.mul_floor(dec).unwrap(), zero); + assert_eq!(two.mul_floor(dec).unwrap(), zero); + assert_eq!(three.mul_floor(dec).unwrap(), one); + + assert_matches!(one.mul_floor(-dec), Err(_)); + assert_matches!(one.mul_floor(-Dec::new(1, 12).unwrap()), Err(_)); + assert_matches!( + Amount::native_whole(1).mul_floor(-Dec::new(1, 12).unwrap()), + Err(_) + ); } #[test] diff --git a/crates/core/src/uint.rs b/crates/core/src/uint.rs index 8c69c2d4b3..9c902c636e 100644 --- a/crates/core/src/uint.rs +++ b/crates/core/src/uint.rs @@ -849,6 +849,53 @@ impl TryFrom for i128 { } } +#[cfg(any(test, feature = "testing"))] +/// Testing helpers +pub mod testing { + use super::*; + + impl Uint { + /// Returns a pair `((self * num) / denom, (self * num) % denom)`. + /// + /// # Panics + /// + /// Panics if `denom` is zero. + pub fn mul_div(&self, num: Self, denom: Self) -> (Self, Self) { + self.checked_mul_div(num, denom).unwrap() + } + } + + impl std::ops::AddAssign for I256 { + fn add_assign(&mut self, rhs: Self) { + *self = self.checked_add(rhs).unwrap(); + } + } + + impl std::ops::Sub for I256 { + type Output = Self; + + fn sub(self, rhs: I256) -> Self::Output { + self.checked_sub(rhs).unwrap() + } + } + + impl std::ops::Mul for I256 { + type Output = Self; + + fn mul(self, rhs: I256) -> Self::Output { + self.checked_mul(rhs).unwrap() + } + } + + impl std::ops::Neg for I256 { + type Output = Self; + + fn neg(self) -> Self::Output { + self.checked_neg().unwrap() + } + } +} + #[cfg(test)] mod test_uint { use std::str::FromStr; @@ -892,19 +939,13 @@ mod test_uint { ); } - /// Test that adding one to the max signed - /// value gives zero. + /// Test that checked add and sub stays below max signed value #[test] fn test_max_signed_value() { 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!( - overflow, - I256::try_from(Uint::zero()).expect("Test failed") - ); - assert!(signed.checked_add(&one).is_none()); - assert!((-signed).checked_sub(&one).is_none()); + assert!(signed.checked_add(one).is_none()); + assert!((-signed).checked_sub(one).is_none()); } /// Sanity on our constants and that the minus zero representation @@ -918,7 +959,7 @@ mod test_uint { assert_eq!(larger, MINUS_ZERO); assert!(I256::try_from(MINUS_ZERO).is_err()); let zero = Uint::zero(); - assert_eq!(zero, zero.negate()); + assert_eq!(zero, zero.negate().0); } /// Test that we correctly reserve the right bit for indicating the @@ -995,8 +1036,8 @@ mod test_uint { /// 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(); + let this = token::Amount::from_uint(1, 0).unwrap().change(); + let that = token::Amount::native_whole(1000).change(); assert!(this <= that); assert!(-this <= that); assert!(-this >= -that); @@ -1023,15 +1064,16 @@ mod test_uint { let one = I256::from(1); let two = I256::from(2); let dec = Dec::from_str("0.25").unwrap(); - assert_eq!(one.mul_ceil(dec), one); - assert_eq!(two.mul_ceil(dec), one); - assert_eq!(I256::from(4).mul_ceil(dec), one); - assert_eq!(I256::from(5).mul_ceil(dec), two); + let neg_dec = dec.checked_neg().unwrap(); + assert_eq!(one.mul_ceil(dec).unwrap(), one); + assert_eq!(two.mul_ceil(dec).unwrap(), one); + assert_eq!(I256::from(4).mul_ceil(dec).unwrap(), one); + assert_eq!(I256::from(5).mul_ceil(dec).unwrap(), two); - assert_eq!((-one).mul_ceil(-dec), one); + assert_eq!((-one).mul_ceil(neg_dec).unwrap(), one); - assert_eq!((-one).mul_ceil(dec), I256::zero()); - assert_eq!(one.mul_ceil(-dec), I256::zero()); + assert_eq!((-one).mul_ceil(dec).unwrap(), I256::zero()); + assert_eq!(one.mul_ceil(neg_dec).unwrap(), I256::zero()); } #[test] diff --git a/crates/core/src/voting_power.rs b/crates/core/src/voting_power.rs index 0696c8afd8..502c7ba866 100644 --- a/crates/core/src/voting_power.rs +++ b/crates/core/src/voting_power.rs @@ -330,6 +330,24 @@ impl<'de> Deserialize<'de> for FractionalVotingPower { } } +/// Helpers for testing with storage types. +#[cfg(any(test, feature = "testing"))] +#[allow(clippy::arithmetic_side_effects)] +pub mod testing { + use super::*; + use crate::token; + + impl Mul for FractionalVotingPower { + type Output = token::Amount; + + fn mul(self, rhs: token::Amount) -> Self::Output { + let whole: Uint = rhs.into(); + let fraction = (self.0 * whole).to_integer(); + Amount::from_uint(fraction, 0u8).unwrap() + } + } +} + #[cfg(test)] mod tests { use super::*; From e0bb56e967b1f4c2e5c15b3a05acd89d6f3d9b30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 30 Apr 2024 18:05:50 +0200 Subject: [PATCH 12/29] storage: add conv from core::arith::Error for convenience --- crates/storage/src/error.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/storage/src/error.rs b/crates/storage/src/error.rs index d0c81122ed..7d54148291 100644 --- a/crates/storage/src/error.rs +++ b/crates/storage/src/error.rs @@ -1,6 +1,7 @@ //! Storage API error type, extensible with custom user errors and static string //! messages. +use namada_core::arith; use thiserror::Error; #[allow(missing_docs)] @@ -16,6 +17,12 @@ pub enum Error { CustomWithMessage(&'static str, CustomError), } +impl From for Error { + fn from(value: arith::Error) -> Self { + Error::new(value) + } +} + /// Result of a storage API call. pub type Result = std::result::Result; From 2594f71d254febb0dc5416e7203e076a585e2b37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 30 Apr 2024 19:46:45 +0200 Subject: [PATCH 13/29] storage/collections/lazy_map: add try_update --- crates/storage/src/collections/lazy_map.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/storage/src/collections/lazy_map.rs b/crates/storage/src/collections/lazy_map.rs index c1153910e0..5a4f3bf5c3 100644 --- a/crates/storage/src/collections/lazy_map.rs +++ b/crates/storage/src/collections/lazy_map.rs @@ -449,6 +449,21 @@ where Ok(()) } + /// Try update a value at the given key with the given function that may + /// fail. If no existing value exists, the closure's argument will be + /// `None`. + pub fn try_update(&self, storage: &mut S, key: K, f: F) -> Result<()> + where + S: StorageWrite + StorageRead, + F: FnOnce(Option) -> Result, + { + let data_key = self.get_data_key(&key); + let current = Self::read_key_val(storage, &data_key)?; + let new = f(current)?; + Self::write_key_val(storage, &data_key, new)?; + Ok(()) + } + /// Returns whether the map contains a key with a value. pub fn contains(&self, storage: &S, key: &K) -> Result where From 3ca57e2ae45cf988923cb2ca78728c4015eebff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Mon, 22 Apr 2024 17:28:20 +0200 Subject: [PATCH 14/29] controller: replacing panicking code --- Cargo.lock | 2 ++ crates/controller/Cargo.toml | 3 ++ crates/controller/src/lib.rs | 64 ++++++++++++++++++++++-------------- 3 files changed, 45 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3431e0340a..8837992bbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4506,6 +4506,8 @@ name = "namada_controller" version = "0.34.0" dependencies = [ "namada_core", + "smooth-operator", + "thiserror", ] [[package]] diff --git a/crates/controller/Cargo.toml b/crates/controller/Cargo.toml index 925766a900..1ac3656d77 100644 --- a/crates/controller/Cargo.toml +++ b/crates/controller/Cargo.toml @@ -16,3 +16,6 @@ version.workspace = true [dependencies] namada_core = { path = "../core" } + +smooth-operator.workspace = true +thiserror.workspace = true diff --git a/crates/controller/src/lib.rs b/crates/controller/src/lib.rs index 4f752ac16e..5b2686ce7d 100644 --- a/crates/controller/src/lib.rs +++ b/crates/controller/src/lib.rs @@ -1,5 +1,7 @@ +use namada_core::arith::{self, checked}; use namada_core::dec::Dec; use namada_core::uint::Uint; +use thiserror::Error; #[derive(Clone, Debug)] pub struct PDController { @@ -13,6 +15,18 @@ pub struct PDController { last_metric: Dec, } +#[derive(Error, Debug)] +pub enum Error { + #[error("Arithmetic {0}")] + Arith(#[from] arith::Error), + #[error("Decimal {0}")] + Dec(#[from] namada_core::dec::Error), + #[error("Max inflation overflow")] + MaxInflationOverflow, + #[error("Inflation amount overflow")] + InflationOverflow, +} + impl PDController { #[allow(clippy::too_many_arguments)] pub fn new( @@ -41,54 +55,56 @@ impl PDController { &self, control_coeff: Dec, current_metric: Dec, - ) -> Uint { - let control = self.compute_control(control_coeff, current_metric); + ) -> Result { + let control = self.compute_control(control_coeff, current_metric)?; self.compute_inflation_aux(control) } - pub fn get_total_native_dec(&self) -> Dec { - Dec::try_from(self.total_native_amount) - .expect("Should not fail to convert Uint to Dec") + pub fn get_total_native_dec(&self) -> Result { + Dec::try_from(self.total_native_amount).map_err(Into::into) } pub fn get_epochs_per_year(&self) -> u64 { self.epochs_per_year } - fn get_max_inflation(&self) -> Uint { - let total_native = self.get_total_native_dec(); + fn get_max_inflation(&self) -> Result { + let total_native = self.get_total_native_dec()?; let epochs_py: Dec = self.epochs_per_year.into(); - - let max_inflation = total_native * self.max_reward_rate / epochs_py; - max_inflation - .to_uint() - .expect("Should not fail to convert Dec to Uint") + let max_inflation = + checked!(total_native * self.max_reward_rate / epochs_py)?; + max_inflation.to_uint().ok_or(Error::MaxInflationOverflow) } // TODO: could possibly use I256 instead of Dec here (need to account for // negative vals) - fn compute_inflation_aux(&self, control: Dec) -> Uint { - let last_inflation_amount = Dec::try_from(self.last_inflation_amount) - .expect("Should not fail to convert Uint to Dec"); - let new_inflation_amount = last_inflation_amount + control; + fn compute_inflation_aux(&self, control: Dec) -> Result { + let last_inflation_amount = Dec::try_from(self.last_inflation_amount)?; + let new_inflation_amount = checked!(last_inflation_amount + control)?; let new_inflation_amount = if new_inflation_amount.is_negative() { Uint::zero() } else { new_inflation_amount .to_uint() - .expect("Should not fail to convert Dec to Uint") + .ok_or(Error::InflationOverflow)? }; - let max_inflation = self.get_max_inflation(); - std::cmp::min(new_inflation_amount, max_inflation) + let max_inflation = self.get_max_inflation()?; + Ok(std::cmp::min(new_inflation_amount, max_inflation)) } // NOTE: This formula is the comactification of all the old intermediate // computations that were done in multiple steps (as in the specs) - fn compute_control(&self, coeff: Dec, current_metric: Dec) -> Dec { - let val = current_metric * (self.d_gain_nom - self.p_gain_nom) - + (self.target_metric * self.p_gain_nom) - - (self.last_metric * self.d_gain_nom); - coeff * val + fn compute_control( + &self, + coeff: Dec, + current_metric: Dec, + ) -> Result { + let val: Dec = checked!( + current_metric * (self.d_gain_nom - self.p_gain_nom) + + (self.target_metric * self.p_gain_nom) + - (self.last_metric * self.d_gain_nom) + )?; + checked!(coeff * val) } } From a5274fe3935e8ffe2f1bd28431d3f847b49bf3d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Mon, 22 Apr 2024 17:40:42 +0200 Subject: [PATCH 15/29] vote_ext: update to non-panicking core API --- crates/vote_ext/src/validator_set_update.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/vote_ext/src/validator_set_update.rs b/crates/vote_ext/src/validator_set_update.rs index 775acb9562..5bc9367bd7 100644 --- a/crates/vote_ext/src/validator_set_update.rs +++ b/crates/vote_ext/src/validator_set_update.rs @@ -203,8 +203,10 @@ pub trait VotingPowersMapExt { fn get_abi_encoded(&self) -> (Vec, Vec) { let sorted = self.get_sorted(); - let total_voting_power: token::Amount = - sorted.iter().map(|&(_, &voting_power)| voting_power).sum(); + let total_voting_power: token::Amount = token::Amount::sum( + sorted.iter().map(|&(_, &voting_power)| voting_power), + ) + .expect("Voting power sum must not overflow"); // split the vec into two portions sorted @@ -219,7 +221,10 @@ pub trait VotingPowersMapExt { "Voting power in map can't be larger than the total \ voting power", ) - .into(); + .try_into() + .expect( + "Must be able to convert to eth bridge voting power", + ); let &EthAddrBook { hot_key_addr, From 0808fb0ecdb7235570aeccd4a0eed1dfbe22aeac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Mon, 22 Apr 2024 17:45:02 +0200 Subject: [PATCH 16/29] trans_token: update to non-panicking core API From 2486f831e0ee3c65de4682f0b7a5a0cfcacf06a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 23 Apr 2024 10:09:30 +0200 Subject: [PATCH 17/29] shielded_token: update to non-panicking core API --- crates/shielded_token/src/conversion.rs | 74 +++++++++++++++++++------ 1 file changed, 56 insertions(+), 18 deletions(-) diff --git a/crates/shielded_token/src/conversion.rs b/crates/shielded_token/src/conversion.rs index 5dbb210110..7c842ffc26 100644 --- a/crates/shielded_token/src/conversion.rs +++ b/crates/shielded_token/src/conversion.rs @@ -50,8 +50,17 @@ pub fn compute_inflation( let metric = Dec::try_from(locked_amount) .expect("Should not fail to convert Uint to Dec"); - let control_coeff = max_reward_rate / controller.get_epochs_per_year(); - controller.compute_inflation(control_coeff, metric) + let control_coeff = max_reward_rate + .checked_div(controller.get_epochs_per_year()) + .expect("Control coefficient overflow"); + + tracing::debug!( + "Shielded token inflation inputs: {controller:#?}, metric: {metric}, \ + coefficient: {control_coeff}" + ); + controller + .compute_inflation(control_coeff, metric) + .expect("Inflation calculation overflow") } /// Compute the precision of MASP rewards for the given token. This function @@ -243,7 +252,7 @@ where use masp_primitives::transaction::components::I128Sum as MaspAmount; use namada_core::masp::encode_asset_type; use namada_core::storage::Epoch; - use namada_storage::ResultExt; + use namada_storage::{Error, ResultExt}; use namada_trans_token::storage_key::balance_key; use namada_trans_token::{MaspDigitPos, NATIVE_MAX_DECIMAL_PLACES}; use rayon::iter::{ @@ -272,7 +281,7 @@ where } }); // The total transparent value of the rewards being distributed - let mut total_reward = Amount::native_whole(0); + let mut total_reward = Amount::zero(); // Construct MASP asset type for rewards. Always deflate and timestamp // reward tokens with the zeroth epoch to minimize the number of convert @@ -319,10 +328,10 @@ where // Reward all tokens according to above reward rates let epoch = storage.get_block_epoch()?; - if epoch == Epoch::default() { - return Ok(()); - } - let prev_epoch = epoch.prev(); + let prev_epoch = match epoch.prev() { + Some(epoch) => epoch, + None => return Ok(()), + }; for token in &masp_reward_keys { let (reward, denom) = calculate_masp_rewards(storage, token)?; masp_reward_denoms.insert(token.clone(), denom); @@ -390,14 +399,28 @@ where if digit == MaspDigitPos::Three { // The reward for each reward.1 units of the current asset // is reward.0 units of the reward token - let native_reward = - addr_bal * (new_normed_inflation, normed_inflation); - total_reward += native_reward - .0 - .checked_add(native_reward.1) - .unwrap_or(Amount::max()) - .checked_sub(addr_bal) - .unwrap_or_default(); + let native_reward = addr_bal + .u128_eucl_div_rem(( + new_normed_inflation, + normed_inflation, + )) + .ok_or_else(|| { + Error::new_const("Three digit reward overflow") + })?; + total_reward = total_reward + .checked_add( + native_reward + .0 + .checked_add(native_reward.1) + .unwrap_or(Amount::max()) + .checked_sub(addr_bal) + .unwrap_or_default(), + ) + .ok_or_else(|| { + Error::new_const( + "Three digit total reward overflow", + ) + })?; // Save the new normed inflation let _ = storage @@ -442,7 +465,20 @@ where if digit == MaspDigitPos::Three { // The reward for each reward.1 units of the current asset // is reward.0 units of the reward token - total_reward += (addr_bal * (reward.0, reward.1)).0; + total_reward = total_reward + .checked_add( + addr_bal + .u128_eucl_div_rem(reward) + .ok_or_else(|| { + Error::new_const( + "Total reward calculation overflow", + ) + })? + .0, + ) + .ok_or_else(|| { + Error::new_const("Total reward overflow") + })?; } } // Add a conversion from the previous asset type @@ -494,7 +530,9 @@ where // is sufficiently backed to redeem rewards let reward_key = balance_key(&native_token, &masp_addr); let addr_bal: Amount = storage.read(&reward_key)?.unwrap_or_default(); - let new_bal = addr_bal + total_reward; + let new_bal = addr_bal + .checked_add(total_reward) + .ok_or_else(|| Error::new_const("Balance with reward overflow"))?; storage.write(&reward_key, new_bal)?; // Try to distribute Merkle tree construction as evenly as possible // across multiple cores From b26749210eb8d5e3a6c449bc7d3e0d2e1140bd0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 25 Apr 2024 16:18:32 +0200 Subject: [PATCH 18/29] state: update to non-panicking core API --- crates/state/src/in_memory.rs | 4 +++- crates/state/src/wl_state.rs | 38 +++++++++++++++++------------------ 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/crates/state/src/in_memory.rs b/crates/state/src/in_memory.rs index 3ab184bb64..1487d879a1 100644 --- a/crates/state/src/in_memory.rs +++ b/crates/state/src/in_memory.rs @@ -223,7 +223,9 @@ where min_num_of_blocks, min_duration, } = parameters.epoch_duration; - self.next_epoch_min_start_height = initial_height + min_num_of_blocks; + self.next_epoch_min_start_height = initial_height + .checked_add(min_num_of_blocks) + .expect("Next epoch min block height shouldn't overflow"); self.next_epoch_min_start_time = genesis_time + min_duration; self.block.pred_epochs = Epochs { first_block_heights: vec![initial_height], diff --git a/crates/state/src/wl_state.rs b/crates/state/src/wl_state.rs index d40481ffcf..dc3dc1ed4a 100644 --- a/crates/state/src/wl_state.rs +++ b/crates/state/src/wl_state.rs @@ -162,8 +162,9 @@ where min_num_of_blocks, min_duration, } = parameters.epoch_duration; - self.in_mem.next_epoch_min_start_height = - height + min_num_of_blocks; + self.in_mem.next_epoch_min_start_height = height + .checked_add(min_num_of_blocks) + .expect("Next epoch min block height shouldn't overflow"); self.in_mem.next_epoch_min_start_time = time + min_duration; self.in_mem.block.pred_epochs.new_epoch(height); @@ -299,16 +300,11 @@ where &mut self, batch: &mut D::WriteBatch, ) -> Result<()> { - if self.in_mem.block.epoch.0 == 0 { - return Ok(()); - } // Prune non-provable stores at the previous epoch - for st in StoreType::iter_non_provable() { - self.0.db.prune_merkle_tree_store( - batch, - st, - self.in_mem.block.epoch.prev(), - )?; + if let Some(prev_epoch) = self.in_mem.block.epoch.prev() { + for st in StoreType::iter_non_provable() { + self.0.db.prune_merkle_tree_store(batch, st, prev_epoch)?; + } } // Prune provable stores let oldest_epoch = self.in_mem.get_oldest_epoch(); @@ -320,7 +316,7 @@ where self.db.prune_merkle_tree_store( batch, st, - oldest_epoch.prev(), + oldest_epoch.prev().unwrap(), )?; } @@ -330,7 +326,7 @@ where None => return Ok(()), }; while oldest_epoch < epoch { - epoch = epoch.prev(); + epoch = epoch.prev().unwrap(); self.db.prune_merkle_tree_store( batch, &StoreType::BridgePool, @@ -383,7 +379,7 @@ where // current one. It has the previous nonce, but it was // incremented during the epoch. while 0 < epoch.0 && oldest_epoch <= epoch { - epoch = epoch.prev(); + epoch = epoch.prev().unwrap(); let height = match self .in_mem .block @@ -550,7 +546,7 @@ where self.prune_merkle_tree_stores(&mut batch)?; } // If there's a previous block, prune non-persisted diffs from it - if let Some(height) = self.in_mem.block.height.checked_prev() { + if let Some(height) = self.in_mem.block.height.prev_height() { self.db.prune_non_persisted_diffs(&mut batch, height)?; } self.db.exec_batch(batch)?; @@ -616,7 +612,10 @@ where #[inline] pub fn get_current_decision_height(&self) -> BlockHeight { - self.in_mem.get_last_block_height() + 1 + self.in_mem + .get_last_block_height() + .checked_add(1) + .expect("Next height shouldn't overflow") } /// Check if we are at a given [`BlockHeight`] offset, `height_offset`, @@ -629,9 +628,10 @@ where fst_heights_of_each_epoch .last() - .map(|&h| { - let height_offset_within_epoch = h + height_offset; - current_decision_height == height_offset_within_epoch + .and_then(|&h| { + let height_offset_within_epoch = + h.checked_add(height_offset)?; + Some(current_decision_height == height_offset_within_epoch) }) .unwrap_or(false) } From d7451168a23408ed531d5c07b364a46a8ce5b06e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 25 Apr 2024 16:58:31 +0200 Subject: [PATCH 19/29] gov: update to non-panicking core API --- Cargo.lock | 1 + .../src/lib/node/ledger/shell/governance.rs | 3 +- crates/core/src/dec.rs | 14 +- crates/governance/Cargo.toml | 1 + crates/governance/src/cli/validation.rs | 11 +- crates/governance/src/pgf/cli/steward.rs | 5 +- crates/governance/src/pgf/inflation.rs | 16 +- crates/governance/src/pgf/storage/steward.rs | 7 +- crates/governance/src/utils.rs | 187 +++++++++++------- crates/sdk/src/rpc.rs | 1 + 10 files changed, 163 insertions(+), 83 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8837992bbd..41bca70310 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4659,6 +4659,7 @@ dependencies = [ "proptest", "serde 1.0.193", "serde_json", + "smooth-operator", "thiserror", "tracing", ] diff --git a/crates/apps/src/lib/node/ledger/shell/governance.rs b/crates/apps/src/lib/node/ledger/shell/governance.rs index 3381cd4f8c..822d9f2a13 100644 --- a/crates/apps/src/lib/node/ledger/shell/governance.rs +++ b/crates/apps/src/lib/node/ledger/shell/governance.rs @@ -113,7 +113,8 @@ where votes, total_active_voting_power, tally_type, - ); + ) + .expect("Proposal result calculation must not over/underflow"); gov_api::write_proposal_result(&mut shell.state, id, proposal_result)?; let transfer_address = match proposal_result.result { diff --git a/crates/core/src/dec.rs b/crates/core/src/dec.rs index ae7e9deac7..53892b241a 100644 --- a/crates/core/src/dec.rs +++ b/crates/core/src/dec.rs @@ -132,6 +132,16 @@ impl Dec { .expect("Cannot overflow") } + /// The representation of 1 / 3 + pub fn one_third() -> Self { + Self::one().checked_div(3).expect("Cannot fail") + } + + /// The representation of 2 / 3 + pub fn two_thirds() -> Self { + Self::two().checked_div(3).expect("Cannot fail") + } + /// Create a new [`Dec`] using a mantissa and a scale. pub fn new(mantissa: i128, scale: u8) -> Option { if scale > POS_DECIMAL_PRECISION { @@ -665,7 +675,6 @@ mod test_dec { / 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"); @@ -674,6 +683,9 @@ mod test_dec { let exp_quot = Dec::new(1249966393025101, 12).expect("Test failed"); assert_eq!(dec1 * dec2, exp_prod); assert_eq!(dec1 / dec2, exp_quot); + + Dec::one_third(); // must not panic + Dec::two_thirds(); // must not panic } /// Test the `Dec` and `token::Amount` interplay diff --git a/crates/governance/Cargo.toml b/crates/governance/Cargo.toml index 3f48aef3cd..f1076d6c7e 100644 --- a/crates/governance/Cargo.toml +++ b/crates/governance/Cargo.toml @@ -33,6 +33,7 @@ linkme = {workspace = true, optional = true} proptest = { workspace = true, optional = true } serde_json.workspace = true serde.workspace = true +smooth-operator.workspace = true thiserror.workspace = true tracing.workspace = true diff --git a/crates/governance/src/cli/validation.rs b/crates/governance/src/cli/validation.rs index be775b80e6..11898f5027 100644 --- a/crates/governance/src/cli/validation.rs +++ b/crates/governance/src/cli/validation.rs @@ -1,14 +1,15 @@ use std::collections::BTreeMap; use namada_core::address::Address; +use namada_core::arith::{self, checked}; use namada_core::storage::Epoch; use namada_core::token; use thiserror::Error; use super::onchain::{PgfFunding, StewardsUpdate}; -/// This enum raprresent a proposal data -#[derive(Clone, Debug, PartialEq, Error)] +/// This enum represents proposal data +#[derive(Debug, Error)] pub enum ProposalValidation { /// The proposal field are correct but there is no signature #[error("The proposal is not signed. Can't vote on it")] @@ -63,6 +64,8 @@ pub enum ProposalValidation { /// The pgf funding data is not valid #[error("invalid proposal extra data: cannot be empty.")] InvalidPgfFundingExtraData, + #[error("Arithmetic {0}.")] + Arith(arith::Error), } pub fn is_valid_author_balance( @@ -109,7 +112,9 @@ pub fn is_valid_end_epoch( ) -> Result<(), ProposalValidation> { let voting_period = proposal_end_epoch.0 - proposal_start_epoch.0; let end_epoch_is_multipler = - proposal_end_epoch % proposal_epoch_multiplier == 0; + checked!(proposal_end_epoch % proposal_epoch_multiplier) + .map_err(ProposalValidation::Arith)? + == Epoch(0); let is_valid_voting_period = voting_period > 0 && voting_period >= min_proposal_voting_period && min_proposal_voting_period <= max_proposal_period; diff --git a/crates/governance/src/pgf/cli/steward.rs b/crates/governance/src/pgf/cli/steward.rs index c6e0196187..a1cefbd24d 100644 --- a/crates/governance/src/pgf/cli/steward.rs +++ b/crates/governance/src/pgf/cli/steward.rs @@ -29,7 +29,10 @@ impl Commission { let mut sum = Dec::zero(); for percentage in self.reward_distribution.values() { - sum = sum.add(percentage); + match sum.checked_add(*percentage) { + Some(new_sum) => sum = new_sum, + None => return false, + } if sum > Dec::one() { return false; } diff --git a/crates/governance/src/pgf/inflation.rs b/crates/governance/src/pgf/inflation.rs index 5ce016ddb7..da0154d382 100644 --- a/crates/governance/src/pgf/inflation.rs +++ b/crates/governance/src/pgf/inflation.rs @@ -25,8 +25,10 @@ where .expect("Epochs per year should exist in storage"); let total_supply = get_effective_total_native_supply(storage)?; - let pgf_inflation_amount = - (pgf_parameters.pgf_inflation_rate * total_supply) / epochs_per_year; + let pgf_inflation_amount = total_supply + .mul_floor(pgf_parameters.pgf_inflation_rate)? + .checked_div_u64(epochs_per_year) + .unwrap_or_default(); credit_tokens( storage, @@ -82,13 +84,15 @@ where // Pgf steward inflation let stewards = get_stewards(storage)?; - let pgf_steward_inflation = (pgf_parameters.stewards_inflation_rate - * total_supply) - / epochs_per_year; + let pgf_steward_inflation = total_supply + .mul_floor(pgf_parameters.stewards_inflation_rate)? + .checked_div_u64(epochs_per_year) + .unwrap_or_default(); for steward in stewards { for (address, percentage) in steward.reward_distribution { - let pgf_steward_reward = percentage * pgf_steward_inflation; + let pgf_steward_reward = + pgf_steward_inflation.mul_floor(percentage)?; if credit_tokens( storage, diff --git a/crates/governance/src/pgf/storage/steward.rs b/crates/governance/src/pgf/storage/steward.rs index 25c2669f1d..3d14aa7426 100644 --- a/crates/governance/src/pgf/storage/steward.rs +++ b/crates/governance/src/pgf/storage/steward.rs @@ -35,11 +35,14 @@ impl StewardDetail { } let mut sum = Dec::zero(); - for percentage in self.reward_distribution.values().cloned() { + for percentage in self.reward_distribution.values().copied() { if percentage < Dec::zero() || percentage > Dec::one() { return false; } - sum += percentage; + match sum.checked_add(percentage) { + Some(new_sum) => sum = new_sum, + None => return false, + } if sum > Dec::one() { return false; } diff --git a/crates/governance/src/utils.rs b/crates/governance/src/utils.rs index 1602d532be..b798f59fd7 100644 --- a/crates/governance/src/utils.rs +++ b/crates/governance/src/utils.rs @@ -1,6 +1,7 @@ use std::fmt::Display; use namada_core::address::Address; +use namada_core::arith::{self, checked}; use namada_core::borsh::{BorshDeserialize, BorshSerialize}; use namada_core::collections::HashMap; use namada_core::dec::Dec; @@ -122,19 +123,19 @@ impl TallyResult { nay_voting_power: VotePower, abstain_voting_power: VotePower, total_voting_power: VotePower, - ) -> Self { + ) -> Result { let passed = match tally_type { TallyType::TwoThirds => { let at_least_two_third_voted = Self::get_total_voted_power( yay_voting_power, nay_voting_power, abstain_voting_power, - ) >= total_voting_power - .mul_ceil(Dec::two() / 3); + )? >= total_voting_power + .mul_ceil(Dec::two_thirds())?; let at_least_two_third_voted_yay = yay_voting_power - >= (nay_voting_power + yay_voting_power) - .mul_ceil(Dec::two() / 3); + >= checked!(nay_voting_power + yay_voting_power)? + .mul_ceil(Dec::two_thirds())?; at_least_two_third_voted && at_least_two_third_voted_yay } @@ -143,8 +144,8 @@ impl TallyResult { yay_voting_power, nay_voting_power, abstain_voting_power, - ) >= total_voting_power - .mul_ceil(Dec::one() / 3); + )? >= total_voting_power + .mul_ceil(Dec::one_third())?; // Yay votes must be more than half of the total votes let more_than_half_voted_yay = @@ -156,8 +157,8 @@ impl TallyResult { yay_voting_power, nay_voting_power, abstain_voting_power, - ) < total_voting_power - .mul_ceil(Dec::one() / 3); + )? < total_voting_power + .mul_ceil(Dec::one_third())?; // Nay votes must be less than half of the total votes let more_than_half_voted_yay = @@ -167,15 +168,15 @@ impl TallyResult { } }; - if passed { Self::Passed } else { Self::Rejected } + Ok(if passed { Self::Passed } else { Self::Rejected }) } fn get_total_voted_power( yay_voting_power: VotePower, nay_voting_power: VotePower, abstain_voting_power: VotePower, - ) -> VotePower { - yay_voting_power + nay_voting_power + abstain_voting_power + ) -> Result { + checked!(yay_voting_power + nay_voting_power + abstain_voting_power) } } @@ -200,18 +201,28 @@ pub struct ProposalResult { impl ProposalResult { /// Return true if at least 2/3 of the total voting power voted and at least - /// two third of the non-abstained voting power voted nay + /// two third of the non-abstained voting power voted nay. + /// Returns `false` if any arithmetic fails. pub fn two_thirds_nay_over_two_thirds_total(&self) -> bool { - let at_least_two_third_voted = self.total_yay_power - + self.total_nay_power - + self.total_abstain_power - >= self.total_voting_power.mul_ceil(Dec::two() / 3); - - let at_least_two_thirds_voted_nay = self.total_nay_power - >= (self.total_yay_power + self.total_nay_power) - .mul_ceil(Dec::two() / 3); - - at_least_two_third_voted && at_least_two_thirds_voted_nay + (|| { + let two_thirds_power = + self.total_voting_power.mul_ceil(Dec::two_thirds())?; + let at_least_two_third_voted = checked!( + self.total_yay_power + + self.total_nay_power + + self.total_abstain_power + >= two_thirds_power + )?; + + let at_least_two_thirds_voted_nay = self.total_nay_power + >= checked!(self.total_yay_power + self.total_nay_power)? + .mul_ceil(Dec::two_thirds())?; + + Ok::( + at_least_two_third_voted && at_least_two_thirds_voted_nay, + ) + })() + .unwrap_or_default() } } @@ -219,13 +230,16 @@ impl Display for ProposalResult { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let threshold = match self.tally_type { TallyType::TwoThirds => { - self.total_voting_power.mul_ceil(Dec::two() / 3) + self.total_voting_power.mul_ceil(Dec::two_thirds()) } - _ => self.total_voting_power.mul_ceil(Dec::one() / 3), - }; + _ => self.total_voting_power.mul_ceil(Dec::one_third()), + } + .unwrap(); - let thresh_frac = - Dec::from(threshold) / Dec::from(self.total_voting_power); + let thresh_frac = Dec::try_from(threshold) + .unwrap() + .checked_div(Dec::try_from(self.total_voting_power).unwrap()) + .unwrap(); write!( f, @@ -299,7 +313,7 @@ pub fn compute_proposal_result( votes: ProposalVotes, total_voting_power: VotePower, tally_type: TallyType, -) -> ProposalResult { +) -> Result { let mut yay_voting_power = VotePower::default(); let mut nay_voting_power = VotePower::default(); let mut abstain_voting_power = VotePower::default(); @@ -308,11 +322,12 @@ pub fn compute_proposal_result( let vote_type = votes.validators_vote.get(&address); if let Some(vote) = vote_type { if vote.is_yay() { - yay_voting_power += vote_power; + yay_voting_power = checked!(yay_voting_power + vote_power)?; } else if vote.is_nay() { - nay_voting_power += vote_power; + nay_voting_power = checked!(nay_voting_power + vote_power)?; } else if vote.is_abstain() { - abstain_voting_power += vote_power; + abstain_voting_power = + checked!(abstain_voting_power + vote_power)?; } } } @@ -322,7 +337,7 @@ pub fn compute_proposal_result( Some(vote) => vote, None => continue, }; - for (validator, voting_power) in delegations { + for (validator, vote_power) in delegations { let validator_vote = votes.validators_vote.get(&validator); if let Some(validator_vote) = validator_vote { let validator_vote_is_same_side = @@ -330,34 +345,44 @@ pub fn compute_proposal_result( if !validator_vote_is_same_side { if delegator_vote.is_yay() { - yay_voting_power += voting_power; + yay_voting_power = + checked!(yay_voting_power + vote_power)?; if validator_vote.is_nay() { - nay_voting_power -= voting_power; + nay_voting_power = + checked!(nay_voting_power - vote_power)?; } else if validator_vote.is_abstain() { - abstain_voting_power -= voting_power; + abstain_voting_power = + checked!(abstain_voting_power - vote_power)?; } } else if delegator_vote.is_nay() { - nay_voting_power += voting_power; + nay_voting_power = + checked!(nay_voting_power + vote_power)?; if validator_vote.is_yay() { - yay_voting_power -= voting_power; + yay_voting_power = + checked!(yay_voting_power - vote_power)?; } else if validator_vote.is_abstain() { - abstain_voting_power -= voting_power; + abstain_voting_power = + checked!(abstain_voting_power - vote_power)?; } } else if delegator_vote.is_abstain() { - abstain_voting_power += voting_power; + abstain_voting_power = + checked!(abstain_voting_power + vote_power)?; if validator_vote.is_yay() { - yay_voting_power -= voting_power; + yay_voting_power = + checked!(yay_voting_power - vote_power)?; } else if validator_vote.is_nay() { - nay_voting_power -= voting_power; + nay_voting_power = + checked!(nay_voting_power - vote_power)?; } } } } else if delegator_vote.is_yay() { - yay_voting_power += voting_power; + yay_voting_power = checked!(yay_voting_power + vote_power)?; } else if delegator_vote.is_nay() { - nay_voting_power += voting_power; + nay_voting_power = checked!(nay_voting_power + vote_power)?; } else if delegator_vote.is_abstain() { - abstain_voting_power += voting_power; + abstain_voting_power = + checked!(abstain_voting_power + vote_power)?; } } } @@ -368,22 +393,23 @@ pub fn compute_proposal_result( nay_voting_power, abstain_voting_power, total_voting_power, - ); + )?; - ProposalResult { + Ok(ProposalResult { result: tally_result, tally_type, total_voting_power, total_yay_power: yay_voting_power, total_nay_power: nay_voting_power, total_abstain_power: abstain_voting_power, - } + }) } /// Calculate the valid voting window for a validator given proposal epoch /// details. The valid window is within 2/3 of the voting period. /// NOTE: technically the window can be more generous than 2/3 since the end /// epoch is a valid epoch for voting too. +/// Returns `false` if any arithmetic fails. pub fn is_valid_validator_voting_period( current_epoch: Epoch, voting_start_epoch: Epoch, @@ -392,10 +418,16 @@ pub fn is_valid_validator_voting_period( if voting_start_epoch >= voting_end_epoch { false } else { - // From e_cur <= e_start + 2/3 * (e_end - e_start) - let is_within_two_thirds = - 3 * current_epoch <= voting_start_epoch + 2 * voting_end_epoch; - current_epoch >= voting_start_epoch && is_within_two_thirds + (|| { + // From e_cur <= e_start + 2/3 * (e_end - e_start) + let is_within_two_thirds = checked!( + current_epoch * 3 <= voting_start_epoch + voting_end_epoch * 2 + ) + .ok()?; + + Some(current_epoch >= voting_start_epoch && is_within_two_thirds) + })() + .unwrap_or_default() } } @@ -420,7 +452,8 @@ mod test { proposal_votes.clone(), token::Amount::from_u64(1), tally_type, - ); + ) + .unwrap(); let _result = if matches!( tally_type, TallyType::LessOneHalfOverOneThirdNay @@ -457,7 +490,8 @@ mod test { proposal_votes.clone(), validator_voting_power, tally_type, - ); + ) + .unwrap(); assert!( matches!(proposal_result.result, TallyResult::Passed), "{tally_type:?}" @@ -505,7 +539,8 @@ mod test { proposal_votes.clone(), validator_voting_power, tally_type, - ); + ) + .unwrap(); assert!( matches!(proposal_result.result, TallyResult::Passed), "{tally_type:?}" @@ -553,7 +588,8 @@ mod test { proposal_votes.clone(), validator_voting_power, tally_type, - ); + ) + .unwrap(); assert!( matches!(proposal_result.result, TallyResult::Rejected), "{tally_type:?}" @@ -613,7 +649,8 @@ mod test { proposal_votes.clone(), validator_voting_power, tally_type, - ); + ) + .unwrap(); assert!( matches!(proposal_result.result, TallyResult::Rejected), "{tally_type:?}" @@ -680,7 +717,8 @@ mod test { proposal_votes.clone(), validator_voting_power, tally_type, - ); + ) + .unwrap(); assert!( matches!(proposal_result.result, TallyResult::Passed), "{tally_type:?}" @@ -738,7 +776,8 @@ mod test { proposal_votes.clone(), validator_voting_power, tally_type, - ); + ) + .unwrap(); assert!( matches!(proposal_result.result, TallyResult::Passed), "{tally_type:?}" @@ -794,7 +833,8 @@ mod test { proposal_votes.clone(), validator_voting_power.add(validator_voting_power_two), tally_type, - ); + ) + .unwrap(); let _result = if matches!( tally_type, TallyType::LessOneHalfOverOneThirdNay @@ -866,7 +906,8 @@ mod test { proposal_votes.clone(), validator_voting_power.add(validator_voting_power_two), tally_type, - ); + ) + .unwrap(); let _result = if matches!(tally_type, TallyType::OneHalfOverOneThird) { TallyResult::Passed @@ -933,7 +974,8 @@ mod test { proposal_votes.clone(), validator_voting_power.add(validator_voting_power_two), TallyType::TwoThirds, - ); + ) + .unwrap(); assert!(matches!(proposal_result.result, TallyResult::Passed)); assert_eq!( @@ -1001,7 +1043,8 @@ mod test { proposal_votes.clone(), validator_voting_power.add(validator_voting_power_two), TallyType::TwoThirds, - ); + ) + .unwrap(); assert!(matches!(proposal_result.result, TallyResult::Rejected)); assert_eq!( @@ -1056,7 +1099,8 @@ mod test { proposal_votes.clone(), delegator_voting_power_two.add(delegator_voting_power), TallyType::TwoThirds, - ); + ) + .unwrap(); assert!(matches!(proposal_result.result, TallyResult::Rejected)); assert_eq!( @@ -1108,7 +1152,8 @@ mod test { proposal_votes.clone(), token::Amount::from(200), TallyType::TwoThirds, - ); + ) + .unwrap(); assert!(matches!(proposal_result.result, TallyResult::Passed)); assert_eq!( @@ -1162,7 +1207,8 @@ mod test { proposal_votes.clone(), token::Amount::from(403), TallyType::OneHalfOverOneThird, - ); + ) + .unwrap(); assert!(matches!(proposal_result.result, TallyResult::Rejected)); assert_eq!( @@ -1216,7 +1262,8 @@ mod test { proposal_votes.clone(), token::Amount::from(402), TallyType::OneHalfOverOneThird, - ); + ) + .unwrap(); assert!(matches!(proposal_result.result, TallyResult::Passed)); assert_eq!( @@ -1270,7 +1317,8 @@ mod test { proposal_votes.clone(), token::Amount::from(100), TallyType::LessOneHalfOverOneThirdNay, - ); + ) + .unwrap(); assert!(matches!(proposal_result.result, TallyResult::Rejected)); assert_eq!( @@ -1326,7 +1374,8 @@ mod test { proposal_votes.clone(), token::Amount::from(271), TallyType::LessOneHalfOverOneThirdNay, - ); + ) + .unwrap(); assert!(matches!(proposal_result.result, TallyResult::Passed)); assert_eq!( diff --git a/crates/sdk/src/rpc.rs b/crates/sdk/src/rpc.rs index a3863bc95d..667cfe01a6 100644 --- a/crates/sdk/src/rpc.rs +++ b/crates/sdk/src/rpc.rs @@ -1016,6 +1016,7 @@ pub async fn query_proposal_result( total_staked_token, tally_type, ) + .expect("Proposal result calculation must not over/underflow"); } }; Ok(Some(proposal_result)) From 3b1e84fbfda83015cc00ab3ab3dc8e7db67848ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 30 Apr 2024 18:07:02 +0200 Subject: [PATCH 20/29] pos: update to non-panicking core API --- Cargo.lock | 1 + crates/proof_of_stake/Cargo.toml | 1 + crates/proof_of_stake/src/epoched.rs | 37 +- crates/proof_of_stake/src/lib.rs | 398 ++++++++++-------- crates/proof_of_stake/src/parameters.rs | 34 +- crates/proof_of_stake/src/queries.rs | 28 +- crates/proof_of_stake/src/rewards.rs | 150 ++++--- crates/proof_of_stake/src/slashing.rs | 180 ++++---- crates/proof_of_stake/src/storage.rs | 16 +- .../proof_of_stake/src/tests/state_machine.rs | 23 +- .../src/tests/state_machine_v2.rs | 49 ++- .../src/tests/test_helper_fns.rs | 49 ++- crates/proof_of_stake/src/tests/test_pos.rs | 51 ++- .../src/tests/test_slash_and_redel.rs | 45 +- crates/proof_of_stake/src/types/mod.rs | 9 +- crates/proof_of_stake/src/types/rev_order.rs | 4 +- .../src/validator_set_update.rs | 17 +- 17 files changed, 639 insertions(+), 453 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 41bca70310..356eb9da5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4786,6 +4786,7 @@ dependencies = [ "proptest", "proptest-state-machine", "serde 1.0.193", + "smooth-operator", "test-log", "thiserror", "tracing", diff --git a/crates/proof_of_stake/Cargo.toml b/crates/proof_of_stake/Cargo.toml index f14c3ad49b..88c76970d3 100644 --- a/crates/proof_of_stake/Cargo.toml +++ b/crates/proof_of_stake/Cargo.toml @@ -40,6 +40,7 @@ num-traits.workspace = true once_cell.workspace = true proptest = { workspace = true, optional = true } serde.workspace = true +smooth-operator.workspace = true thiserror.workspace = true tracing.workspace = true diff --git a/crates/proof_of_stake/src/epoched.rs b/crates/proof_of_stake/src/epoched.rs index b8d6bf4793..f471371727 100644 --- a/crates/proof_of_stake/src/epoched.rs +++ b/crates/proof_of_stake/src/epoched.rs @@ -1,11 +1,12 @@ //! [`Epoched`] and [`EpochedDelta`] are structures for data that is set for //! future (and possibly past) epochs. +use std::cmp; use std::fmt::Debug; use std::marker::PhantomData; -use std::{cmp, ops}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use namada_core::arith::{checked, CheckedAdd}; use namada_core::collections::HashMap; use namada_core::storage::{self, Epoch}; use namada_macros::BorshDeserializer; @@ -106,7 +107,7 @@ where Some(last_update) => { let data_handler = self.get_data_handler(); let future_most_epoch = - last_update + FutureEpochs::value(params); + last_update.unchecked_add(FutureEpochs::value(params)); // Epoch can be a lot greater than the epoch where // a value is recorded, we check the upper bound // epoch of the LazyMap data @@ -158,7 +159,7 @@ where S: StorageWrite + StorageRead, { let data_handler = self.get_data_handler(); - let epoch = current_epoch + offset; + let epoch = current_epoch.unchecked_add(offset); let _prev = data_handler.insert(storage, epoch, value)?; Ok(()) } @@ -186,7 +187,7 @@ where .checked_sub(PastEpochs::value(params)) .unwrap_or_default(); if oldest_epoch < oldest_to_keep { - let diff = u64::from(oldest_to_keep - oldest_epoch); + let diff = u64::from(checked!(oldest_to_keep - oldest_epoch)?); // Go through the epochs before the expected oldest epoch and // keep the latest one tracing::debug!( @@ -421,7 +422,7 @@ where .checked_sub(PastEpochs::value(params)) .unwrap_or_default(); if oldest_epoch < oldest_to_keep { - let diff = u64::from(oldest_to_keep - oldest_epoch); + let diff = u64::from(checked!(oldest_to_keep - oldest_epoch)?); // Go through the epochs before the expected oldest epoch and // keep the latest one tracing::debug!( @@ -463,12 +464,7 @@ impl where FutureEpochs: EpochOffset, PastEpochs: EpochOffset, - Data: BorshSerialize - + BorshDeserialize - + ops::Add - + ops::AddAssign - + 'static - + Debug, + Data: BorshSerialize + BorshDeserialize + CheckedAdd + 'static + Debug, { /// Open the handle pub fn open(key: storage::Key) -> Self { @@ -525,7 +521,7 @@ where let data_handler = self.get_data_handler(); let start_epoch = Self::sub_past_epochs(params, last_update); let future_most_epoch = - last_update + FutureEpochs::value(params); + last_update.unchecked_add(FutureEpochs::value(params)); // Epoch can be a lot greater than the epoch where // a value is recorded, we check the upper bound @@ -538,7 +534,7 @@ where data_handler.get(storage, &Epoch(ep))? { match sum.as_mut() { - Some(sum) => *sum += delta, + Some(sum) => *sum = checked!(sum + delta)?, None => sum = Some(delta), } } @@ -564,9 +560,10 @@ where let params = read_pos_params(storage)?; self.update_data(storage, ¶ms, current_epoch)?; let cur_value = self - .get_delta_val(storage, current_epoch + offset)? + .get_delta_val(storage, current_epoch.unchecked_add(offset))? .unwrap_or_default(); - self.set_at_epoch(storage, cur_value + value, current_epoch, offset) + let new_value = checked!(cur_value + value)?; + self.set_at_epoch(storage, new_value, current_epoch, offset) } /// Initialize or set the value at the given epoch offset. @@ -596,7 +593,7 @@ where S: StorageWrite + StorageRead, { let data_handler = self.get_data_handler(); - let epoch = current_epoch + offset; + let epoch = current_epoch.unchecked_add(offset); let _prev = data_handler.insert(storage, epoch, value)?; Ok(()) } @@ -622,7 +619,7 @@ where .checked_sub(PastEpochs::value(params)) .unwrap_or_default(); if oldest_epoch < oldest_to_keep { - let diff = u64::from(oldest_to_keep - oldest_epoch); + let diff = u64::from(checked!(oldest_to_keep - oldest_epoch)?); // Go through the epochs before the expected oldest epoch and // sum them into it tracing::debug!( @@ -639,7 +636,7 @@ where "Removed delta value at epoch {epoch}: {removed:?}" ); match sum.as_mut() { - Some(sum) => *sum += removed, + Some(sum) => *sum = checked!(sum + removed)?, None => sum = Some(removed), } } @@ -649,7 +646,9 @@ where Self::sub_past_epochs(params, current_epoch); let new_oldest_epoch_data = match data_handler.get(storage, &new_oldest_epoch)? { - Some(oldest_epoch_data) => oldest_epoch_data + sum, + Some(oldest_epoch_data) => { + checked!(oldest_epoch_data + sum)? + } None => sum, }; tracing::debug!( diff --git a/crates/proof_of_stake/src/lib.rs b/crates/proof_of_stake/src/lib.rs index d10e97e977..82bd7a1cf1 100644 --- a/crates/proof_of_stake/src/lib.rs +++ b/crates/proof_of_stake/src/lib.rs @@ -23,12 +23,13 @@ mod error; mod tests; use core::fmt::Debug; -use std::cmp::{self}; +use std::cmp; use std::collections::{BTreeMap, BTreeSet}; use epoched::EpochOffset; pub use error::*; use namada_core::address::{Address, InternalAddress}; +use namada_core::arith::checked; use namada_core::collections::HashSet; use namada_core::dec::Dec; use namada_core::event::EmitEvents; @@ -37,7 +38,7 @@ use namada_core::storage::BlockHeight; pub use namada_core::storage::{Epoch, Key, KeySeg}; use namada_core::tendermint::abci::types::Misbehavior; use namada_storage::collections::lazy_map::{self, Collectable, LazyMap}; -use namada_storage::{StorageRead, StorageWrite}; +use namada_storage::{OptionExt, StorageRead, StorageWrite}; pub use namada_trans_token as token; pub use parameters::{OwnedPosParams, PosParams}; use types::{into_tm_voting_power, DelegationEpochs}; @@ -248,7 +249,7 @@ where let params = read_pos_params(storage)?; let offset = offset_opt.unwrap_or(params.pipeline_len); - let offset_epoch = current_epoch + offset; + let offset_epoch = checked!(current_epoch + offset)?; // Check that the validator is actually a validator let validator_state_handle = validator_state_handle(validator); @@ -278,7 +279,7 @@ where storage, source, validator, - current_epoch + offset, + offset_epoch, current_epoch, )?; @@ -393,8 +394,9 @@ where } let params = read_pos_params(storage)?; - let pipeline_epoch = current_epoch + params.pipeline_len; - let withdrawable_epoch = current_epoch + params.withdrawable_epoch_offset(); + let pipeline_epoch = checked!(current_epoch + params.pipeline_len)?; + let withdrawable_epoch = + checked!(current_epoch + params.withdrawable_epoch_offset())?; tracing::debug!( "Unbonding token amount {} at epoch {}, withdrawable at epoch {}", amount.to_string_native(), @@ -469,7 +471,7 @@ where storage, &redelegated_bonds.at(&bond_epoch), bond_epoch, - cur_bond_amount - new_bond_amount, + checked!(cur_bond_amount - new_bond_amount)?, )? } else { ModifiedRedelegation::default() @@ -498,23 +500,22 @@ where .into_iter() .map(|epoch| { let cur_bond_value = bonds_handle - .get_delta_val(storage, epoch) - .unwrap() + .get_delta_val(storage, epoch)? .unwrap_or_default(); let value = if let Some((start_epoch, new_bond_amount)) = bonds_to_unbond.new_entry { if start_epoch == epoch { - cur_bond_value - new_bond_amount + checked!(cur_bond_value - new_bond_amount)? } else { cur_bond_value } } else { cur_bond_value }; - (epoch, value) + Ok((epoch, value)) }) - .collect::>(); + .collect::>>()?; // `updatedBonded` // Remove bonds for all the full unbonds. @@ -546,10 +547,13 @@ where // Update the unbonds in storage using the eager map computed above if !is_redelegation { for (start_epoch, &unbond_amount) in new_unbonds_map.iter() { - unbonds.at(start_epoch).update( + unbonds.at(start_epoch).try_update( storage, withdrawable_epoch, - |cur_val| cur_val.unwrap_or_default() + unbond_amount, + |current| { + let current = current.unwrap_or_default(); + Ok(checked!(current + unbond_amount)?) + }, )?; } } @@ -603,10 +607,13 @@ where let redelegated_unbonded = this_redelegated_unbonded.at(src_validator); for (&redelegation_epoch, &change) in redelegated_unbonds { - redelegated_unbonded.update( + redelegated_unbonded.try_update( storage, redelegation_epoch, - |current| current.unwrap_or_default() + change, + |current| { + let current = current.unwrap_or_default(); + Ok(checked!(current + change)?) + }, )?; } } @@ -619,11 +626,13 @@ where let total_bonded = total_bonded_handle(validator).get_data_handler(); let total_unbonded = total_unbonded_handle(validator).at(&pipeline_epoch); for (&start_epoch, &amount) in &new_unbonds_map { - total_bonded.update(storage, start_epoch, |current| { - current.unwrap_or_default() - amount + total_bonded.try_update(storage, start_epoch, |current| { + let current = current.unwrap_or_default(); + Ok(checked!(current - amount)?) })?; - total_unbonded.update(storage, start_epoch, |current| { - current.unwrap_or_default() + amount + total_unbonded.try_update(storage, start_epoch, |current| { + let current = current.unwrap_or_default(); + Ok(checked!(current + amount)?) })?; } @@ -638,10 +647,13 @@ where let bonded_sub_map = total_redelegated_bonded .at(redelegation_start_epoch) .at(src_validator); - bonded_sub_map.update( + bonded_sub_map.try_update( storage, *bond_start_epoch, - |current| current.unwrap_or_default() - *change, + |current| { + let current = current.unwrap_or_default(); + Ok(checked!(current - *change)?) + }, )?; // total redelegated unbonded @@ -649,10 +661,13 @@ where .at(&pipeline_epoch) .at(redelegation_start_epoch) .at(src_validator); - unbonded_sub_map.update( + unbonded_sub_map.try_update( storage, *bond_start_epoch, - |current| current.unwrap_or_default() + *change, + |current| { + let current = current.unwrap_or_default(); + Ok(checked!(current + *change)?) + }, )?; } } @@ -677,7 +692,7 @@ where amount.to_string_native(), ); - let change_after_slashing = -result_slashing.sum.change(); + let change_after_slashing = checked!(-result_slashing.sum.change())?; // Update the validator set at the pipeline offset. Since unbonding from a // jailed validator who is no longer frozen is allowed, only update the // validator set if the validator is not jailed @@ -735,13 +750,17 @@ where let cur_bond = bonds_handle .get_delta_val(storage, epoch)? .unwrap_or_default(); - let redelegated_deltas = redelegated_bonds - .at(&epoch) - // Sum of redelegations from any src validator - .collect_map(storage)? - .into_values() - .map(|redeleg| redeleg.into_values().sum()) - .sum(); + let redelegated_deltas = token::Amount::sum( + redelegated_bonds + .at(&epoch) + // Sum of redelegations from any src validator + .collect_map(storage)? + .into_values() + .map(|redeleg| { + token::Amount::sum(redeleg.into_values()).unwrap() + }), + ) + .unwrap(); debug_assert!( cur_bond >= redelegated_deltas, "After unbonding, in epoch {epoch} the bond amount {} must be \ @@ -757,7 +776,7 @@ where } // Tally rewards (only call if this is not the first epoch) - if current_epoch > Epoch::default() { + if let Some(prev_epoch) = current_epoch.prev() { let mut rewards = token::Amount::zero(); let last_claim_epoch = @@ -768,16 +787,15 @@ where for (start_epoch, slashed_amount) in &result_slashing.epoch_map { // Stop collecting rewards at the moment the unbond is initiated // (right now) - for ep in - Epoch::iter_bounds_inclusive(*start_epoch, current_epoch.prev()) - { + for ep in Epoch::iter_bounds_inclusive(*start_epoch, prev_epoch) { // Consider the last epoch when rewards were claimed if ep < last_claim_epoch { continue; } let rp = rewards_products.get(storage, &ep)?.unwrap_or_default(); - rewards += rp * (*slashed_amount); + let slashed_rewards = slashed_amount.mul_floor(rp)?; + rewards = checked!(rewards + slashed_rewards)?; } } @@ -804,43 +822,44 @@ fn fold_and_slash_redelegated_bonds( start_epoch: Epoch, list_slashes: &[Slash], slash_epoch_filter: impl Fn(Epoch) -> bool, -) -> FoldRedelegatedBondsResult +) -> namada_storage::Result where S: StorageRead, { let mut result = FoldRedelegatedBondsResult::default(); for (src_validator, bonds_map) in redelegated_unbonds { for (bond_start, &change) in bonds_map { - // Merge the two lists of slashes - let mut merged: Vec = // Look-up slashes for this validator ... + let validator_slashes: Vec = validator_slashes_handle(src_validator) - .iter(storage) - .unwrap() - .map(Result::unwrap) - .filter(|slash| { - params.in_redelegation_slashing_window( - slash.epoch, - params.redelegation_start_epoch_from_end( - start_epoch, - ), - start_epoch, - ) && *bond_start <= slash.epoch - && slash_epoch_filter(slash.epoch) - }) - // ... and add `list_slashes` - .chain(list_slashes.iter().cloned()) - .collect(); + .iter(storage)? + .collect::>>()?; + // Merge the two lists of slashes + let mut merged: Vec = validator_slashes + .into_iter() + .filter(|slash| { + params.in_redelegation_slashing_window( + slash.epoch, + params.redelegation_start_epoch_from_end(start_epoch), + start_epoch, + ) && *bond_start <= slash.epoch + && slash_epoch_filter(slash.epoch) + }) + // ... and add `list_slashes` + .chain(list_slashes.iter().cloned()) + .collect(); // Sort slashes by epoch merged.sort_by(|s1, s2| s1.epoch.partial_cmp(&s2.epoch).unwrap()); - result.total_redelegated += change; - result.total_after_slashing += - apply_list_slashes(params, &merged, change); + result.total_redelegated = + checked!(result.total_redelegated + change)?; + let list_slashes = apply_list_slashes(params, &merged, change)?; + result.total_after_slashing = + checked!(result.total_after_slashing + list_slashes)?; } } - result + Ok(result) } /// Epochs for full and partial unbonds. @@ -878,9 +897,9 @@ where bonds_for_removal.epochs.insert(bond_epoch); } else { bonds_for_removal.new_entry = - Some((bond_epoch, bond_amount - to_unbond)); + Some((bond_epoch, checked!(bond_amount - to_unbond)?)); } - remaining -= to_unbond; + remaining = checked!(remaining - to_unbond)?; if remaining.is_zero() { break; } @@ -921,7 +940,7 @@ where }, amount, ) = rb?; - total_redelegated += amount; + total_redelegated = checked!(total_redelegated + amount)?; src_validators.insert(src_validator); } @@ -939,13 +958,17 @@ where break; } let rbonds = redelegated_bonds.at(&src_validator); - let total_src_val_amount = rbonds - .iter(storage)? - .map(|res| { - let (_, amount) = res?; - Ok(amount) - }) - .sum::>()?; + let total_src_val_amount = token::Amount::sum( + rbonds + .iter(storage)? + .map(|res| { + let (_, amount) = res?; + Ok(amount) + }) + .collect::>>()? + .into_iter(), + ) + .ok_or_err_msg("token amount overflow")?; // TODO: move this into the `if total_redelegated <= remaining` branch // below, then we don't have to remove it in `fn @@ -958,7 +981,7 @@ where .validators_to_remove .insert(src_validator.clone()); if total_src_val_amount <= remaining { - remaining -= total_src_val_amount; + remaining = checked!(remaining - total_src_val_amount)?; } else { let bonds_to_remove = find_bonds_to_remove(storage, &rbonds, remaining)?; @@ -1129,20 +1152,20 @@ where .unwrap_or(true) || modified.validators_to_remove.is_empty() { - for res in redelegated_bonds.at(&start).iter(storage).unwrap() { + for res in redelegated_bonds.at(&start).iter(storage)? { let ( lazy_map::NestedSubKey::Data { key: validator, nested_sub_key: lazy_map::SubKey::Data(epoch), }, amount, - ) = res.unwrap(); + ) = res?; rbonds .entry(validator.clone()) .or_default() .insert(epoch, amount); } - (start, rbonds) + Ok::<_, namada_storage::Error>((start, rbonds)) } else { for src_validator in &modified.validators_to_remove { if modified @@ -1165,8 +1188,7 @@ where let cur_redel_bond_amount = redelegated_bonds .at(&start) .at(src_validator) - .get(storage, bond_start) - .unwrap() + .get(storage, bond_start)? .unwrap_or_default(); let raw_bonds = rbonds .entry(src_validator.clone()) @@ -1180,24 +1202,26 @@ where raw_bonds .insert(*bond_start, cur_redel_bond_amount); } else { + let new_amount = modified + .new_amount + // Safe unwrap - it shouldn't + // get to + // this if it's None + .unwrap(); raw_bonds.insert( *bond_start, - cur_redel_bond_amount - - modified - .new_amount - // Safe unwrap - it shouldn't - // get to - // this if it's None - .unwrap(), + checked!( + cur_redel_bond_amount - new_amount + )?, ); } } } } - (start, rbonds) + Ok((start, rbonds)) } }) - .collect(); + .collect::>()?; Ok(new_redelegated_unbonds) } @@ -1276,7 +1300,7 @@ where // This will fail if the key is already being used try_insert_consensus_key(storage, consensus_key)?; - let pipeline_epoch = current_epoch + offset; + let pipeline_epoch = checked!(current_epoch + offset)?; validator_addresses_handle() .at(&pipeline_epoch) .insert(storage, address.clone())?; @@ -1553,17 +1577,14 @@ where } let max_change = - read_validator_max_commission_rate_change(storage, validator)?; - if max_change.is_none() { - return Err(CommissionRateChangeError::NoMaxSetInStorage( - validator.clone(), - ) - .into()); - } + read_validator_max_commission_rate_change(storage, validator)? + .ok_or_else(|| { + CommissionRateChangeError::NoMaxSetInStorage(validator.clone()) + })?; let params = read_pos_params(storage)?; let commission_handle = validator_commission_rate_handle(validator); - let pipeline_epoch = current_epoch + params.pipeline_len; + let pipeline_epoch = checked!(current_epoch + params.pipeline_len)?; let rate_at_pipeline = commission_handle .get(storage, pipeline_epoch, ¶ms)? @@ -1572,11 +1593,15 @@ where return Ok(()); } let rate_before_pipeline = commission_handle - .get(storage, pipeline_epoch.prev(), ¶ms)? + .get( + storage, + pipeline_epoch.prev().expect("Pipeline epoch cannot be 0"), + ¶ms, + )? .expect("Could not find a rate in given epoch"); - let change_from_prev = new_rate.abs_diff(&rate_before_pipeline); - if change_from_prev > max_change.unwrap() { + let change_from_prev = new_rate.abs_diff(rate_before_pipeline)?; + if change_from_prev > max_change { return Err(CommissionRateChangeError::RateChangeTooLarge( change_from_prev, validator.clone(), @@ -1606,7 +1631,7 @@ where let (start, delta) = next?; if start <= epoch { let amount = amounts.entry(start).or_default(); - *amount += delta; + *amount = checked!(amount + delta)?; } } @@ -1622,12 +1647,14 @@ where ) = next?; // This is the first epoch in which the unbond stops contributing to // voting power - let end = withdrawable_epoch - params.withdrawable_epoch_offset() - + params.pipeline_len; + let end = checked!( + withdrawable_epoch - params.withdrawable_epoch_offset() + + params.pipeline_len + )?; if start <= epoch && end > epoch { let amount = amounts.entry(start).or_default(); - *amount += delta; + *amount = checked!(amount + delta)?; } } @@ -1659,7 +1686,7 @@ where && end > epoch { let amount = amounts.entry(start).or_default(); - *amount += delta; + *amount = checked!(amount + delta)?; } } @@ -1695,7 +1722,7 @@ where && start <= epoch { let amount = amounts.entry(start).or_default(); - *amount += delta; + *amount = checked!(amount + delta)?; } } } @@ -1714,7 +1741,8 @@ where { let params = read_pos_params(storage)?; let amounts = bond_amounts_for_query(storage, ¶ms, bond_id, epoch)?; - Ok(amounts.values().cloned().sum()) + token::Amount::sum(amounts.values().copied()) + .ok_or_err_msg("token amount overflow") } /// Get the total bond amount, including slashes, for a given bond ID and epoch. @@ -1742,8 +1770,9 @@ where let list_slashes = slashes .iter() .filter(|slash| { - let processing_epoch = - slash.epoch + params.slash_processing_epoch_offset(); + let processing_epoch = slash + .epoch + .unchecked_add(params.slash_processing_epoch_offset()); // Only use slashes that were processed before or at the // epoch associated with the bond amount. This assumes // that slashes are applied before inflation. @@ -1752,8 +1781,9 @@ where .cloned() .collect::>(); - let slash_epoch_filter = - |e: Epoch| e + params.slash_processing_epoch_offset() <= epoch; + let slash_epoch_filter = |e: Epoch| { + e.unchecked_add(params.slash_processing_epoch_offset()) <= epoch + }; let redelegated_bonds = redelegated_bonded.at(&start).collect_map(storage)?; @@ -1765,21 +1795,25 @@ where start, &list_slashes, slash_epoch_filter, - ); + )?; - let total_not_redelegated = *amount - result_fold.total_redelegated; + let total_not_redelegated = + checked!(amount - result_fold.total_redelegated)?; let after_not_redelegated = apply_list_slashes( ¶ms, &list_slashes, total_not_redelegated, - ); + )?; - *amount = after_not_redelegated + result_fold.total_after_slashing; + *amount = checked!( + after_not_redelegated + result_fold.total_after_slashing + )?; } } - Ok(amounts.values().cloned().sum()) + token::Amount::sum(amounts.values().copied()) + .ok_or_err_msg("token amount overflow") } /// Get bond amounts within the `claim_start..=claim_end` epoch range for @@ -1819,7 +1853,7 @@ where if start <= ep { let amount = amounts.entry(ep).or_default().entry(start).or_default(); - *amount += delta; + *amount = checked!(amount + delta)?; } } } @@ -1836,8 +1870,9 @@ where let list_slashes = slashes .iter() .filter(|slash| { - let processing_epoch = slash.epoch - + params.slash_processing_epoch_offset(); + let processing_epoch = slash.epoch.unchecked_add( + params.slash_processing_epoch_offset(), + ); // Only use slashes that were processed before or at the // epoch associated with the bond amount. This assumes // that slashes are applied before inflation. @@ -1846,8 +1881,10 @@ where .cloned() .collect::>(); - let slash_epoch_filter = - |e: Epoch| e + params.slash_processing_epoch_offset() <= ep; + let slash_epoch_filter = |e: Epoch| { + e.unchecked_add(params.slash_processing_epoch_offset()) + <= ep + }; let redelegated_bonds = redelegated_bonded.at(&start).collect_map(storage)?; @@ -1859,28 +1896,35 @@ where start, &list_slashes, slash_epoch_filter, - ); + )?; let total_not_redelegated = - *amount - result_fold.total_redelegated; + checked!(amount - result_fold.total_redelegated)?; let after_not_redelegated = apply_list_slashes( ¶ms, &list_slashes, total_not_redelegated, - ); + )?; - *amount = - after_not_redelegated + result_fold.total_after_slashing; + *amount = checked!( + after_not_redelegated + result_fold.total_after_slashing + )?; } } } - Ok(amounts + amounts .into_iter() // Flatten the inner maps to discard bond start epochs - .map(|(ep, amounts)| (ep, amounts.values().cloned().sum())) - .collect()) + .map(|(ep, amounts)| { + Ok(( + ep, + token::Amount::sum(amounts.values().copied()) + .ok_or_err_msg("token amount overflow")?, + )) + }) + .collect() } /// Get the genesis consensus validators stake and consensus key for Tendermint, @@ -1954,8 +1998,9 @@ where // and the most recent infraction epoch let last_slash_epoch = read_validator_last_slash_epoch(storage, validator)?; if let Some(last_slash_epoch) = last_slash_epoch { - let eligible_epoch = - last_slash_epoch + params.slash_processing_epoch_offset(); + let eligible_epoch = checked!( + last_slash_epoch + params.slash_processing_epoch_offset() + )?; if current_epoch < eligible_epoch { return Err(UnjailValidatorError::NotEligible( validator.clone(), @@ -1967,7 +2012,7 @@ where } // Re-insert the validator into the validator set and update its state - let pipeline_epoch = current_epoch + params.pipeline_len; + let pipeline_epoch = checked!(current_epoch + params.pipeline_len)?; let stake = read_validator_stake(storage, ¶ms, validator, pipeline_epoch)?; @@ -1997,8 +2042,8 @@ where let last_infraction_epoch = read_validator_last_slash_epoch(storage, validator)?; if let Some(last_epoch) = last_infraction_epoch { - let is_frozen = - current_epoch < last_epoch + params.slash_processing_epoch_offset(); + let is_frozen = current_epoch + < checked!(last_epoch + params.slash_processing_epoch_offset())?; Ok(is_frozen) } else { Ok(false) @@ -2066,7 +2111,7 @@ where } let params = read_pos_params(storage)?; - let pipeline_epoch = current_epoch + params.pipeline_len; + let pipeline_epoch = checked!(current_epoch + params.pipeline_len)?; let src_redel_end_epoch = validator_incoming_redelegations_handle(src_validator) .get(storage, delegator)?; @@ -2078,12 +2123,13 @@ where // started contributing to the src validator's voting power, these tokens // cannot be slashed anymore let is_not_chained = if let Some(end_epoch) = src_redel_end_epoch { - let last_contrib_epoch = end_epoch.prev(); + let last_contrib_epoch = + end_epoch.prev().expect("End epoch cannot be 0"); // If the source validator's slashes that would cause slash on // redelegation are now outdated (would have to be processed before or // on start of the current epoch), the redelegation can be redelegated // again - last_contrib_epoch + params.slash_processing_epoch_offset() + checked!(last_contrib_epoch + params.slash_processing_epoch_offset())? <= current_epoch } else { true @@ -2119,8 +2165,9 @@ where .at(&pipeline_epoch) .at(src_validator); for (&epoch, &unbonded_amount) in result_unbond.epoch_map.iter() { - redelegated_bonds.update(storage, epoch, |current| { - current.unwrap_or_default() + unbonded_amount + redelegated_bonds.try_update(storage, epoch, |current| { + let current = current.unwrap_or_default(); + Ok(checked!(current + unbonded_amount)?) })?; } @@ -2161,10 +2208,13 @@ where validator_outgoing_redelegations_handle(src_validator) .at(dest_validator); for (start, &unbonded_amount) in result_unbond.epoch_map.iter() { - outgoing_redelegations.at(start).update( + outgoing_redelegations.at(start).try_update( storage, current_epoch, - |current| current.unwrap_or_default() + unbonded_amount, + |current| { + let current = current.unwrap_or_default(); + Ok(checked!(current + unbonded_amount)?) + }, )?; } @@ -2174,9 +2224,14 @@ where .at(&pipeline_epoch) .at(src_validator); for (&epoch, &amount) in &result_unbond.epoch_map { - dest_total_redelegated_bonded.update(storage, epoch, |current| { - current.unwrap_or_default() + amount - })?; + dest_total_redelegated_bonded.try_update( + storage, + epoch, + |current| { + let current = current.unwrap_or_default(); + Ok(checked!(current + amount)?) + }, + )?; } // Set the epoch of the validator incoming redelegation from this delegator @@ -2249,7 +2304,7 @@ where S: StorageRead + StorageWrite, { let params = read_pos_params(storage)?; - let pipeline_epoch = current_epoch + params.pipeline_len; + let pipeline_epoch = checked!(current_epoch + params.pipeline_len)?; let pipeline_state = match validator_state_handle(validator).get( storage, @@ -2330,7 +2385,7 @@ where S: StorageRead + StorageWrite, { let params = read_pos_params(storage)?; - let pipeline_epoch = current_epoch + params.pipeline_len; + let pipeline_epoch = checked!(current_epoch + params.pipeline_len)?; // Make sure state is Inactive at every epoch up through the pipeline for epoch in Epoch::iter_bounds_inclusive(current_epoch, pipeline_epoch) { @@ -2507,15 +2562,16 @@ where S: StorageRead + StorageWrite, { // Derive the actual missing votes limit from the percentage - let missing_votes_threshold = ((Dec::one() - params.liveness_threshold) - * params.liveness_window_check) - .to_uint() - .ok_or_else(|| { - namada_storage::Error::SimpleMessage( - "Found negative liveness threshold", - ) - })? - .as_u64(); + let missing_votes_threshold = checked!( + (Dec::one() - params.liveness_threshold) * params.liveness_window_check + )? + .to_uint() + .ok_or_else(|| { + namada_storage::Error::SimpleMessage( + "Found negative liveness threshold", + ) + })? + .as_u64(); // Jail inactive validators let validators_to_jail = liveness_sum_missed_votes_handle() @@ -2735,7 +2791,9 @@ where )?; // Add reward tokens tallied during previous withdrawals - reward_tokens += take_rewards_from_counter(storage, &source, validator)?; + let counter_rewards = + take_rewards_from_counter(storage, &source, validator)?; + reward_tokens = checked!(reward_tokens + counter_rewards)?; // Update the last claim epoch in storage write_last_reward_claim_epoch(storage, &source, validator, current_epoch)?; @@ -2768,7 +2826,8 @@ where let rewards_from_counter = read_rewards_counter(storage, &source, validator)?; - Ok(rewards_from_bonds + rewards_from_counter) + let res = checked!(rewards_from_bonds + rewards_from_counter)?; + Ok(res) } /// Jail a validator by removing it from and updating the validator sets and @@ -2799,7 +2858,7 @@ where let end = params.pipeline_len; for offset in start..=end { - let epoch = current_epoch + offset; + let epoch = checked!(current_epoch + offset)?; let prev_state = validator_state_handle(validator) .get(storage, epoch, params)? .expect("Expected to find a valid validator."); @@ -2889,7 +2948,7 @@ where storage, &pos_params, current_epoch, - current_epoch + pos_params.pipeline_len, + checked!(current_epoch + pos_params.pipeline_len)?, )?; // Compute the total stake of the consensus validator set and record @@ -2940,19 +2999,20 @@ where // Consensus set liveness check if !votes.is_empty() { - let vote_height = height.prev_height(); - let epoch_of_votes = - storage.get_pred_epochs()?.get_epoch(vote_height).expect( - "Should always find an epoch when looking up the vote height \ - before recording liveness data.", - ); - record_liveness_data( - storage, - &votes, - epoch_of_votes, - vote_height, - &pos_params, - )?; + if let Some(vote_height) = height.prev_height() { + let epoch_of_votes = + storage.get_pred_epochs()?.get_epoch(vote_height).expect( + "Should always find an epoch when looking up the vote \ + height before recording liveness data.", + ); + record_liveness_data( + storage, + &votes, + epoch_of_votes, + vote_height, + &pos_params, + )?; + } } // Jail validators for inactivity diff --git a/crates/proof_of_stake/src/parameters.rs b/crates/proof_of_stake/src/parameters.rs index c35f8fd59b..345f97ef50 100644 --- a/crates/proof_of_stake/src/parameters.rs +++ b/crates/proof_of_stake/src/parameters.rs @@ -3,6 +3,7 @@ use std::str::FromStr; use borsh::{BorshDeserialize, BorshSerialize}; +use namada_core::arith::checked; use namada_core::dec::Dec; use namada_core::storage::Epoch; use namada_core::token; @@ -126,6 +127,8 @@ pub enum ValidationError { TotalVotingPowerTooLarge(Uint), #[error("Votes per token cannot be greater than 1, got {0}")] VotesPerTokenGreaterThanOne(Dec), + #[error("Liveness threshold cannot be greater than 1, got {0}")] + LivenessThresholdGreaterThanOne(Dec), #[error("Pipeline length must be >= 2, got {0}")] PipelineLenTooShort(u64), #[error( @@ -168,11 +171,14 @@ impl OwnedPosParams { // Check maximum total voting power cannot get larger than what // Tendermint allows - let max_total_voting_power = (self.tm_votes_per_token - * TOKEN_MAX_AMOUNT - * self.max_validator_slots) - .to_uint() - .expect("Cannot fail"); + let max_total_voting_power = checked!( + self.tm_votes_per_token + * TOKEN_MAX_AMOUNT + * self.max_validator_slots + ) + .expect("Must be able to calculate max total voting power") + .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 { @@ -193,6 +199,12 @@ impl OwnedPosParams { )) } + if self.liveness_threshold > Dec::one() { + errors.push(ValidationError::LivenessThresholdGreaterThanOne( + self.liveness_threshold, + )) + } + errors } @@ -215,18 +227,20 @@ impl OwnedPosParams { ) -> (Epoch, Epoch) { let start = infraction_epoch .sub_or_default(Epoch(self.cubic_slashing_window_length)); - let end = infraction_epoch + self.cubic_slashing_window_length; + let end = + infraction_epoch.unchecked_add(self.cubic_slashing_window_length); (start, end) } /// Get the redelegation end epoch from the start epoch pub fn redelegation_end_epoch_from_start(&self, end: Epoch) -> Epoch { - end + self.pipeline_len + end.unchecked_add(self.pipeline_len) } /// Get the redelegation start epoch from the end epoch pub fn redelegation_start_epoch_from_end(&self, end: Epoch) -> Epoch { - end - self.pipeline_len + end.checked_sub(self.pipeline_len) + .expect("End epoch is always gt. pipeline") } /// Determine if the infraction is in the lazy slashing window for a @@ -245,8 +259,8 @@ impl OwnedPosParams { redel_start: Epoch, redel_end: Epoch, ) -> bool { - let processing_epoch = - infraction_epoch + self.slash_processing_epoch_offset(); + let processing_epoch = infraction_epoch + .unchecked_add(self.slash_processing_epoch_offset()); redel_start < processing_epoch && infraction_epoch < redel_end } diff --git a/crates/proof_of_stake/src/queries.rs b/crates/proof_of_stake/src/queries.rs index 5d643d2f48..84853b06c5 100644 --- a/crates/proof_of_stake/src/queries.rs +++ b/crates/proof_of_stake/src/queries.rs @@ -147,11 +147,7 @@ where { let max_epoch = Epoch(u64::MAX); let delegations = find_delegations(storage, source, &max_epoch)?; - Ok(!delegations - .values() - .cloned() - .sum::() - .is_zero()) + Ok(!delegations.values().all(token::Amount::is_zero)) } /// Find raw bond deltas for the given source and validator address. @@ -440,7 +436,10 @@ fn make_bond_details( for slash in slashes { if slash.epoch >= start { let cur_rate = slash_rates_by_epoch.entry(slash.epoch).or_default(); - *cur_rate = cmp::min(Dec::one(), *cur_rate + slash.rate); + *cur_rate = cmp::min( + Dec::one(), + cur_rate.checked_add(slash.rate).unwrap_or_else(Dec::one), + ); if !prev_applied_slashes.iter().any(|s| s == slash) { validator_slashes.push(slash.clone()); @@ -454,7 +453,11 @@ fn make_bond_details( let amount_after_slashing = get_slashed_amount(params, deltas_sum, &slash_rates_by_epoch) .unwrap(); - Some(deltas_sum - amount_after_slashing) + Some( + deltas_sum + .checked_sub(amount_after_slashing) + .unwrap_or_default(), + ) }; BondDetails { @@ -492,7 +495,10 @@ fn make_unbond_details( .unwrap_or_default() { let cur_rate = slash_rates_by_epoch.entry(slash.epoch).or_default(); - *cur_rate = cmp::min(Dec::one(), *cur_rate + slash.rate); + *cur_rate = cmp::min( + Dec::one(), + cur_rate.checked_add(slash.rate).unwrap_or_else(Dec::one), + ); if !prev_applied_slashes.iter().any(|s| s == slash) { validator_slashes.push(slash.clone()); @@ -505,7 +511,11 @@ fn make_unbond_details( } else { let amount_after_slashing = get_slashed_amount(params, amount, &slash_rates_by_epoch).unwrap(); - Some(amount - amount_after_slashing) + Some( + amount + .checked_sub(amount_after_slashing) + .unwrap_or_default(), + ) }; UnbondDetails { diff --git a/crates/proof_of_stake/src/rewards.rs b/crates/proof_of_stake/src/rewards.rs index 6258d1c94d..4f20045585 100644 --- a/crates/proof_of_stake/src/rewards.rs +++ b/crates/proof_of_stake/src/rewards.rs @@ -2,6 +2,7 @@ use namada_controller::PDController; use namada_core::address::{self, Address}; +use namada_core::arith::{self, checked}; use namada_core::collections::{HashMap, HashSet}; use namada_core::dec::Dec; use namada_core::storage::{BlockHeight, Epoch}; @@ -48,6 +49,10 @@ pub enum RewardsError { /// rewards coefficients are not set #[error("Rewards coefficients are not properly set.")] CoeffsNotSet, + #[error("Arith {0}")] + Arith(#[from] arith::Error), + #[error("Dec {0}")] + Dec(#[from] namada_core::dec::Error), } /// Compute PoS inflation amount @@ -73,10 +78,16 @@ pub fn compute_inflation( target_ratio, last_ratio, ); - let metric = Dec::from(locked_amount) / Dec::from(total_native_amount); - let control_coeff = controller.get_total_native_dec() * max_reward_rate - / controller.get_epochs_per_year(); - let amount_uint = controller.compute_inflation(control_coeff, metric); + let locked_amount = Dec::try_from(locked_amount).into_storage_result()?; + let total_native_dec = + controller.get_total_native_dec().into_storage_result()?; + let metric = checked!(locked_amount / total_native_dec)?; + let control_coeff = checked!( + total_native_dec * max_reward_rate / controller.get_epochs_per_year() + )?; + let amount_uint = controller + .compute_inflation(control_coeff, metric) + .into_storage_result()?; token::Amount::from_uint(amount_uint, 0).into_storage_result() } @@ -125,12 +136,17 @@ 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_reward_coeff = Dec::try_from( + checked!(signing_stake - votes_needed)? + .mul_floor(proposer_reward)?, + )?; + let total_stake_dec = Dec::try_from(total_stake)?; + let proposer_coeff = checked!( + proposer_reward_coeff / total_stake_dec + MIN_PROPOSER_REWARD + )?; let signer_coeff = signer_reward; - let active_val_coeff = Dec::one() - proposer_coeff - signer_coeff; + let active_val_coeff = + checked!(Dec::one() - proposer_coeff - signer_coeff)?; let coeffs = PosRewards { proposer_coeff, @@ -145,11 +161,12 @@ impl PosRewardsCalculator { fn get_min_required_votes(&self) -> Amount { (self .total_stake - .checked_mul(2.into()) + .checked_mul(2_u64) .expect("Amount overflow while computing minimum required votes") .checked_add((3u64 - 1u64).into()) .expect("Amount overflow while computing minimum required votes")) - / 3u64 + .checked_div_u64(3u64) + .expect("Div by non-zero cannot fail") } } @@ -172,7 +189,7 @@ where log_block_rewards_aux( storage, if new_epoch { - current_epoch.prev() + current_epoch.prev().expect("New epoch must have prev") } else { current_epoch }, @@ -254,7 +271,8 @@ where } signer_set.insert(validator_address); - total_signing_stake += stake_from_deltas; + total_signing_stake = + checked!(total_signing_stake + stake_from_deltas)?; } // Get the block rewards coefficients (proposing, signing/voting, @@ -280,8 +298,10 @@ 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 signing_stake_unscaled: Dec = total_signing_stake.into(); + let consensus_stake_unscaled: Dec = + Dec::try_from(total_consensus_stake).into_storage_result()?; + let signing_stake_unscaled: Dec = + Dec::try_from(total_signing_stake).into_storage_result()?; let mut values: HashMap = HashMap::new(); for validator in consensus_validators.iter(storage)? { let ( @@ -297,7 +317,7 @@ where } let mut rewards_frac = Dec::zero(); - let stake_unscaled: Dec = stake.into(); + let stake_unscaled: Dec = Dec::try_from(stake).into_storage_result()?; // tracing::debug!( // "NAMADA VALIDATOR STAKE (LOGGING BLOCK REWARDS) OF EPOCH {} = // {}", epoch, stake @@ -305,24 +325,31 @@ where // Proposer reward if address == *proposer_address { - rewards_frac += coeffs.proposer_coeff; + rewards_frac = checked!(rewards_frac + coeffs.proposer_coeff)?; } + // Signer reward if signer_set.contains(&address) { - let signing_frac = stake_unscaled / signing_stake_unscaled; - rewards_frac += coeffs.signer_coeff * signing_frac; + let signing_frac = + checked!(stake_unscaled / signing_stake_unscaled)?; + rewards_frac = + checked!(rewards_frac + (coeffs.signer_coeff * signing_frac))?; } // Consensus validator reward - rewards_frac += coeffs.active_val_coeff - * (stake_unscaled / consensus_stake_unscaled); + rewards_frac = checked!( + rewards_frac + + (coeffs.active_val_coeff + * (stake_unscaled / consensus_stake_unscaled)) + )?; // To be added to the rewards accumulator values.insert(address, rewards_frac); } for (address, value) in values.into_iter() { // Update the rewards accumulator - rewards_accumulator_handle().update(storage, address, |prev| { - prev.unwrap_or_default() + value + rewards_accumulator_handle().try_update(storage, address, |prev| { + let prev = prev.unwrap_or_default(); + Ok(checked!(prev + value)?) })?; } @@ -387,9 +414,9 @@ where // Write new rewards parameters that will be used for the inflation of // the current new epoch - let locked_amount = Dec::from(locked_amount); - let total_amount = Dec::from(total_tokens); - let locked_ratio = locked_amount / total_amount; + let locked_amount = Dec::try_from(locked_amount).into_storage_result()?; + let total_amount = Dec::try_from(total_tokens).into_storage_result()?; + let locked_ratio = checked!(locked_amount / total_amount)?; write_last_staked_ratio(storage, locked_ratio)?; write_last_pos_inflation_amount(storage, inflation)?; @@ -426,16 +453,17 @@ where let mut accumulators_sum = Dec::zero(); for acc in rewards_accumulator_handle().iter(storage)? { let (validator, value) = acc?; - accumulators_sum += value; + accumulators_sum = checked!(accumulators_sum + value)?; // Get reward token amount for this validator - let fractional_claim = value / num_blocks_in_last_epoch; - let reward_tokens = fractional_claim * inflation; + let fractional_claim = checked!(value / num_blocks_in_last_epoch)?; + let reward_tokens = inflation.mul_floor(fractional_claim)?; // Get validator stake at the last epoch - let stake = Dec::from(read_validator_stake( + let stake = Dec::try_from(read_validator_stake( storage, params, &validator, last_epoch, - )?); + )?) + .into_storage_result()?; let commission_rate = validator_commission_rate_handle(&validator) .get(storage, last_epoch, params)? @@ -446,13 +474,16 @@ where // a single product, we're also taking out commission on validator's // self-bonds, but it is then included in the rewards claimable by the // validator so they get it back. - let product = - (Dec::one() - commission_rate) * Dec::from(reward_tokens) / stake; + let reward_tokens_dec = + Dec::try_from(reward_tokens).into_storage_result()?; + let product = checked!( + (Dec::one() - commission_rate) * reward_tokens_dec / stake + )?; // Tally the commission tokens earned by the validator. // TODO: think abt Dec rounding and if `new_product` should be used // instead of `reward_tokens` - let commissions = commission_rate * reward_tokens; + let commissions = reward_tokens.mul_floor(commission_rate)?; new_rewards_products.insert( validator, @@ -462,7 +493,8 @@ where }, ); - reward_tokens_remaining -= reward_tokens; + reward_tokens_remaining = + checked!(reward_tokens_remaining - reward_tokens)?; } for ( validator, @@ -479,7 +511,7 @@ where } // Mint tokens to the PoS account for the last epoch's inflation - let pos_reward_tokens = inflation - reward_tokens_remaining; + let pos_reward_tokens = checked!(inflation - reward_tokens_remaining)?; tracing::info!( "Minting tokens for PoS rewards distribution into the PoS account. \ Amount: {}. Total inflation: {}. Total native supply: {}. Number of \ @@ -546,8 +578,9 @@ where // rewards are computed at the end of an epoch let (claim_start, claim_end) = ( last_claim_epoch.unwrap_or_default(), - // Safe because of the check above - current_epoch.prev(), + current_epoch + .prev() + .expect("Safe because of the check above"), ); let bond_amounts = bond_amounts_for_rewards( storage, @@ -564,8 +597,8 @@ where debug_assert!(ep >= claim_start); debug_assert!(ep <= claim_end); let rp = rewards_products.get(storage, &ep)?.unwrap_or_default(); - let reward = rp * bond_amount; - reward_tokens += reward; + let reward = bond_amount.mul_floor(rp)?; + reward_tokens = checked!(reward_tokens + reward)?; } Ok(reward_tokens) @@ -584,7 +617,7 @@ where let key = storage_key::rewards_counter_key(source, validator); let current_rewards = storage.read::(&key)?.unwrap_or_default(); - storage.write(&key, current_rewards + new_rewards) + storage.write(&key, checked!(current_rewards + new_rewards)?) } /// Take tokens from a rewards counter. Deletes the record after reading. @@ -645,8 +678,8 @@ mod tests { Dec::from_str("0.5").unwrap(), ) .unwrap(); - let locked_ratio_0 = - Dec::from(locked_amount) / Dec::from(total_native_amount); + let locked_ratio_0 = Dec::try_from(locked_amount).unwrap() + / Dec::try_from(total_native_amount).unwrap(); println!( "Round 0: Locked ratio: {locked_ratio_0}, inflation: {inflation_0}" @@ -673,8 +706,8 @@ mod tests { // BUG: DIDN'T ADD TO TOTAL AMOUNT - let locked_ratio_1 = - Dec::from(locked_amount) / Dec::from(total_native_amount); + let locked_ratio_1 = Dec::try_from(locked_amount).unwrap() + / Dec::try_from(total_native_amount).unwrap(); println!( "Round 1: Locked ratio: {locked_ratio_1}, inflation: {inflation_1}" @@ -701,8 +734,8 @@ mod tests { ) .unwrap(); - let locked_ratio_2 = - Dec::from(locked_amount) / Dec::from(total_native_amount); + let locked_ratio_2 = Dec::try_from(locked_amount).unwrap() + / Dec::try_from(total_native_amount).unwrap(); println!( "Round 2: Locked ratio: {locked_ratio_2}, inflation: {inflation_2}", ); @@ -735,8 +768,8 @@ mod tests { Dec::from_str("0.9").unwrap(), ) .unwrap(); - let locked_ratio_0 = - Dec::from(locked_amount) / Dec::from(total_native_amount); + let locked_ratio_0 = Dec::try_from(locked_amount).unwrap() + / Dec::try_from(total_native_amount).unwrap(); println!( "Round 0: Locked ratio: {locked_ratio_0}, inflation: {inflation_0}" @@ -763,8 +796,8 @@ mod tests { // BUG: DIDN'T ADD TO TOTAL AMOUNT - let locked_ratio_1 = - Dec::from(locked_amount) / Dec::from(total_native_amount); + let locked_ratio_1 = Dec::try_from(locked_amount).unwrap() + / Dec::try_from(total_native_amount).unwrap(); println!( "Round 1: Locked ratio: {locked_ratio_1}, inflation: {inflation_1}" @@ -791,8 +824,8 @@ mod tests { ) .unwrap(); - let locked_ratio_2 = - Dec::from(locked_amount) / Dec::from(total_native_amount); + let locked_ratio_2 = Dec::try_from(locked_amount).unwrap() + / Dec::try_from(total_native_amount).unwrap(); println!( "Round 2: Locked ratio: {locked_ratio_2}, inflation: {inflation_2}", ); @@ -839,11 +872,12 @@ mod tests { last_locked_ratio, ) .unwrap(); - let locked_ratio = - Dec::from(locked_amount) / Dec::from(total_native_tokens); + let locked_ratio = Dec::try_from(locked_amount).unwrap() + / Dec::try_from(total_native_tokens).unwrap(); - let rate = Dec::from(inflation) * Dec::from(epochs_per_year) - / Dec::from(total_native_tokens); + let rate = Dec::try_from(inflation).unwrap() + * Dec::from(epochs_per_year) + / Dec::try_from(total_native_tokens).unwrap(); println!( "Round {round}: Locked ratio: {locked_ratio}, inflation rate: \ {rate}", @@ -862,7 +896,7 @@ mod tests { let tot_tokens = Dec::try_from(total_native_tokens.raw_amount()).unwrap(); let change_staked_tokens = - token::Amount::from(staking_growth * tot_tokens); + token::Amount::try_from(staking_growth * tot_tokens).unwrap(); locked_amount = std::cmp::min( total_native_tokens, diff --git a/crates/proof_of_stake/src/slashing.rs b/crates/proof_of_stake/src/slashing.rs index 3965ff6014..163c15294c 100644 --- a/crates/proof_of_stake/src/slashing.rs +++ b/crates/proof_of_stake/src/slashing.rs @@ -5,6 +5,7 @@ use std::collections::{BTreeMap, BTreeSet}; use borsh::BorshDeserialize; use namada_core::address::Address; +use namada_core::arith::{self, checked}; use namada_core::collections::HashMap; use namada_core::dec::Dec; use namada_core::key::tm_raw_hash_to_string; @@ -15,7 +16,7 @@ use namada_storage::collections::lazy_map::{ Collectable, NestedMap, NestedSubKey, SubKey, }; use namada_storage::collections::LazyMap; -use namada_storage::{StorageRead, StorageWrite}; +use namada_storage::{OptionExt, ResultExt, StorageRead, StorageWrite}; use crate::storage::{ enqueued_slashes_handle, read_pos_params, read_validator_last_slash_epoch, @@ -67,9 +68,10 @@ where }; // Disregard evidences that should have already been processed // at this time - if evidence_epoch + pos_params.slash_processing_epoch_offset() - - pos_params.cubic_slashing_window_length - <= current_epoch + if checked!( + evidence_epoch + pos_params.slash_processing_epoch_offset() + - pos_params.cubic_slashing_window_length + )? <= current_epoch { tracing::info!( "Skipping outdated evidence from epoch {evidence_epoch}" @@ -157,7 +159,7 @@ where }; // Need `+1` because we process at the beginning of a new epoch let processing_epoch = - evidence_epoch + params.slash_processing_epoch_offset(); + checked!(evidence_epoch + params.slash_processing_epoch_offset())?; // Add the slash to the list of enqueued slashes to be processed at a later // epoch. If a slash at the same block height already exists, return early. @@ -212,7 +214,7 @@ where return Ok(()); } let infraction_epoch = - current_epoch - params.slash_processing_epoch_offset(); + checked!(current_epoch - params.slash_processing_epoch_offset())?; // Slashes to be processed in the current epoch let enqueued_slashes = enqueued_slashes_handle().at(¤t_epoch); @@ -266,7 +268,8 @@ where cur_slashes.push(updated_slash); let cur_rate = eager_validator_slash_rates.entry(validator).or_default(); - *cur_rate = cmp::min(Dec::one(), *cur_rate + slash_rate); + let new_rate = checked!(cur_rate + slash_rate)?; + *cur_rate = cmp::min(Dec::one(), new_rate); } // Update the epochs of enqueued slashes in storage @@ -314,7 +317,7 @@ where storage, ¶ms, &validator, - -slash_amount.change(), + checked!(-slash_amount.change())?, epoch, Some(0), )?; @@ -322,14 +325,15 @@ where } // Then update validator and total deltas for (epoch, slash_amount) in slash_amounts { - let slash_delta = slash_amount - slash_acc; - slash_acc += slash_delta; + let slash_delta = checked!(slash_amount - slash_acc)?; + slash_acc = checked!(slash_acc + slash_delta)?; + let neg_slash_delta = checked!(-slash_delta.change())?; update_validator_deltas( storage, ¶ms, &validator, - -slash_delta.change(), + neg_slash_delta, epoch, Some(0), )?; @@ -343,7 +347,7 @@ where update_total_deltas( storage, ¶ms, - -slash_delta.change(), + neg_slash_delta, epoch, Some(0), !is_jailed_or_inactive, @@ -398,7 +402,7 @@ where S: StorageRead, { let infraction_epoch = - current_epoch - params.slash_processing_epoch_offset(); + checked!(current_epoch - params.slash_processing_epoch_offset())?; for res in outgoing_redelegations.iter(storage)? { let ( @@ -465,13 +469,13 @@ where ); let infraction_epoch = - current_epoch - params.slash_processing_epoch_offset(); + checked!(current_epoch - params.slash_processing_epoch_offset())?; // Slash redelegation destination validator from the next epoch only // as they won't be jailed let set_update_epoch = current_epoch.next(); - let mut init_tot_unbonded = + let redelegated_unbonded: Vec = Epoch::iter_bounds_inclusive(infraction_epoch.next(), set_update_epoch) .map(|epoch| { let redelegated_unbonded = total_redelegated_unbonded @@ -480,9 +484,12 @@ where .at(src_validator) .get(storage, &bond_start)? .unwrap_or_default(); - Ok(redelegated_unbonded) + Ok::<_, namada_storage::Error>(redelegated_unbonded) }) - .sum::>()?; + .collect::>()?; + let mut init_tot_unbonded = + token::Amount::sum(redelegated_unbonded.into_iter()) + .ok_or_err_msg("token amount overflow")?; for epoch in Epoch::iter_range(set_update_epoch, params.pipeline_len) { let updated_total_unbonded = { @@ -492,7 +499,7 @@ where .at(src_validator) .get(storage, &bond_start)? .unwrap_or_default(); - init_tot_unbonded + redelegated_unbonded + checked!(init_tot_unbonded + redelegated_unbonded)? }; let list_slashes = slashes @@ -504,7 +511,7 @@ where params.redelegation_start_epoch_from_end(redel_bond_start), redel_bond_start, ) && bond_start <= slash.epoch - && slash.epoch + params.slash_processing_epoch_offset() + && slash.epoch.unchecked_add(params.slash_processing_epoch_offset()) // We're looking for slashes that were processed before or in the epoch // in which slashes that are currently being processed // occurred. Because we're slashing in the beginning of an @@ -520,8 +527,8 @@ where .unwrap_or_default(); let slashed = - apply_list_slashes(params, &list_slashes, slashable_amount) - .mul_ceil(slash_rate); + apply_list_slashes(params, &list_slashes, slashable_amount)? + .mul_ceil(slash_rate)?; let list_slashes = slashes .iter(storage)? @@ -536,14 +543,14 @@ where .collect::>(); let slashable_stake = - apply_list_slashes(params, &list_slashes, slashable_amount) - .mul_ceil(slash_rate); + apply_list_slashes(params, &list_slashes, slashable_amount)? + .mul_ceil(slash_rate)?; init_tot_unbonded = updated_total_unbonded; let to_slash = cmp::min(slashed, slashable_stake); if !to_slash.is_zero() { let map_value = slashed_amounts.entry(epoch).or_default(); - *map_value += to_slash; + *map_value = checked!(map_value + to_slash)?; } } @@ -575,7 +582,7 @@ where { tracing::debug!("Slashing validator {} at rate {}", validator, slash_rate); let infraction_epoch = - current_epoch - params.slash_processing_epoch_offset(); + checked!(current_epoch - params.slash_processing_epoch_offset())?; let total_unbonded = total_unbonded_handle(validator); let total_redelegated_unbonded = @@ -618,10 +625,10 @@ where .iter_range(params.pipeline_len) .collect::>(); for epoch in eps.into_iter().rev() { - let amount = tot_bonds.iter().fold( + let amount = tot_bonds.iter().try_fold( token::Amount::zero(), |acc, (bond_start, bond_amount)| { - acc + compute_slash_bond_at_epoch( + let slashed = compute_slash_bond_at_epoch( storage, params, validator, @@ -631,10 +638,12 @@ where *bond_amount, redelegated_bonds.get(bond_start), slash_rate, - ) - .unwrap() + )?; + Ok::(checked!( + acc + slashed + )?) }, - ); + )?; let new_bonds = total_unbonded.at(&epoch); tot_bonds = new_bonds @@ -664,16 +673,20 @@ where redelegated_bonds = new_redelegated_bonds; // `newSum` - sum += amount; + sum = checked!(sum + amount)?; // `newSlashesMap` let cur = slashed_amounts.entry(epoch).or_default(); - *cur += sum; + *cur = checked!(cur + sum)?; } // Hack - should this be done differently? (think this is safe) - let pipeline_epoch = current_epoch + params.pipeline_len; + let pipeline_epoch = checked!(current_epoch + params.pipeline_len)?; let last_amt = slashed_amounts - .get(&pipeline_epoch.prev()) + .get( + &pipeline_epoch + .prev() + .expect("Pipeline epoch must have prev"), + ) .cloned() .unwrap(); slashed_amounts.insert(pipeline_epoch, last_amt); @@ -704,12 +717,16 @@ where .map(Result::unwrap) .filter(|slash| { start <= slash.epoch - && slash.epoch + params.slash_processing_epoch_offset() <= epoch + && slash + .epoch + .unchecked_add(params.slash_processing_epoch_offset()) + <= epoch }) .collect::>(); - let slash_epoch_filter = - |e: Epoch| e + params.slash_processing_epoch_offset() <= epoch; + let slash_epoch_filter = |e: Epoch| { + e.unchecked_add(params.slash_processing_epoch_offset()) <= epoch + }; let result_fold = redelegated_bonds .map(|redelegated_bonds| { @@ -722,13 +739,17 @@ where slash_epoch_filter, ) }) + .transpose()? .unwrap_or_default(); - let total_not_redelegated = amount - result_fold.total_redelegated; + let total_not_redelegated = + checked!(amount - result_fold.total_redelegated)?; let after_not_redelegated = - apply_list_slashes(params, &list_slashes, total_not_redelegated); + apply_list_slashes(params, &list_slashes, total_not_redelegated)?; - Ok(after_not_redelegated + result_fold.total_after_slashing) + Ok(checked!( + after_not_redelegated + result_fold.total_after_slashing + )?) } /// Uses `fn compute_bond_at_epoch` to compute the token amount to slash in @@ -757,7 +778,7 @@ where bond_amount, redelegated_bonds, )? - .mul_ceil(slash_rate); + .mul_ceil(slash_rate)?; let slashable_amount = compute_bond_at_epoch( storage, params, @@ -789,7 +810,8 @@ where && end.map(|end| slash.epoch < end).unwrap_or(true) { let cur_rate = slashes.entry(slash.epoch).or_default(); - *cur_rate = cmp::min(*cur_rate + slash.rate, Dec::one()); + let new_rate = checked!(cur_rate + slash.rate)?; + *cur_rate = cmp::min(new_rate, Dec::one()); } } Ok(slashes) @@ -805,17 +827,17 @@ pub fn apply_list_slashes( params: &OwnedPosParams, slashes: &[Slash], amount: token::Amount, -) -> token::Amount { +) -> Result { let mut final_amount = amount; let mut computed_slashes = BTreeMap::::new(); for slash in slashes { let slashed_amount = - compute_slashable_amount(params, slash, amount, &computed_slashes); + compute_slashable_amount(params, slash, amount, &computed_slashes)?; final_amount = final_amount.checked_sub(slashed_amount).unwrap_or_default(); computed_slashes.insert(slash.epoch, slashed_amount); } - final_amount + Ok(final_amount) } /// Computes how much is left from a bond or unbond after applying a slash given @@ -826,7 +848,7 @@ pub fn compute_slashable_amount( slash: &Slash, amount: token::Amount, computed_slashes: &BTreeMap, -) -> token::Amount { +) -> Result { let updated_amount = computed_slashes .iter() .filter(|(&epoch, _)| { @@ -834,7 +856,8 @@ pub fn compute_slashable_amount( // current slash occurred. We use `<=` because slashes processed at // `slash.epoch` (at the start of the epoch) are also processed // before this slash occurred. - epoch + params.slash_processing_epoch_offset() <= slash.epoch + epoch.unchecked_add(params.slash_processing_epoch_offset()) + <= slash.epoch }) .fold(amount, |acc, (_, &amnt)| { acc.checked_sub(amnt).unwrap_or_default() @@ -942,8 +965,9 @@ pub fn get_slashed_amount( for (ix, slashed_amount) in computed_amounts.iter().enumerate() { // Update amount with slashes that happened more than unbonding_len // epochs before this current slash - if slashed_amount.epoch + params.slash_processing_epoch_offset() - <= infraction_epoch + if checked!( + slashed_amount.epoch + params.slash_processing_epoch_offset() + )? <= infraction_epoch { updated_amount = updated_amount .checked_sub(slashed_amount.amount) @@ -958,15 +982,15 @@ pub fn get_slashed_amount( computed_amounts.remove(item.0); } computed_amounts.push(SlashedAmount { - amount: updated_amount.mul_ceil(slash_rate), + amount: updated_amount.mul_ceil(slash_rate)?, epoch: infraction_epoch, }); } - let total_computed_amounts = computed_amounts - .into_iter() - .map(|slashed| slashed.amount) - .sum(); + let total_computed_amounts = token::Amount::sum( + computed_amounts.into_iter().map(|slashed| slashed.amount), + ) + .ok_or_err_msg("token amount overflow")?; let final_amount = updated_amount .checked_sub(total_computed_amounts) @@ -1007,7 +1031,7 @@ where start_epoch, &list_slashes, |_| true, - ) + )? } else { FoldRedelegatedBondsResult::default() }; @@ -1017,12 +1041,13 @@ where .unwrap_or_default(); // `val afterNoRedelegated` let after_not_redelegated = - apply_list_slashes(params, &list_slashes, total_not_redelegated); + apply_list_slashes(params, &list_slashes, total_not_redelegated)?; // `val amountAfterSlashing` let amount_after_slashing = - after_not_redelegated + result_fold.total_after_slashing; + checked!(after_not_redelegated + result_fold.total_after_slashing)?; // Accumulation step - result_slashing.sum += amount_after_slashing; + result_slashing.sum = + checked!(result_slashing.sum + amount_after_slashing)?; result_slashing .epoch_map .insert(start_epoch, amount_after_slashing); @@ -1052,9 +1077,11 @@ where { // TODO: check if slashes in the same epoch can be // folded into one effective slash - let end_epoch = *withdraw_epoch - - params.unbonding_len - - params.cubic_slashing_window_length; + let end_epoch = checked!( + withdraw_epoch + - params.unbonding_len + - params.cubic_slashing_window_length + )?; // Find slashes that apply to `start_epoch..end_epoch` let list_slashes = slashes .iter() @@ -1075,19 +1102,21 @@ where *start_epoch, &list_slashes, |_| true, - ); + )?; // Unbond amount that didn't come from a redelegation - let total_not_redelegated = *amount - result_fold.total_redelegated; + let total_not_redelegated = + checked!(amount - result_fold.total_redelegated)?; // Find how much remains after slashing non-redelegated amount let after_not_redelegated = - apply_list_slashes(params, &list_slashes, total_not_redelegated); + apply_list_slashes(params, &list_slashes, total_not_redelegated)?; // Add back the unbond and redelegated unbond amount after slashing let amount_after_slashing = - after_not_redelegated + result_fold.total_after_slashing; + checked!(after_not_redelegated + result_fold.total_after_slashing)?; - result_slashing.sum += amount_after_slashing; + result_slashing.sum = + checked!(result_slashing.sum + amount_after_slashing)?; result_slashing .epoch_map .insert(*start_epoch, amount_after_slashing); @@ -1205,13 +1234,15 @@ where for epoch in Epoch::iter_bounds_inclusive(start_epoch, end_epoch) { let consensus_stake = - Dec::from(get_total_consensus_stake(storage, epoch, params)?); + Dec::try_from(get_total_consensus_stake(storage, epoch, params)?) + .into_storage_result()?; tracing::debug!( "Total consensus stake in epoch {}: {}", epoch, consensus_stake ); - let processing_epoch = epoch + params.slash_processing_epoch_offset(); + let processing_epoch = + checked!(epoch + params.slash_processing_epoch_offset())?; let slashes = enqueued_slashes_handle().at(&processing_epoch); let infracting_stake = slashes.iter(storage)?.try_fold(Dec::zero(), |acc, res| { @@ -1228,14 +1259,15 @@ where // tracing::debug!("Val {} stake: {}", &validator, // validator_stake); - Ok::( - acc + Dec::from(validator_stake), - ) + let stake = + Dec::try_from(validator_stake).into_storage_result()?; + Ok::(checked!(acc + stake)?) })?; - sum_vp_fraction += infracting_stake / consensus_stake; + sum_vp_fraction = + checked!(sum_vp_fraction + (infracting_stake / consensus_stake))?; } - let cubic_rate = - Dec::new(9, 0).unwrap() * sum_vp_fraction * sum_vp_fraction; + let nine = Dec::from(9_u64); + let cubic_rate = checked!(nine * sum_vp_fraction * sum_vp_fraction)?; tracing::debug!("Cubic slash rate: {}", cubic_rate); Ok(cubic_rate) } diff --git a/crates/proof_of_stake/src/storage.rs b/crates/proof_of_stake/src/storage.rs index d431d9a7e4..6a8bd748db 100644 --- a/crates/proof_of_stake/src/storage.rs +++ b/crates/proof_of_stake/src/storage.rs @@ -5,6 +5,7 @@ use std::collections::BTreeSet; use namada_account::protocol_pk_key; use namada_core::address::Address; +use namada_core::arith::checked; use namada_core::collections::HashSet; use namada_core::dec::Dec; use namada_core::key::{common, tm_consensus_key_raw_hash}; @@ -14,7 +15,6 @@ use namada_governance::storage::get_max_proposal_period; use namada_storage::collections::lazy_map::NestedSubKey; use namada_storage::collections::{LazyCollection, LazySet}; use namada_storage::{Result, StorageRead, StorageWrite}; -use num_traits::CheckedAdd; use crate::storage_key::consensus_keys_key; use crate::types::{ @@ -496,12 +496,13 @@ where { let handle = validator_deltas_handle(validator); let offset = offset_opt.unwrap_or(params.pipeline_len); + let offset_epoch = checked!(current_epoch + offset)?; let val = handle - .get_delta_val(storage, current_epoch + offset)? + .get_delta_val(storage, offset_epoch)? .unwrap_or_default(); handle.set( storage, - val.checked_add(&delta) + val.checked_add(delta) .expect("Validator deltas updated amount should not overflow"), current_epoch, offset, @@ -704,15 +705,16 @@ where let offset = offset_opt.unwrap_or(params.pipeline_len); let total_deltas = total_deltas_handle(); let total_active_deltas = total_active_deltas_handle(); + let offset_epoch = checked!(current_epoch + offset)?; // Update total deltas let total_deltas_val = total_deltas - .get_delta_val(storage, current_epoch + offset)? + .get_delta_val(storage, offset_epoch)? .unwrap_or_default(); total_deltas.set( storage, total_deltas_val - .checked_add(&delta) + .checked_add(delta) .expect("Total deltas updated amount should not overflow"), current_epoch, offset, @@ -721,11 +723,11 @@ where // Update total active voting power if update_active_voting_power { let active_delta = total_active_deltas - .get_delta_val(storage, current_epoch + offset)? + .get_delta_val(storage, offset_epoch)? .unwrap_or_default(); total_active_deltas.set( storage, - active_delta.checked_add(&delta).expect( + active_delta.checked_add(delta).expect( "Total active voting power updated amount should not overflow", ), current_epoch, diff --git a/crates/proof_of_stake/src/tests/state_machine.rs b/crates/proof_of_stake/src/tests/state_machine.rs index 0ad85e9448..3fdeb0e5f9 100644 --- a/crates/proof_of_stake/src/tests/state_machine.rs +++ b/crates/proof_of_stake/src/tests/state_machine.rs @@ -824,7 +824,7 @@ impl ConcretePosState { fn check_next_epoch_post_conditions(&self, params: &PosParams) { let pipeline = self.current_epoch() + params.pipeline_len; - let before_pipeline = pipeline.prev(); + let before_pipeline = pipeline.prev().unwrap(); // Post-condition: Consensus validator sets at pipeline offset // must be the same as at the epoch before it. @@ -3604,7 +3604,7 @@ impl AbstractPosState { /// Copy validator sets and validator states at the given epoch from its /// predecessor fn copy_discrete_epoched_data(&mut self, epoch: Epoch) { - let prev_epoch = epoch.prev(); + let prev_epoch = epoch.prev().unwrap(); // Copy the non-delta data from the last epoch into the new one self.consensus_set.insert( epoch, @@ -4699,7 +4699,7 @@ impl AbstractPosState { } // Hack - should this be done differently? (think this is safe) let last_amt = slashed_amounts - .get(&self.pipeline().prev()) + .get(&self.pipeline().prev().unwrap()) .cloned() .unwrap(); slashed_amounts.insert(self.pipeline(), last_amt); @@ -4820,7 +4820,8 @@ impl AbstractPosState { redel_bonds, validator, ) - .mul_ceil(slash_rate); + .mul_ceil(slash_rate) + .unwrap(); let slashable_amount = self.compute_bond_at_epoch( epoch, bond_start, @@ -4993,7 +4994,8 @@ impl AbstractPosState { &list_slashes, slashable_amount, ) - .mul_ceil(slash_rate); + .mul_ceil(slash_rate) + .unwrap(); let list_slashes = slashes .iter() @@ -5014,7 +5016,8 @@ impl AbstractPosState { &list_slashes, slashable_amount, ) - .mul_ceil(slash_rate); + .mul_ceil(slash_rate) + .unwrap(); tot_unbonded = updated_total_unbonded; @@ -5196,8 +5199,8 @@ impl AbstractPosState { val_stake.to_string_native(), ); vp_frac_sum += Dec::from(slashes.len()) - * Dec::from(val_stake) - / Dec::from(consensus_stake); + * Dec::try_from(val_stake).unwrap() + / Dec::try_from(consensus_stake).unwrap(); } } } @@ -5392,7 +5395,7 @@ impl AbstractPosState { incoming_redelegations.get(src_validator); if let Some(incoming) = src_incoming_redelegations { if let Some(redel_end_epoch) = incoming.get(delegator) { - return redel_end_epoch.prev() + return redel_end_epoch.prev().unwrap() + params.slash_processing_epoch_offset() > current_epoch; } @@ -5736,7 +5739,7 @@ impl AbstractPosState { .fold(amount, |acc, (_, amnt)| { acc.checked_sub(*amnt).unwrap_or_default() }); - updated_amount.mul_ceil(slash.rate) + updated_amount.mul_ceil(slash.rate).unwrap() } } diff --git a/crates/proof_of_stake/src/tests/state_machine_v2.rs b/crates/proof_of_stake/src/tests/state_machine_v2.rs index 5c89bc6d98..652bc9d1e1 100644 --- a/crates/proof_of_stake/src/tests/state_machine_v2.rs +++ b/crates/proof_of_stake/src/tests/state_machine_v2.rs @@ -106,7 +106,7 @@ impl AbstractPosState { /// Copy validator sets and validator states at the given epoch from its /// predecessor fn copy_discrete_epoched_data(&mut self, epoch: Epoch) { - let prev_epoch = epoch.prev(); + let prev_epoch = epoch.prev().unwrap(); // Copy the non-delta data from the last epoch into the new one self.consensus_set.insert( epoch, @@ -163,7 +163,7 @@ impl AbstractPosState { amount: token::Amount, ) { // Last epoch in which it contributes to stake - let end = self.pipeline().prev(); + let end = self.pipeline().prev().unwrap(); let withdrawable_epoch = self.epoch + self.params.withdrawable_epoch_offset(); let pipeline_len = self.params.pipeline_len; @@ -195,8 +195,9 @@ impl AbstractPosState { mem::take(redeleg) } else { // We have to divide this bond in case there are slashes - let unbond_slash = - to_unbond.mul_ceil(redeleg.slash_rates_sum()); + let unbond_slash = to_unbond + .mul_ceil(redeleg.slash_rates_sum()) + .unwrap(); let to_unbond_after_slash = to_unbond - unbond_slash; to_unbond = token::Amount::zero(); @@ -253,8 +254,9 @@ impl AbstractPosState { mem::take(&mut bond.tokens) } else { // We have to divide this bond in case there are slashes - let unbond_slash = - to_unbond.mul_ceil(bond.tokens.slash_rates_sum()); + let unbond_slash = to_unbond + .mul_ceil(bond.tokens.slash_rates_sum()) + .unwrap(); let to_unbond_after_slash = to_unbond - unbond_slash; to_unbond = token::Amount::zero(); @@ -318,7 +320,7 @@ impl AbstractPosState { // Last epoch in which it contributes to stake of thhe source validator let current_epoch = self.epoch; let pipeline = self.pipeline(); - let src_end = pipeline.prev(); + let src_end = pipeline.prev().unwrap(); let withdrawable_epoch_offset = self.params.withdrawable_epoch_offset(); let pipeline_len = self.params.pipeline_len; @@ -361,8 +363,9 @@ impl AbstractPosState { } else { // We have to divide this bond in case there are // slashes - let unbond_slash = - to_unbond.mul_ceil(redeleg.slash_rates_sum()); + let unbond_slash = to_unbond + .mul_ceil(redeleg.slash_rates_sum()) + .unwrap(); let to_unbond_after_slash = to_unbond - unbond_slash; @@ -415,8 +418,9 @@ impl AbstractPosState { mem::take(&mut bond.tokens) } else { // We have to divide this bond in case there are slashes - let unbond_slash = - to_unbond.mul_ceil(bond.tokens.slash_rates_sum()); + let unbond_slash = to_unbond + .mul_ceil(bond.tokens.slash_rates_sum()) + .unwrap(); let to_unbond_after_slash = to_unbond - unbond_slash; to_unbond = token::Amount::zero(); @@ -1272,8 +1276,8 @@ impl AbstractPosState { val_stake.to_string_native(), ); vp_frac_sum += Dec::from(slashes.len()) - * Dec::from(val_stake) - / Dec::from(consensus_stake); + * Dec::try_from(val_stake).unwrap() + / Dec::try_from(consensus_stake).unwrap(); } } } @@ -1713,7 +1717,8 @@ impl TokensWithSlashes { // (applied after infraction epoch) let slashable_amount = self.amount + self.slashes_sum_after_epoch(infraction_epoch); - let amount = cmp::min(slashable_amount.mul_ceil(rate), self.amount); + let amount = + cmp::min(slashable_amount.mul_ceil(rate).unwrap(), self.amount); if !amount.is_zero() { self.amount -= amount; let slash = self.slashes.entry(processing_epoch).or_default(); @@ -2351,7 +2356,7 @@ impl StateMachineTest for ConcretePosState { + params.slash_processing_epoch_offset() > redeleg_start { - let slash = delta.mul_ceil(rate); + let slash = delta.mul_ceil(rate).unwrap(); this_amount_after_slash = this_amount_after_slash .checked_sub(slash) @@ -2367,7 +2372,7 @@ impl StateMachineTest for ConcretePosState { ) .unwrap(); for (_slash_epoch, rate) in slashes { - let slash = delta.mul_ceil(rate); + let slash = delta.mul_ceil(rate).unwrap(); this_amount_after_slash = this_amount_after_slash .checked_sub(slash) .unwrap_or_default(); @@ -2380,8 +2385,8 @@ impl StateMachineTest for ConcretePosState { // We have to divide this bond in case there are // slashes let slash_ratio = - Dec::from(this_amount_after_slash) - / Dec::from(delta); + Dec::try_from(this_amount_after_slash).unwrap() + / Dec::try_from(delta).unwrap(); amount_after_slash += slash_ratio * to_redelegate; to_redelegate = token::Amount::zero(); } @@ -2406,7 +2411,7 @@ impl StateMachineTest for ConcretePosState { ) .unwrap(); for (_slash_epoch, rate) in slashes { - let slash = bond_delta.mul_ceil(rate); + let slash = bond_delta.mul_ceil(rate).unwrap(); this_amount_after_slash = this_amount_after_slash .checked_sub(slash) .unwrap_or_default(); @@ -2419,8 +2424,8 @@ impl StateMachineTest for ConcretePosState { // We have to divide this bond in case there are // slashes let slash_ratio = - Dec::from(this_amount_after_slash) - / Dec::from(bond_delta); + Dec::try_from(this_amount_after_slash).unwrap() + / Dec::try_from(bond_delta).unwrap(); amount_after_slash += slash_ratio * to_redelegate; to_redelegate = token::Amount::zero(); } @@ -2731,7 +2736,7 @@ impl ConcretePosState { fn check_next_epoch_post_conditions(&self, params: &PosParams) { let pipeline = self.current_epoch() + params.pipeline_len; - let before_pipeline = pipeline.prev(); + let before_pipeline = pipeline.prev().unwrap(); // Post-condition: Consensus validator sets at pipeline offset // must be the same as at the epoch before it. diff --git a/crates/proof_of_stake/src/tests/test_helper_fns.rs b/crates/proof_of_stake/src/tests/test_helper_fns.rs index 98c2ac37b0..ef685b0a53 100644 --- a/crates/proof_of_stake/src/tests/test_helper_fns.rs +++ b/crates/proof_of_stake/src/tests/test_helper_fns.rs @@ -499,7 +499,7 @@ fn test_compute_slash_bond_at_epoch() { .push( &mut storage, Slash { - epoch: infraction_epoch.prev(), + epoch: infraction_epoch.prev().unwrap(), block_height: 0, r#type: SlashType::DuplicateVote, rate: Dec::one(), @@ -735,19 +735,24 @@ fn test_apply_list_slashes() { let list3 = vec![slash1.clone(), slash1.clone()]; let list4 = vec![slash1.clone(), slash1, slash2]; - let res = apply_list_slashes(¶ms, &[], token::Amount::from(100)); + let res = + apply_list_slashes(¶ms, &[], token::Amount::from(100)).unwrap(); assert_eq!(res, token::Amount::from(100)); - let res = apply_list_slashes(¶ms, &list1, token::Amount::from(100)); + let res = + apply_list_slashes(¶ms, &list1, token::Amount::from(100)).unwrap(); assert_eq!(res, token::Amount::zero()); - let res = apply_list_slashes(¶ms, &list2, token::Amount::from(100)); + let res = + apply_list_slashes(¶ms, &list2, token::Amount::from(100)).unwrap(); assert_eq!(res, token::Amount::zero()); - let res = apply_list_slashes(¶ms, &list3, token::Amount::from(100)); + let res = + apply_list_slashes(¶ms, &list3, token::Amount::from(100)).unwrap(); assert_eq!(res, token::Amount::zero()); - let res = apply_list_slashes(¶ms, &list4, token::Amount::from(100)); + let res = + apply_list_slashes(¶ms, &list4, token::Amount::from(100)).unwrap(); assert_eq!(res, token::Amount::zero()); } @@ -788,7 +793,8 @@ fn test_compute_slashable_amount() { &slash1, token::Amount::from(100), &BTreeMap::new(), - ); + ) + .unwrap(); assert_eq!(res, token::Amount::from(100)); let res = compute_slashable_amount( @@ -796,7 +802,8 @@ fn test_compute_slashable_amount() { &slash2, token::Amount::from(100), &test_map, - ); + ) + .unwrap(); assert_eq!(res, token::Amount::from(50)); let res = compute_slashable_amount( @@ -804,7 +811,8 @@ fn test_compute_slashable_amount() { &slash1, token::Amount::from(100), &test_map, - ); + ) + .unwrap(); assert_eq!(res, token::Amount::from(100)); } @@ -853,7 +861,8 @@ fn test_fold_and_slash_redelegated_bonds() { start_epoch, &[], |_| true, - ); + ) + .unwrap(); assert_eq!( res, FoldRedelegatedBondsResult { @@ -870,7 +879,8 @@ fn test_fold_and_slash_redelegated_bonds() { start_epoch, &[test_slash], |_| true, - ); + ) + .unwrap(); assert_eq!( res, FoldRedelegatedBondsResult { @@ -897,7 +907,8 @@ fn test_fold_and_slash_redelegated_bonds() { start_epoch, &[], |_| true, - ); + ) + .unwrap(); assert_eq!( res, FoldRedelegatedBondsResult { @@ -1353,12 +1364,12 @@ fn test_slash_validator() { // Test case 3 total_redelegated_bonded - .at(&infraction_epoch.prev()) + .at(&infraction_epoch.prev().unwrap()) .at(&alice) .insert(&mut storage, Epoch(2), 5.into()) .unwrap(); total_redelegated_bonded - .at(&infraction_epoch.prev()) + .at(&infraction_epoch.prev().unwrap()) .at(&alice) .insert(&mut storage, Epoch(3), 1.into()) .unwrap(); @@ -1385,13 +1396,13 @@ fn test_slash_validator() { .unwrap(); total_redelegated_unbonded .at(&(current_epoch + params.pipeline_len)) - .at(&infraction_epoch.prev()) + .at(&infraction_epoch.prev().unwrap()) .at(&alice) .insert(&mut storage, Epoch(2), 5.into()) .unwrap(); total_redelegated_unbonded .at(&(current_epoch + params.pipeline_len)) - .at(&infraction_epoch.prev()) + .at(&infraction_epoch.prev().unwrap()) .at(&alice) .insert(&mut storage, Epoch(3), 1.into()) .unwrap(); @@ -1421,13 +1432,13 @@ fn test_slash_validator() { .unwrap(); total_redelegated_unbonded .at(&(current_epoch + params.pipeline_len)) - .at(&infraction_epoch.prev()) + .at(&infraction_epoch.prev().unwrap()) .at(&alice) .remove(&mut storage, &Epoch(3)) .unwrap(); total_redelegated_unbonded .at(&(current_epoch + params.pipeline_len)) - .at(&infraction_epoch.prev()) + .at(&infraction_epoch.prev().unwrap()) .at(&alice) .insert(&mut storage, Epoch(2), 4.into()) .unwrap(); @@ -1645,7 +1656,7 @@ fn test_slash_validator() { .push( &mut storage, Slash { - epoch: infraction_epoch.prev(), + epoch: infraction_epoch.prev().unwrap(), block_height: 0, r#type: SlashType::DuplicateVote, rate: Dec::one(), diff --git a/crates/proof_of_stake/src/tests/test_pos.rs b/crates/proof_of_stake/src/tests/test_pos.rs index 0150cba4d5..3115e73438 100644 --- a/crates/proof_of_stake/src/tests/test_pos.rs +++ b/crates/proof_of_stake/src/tests/test_pos.rs @@ -457,7 +457,7 @@ fn test_bonds_aux(params: OwnedPosParams, validators: Vec) { &s, ¶ms, &validator.address, - pipeline_epoch.prev(), + pipeline_epoch.prev().unwrap(), ) .unwrap(); let val_stake_post = @@ -471,7 +471,7 @@ fn test_bonds_aux(params: OwnedPosParams, validators: Vec) { let delegation = bond_handle(&delegator, &validator.address); assert_eq!( delegation - .get_sum(&s, pipeline_epoch.prev(), ¶ms) + .get_sum(&s, pipeline_epoch.prev().unwrap(), ¶ms) .unwrap() .unwrap_or_default(), token::Amount::zero() @@ -584,7 +584,7 @@ fn test_bonds_aux(params: OwnedPosParams, validators: Vec) { &s, ¶ms, &validator.address, - pipeline_epoch.prev(), + pipeline_epoch.prev().unwrap(), ) .unwrap(); @@ -718,7 +718,7 @@ fn test_bonds_aux(params: OwnedPosParams, validators: Vec) { &s, ¶ms, &validator.address, - pipeline_epoch.prev(), + pipeline_epoch.prev().unwrap(), ) .unwrap(); let val_stake_post = @@ -836,7 +836,7 @@ fn test_unjail_validator_aux( // cubic slash rate small let num_vals = validators.len(); validators.sort_by_key(|a| a.tokens); - validators[num_vals - 1].tokens = 100 * validators[num_vals - 1].tokens; + validators[num_vals - 1].tokens = validators[num_vals - 1].tokens * 100; // Get second highest stake validator to misbehave let val_addr = &validators[num_vals - 2].address; @@ -1199,7 +1199,8 @@ fn test_log_block_rewards_aux_aux( // A helper closure to prepare minimum required votes let prep_votes = |epoch| { // Ceil of 2/3 of total stake - let min_required_votes = total_stake.mul_ceil(Dec::two() / 3); + let min_required_votes = + total_stake.mul_ceil(Dec::two() / 3).unwrap(); let mut total_votes = token::Amount::zero(); let mut non_voters = HashSet::
::default(); @@ -1261,8 +1262,8 @@ fn test_log_block_rewards_aux_aux( .unwrap(); let proposer_signing_reward = votes.iter().find_map(|vote| { if vote.validator_address == proposer_address { - let signing_fraction = - Dec::from(stake) / Dec::from(signing_stake); + let signing_fraction = Dec::try_from(stake).unwrap() + / Dec::try_from(signing_stake).unwrap(); Some(coeffs.signer_coeff * signing_fraction) } else { None @@ -1273,7 +1274,7 @@ fn test_log_block_rewards_aux_aux( coeffs.proposer_coeff // Consensus validator reward + (coeffs.active_val_coeff - * (Dec::from(stake) / Dec::from(total_stake))) + * (Dec::try_from(stake).unwrap() / Dec::try_from(total_stake).unwrap())) // Signing reward (if proposer voted) + proposer_signing_reward .unwrap_or_default(); @@ -1304,14 +1305,16 @@ fn test_log_block_rewards_aux_aux( current_epoch, ) .unwrap(); - let signing_fraction = Dec::from(stake) / Dec::from(signing_stake); + let signing_fraction = Dec::try_from(stake).unwrap() + / Dec::try_from(signing_stake).unwrap(); let expected_signer_rewards = last_rewards .get(validator_address) .copied() .unwrap_or_default() + coeffs.signer_coeff * signing_fraction + (coeffs.active_val_coeff - * (Dec::from(stake) / Dec::from(total_stake))); + * (Dec::try_from(stake).unwrap() + / Dec::try_from(total_stake).unwrap())); tracing::info!( "Expected signer {validator_address} rewards: \ {expected_signer_rewards}" @@ -1336,7 +1339,8 @@ fn test_log_block_rewards_aux_aux( let expected_non_signer_rewards = last_rewards.get(&address).copied().unwrap_or_default() + coeffs.active_val_coeff - * (Dec::from(stake) / Dec::from(total_stake)); + * (Dec::try_from(stake).unwrap() + / Dec::try_from(total_stake).unwrap()); tracing::info!( "Expected non-signer {address} rewards: \ {expected_non_signer_rewards}" @@ -1424,7 +1428,7 @@ fn test_update_rewards_products_aux(validators: Vec) { let total_native_tokens = get_effective_total_native_supply(&s).unwrap(); // Distribute inflation into rewards - let last_epoch = current_epoch.prev(); + let last_epoch = current_epoch.prev().unwrap(); let inflation = token::Amount::native_whole(10_000_000); update_rewards_products_and_mint_inflation( &mut s, @@ -1854,9 +1858,10 @@ fn test_delegation_targets() { // Check the delegation targets now let pipeline_epoch = current_epoch + params.pipeline_len; - for epoch in - Epoch::iter_bounds_inclusive(Epoch::default(), pipeline_epoch.prev()) - { + for epoch in Epoch::iter_bounds_inclusive( + Epoch::default(), + pipeline_epoch.prev().unwrap(), + ) { let delegatees1 = find_delegation_validators(&storage, &validator1, &epoch).unwrap(); let delegatees2 = @@ -1914,9 +1919,10 @@ fn test_delegation_targets() { let pipeline_epoch = current_epoch + params.pipeline_len; // Up to epoch 2 - for epoch in - Epoch::iter_bounds_inclusive(Epoch::default(), current_epoch.prev()) - { + for epoch in Epoch::iter_bounds_inclusive( + Epoch::default(), + current_epoch.prev().unwrap(), + ) { let delegatees1 = find_delegation_validators(&storage, &validator1, &epoch).unwrap(); let delegatees2 = @@ -1931,9 +1937,10 @@ fn test_delegation_targets() { } // Epochs 3-4 - for epoch in - Epoch::iter_bounds_inclusive(current_epoch, pipeline_epoch.prev()) - { + for epoch in Epoch::iter_bounds_inclusive( + current_epoch, + pipeline_epoch.prev().unwrap(), + ) { let delegatees1 = find_delegation_validators(&storage, &validator1, &epoch).unwrap(); let delegatees2 = diff --git a/crates/proof_of_stake/src/tests/test_slash_and_redel.rs b/crates/proof_of_stake/src/tests/test_slash_and_redel.rs index 9df89eeef4..708bfe1918 100644 --- a/crates/proof_of_stake/src/tests/test_slash_and_redel.rs +++ b/crates/proof_of_stake/src/tests/test_slash_and_redel.rs @@ -175,7 +175,7 @@ fn test_simple_redelegation_aux( let redelegated = validator_outgoing_redelegations_handle(&src_validator) .at(&dest_validator) - .at(¤t_epoch.prev()) + .at(¤t_epoch.prev().unwrap()) .get(&storage, ¤t_epoch) .unwrap() .unwrap(); @@ -418,16 +418,21 @@ fn test_slashes_with_unbonding_aux( .unwrap() .rate; - let expected_withdrawn_amount = Dec::from( - (Dec::one() - slash_rate_1) - * (Dec::one() - slash_rate_0) - * unbond_amount, - ); + let expected_withdrawn_amount = Dec::try_from( + unbond_amount + .mul_floor( + (Dec::one() - slash_rate_1) * (Dec::one() - slash_rate_0), + ) + .unwrap(), + ) + .unwrap(); // Allow some rounding error, 1 NAMNAM per each slash let rounding_error_tolerance = Dec::new(2, NATIVE_MAX_DECIMAL_PLACES).unwrap(); assert!( - expected_withdrawn_amount.abs_diff(&Dec::from(withdrawn_tokens)) + expected_withdrawn_amount + .abs_diff(Dec::try_from(withdrawn_tokens).unwrap()) + .unwrap() <= rounding_error_tolerance ); @@ -550,7 +555,7 @@ fn test_redelegation_with_slashing_aux( let redelegated = validator_outgoing_redelegations_handle(&src_validator) .at(&dest_validator) - .at(¤t_epoch.prev()) + .at(¤t_epoch.prev().unwrap()) .get(&storage, ¤t_epoch) .unwrap() .unwrap(); @@ -860,11 +865,11 @@ fn test_chain_redelegations_aux(mut validators: Vec) { // Advance to right before the redelegation can be redelegated again assert_eq!(redel_end, current_epoch); let epoch_can_redel = - redel_end.prev() + params.slash_processing_epoch_offset(); + redel_end.prev().unwrap() + params.slash_processing_epoch_offset(); loop { current_epoch = advance_epoch(&mut storage, ¶ms); process_slashes(&mut storage, current_epoch).unwrap(); - if current_epoch == epoch_can_redel.prev() { + if current_epoch == epoch_can_redel.prev().unwrap() { break; } } @@ -1160,11 +1165,12 @@ fn test_overslashing_aux(mut validators: Vec) { } } - let total_stake_1 = offending_stake + 3 * other_stake; - let stake_frac = Dec::from(offending_stake) / Dec::from(total_stake_1); + let total_stake_1 = offending_stake + other_stake * 3; + let stake_frac = Dec::try_from(offending_stake).unwrap() + / Dec::try_from(total_stake_1).unwrap(); let slash_rate_1 = Dec::from_str("9.0").unwrap() * stake_frac * stake_frac; - let exp_slashed_1 = offending_stake.mul_ceil(slash_rate_1); + let exp_slashed_1 = offending_stake.mul_ceil(slash_rate_1).unwrap(); // Check that the proper amount was slashed let epoch = current_epoch.next(); @@ -1175,7 +1181,7 @@ fn test_overslashing_aux(mut validators: Vec) { let total_stake = read_total_stake(&storage, ¶ms, epoch).unwrap(); let exp_total_stake = - offending_stake - exp_slashed_1 + amount_del + 3 * other_stake; + offending_stake - exp_slashed_1 + amount_del + other_stake * 3; assert_eq!(total_stake, exp_total_stake); let self_bond_id = BondId { @@ -1196,12 +1202,13 @@ fn test_overslashing_aux(mut validators: Vec) { } } - let total_stake_2 = offending_stake + amount_del + 3 * other_stake; - let stake_frac = - Dec::from(offending_stake + amount_del) / Dec::from(total_stake_2); + let total_stake_2 = offending_stake + amount_del + other_stake * 3; + let stake_frac = Dec::try_from(offending_stake + amount_del).unwrap() + / Dec::try_from(total_stake_2).unwrap(); let slash_rate_2 = Dec::from_str("9.0").unwrap() * stake_frac * stake_frac; - let exp_slashed_from_delegation = amount_del.mul_ceil(slash_rate_2); + let exp_slashed_from_delegation = + amount_del.mul_ceil(slash_rate_2).unwrap(); // Check that the proper amount was slashed. We expect that all of the // validator self-bond has been slashed and some of the delegation has been @@ -1215,7 +1222,7 @@ fn test_overslashing_aux(mut validators: Vec) { let total_stake = read_total_stake(&storage, ¶ms, epoch).unwrap(); let exp_total_stake = - amount_del - exp_slashed_from_delegation + 3 * other_stake; + amount_del - exp_slashed_from_delegation + other_stake * 3; assert_eq!(total_stake, exp_total_stake); let delegation_id = BondId { diff --git a/crates/proof_of_stake/src/types/mod.rs b/crates/proof_of_stake/src/types/mod.rs index 1d47e22176..d5df577769 100644 --- a/crates/proof_of_stake/src/types/mod.rs +++ b/crates/proof_of_stake/src/types/mod.rs @@ -740,10 +740,11 @@ impl Display for SlashType { /// 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"); - i64::try_from(pow.to_uint().expect("Can't fail")) - .expect("Invalid voting power") + let prod = tokens + .mul_floor(votes_per_token) + .expect("Must be able to convert tokens to TM votes"); + let res = i128::try_from(prod.change()).expect("Failed conversion to i128"); + i64::try_from(res).expect("Invalid validator voting power (i64)") } #[cfg(test)] diff --git a/crates/proof_of_stake/src/types/rev_order.rs b/crates/proof_of_stake/src/types/rev_order.rs index ef23fb9e53..32ce6eaf2f 100644 --- a/crates/proof_of_stake/src/types/rev_order.rs +++ b/crates/proof_of_stake/src/types/rev_order.rs @@ -23,7 +23,9 @@ impl From for ReverseOrdTokenAmount { /// Invert the token amount fn invert(amount: token::Amount) -> token::Amount { - token::Amount::max_signed() - amount + token::Amount::max_signed() + .checked_sub(amount) + .expect("Cannot underflow") } impl KeySeg for ReverseOrdTokenAmount { diff --git a/crates/proof_of_stake/src/validator_set_update.rs b/crates/proof_of_stake/src/validator_set_update.rs index 46cad686c8..5b2d2dba45 100644 --- a/crates/proof_of_stake/src/validator_set_update.rs +++ b/crates/proof_of_stake/src/validator_set_update.rs @@ -1,13 +1,13 @@ //! Validator set updates use namada_core::address::Address; +use namada_core::arith::checked; use namada_core::collections::{HashMap, HashSet}; use namada_core::key::PublicKeyTmRawHash; use namada_core::storage::Epoch; use namada_core::token; use namada_storage::collections::lazy_map::{NestedSubKey, SubKey}; use namada_storage::{StorageRead, StorageWrite}; -use num_traits::ops::checked::CheckedAdd; use once_cell::unsync::Lazy; use crate::storage::{ @@ -40,7 +40,7 @@ where return Ok(()); } let offset = offset.unwrap_or(params.pipeline_len); - let epoch = current_epoch + offset; + let epoch = checked!(current_epoch + offset)?; tracing::debug!( "Update epoch for validator set: {epoch}, validator: {validator}" ); @@ -53,10 +53,7 @@ where let tokens_pre = read_validator_stake(storage, params, validator, epoch)?; - let tokens_post = tokens_pre - .change() - .checked_add(&token_change) - .expect("Post-validator set update token amount has overflowed"); + let tokens_post = checked!(tokens_pre.change() + token_change)?; debug_assert!(tokens_post.non_negative()); let tokens_post = token::Amount::from_change(tokens_post); @@ -364,7 +361,7 @@ pub fn insert_validator_into_validator_set( where S: StorageRead + StorageWrite, { - let target_epoch = current_epoch + offset; + let target_epoch = checked!(current_epoch + offset)?; let consensus_set = consensus_validator_set_handle().at(&target_epoch); let below_cap_set = below_capacity_validator_set_handle().at(&target_epoch); @@ -526,7 +523,7 @@ pub fn promote_next_below_capacity_validator_to_consensus( where S: StorageRead + StorageWrite, { - let epoch = current_epoch + offset; + let epoch = checked!(current_epoch + offset)?; let below_cap_set = below_capacity_validator_set_handle().at(&epoch); let max_below_capacity_amount = get_max_below_capacity_validator_amount(&below_cap_set, storage)?; @@ -773,7 +770,7 @@ pub fn copy_validator_sets_and_positions( where S: StorageRead + StorageWrite, { - let prev_epoch = target_epoch.prev(); + let prev_epoch = target_epoch.prev().expect("Must have a prev epoch"); let consensus_validator_set = consensus_validator_set_handle(); let below_capacity_validator_set = below_capacity_validator_set_handle(); @@ -901,7 +898,7 @@ where .remove(storage, &last_position_of_min_consensus_vals)? .expect("There must be always be at least 1 consensus validator"); - let offset_epoch = current_epoch + offset; + let offset_epoch = checked!(current_epoch + offset)?; // Insert the min consensus validator into the below-capacity // set From 76fe7d2db5c8676590bae3c3e63eac323120d434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Wed, 1 May 2024 17:14:51 +0200 Subject: [PATCH 21/29] ibc: update to non-panicking core API --- crates/ibc/src/context/common.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ibc/src/context/common.rs b/crates/ibc/src/context/common.rs index 8d7f6137e0..edbdf9e970 100644 --- a/crates/ibc/src/context/common.rs +++ b/crates/ibc/src/context/common.rs @@ -278,7 +278,7 @@ pub trait IbcCommonContext: IbcStorageContext { // `FinalizeBlock` phase, e.g. dry-run, use the previous // header's time. It should be OK though the constraints // become a bit stricter when checking timeouts. - self.get_block_header(height.prev_height())? + self.get_block_header(height.prev_height().unwrap())? } else { None } From ac74541e7fe0c33f2746616371def39c8e91ac4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Wed, 1 May 2024 17:28:26 +0200 Subject: [PATCH 22/29] eth_bridge: update to non-panicking core API --- .../transactions/validator_set_update/mod.rs | 2 +- .../src/protocol/transactions/votes.rs | 11 ++++++++--- .../src/protocol/transactions/votes/update.rs | 4 +++- .../src/storage/eth_bridge_queries.rs | 16 ++++++++++++---- 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/crates/ethereum_bridge/src/protocol/transactions/validator_set_update/mod.rs b/crates/ethereum_bridge/src/protocol/transactions/validator_set_update/mod.rs index de1f817705..87dda51854 100644 --- a/crates/ethereum_bridge/src/protocol/transactions/validator_set_update/mod.rs +++ b/crates/ethereum_bridge/src/protocol/transactions/validator_set_update/mod.rs @@ -97,7 +97,7 @@ where // the end of an epoch, and even if we cross an epoch boundary without // a complete proof, we should get one shortly after. .expect("The first block height of the signing epoch should be known") - + 1; + .next_height(); let voting_powers = utils::get_voting_powers(state, (&ext, epoch_2nd_height))?; let changed_keys = apply_update( diff --git a/crates/ethereum_bridge/src/protocol/transactions/votes.rs b/crates/ethereum_bridge/src/protocol/transactions/votes.rs index 91388206dd..87f7805ccd 100644 --- a/crates/ethereum_bridge/src/protocol/transactions/votes.rs +++ b/crates/ethereum_bridge/src/protocol/transactions/votes.rs @@ -92,7 +92,9 @@ pub trait EpochedVotingPowerExt { // arbitrarily faulty nodes. Therefore, we can consider a tally secure // if has accumulated an amount of stake greater than the threshold // stake of S_max - F = 2/3 S_max. - let threshold = FractionalVotingPower::TWO_THIRDS * max_voting_power; + let threshold = FractionalVotingPower::TWO_THIRDS + .checked_mul_amount(max_voting_power) + .expect("Cannot overflow"); self.tallied_stake() > threshold } } @@ -115,7 +117,8 @@ impl EpochedVotingPowerExt for EpochedVotingPower { } fn tallied_stake(&self) -> token::Amount { - self.values().copied().sum::() + token::Amount::sum(self.values().copied()) + .expect("Talling stake shouldn't overflow") } } @@ -168,7 +171,9 @@ where let aggregated = seen_by_voting_power .entry(epoch) .or_insert_with(token::Amount::zero); - *aggregated += voting_power; + *aggregated = aggregated + .checked_add(voting_power) + .ok_or_else(|| eyre!("Aggregated voting power overflow"))?; } None => { return Err(eyre!( diff --git a/crates/ethereum_bridge/src/protocol/transactions/votes/update.rs b/crates/ethereum_bridge/src/protocol/transactions/votes/update.rs index a1da975531..b4ff7788ec 100644 --- a/crates/ethereum_bridge/src/protocol/transactions/votes/update.rs +++ b/crates/ethereum_bridge/src/protocol/transactions/votes/update.rs @@ -175,7 +175,9 @@ where let aggregated = voting_power_post .entry(epoch) .or_insert_with(token::Amount::zero); - *aggregated += voting_power; + *aggregated = aggregated + .checked_add(voting_power) + .ok_or_else(|| eyre!("Aggregated voting power overflow"))?; } let seen_post = voting_power_post.has_majority_quorum(state); diff --git a/crates/ethereum_bridge/src/storage/eth_bridge_queries.rs b/crates/ethereum_bridge/src/storage/eth_bridge_queries.rs index 014ca2515b..038a966f70 100644 --- a/crates/ethereum_bridge/src/storage/eth_bridge_queries.rs +++ b/crates/ethereum_bridge/src/storage/eth_bridge_queries.rs @@ -403,7 +403,8 @@ where let voting_power: EthBridgeVotingPower = FractionalVotingPower::new(power.into(), total_power) .expect("Fractional voting power should be >1") - .into(); + .try_into() + .unwrap(); (select_validator(addr_book), voting_power) }) .unzip(); @@ -524,9 +525,16 @@ where ); } - if amount_to_mint + supply > cap { - let erc20_amount = cap - supply; - let nut_amount = amount_to_mint - erc20_amount; + if amount_to_mint + .checked_add(supply) + .expect("Token amount shouldn't overflow") + > cap + { + let erc20_amount = + cap.checked_sub(supply).expect("Cannot underflow"); + let nut_amount = amount_to_mint + .checked_sub(erc20_amount) + .expect("Cannot underflow"); return EthAssetMint { nut_amount, From 410f58e086878002392275ff97956dd9277f326f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 2 May 2024 16:13:45 +0200 Subject: [PATCH 23/29] sdk: update to non-panicking core API --- Cargo.lock | 1 + crates/sdk/Cargo.toml | 1 + crates/sdk/src/error.rs | 4 +- crates/sdk/src/eth_bridge/bridge_pool.rs | 32 ++++++----- crates/sdk/src/eth_bridge/validator_set.rs | 6 +- crates/sdk/src/masp.rs | 35 ++++++----- crates/sdk/src/queries/shell.rs | 4 +- crates/sdk/src/queries/shell/eth_bridge.rs | 13 +++-- crates/sdk/src/queries/vp/pos.rs | 67 ++++++++++++++-------- crates/sdk/src/rpc.rs | 8 +-- crates/sdk/src/signing.rs | 6 +- crates/sdk/src/tx.rs | 45 +++++++++------ 12 files changed, 135 insertions(+), 87 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 356eb9da5c..fe8dfedfc8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4858,6 +4858,7 @@ dependencies = [ "serde_json", "sha2 0.9.9", "slip10_ed25519", + "smooth-operator", "tempfile", "tendermint-config", "tendermint-rpc", diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index 77b6920fb7..931efb1390 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -120,6 +120,7 @@ serde.workspace = true serde_json.workspace = true sha2.workspace = true slip10_ed25519.workspace = true +smooth-operator.workspace = true tendermint-config.workspace = true tendermint-rpc = { workspace = true, optional = true } thiserror.workspace = true diff --git a/crates/sdk/src/error.rs b/crates/sdk/src/error.rs index c2f87b7651..7a7343cdd6 100644 --- a/crates/sdk/src/error.rs +++ b/crates/sdk/src/error.rs @@ -4,8 +4,8 @@ use namada_core::address::Address; use namada_core::dec::Dec; use namada_core::ethereum_events::EthAddress; use namada_core::event::EventError; -use namada_core::storage; use namada_core::storage::Epoch; +use namada_core::{arith, storage}; use namada_tx::Tx; use prost::EncodeError; use tendermint_rpc::Error as RpcError; @@ -43,6 +43,8 @@ pub enum Error { /// Ethereum bridge related errors #[error("{0}")] EthereumBridge(#[from] EthereumBridgeError), + #[error("Arithmetic {0}")] + Arith(#[from] arith::Error), /// Any Other errors that are uncategorized #[error("{0}")] Other(String), diff --git a/crates/sdk/src/eth_bridge/bridge_pool.rs b/crates/sdk/src/eth_bridge/bridge_pool.rs index c3cf05d513..a70957d9e7 100644 --- a/crates/sdk/src/eth_bridge/bridge_pool.rs +++ b/crates/sdk/src/eth_bridge/bridge_pool.rs @@ -9,6 +9,7 @@ use ethbridge_bridge_contract::Bridge; use ethers::providers::Middleware; use futures::future::FutureExt; use namada_core::address::{Address, InternalAddress}; +use namada_core::arith::checked; use namada_core::collections::{HashMap, HashSet}; use namada_core::eth_abi::Encode; use namada_core::eth_bridge_pool::{ @@ -22,7 +23,6 @@ use namada_ethereum_bridge::storage::bridge_pool::get_pending_key; use namada_token::storage_key::balance_key; use namada_token::Amount; use namada_tx::Tx; -use num_traits::ops::checked::CheckedSub; use owo_colors::OwoColorize; use serde::Serialize; @@ -246,7 +246,7 @@ async fn validate_bridge_pool_tx( )); } - if flow_control.exceeds_token_caps(transfer.transfer.amount) { + if flow_control.exceeds_token_caps(transfer.transfer.amount)? { return Err(Error::EthereumBridge( EthereumBridgeError::Erc20TokenCapsExceeded(wnam_addr), )); @@ -255,7 +255,8 @@ async fn validate_bridge_pool_tx( // validate balances let maybe_balance_error = if token_addr == transfer.gas_fee.token { - let expected_debit = transfer.transfer.amount + transfer.gas_fee.amount; + let expected_debit = + checked!(transfer.transfer.amount + transfer.gas_fee.amount)?; let balance: Amount = query_storage_value( context.client(), &balance_key(&token_addr, &transfer.transfer.sender), @@ -939,7 +940,7 @@ mod recommendations { // This is the gas cost for hashing the validator set and // checking a quorum of signatures (in gwei). let validator_gas = signature_fee() - * signature_checks(voting_powers, &bp_root.signatures) + * signature_checks(voting_powers, &bp_root.signatures)? + valset_fee() * valset_size; // we don't recommend transfers that have already been relayed @@ -1007,13 +1008,16 @@ mod recommendations { fn signature_checks( voting_powers: VotingPowersMap, sigs: &HashMap, - ) -> Uint { + ) -> Result { let voting_powers = voting_powers.get_sorted(); - let total_power = voting_powers.iter().map(|(_, &y)| y).sum::(); + let total_power = Amount::sum(voting_powers.iter().map(|(_, &y)| y)) + .ok_or_else(|| { + Error::Other("Voting power sum overflow".to_owned()) + })?; // Find the total number of signature checks Ethereum will make let mut power = FractionalVotingPower::NULL; - Uint::from_u64( + Ok(Uint::from_u64( voting_powers .iter() .filter_map(|(a, &p)| sigs.get(*a).map(|_| p)) @@ -1033,7 +1037,7 @@ mod recommendations { } }) .count() as u64, - ) + )) } /// Generate eligible recommendations. @@ -1095,7 +1099,7 @@ mod recommendations { .map_err(|err| err.to_string()) .and_then(|amt_of_earned_gwei| { transfer_fee() - .checked_sub(&amt_of_earned_gwei) + .checked_sub(amt_of_earned_gwei) .ok_or_else(|| { "Underflowed calculating relaying cost" .into() @@ -1157,8 +1161,8 @@ mod recommendations { pending_transfer: transfer, } in contents.into_iter() { - let next_total_gas = total_gas + unsigned_transfer_fee(); - let next_total_cost = total_cost + cost; + let next_total_gas = checked!(total_gas + unsigned_transfer_fee())?; + let next_total_cost = checked!(total_cost + cost)?; if cost.is_negative() { if next_total_gas <= max_gas && next_total_cost <= max_cost { state.feasible_region = true; @@ -1190,7 +1194,7 @@ mod recommendations { Some(RecommendedBatch { transfer_hashes: recommendation, ethereum_gas_fees: total_gas, - net_profit: -total_cost, + net_profit: checked!(-total_cost)?, bridge_pool_gas_fees: total_fees, }) } else { @@ -1394,7 +1398,7 @@ mod recommendations { (address_book(2), 0), (address_book(3), 0), ]); - let checks = signature_checks(voting_powers, &signatures); + let checks = signature_checks(voting_powers, &signatures).unwrap(); assert_eq!(checks, uint::ONE) } @@ -1411,7 +1415,7 @@ mod recommendations { (address_book(3), 0), (address_book(4), 0), ]); - let checks = signature_checks(voting_powers, &signatures); + let checks = signature_checks(voting_powers, &signatures).unwrap(); assert_eq!(checks, Uint::from_u64(3)) } diff --git a/crates/sdk/src/eth_bridge/validator_set.rs b/crates/sdk/src/eth_bridge/validator_set.rs index 59883ebc45..be47fceeb7 100644 --- a/crates/sdk/src/eth_bridge/validator_set.rs +++ b/crates/sdk/src/eth_bridge/validator_set.rs @@ -218,7 +218,7 @@ impl ShouldRelay for CheckNonce { }); let gov_current_epoch = bridge_epoch_fut.await?; - if epoch == gov_current_epoch + 1u64 { + if epoch == gov_current_epoch.next() { Ok(()) } else { Err(RelayResult::NonceError { @@ -694,7 +694,9 @@ where }) }); - let bridge_current_epoch = epoch_to_relay - 1; + let bridge_current_epoch = epoch_to_relay + .prev() + .expect("Epoch to relay must have prev"); let shell = RPC.shell().eth_bridge(); let validator_set_args_fut = shell .read_bridge_valset(nam_client, &bridge_current_epoch) diff --git a/crates/sdk/src/masp.rs b/crates/sdk/src/masp.rs index 87b90b5d91..fbae30d415 100644 --- a/crates/sdk/src/masp.rs +++ b/crates/sdk/src/masp.rs @@ -51,6 +51,7 @@ use masp_proofs::prover::LocalTxProver; #[cfg(not(feature = "testing"))] use masp_proofs::sapling::SaplingVerificationContext; use namada_core::address::{Address, MASP}; +use namada_core::arith::checked; use namada_core::collections::{HashMap, HashSet}; use namada_core::dec::Dec; pub use namada_core::masp::{ @@ -1173,12 +1174,12 @@ impl ShieldedContext { ) })?; - let amount = transp_bundle - .vin - .iter() - .fold(Amount::zero(), |acc, vin| { - acc + Amount::from_u64(vin.value) - }); + let amount = transp_bundle.vin.iter().try_fold( + Amount::zero(), + |acc, vin| { + checked!(acc + Amount::from_u64(vin.value)) + }, + )?; ( addresses[1].to_owned(), @@ -1224,12 +1225,12 @@ impl ShieldedContext { ) })?[0]; - let amount = transp_bundle - .vout - .iter() - .fold(Amount::zero(), |acc, vout| { - acc + Amount::from_u64(vout.value) - }); + let amount = transp_bundle.vout.iter().try_fold( + Amount::zero(), + |acc, vout| { + checked!(acc + Amount::from_u64(vout.value)) + }, + )?; (MASP, token.to_owned(), amount) } (_, _) => { @@ -1251,7 +1252,7 @@ impl ShieldedContext { source, MaspChange { asset: token, - change: -amount.change(), + change: checked!(-amount.change())?, }, ); self.delta_map @@ -2375,7 +2376,10 @@ impl ShieldedContext { }, |indexed| IndexedTx { height: indexed.height, - index: indexed.index + 1, + index: indexed + .index + .checked_add(1) + .expect("Tx index shouldn't overflow"), is_wrapper: false, }, ); @@ -2500,11 +2504,12 @@ impl ShieldedContext { // Describe how a Transfer simply subtracts from one // account and adds the same to another + let change = transfer.amount.amount().change(); let delta = TransferDelta::from([( transfer.source.clone(), MaspChange { asset: transfer.token.clone(), - change: -transfer.amount.amount().change(), + change: checked!(-change)?, }, )]); diff --git a/crates/sdk/src/queries/shell.rs b/crates/sdk/src/queries/shell.rs index d6cb69ad88..e719c4e848 100644 --- a/crates/sdk/src/queries/shell.rs +++ b/crates/sdk/src/queries/shell.rs @@ -9,6 +9,7 @@ use masp_primitives::merkle_tree::MerklePath; use masp_primitives::sapling::Node; use namada_account::{Account, AccountPublicKeysMap}; use namada_core::address::Address; +use namada_core::arith::checked; use namada_core::dec::Dec; use namada_core::hash::Hash; use namada_core::hints; @@ -389,7 +390,8 @@ where }; if let Some(past_height_limit) = ctx.storage_read_past_height_limit { - if queried_height + past_height_limit < last_committed_height { + if checked!(queried_height + past_height_limit)? < last_committed_height + { return Err(namada_storage::Error::new(std::io::Error::new( std::io::ErrorKind::InvalidInput, format!( diff --git a/crates/sdk/src/queries/shell/eth_bridge.rs b/crates/sdk/src/queries/shell/eth_bridge.rs index 28f112c37e..d924366a97 100644 --- a/crates/sdk/src/queries/shell/eth_bridge.rs +++ b/crates/sdk/src/queries/shell/eth_bridge.rs @@ -6,6 +6,7 @@ use std::str::FromStr; use borsh::{BorshDeserialize, BorshSerialize}; use borsh_ext::BorshSerializeExt; use namada_core::address::Address; +use namada_core::arith::checked; use namada_core::collections::{HashMap, HashSet}; use namada_core::eth_abi::{Encode, EncodeCell}; use namada_core::eth_bridge_pool::{PendingTransfer, PendingTransferAppendix}; @@ -104,8 +105,11 @@ impl Erc20FlowControl { /// Check if the `transferred_amount` exceeds the token caps of some ERC20 /// asset. #[inline] - pub fn exceeds_token_caps(&self, transferred_amount: Amount) -> bool { - self.supply + transferred_amount > self.cap + pub fn exceeds_token_caps( + &self, + transferred_amount: Amount, + ) -> crate::error::Result { + Ok(checked!(self.supply + transferred_amount)? > self.cap) } } @@ -829,7 +833,7 @@ where H: 'static + StorageHasher + Sync, { let current_epoch = ctx.state.in_mem().get_current_epoch().0; - if epoch > current_epoch + 1u64 { + if epoch > checked!(current_epoch + 1u64)? { return Err(namada_storage::Error::SimpleMessage( "The requested epoch cannot be queried", )); @@ -913,7 +917,8 @@ mod test_ethbridge_router { let voting_power: EthBridgeVotingPower = FractionalVotingPower::new(power.into(), total_power) .expect("Fractional voting power should be >1") - .into(); + .try_into() + .unwrap(); (hot_key_addr, voting_power) }) .unzip(); diff --git a/crates/sdk/src/queries/vp/pos.rs b/crates/sdk/src/queries/vp/pos.rs index 3947e9b01c..c1f2bf31db 100644 --- a/crates/sdk/src/queries/vp/pos.rs +++ b/crates/sdk/src/queries/vp/pos.rs @@ -4,6 +4,7 @@ use std::collections::{BTreeMap, BTreeSet}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use namada_core::address::Address; +use namada_core::arith::{self, checked}; use namada_core::collections::{HashMap, HashSet}; use namada_core::key::common; use namada_core::storage::Epoch; @@ -164,13 +165,13 @@ pub type EnrichedBondsAndUnbondsDetail = Enriched; impl Enriched { /// The bonds amount reduced by slashes - pub fn bonds_total_active(&self) -> token::Amount { - self.bonds_total - self.bonds_total_slashed + pub fn bonds_total_active(&self) -> Option { + self.bonds_total.checked_sub(self.bonds_total_slashed) } /// The unbonds amount reduced by slashes - pub fn unbonds_total_active(&self) -> token::Amount { - self.unbonds_total - self.unbonds_total_slashed + pub fn unbonds_total_active(&self) -> Option { + self.unbonds_total.checked_sub(self.unbonds_total_slashed) } } @@ -429,8 +430,12 @@ where H: 'static + StorageHasher + Sync, { let params = read_pos_params(ctx.state)?; - let epoch = - epoch.unwrap_or(ctx.state.in_mem().last_epoch + params.pipeline_len); + let epoch = epoch.unwrap_or( + ctx.state + .in_mem() + .last_epoch + .unchecked_add(params.pipeline_len), + ); let handle = bond_handle(&source, &validator); handle @@ -528,7 +533,7 @@ where amount, ) = result?; if epoch >= withdrawable { - total += amount; + total = checked!(total + amount)?; } } Ok(total) @@ -689,7 +694,9 @@ pub mod client_only_methods { .pos() .bonds_and_unbonds(client, source, validator) .await?; - Ok(enrich_bonds_and_unbonds(current_epoch, data)) + Ok(enrich_bonds_and_unbonds(current_epoch, data).map_err(|e| { + std::io::Error::new(std::io::ErrorKind::Other, e) + })?) } } } @@ -698,7 +705,7 @@ pub mod client_only_methods { fn enrich_bonds_and_unbonds( current_epoch: Epoch, bonds_and_unbonds: BondsAndUnbondsDetails, -) -> EnrichedBondsAndUnbondsDetails { +) -> Result { 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(); @@ -716,26 +723,33 @@ fn enrich_bonds_and_unbonds( let mut withdrawable: token::Amount = 0.into(); for bond in &detail.bonds { - bond_total += bond.amount; - bond_total_slashed += - bond.slashed_amount.unwrap_or_default(); + let slashed_bond = bond.slashed_amount.unwrap_or_default(); + bond_total = checked!(bond_total + bond.amount)?; + bond_total_slashed = + checked!(bond_total_slashed + slashed_bond)?; } for unbond in &detail.unbonds { - unbond_total += unbond.amount; - unbond_total_slashed += + let slashed_unbond = unbond.slashed_amount.unwrap_or_default(); + unbond_total = checked!(unbond_total + unbond.amount)?; + unbond_total_slashed = + checked!(unbond_total_slashed + slashed_unbond)?; if current_epoch >= unbond.withdraw { - withdrawable += unbond.amount - - unbond.slashed_amount.unwrap_or_default() + withdrawable = checked!( + withdrawable + unbond.amount - slashed_unbond + )?; } } - bonds_total += bond_total; - bonds_total_slashed += bond_total_slashed; - unbonds_total += unbond_total; - unbonds_total_slashed += unbond_total_slashed; - total_withdrawable += withdrawable; + bonds_total = checked!(bonds_total + bond_total)?; + bonds_total_slashed = + checked!(bonds_total_slashed + bond_total_slashed)?; + unbonds_total = checked!(unbonds_total + unbond_total)?; + unbonds_total_slashed = + checked!(unbonds_total_slashed + unbond_total_slashed)?; + total_withdrawable = + checked!(total_withdrawable + withdrawable)?; let enriched_detail = EnrichedBondsAndUnbondsDetail { data: detail, @@ -745,15 +759,18 @@ fn enrich_bonds_and_unbonds( unbonds_total_slashed: unbond_total_slashed, total_withdrawable: withdrawable, }; - (bond_id, enriched_detail) + Ok::<_, arith::Error>((bond_id, enriched_detail)) }) - .collect(); - EnrichedBondsAndUnbondsDetails { + .collect::, + arith::Error, + >>()?; + Ok(EnrichedBondsAndUnbondsDetails { data: enriched_details, bonds_total, bonds_total_slashed, unbonds_total, unbonds_total_slashed, total_withdrawable, - } + }) } diff --git a/crates/sdk/src/rpc.rs b/crates/sdk/src/rpc.rs index 667cfe01a6..da97b9d8e2 100644 --- a/crates/sdk/src/rpc.rs +++ b/crates/sdk/src/rpc.rs @@ -11,6 +11,7 @@ use masp_primitives::merkle_tree::MerklePath; use masp_primitives::sapling::Node; use namada_account::Account; use namada_core::address::{Address, InternalAddress}; +use namada_core::arith::checked; use namada_core::collections::{HashMap, HashSet}; use namada_core::hash::Hash; use namada_core::ibc::IbcTokenHash; @@ -1015,8 +1016,7 @@ pub async fn query_proposal_result( proposal_votes, total_staked_token, tally_type, - ) - .expect("Proposal result calculation must not over/underflow"); + )? } }; Ok(Some(proposal_result)) @@ -1036,11 +1036,11 @@ pub async fn query_and_print_unbonds( let mut not_yet_withdrawable = HashMap::::new(); for ((_start_epoch, withdraw_epoch), amount) in unbonds.into_iter() { if withdraw_epoch <= current_epoch { - total_withdrawable += amount; + total_withdrawable = checked!(total_withdrawable + amount)?; } else { let withdrawable_amount = not_yet_withdrawable.entry(withdraw_epoch).or_default(); - *withdrawable_amount += amount; + *withdrawable_amount = checked!(withdrawable_amount + amount)?; } } if !total_withdrawable.is_zero() { diff --git a/crates/sdk/src/signing.rs b/crates/sdk/src/signing.rs index 1e138a3a6a..8cfa22690e 100644 --- a/crates/sdk/src/signing.rs +++ b/crates/sdk/src/signing.rs @@ -13,6 +13,7 @@ use masp_primitives::transaction::components::sapling::fees::{ use masp_primitives::transaction::Transaction; use namada_account::{AccountPublicKeysMap, InitAccount, UpdateAccount}; use namada_core::address::{Address, ImplicitAddress, InternalAddress, MASP}; +use namada_core::arith::checked; use namada_core::collections::{HashMap, HashSet}; use namada_core::key::*; use namada_core::masp::{AssetData, ExtendedViewingKey, PaymentAddress}; @@ -487,7 +488,7 @@ pub async fn validate_fee_and_gen_unshield( .await .unwrap_or_default(); - let total_fee = fee_amount.amount() * u64::from(args.gas_limit); + let total_fee = checked!(fee_amount.amount() * u64::from(args.gas_limit))?; let mut updated_balance = TxSourcePostBalance { post_balance: balance, source: fee_payer_address.clone(), @@ -622,7 +623,8 @@ pub async fn validate_fee_and_gen_unshield( .to_string(), )); } - updated_balance.post_balance -= total_fee; + updated_balance.post_balance = + checked!(updated_balance.post_balance - total_fee)?; None } }; diff --git a/crates/sdk/src/tx.rs b/crates/sdk/src/tx.rs index 66514ca6cb..c013375e28 100644 --- a/crates/sdk/src/tx.rs +++ b/crates/sdk/src/tx.rs @@ -21,6 +21,7 @@ use masp_primitives::transaction::{builder, Transaction as MaspTransaction}; use masp_primitives::zip32::ExtendedFullViewingKey; use namada_account::{InitAccount, UpdateAccount}; use namada_core::address::{Address, InternalAddress, MASP}; +use namada_core::arith::checked; use namada_core::collections::HashSet; use namada_core::dec::Dec; use namada_core::hash::Hash; @@ -639,7 +640,8 @@ pub async fn build_validator_commission_change( ))); } - let pipeline_epoch_minus_one = epoch + params.pipeline_len - 1; + let pipeline_epoch_minus_one = + epoch.unchecked_add(params.pipeline_len - 1); let CommissionPair { commission_rate, @@ -666,7 +668,7 @@ pub async fn build_validator_commission_change( )); } } - if rate.abs_diff(&commission_rate) + if rate.abs_diff(commission_rate)? > max_commission_change_per_epoch { edisplay_line!( @@ -863,7 +865,8 @@ pub async fn build_validator_metadata_change( ))); } } - let pipeline_epoch_minus_one = epoch + params.pipeline_len - 1; + let pipeline_epoch_minus_one = + epoch.unchecked_add(params.pipeline_len - 1); let CommissionPair { commission_rate, @@ -890,7 +893,7 @@ pub async fn build_validator_metadata_change( )); } } - if rate.abs_diff(&commission_rate) + if rate.abs_diff(commission_rate)? > max_commission_change_per_epoch { edisplay_line!( @@ -1116,7 +1119,7 @@ pub async fn build_unjail_validator( let params: PosParams = rpc::get_pos_params(context.client()).await?; let current_epoch = rpc::query_epoch(context.client()).await?; - let pipeline_epoch = current_epoch + params.pipeline_len; + let pipeline_epoch = current_epoch.unchecked_add(params.pipeline_len); let (validator_state_at_pipeline, _) = rpc::get_validator_state( context.client(), @@ -1143,8 +1146,8 @@ pub async fn build_unjail_validator( match last_slash_epoch { Ok(Some(last_slash_epoch)) => { // Jailed due to slashing - let eligible_epoch = - last_slash_epoch + params.slash_processing_epoch_offset(); + let eligible_epoch = last_slash_epoch + .unchecked_add(params.slash_processing_epoch_offset()); if current_epoch < eligible_epoch { edisplay_line!( context.io(), @@ -1224,7 +1227,7 @@ pub async fn build_deactivate_validator( let params: PosParams = rpc::get_pos_params(context.client()).await?; let current_epoch = rpc::query_epoch(context.client()).await?; - let pipeline_epoch = current_epoch + params.pipeline_len; + let pipeline_epoch = current_epoch.unchecked_add(params.pipeline_len); let (validator_state_at_pipeline, _) = rpc::get_validator_state( context.client(), @@ -1302,7 +1305,7 @@ pub async fn build_reactivate_validator( let params: PosParams = rpc::get_pos_params(context.client()).await?; let current_epoch = rpc::query_epoch(context.client()).await?; - let pipeline_epoch = current_epoch + params.pipeline_len; + let pipeline_epoch = current_epoch.unchecked_add(params.pipeline_len); for epoch in Epoch::iter_bounds_inclusive(current_epoch, pipeline_epoch) { let (validator_state, _) = @@ -1410,8 +1413,9 @@ pub async fn build_redelegation( .await?; let current_epoch = rpc::query_epoch(context.client()).await?; let is_not_chained = if let Some(redel_end_epoch) = incoming_redel_epoch { - let last_contrib_epoch = redel_end_epoch.prev(); - last_contrib_epoch + params.slash_processing_epoch_offset() + let last_contrib_epoch = + redel_end_epoch.prev().expect("End epoch must have a prev"); + last_contrib_epoch.unchecked_add(params.slash_processing_epoch_offset()) <= current_epoch } else { true @@ -1437,7 +1441,7 @@ pub async fn build_redelegation( // Give a redelegation warning based on the pipeline state of the dest // validator - let pipeline_epoch = current_epoch + params.pipeline_len; + let pipeline_epoch = current_epoch.unchecked_add(params.pipeline_len); let (dest_validator_state_at_pipeline, _) = rpc::get_validator_state( context.client(), &dest_validator, @@ -1718,8 +1722,8 @@ pub async fn build_unbond( let params = rpc::get_pos_params(context.client()).await?; let current_epoch = rpc::query_epoch(context.client()).await?; - let eligible_epoch = - infraction_epoch + params.slash_processing_epoch_offset(); + let eligible_epoch = infraction_epoch + .unchecked_add(params.slash_processing_epoch_offset()); if current_epoch < eligible_epoch { edisplay_line!( context.io(), @@ -1793,7 +1797,7 @@ pub async fn build_unbond( let mut withdrawable = BTreeMap::::new(); for ((_start_epoch, withdraw_epoch), amount) in unbonds.into_iter() { let to_withdraw = withdrawable.entry(withdraw_epoch).or_default(); - *to_withdraw += amount; + *to_withdraw = checked!(to_withdraw + amount)?; } let latest_withdrawal_pre = withdrawable.into_iter().last(); @@ -1837,7 +1841,7 @@ pub async fn query_unbonds( let mut withdrawable = BTreeMap::::new(); for ((_start_epoch, withdraw_epoch), amount) in unbonds.into_iter() { let to_withdraw = withdrawable.entry(withdraw_epoch).or_default(); - *to_withdraw += amount; + *to_withdraw = checked!(to_withdraw + amount)?; } let (latest_withdraw_epoch_post, latest_withdraw_amount_post) = withdrawable.into_iter().last().ok_or_else(|| { @@ -1863,8 +1867,11 @@ pub async fn query_unbonds( display_line!( context.io(), "Amount {} withdrawable starting from epoch {}", - (latest_withdraw_amount_post - latest_withdraw_amount_pre) - .to_string_native(), + checked!( + latest_withdraw_amount_post + - latest_withdraw_amount_pre + )? + .to_string_native(), latest_withdraw_epoch_post ); } @@ -1948,7 +1955,7 @@ pub async fn build_bond( // Give a bonding warning based on the pipeline state let params: PosParams = rpc::get_pos_params(context.client()).await?; let current_epoch = rpc::query_epoch(context.client()).await?; - let pipeline_epoch = current_epoch + params.pipeline_len; + let pipeline_epoch = current_epoch.unchecked_add(params.pipeline_len); let (validator_state_at_pipeline, _) = rpc::get_validator_state( context.client(), &validator, From 80e22a2a98f762576c3c8a3ee3cf3af4ee02c4f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 2 May 2024 16:35:53 +0200 Subject: [PATCH 24/29] namada: update to non-panicking core API --- Cargo.lock | 1 + crates/namada/Cargo.toml | 1 + crates/namada/src/ledger/governance/mod.rs | 35 +++++++++++---- .../ethereum_bridge/bridge_pool_vp.rs | 45 +++++++++++-------- crates/namada/src/ledger/native_vp/masp.rs | 7 ++- crates/namada/src/ledger/pos/mod.rs | 4 +- 6 files changed, 62 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fe8dfedfc8..58a876b593 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4347,6 +4347,7 @@ dependencies = [ "serde_json", "sha2 0.9.9", "slip10_ed25519", + "smooth-operator", "tempfile", "tendermint-rpc", "test-log", diff --git a/crates/namada/Cargo.toml b/crates/namada/Cargo.toml index 7d3538cab5..a70adf8442 100644 --- a/crates/namada/Cargo.toml +++ b/crates/namada/Cargo.toml @@ -141,6 +141,7 @@ serde.workspace = true serde_json.workspace = true sha2.workspace = true slip10_ed25519.workspace = true +smooth-operator.workspace = true tempfile = { version = "3.2.0", optional = true } tendermint-rpc = { workspace = true, optional = true } thiserror.workspace = true diff --git a/crates/namada/src/ledger/governance/mod.rs b/crates/namada/src/ledger/governance/mod.rs index 4bacadac4d..44fde91afd 100644 --- a/crates/namada/src/ledger/governance/mod.rs +++ b/crates/namada/src/ledger/governance/mod.rs @@ -5,6 +5,7 @@ pub mod utils; use std::collections::BTreeSet; use borsh::BorshDeserialize; +use namada_core::arith::checked; use namada_core::booleans::{BoolResultUnitExt, ResultBoolExt}; use namada_governance::storage::proposal::{ AddRemove, PGFAction, ProposalType, @@ -687,7 +688,10 @@ where } let is_valid_activation_epoch = end_epoch < activation_epoch - && (activation_epoch - end_epoch).0 >= min_grace_epochs; + && checked!(activation_epoch - end_epoch) + .map_err(|e| Error::NativeVpError(e.into()))? + .0 + >= min_grace_epochs; if !is_valid_activation_epoch { let error = native_vp::Error::new_alloc(format!( "Expected min duration between the end and grace epoch \ @@ -787,7 +791,10 @@ where } let proposal_period_multiple_of_min_period = - (end_epoch - start_epoch) % min_period == 0; + checked!((end_epoch - start_epoch) % min_period) + .map_err(|e| Error::NativeVpError(e.into()))? + .0 + == 0; if !proposal_period_multiple_of_min_period { return Err(native_vp::Error::new_alloc(format!( "Proposal with id {proposal_id} does not have a voting period \ @@ -798,8 +805,10 @@ where .into()); } - let proposal_meets_min_period = - (end_epoch - start_epoch).0 >= min_period; + let proposal_meets_min_period = checked!(end_epoch - start_epoch) + .map_err(|e| Error::NativeVpError(e.into()))? + .0 + >= min_period; if !proposal_meets_min_period { return Err(native_vp::Error::new_alloc(format!( "Proposal with id {proposal_id} does not meet the required \ @@ -867,7 +876,10 @@ where } let proposal_period_multiple_of_min_period = - (end_epoch - start_epoch) % min_period == 0; + checked!((end_epoch - start_epoch) % min_period) + .map_err(|e| Error::NativeVpError(e.into()))? + .0 + == 0; if !proposal_period_multiple_of_min_period { return Err(native_vp::Error::new_alloc(format!( "Proposal with id {proposal_id} does not have a voting period \ @@ -878,8 +890,9 @@ where .into()); } - let valid_voting_period = (end_epoch - start_epoch).0 >= min_period - && (end_epoch - start_epoch).0 <= max_period; + let diff = checked!(end_epoch - start_epoch) + .map_err(|e| Error::NativeVpError(e.into()))?; + let valid_voting_period = diff.0 >= min_period && diff.0 <= max_period; valid_voting_period.ok_or_else(|| { native_vp::Error::new_alloc(format!( @@ -950,7 +963,9 @@ where })?; let is_valid_funds = post_balance >= pre_balance - && post_balance - pre_balance == post_funds; + && checked!(post_balance - pre_balance) + .map_err(|e| Error::NativeVpError(e.into()))? + == post_funds; is_valid_funds.ok_or_else(|| { native_vp::Error::new_alloc(format!( "Invalid funds {} have been written to storage", @@ -980,7 +995,9 @@ where let balance_is_valid = if let Some(pre_balance) = pre_balance { post_balance > pre_balance - && post_balance - pre_balance >= min_funds_parameter + && checked!(post_balance - pre_balance) + .map_err(|e| Error::NativeVpError(e.into()))? + >= min_funds_parameter } else { post_balance >= min_funds_parameter }; diff --git a/crates/namada/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/crates/namada/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index 1e7e7cf388..14f728b2e9 100644 --- a/crates/namada/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/crates/namada/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -17,6 +17,7 @@ use std::fmt::Debug; use std::marker::PhantomData; use borsh::BorshDeserialize; +use namada_core::arith::checked; use namada_core::booleans::BoolResultUnitExt; use namada_core::eth_bridge_pool::erc20_token_address; use namada_core::hints; @@ -63,11 +64,12 @@ struct AmountDelta { impl AmountDelta { /// Resolve the updated amount by applying the delta value. #[inline] - fn resolve(self) -> Amount { + fn resolve(self) -> Result { match self.delta { - SignedAmount::Positive(delta) => self.base + delta, - SignedAmount::Negative(delta) => self.base - delta, + SignedAmount::Positive(delta) => checked!(self.base + delta), + SignedAmount::Negative(delta) => checked!(self.base - delta), } + .map_err(|e| Error(e.into())) } } @@ -92,33 +94,38 @@ where &self, token: &Address, address: &Address, - ) -> Option { + ) -> Result, Error> { let account_key = balance_key(token, address); let before: Amount = (&self.ctx) .read_pre_value(&account_key) .map_err(|error| { tracing::warn!(?error, %account_key, "reading pre value"); - }) - .ok()? + error + })? // NB: the previous balance of the given account might // have been null. this is valid if the account is // being credited, such as when we escrow gas under // the Bridge pool .unwrap_or_default(); - let after: Amount = (&self.ctx) - .read_post_value(&account_key) - .unwrap_or_else(|error| { - tracing::warn!(?error, %account_key, "reading post value"); - None - })?; - Some(AmountDelta { + let after: Amount = match (&self.ctx).read_post_value(&account_key)? { + Some(after) => after, + None => { + tracing::warn!(%account_key, "no post value"); + return Ok(None); + } + }; + Ok(Some(AmountDelta { base: before, delta: if before > after { - SignedAmount::Negative(before - after) + SignedAmount::Negative( + checked!(before - after).map_err(|e| Error(e.into()))?, + ) } else { - SignedAmount::Positive(after - before) + SignedAmount::Positive( + checked!(after - before).map_err(|e| Error(e.into()))?, + ) }, - }) + })) } /// Check that the correct amount of tokens were sent @@ -147,8 +154,8 @@ where expected_credit, .. } = delta; - let debit = self.account_balance_delta(&token, payer_account); - let credit = self.account_balance_delta(&token, escrow_account); + let debit = self.account_balance_delta(&token, payer_account)?; + let credit = self.account_balance_delta(&token, escrow_account)?; match (debit, credit) { // success case @@ -291,7 +298,7 @@ where // storage. let escrowed_balance = match self.check_escrowed_toks_balance(token_check)? { - Some(balance) => balance.resolve(), + Some(balance) => balance.resolve()?, None => return Ok(false), }; diff --git a/crates/namada/src/ledger/native_vp/masp.rs b/crates/namada/src/ledger/native_vp/masp.rs index b30b263ed9..0a5b73d9b1 100644 --- a/crates/namada/src/ledger/native_vp/masp.rs +++ b/crates/namada/src/ledger/native_vp/masp.rs @@ -11,6 +11,7 @@ use masp_primitives::transaction::components::I128Sum; use masp_primitives::transaction::Transaction; use namada_core::address::Address; use namada_core::address::InternalAddress::Masp; +use namada_core::arith::checked; use namada_core::booleans::BoolResultUnitExt; use namada_core::collections::{HashMap, HashSet}; use namada_core::masp::encode_asset_type; @@ -376,12 +377,14 @@ where )); } Ordering::Less => ( - post_masp_balance - pre_masp_balance, + checked!(post_masp_balance - pre_masp_balance) + .map_err(|e| Error::NativeVpError(e.into()))?, counterpart, Address::Internal(Masp), ), Ordering::Greater => ( - pre_masp_balance - post_masp_balance, + checked!(pre_masp_balance - post_masp_balance) + .map_err(|e| Error::NativeVpError(e.into()))?, Address::Internal(Masp), counterpart, ), diff --git a/crates/namada/src/ledger/pos/mod.rs b/crates/namada/src/ledger/pos/mod.rs index 7d7e552b63..897b444ee0 100644 --- a/crates/namada/src/ledger/pos/mod.rs +++ b/crates/namada/src/ledger/pos/mod.rs @@ -31,7 +31,9 @@ pub fn into_tm_voting_power( tokens: token::Amount, ) -> i64 { let tokens = tokens.change(); - let prod = votes_per_token * tokens; + let prod = tokens + .mul_ceil(votes_per_token) + .expect("Must be able to convert tokens to TM votes"); let res = i128::try_from(prod).expect("Failed conversion to i128"); i64::try_from(res).expect("Invalid validator voting power (i64)") } From 2fbcae3d0cfcaed9f8ba85794c0f874514b70681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 2 May 2024 17:22:37 +0200 Subject: [PATCH 25/29] apps: update to non-panicking core API --- crates/apps/src/bin/namada-node/cli.rs | 2 +- crates/apps/src/lib/bench_utils.rs | 8 +-- crates/apps/src/lib/client/rpc.rs | 12 +++-- crates/apps/src/lib/client/utils.rs | 5 +- .../src/lib/config/genesis/transactions.rs | 4 +- .../lib/node/ledger/ethereum_oracle/mod.rs | 7 +-- .../lib/node/ledger/shell/finalize_block.rs | 50 +++++++++++-------- .../src/lib/node/ledger/shell/governance.rs | 12 +++-- .../src/lib/node/ledger/shell/init_chain.rs | 4 +- .../src/lib/node/ledger/shell/testing/node.rs | 5 +- .../src/lib/node/ledger/storage/rocksdb.rs | 3 +- 11 files changed, 70 insertions(+), 42 deletions(-) diff --git a/crates/apps/src/bin/namada-node/cli.rs b/crates/apps/src/bin/namada-node/cli.rs index 8be47b4cfc..70eac446ae 100644 --- a/crates/apps/src/bin/namada-node/cli.rs +++ b/crates/apps/src/bin/namada-node/cli.rs @@ -59,7 +59,7 @@ pub fn main() -> Result<()> { let wasm_dir = chain_ctx.wasm_dir(); chain_ctx.config.ledger.shell.action_at_height = Some(ActionAtHeight { - height: args.last_height + 2, + height: args.last_height.checked_add(2).unwrap(), action: Action::Halt, }); std::env::set_var( diff --git a/crates/apps/src/lib/bench_utils.rs b/crates/apps/src/lib/bench_utils.rs index cc72bddb48..d7984c70c7 100644 --- a/crates/apps/src/lib/bench_utils.rs +++ b/crates/apps/src/lib/bench_utils.rs @@ -254,8 +254,8 @@ impl Default for BenchShell { author: defaults::albert_address(), r#type: ProposalType::Default, voting_start_epoch, - voting_end_epoch: voting_start_epoch + 3_u64, - activation_epoch: voting_start_epoch + 9_u64, + voting_end_epoch: voting_start_epoch.unchecked_add(3_u64), + activation_epoch: voting_start_epoch.unchecked_add(9_u64), }, None, Some(vec![content_section]), @@ -414,7 +414,7 @@ impl BenchShell { &mut self.state, ¶ms, current_epoch, - current_epoch + params.pipeline_len, + current_epoch.unchecked_add(params.pipeline_len), ) .unwrap(); @@ -570,7 +570,7 @@ impl BenchShell { self.inner .state .in_mem_mut() - .begin_block(last_height + 1) + .begin_block(last_height.next_height()) .unwrap(); self.inner.commit(); diff --git a/crates/apps/src/lib/client/rpc.rs b/crates/apps/src/lib/client/rpc.rs index 7364e5a8c2..1ec5e40a5a 100644 --- a/crates/apps/src/lib/client/rpc.rs +++ b/crates/apps/src/lib/client/rpc.rs @@ -1564,11 +1564,13 @@ pub async fn query_and_print_unbonds( let mut not_yet_withdrawable = HashMap::::new(); for ((_start_epoch, withdraw_epoch), amount) in unbonds.into_iter() { if withdraw_epoch <= current_epoch { - total_withdrawable += amount; + total_withdrawable = + total_withdrawable.checked_add(amount).unwrap(); } else { let withdrawable_amount = not_yet_withdrawable.entry(withdraw_epoch).or_default(); - *withdrawable_amount += amount; + *withdrawable_amount = + withdrawable_amount.checked_add(amount).unwrap(); } } if !total_withdrawable.is_zero() { @@ -1653,7 +1655,7 @@ pub async fn query_bonds( context.io(), &mut w; "Active (slashable) bonds total: {}", - details.bonds_total_active().to_string_native() + details.bonds_total_active().unwrap().to_string_native() )?; } display_line!(context.io(), &mut w; "Bonds total: {}", details.bonds_total.to_string_native())?; @@ -1697,7 +1699,7 @@ pub async fn query_bonds( context.io(), &mut w; "All bonds total active: {}", - bonds_and_unbonds.bonds_total_active().to_string_native() + bonds_and_unbonds.bonds_total_active().unwrap().to_string_native() )?; } display_line!( @@ -1720,7 +1722,7 @@ pub async fn query_bonds( context.io(), &mut w; "All unbonds total active: {}", - bonds_and_unbonds.unbonds_total_active().to_string_native() + bonds_and_unbonds.unbonds_total_active().unwrap().to_string_native() )?; } display_line!( diff --git a/crates/apps/src/lib/client/utils.rs b/crates/apps/src/lib/client/utils.rs index 149a20eef4..840991433d 100644 --- a/crates/apps/src/lib/client/utils.rs +++ b/crates/apps/src/lib/client/utils.rs @@ -430,7 +430,10 @@ pub fn init_network( if tm_votes_per_token > Dec::from(1) { Uint::one() } else { - (Dec::from(1) / tm_votes_per_token).ceil().abs() + (Dec::from(1).checked_div(tm_votes_per_token).unwrap()) + .ceil() + .unwrap() + .abs() }, token::NATIVE_MAX_DECIMAL_PLACES, ) diff --git a/crates/apps/src/lib/config/genesis/transactions.rs b/crates/apps/src/lib/config/genesis/transactions.rs index 107b5148fa..b59e6235f1 100644 --- a/crates/apps/src/lib/config/genesis/transactions.rs +++ b/crates/apps/src/lib/config/genesis/transactions.rs @@ -541,7 +541,9 @@ impl Transactions { BTreeMap::new(); for tx in txs { let entry = stakes.entry(&tx.validator).or_default(); - *entry += tx.amount.amount(); + *entry = entry + .checked_add(tx.amount.amount()) + .expect("Validator total stake must not overflow"); } stakes.into_values().any(|stake| { diff --git a/crates/apps/src/lib/node/ledger/ethereum_oracle/mod.rs b/crates/apps/src/lib/node/ledger/ethereum_oracle/mod.rs index 9bd964c120..aa73ad29a7 100644 --- a/crates/apps/src/lib/node/ledger/ethereum_oracle/mod.rs +++ b/crates/apps/src/lib/node/ledger/ethereum_oracle/mod.rs @@ -452,7 +452,7 @@ async fn run_oracle_aux(mut oracle: Oracle) { if !config.active { config = oracle.wait_on_reactivation().await; } - next_block_to_process += 1.into(); + next_block_to_process = next_block_to_process.next(); } } @@ -481,8 +481,9 @@ async fn process_events_in_block( SyncStatus::Syncing => return Err(Error::FallenBehind), } .into(); - let minimum_latest_block = - block_to_process.clone() + config.min_confirmations.into(); + let minimum_latest_block = block_to_process.clone().unchecked_add( + ethereum_structs::BlockHeight::from(config.min_confirmations), + ); if minimum_latest_block > latest_block { tracing::debug!( ?block_to_process, diff --git a/crates/apps/src/lib/node/ledger/shell/finalize_block.rs b/crates/apps/src/lib/node/ledger/shell/finalize_block.rs index 4c6b6c419a..1fb4898036 100644 --- a/crates/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/crates/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -545,7 +545,7 @@ where /// if necessary. Returns a bool indicating if a new epoch began and the /// height of the new block. fn update_state(&mut self, header: Header) -> (BlockHeight, bool) { - let height = self.state.in_mem().get_last_block_height() + 1; + let height = self.state.in_mem().get_last_block_height().next_height(); self.state .in_mem_mut() @@ -597,7 +597,9 @@ where current_epoch: Epoch, events: &mut impl EmitEvents, ) -> Result<()> { - let last_epoch = current_epoch.prev(); + let last_epoch = current_epoch + .prev() + .expect("Must have a prev epoch when applying inflation"); // Get the number of blocks in the last epoch let first_block_of_last_epoch = @@ -623,11 +625,9 @@ where // Take IBC events that may be emitted from PGF for ibc_event in self.state.write_log_mut().take_ibc_events() { // Add the height for IBC event query - events.emit( - ibc_event.with(Height( - self.state.in_mem().get_last_block_height() + 1, - )), - ); + events.emit(ibc_event.with(Height( + self.state.in_mem().get_last_block_height().next_height(), + ))); } Ok(()) @@ -1912,7 +1912,8 @@ mod test_finalize_block { // The total rewards claimed should be approximately equal to the total // minted inflation, minus (unbond_amount / initial_stake) * rewards // from the unbond epoch and the following epoch (the missed_rewards) - let ratio = Dec::from(unbond_amount) / Dec::from(init_stake); + let ratio = Dec::try_from(unbond_amount).unwrap() + / Dec::try_from(init_stake).unwrap(); let lost_rewards = ratio * missed_rewards; let uncertainty = Dec::from_str("0.07").unwrap(); let token_uncertainty = uncertainty * lost_rewards; @@ -2076,8 +2077,8 @@ mod test_finalize_block { )); // Check that the commission earned is expected - let del_stake = Dec::from(del_amount); - let tot_stake = Dec::from(init_stake + del_amount); + let del_stake = Dec::try_from(del_amount).unwrap(); + let tot_stake = Dec::try_from(init_stake + del_amount).unwrap(); let stake_ratio = del_stake / tot_stake; let del_rewards_no_commission = stake_ratio * inflation_3; let commission = commission_rate * del_rewards_no_commission; @@ -3591,7 +3592,7 @@ mod test_finalize_block { let total_stake = read_total_stake(&shell.state, ¶ms, pipeline_epoch)?; - let expected_slashed = initial_stake.mul_ceil(cubic_rate); + let expected_slashed = initial_stake.mul_ceil(cubic_rate).unwrap(); println!( "Initial stake = {}\nCubic rate = {}\nExpected slashed = {}\n", @@ -4117,8 +4118,10 @@ mod test_finalize_block { ) .unwrap(); - let vp_frac_3 = Dec::from(val_stake_3) / Dec::from(tot_stake_3); - let vp_frac_4 = Dec::from(val_stake_4) / Dec::from(tot_stake_4); + let vp_frac_3 = Dec::try_from(val_stake_3).unwrap() + / Dec::try_from(tot_stake_3).unwrap(); + let vp_frac_4 = Dec::try_from(val_stake_4).unwrap() + / Dec::try_from(tot_stake_4).unwrap(); let tot_frac = Dec::two() * vp_frac_3 + vp_frac_4; let cubic_rate = std::cmp::min( Dec::one(), @@ -4128,7 +4131,7 @@ mod test_finalize_block { let equal_enough = |rate1: Dec, rate2: Dec| -> bool { let tolerance = Dec::new(1, 9).unwrap(); - rate1.abs_diff(&rate2) < tolerance + rate1.abs_diff(rate2).unwrap() < tolerance }; // There should be 2 slashes processed for the validator, each with rate @@ -4164,7 +4167,8 @@ mod test_finalize_block { - del_unbond_1_amount + self_bond_1_amount - self_unbond_2_amount) - .mul_ceil(slash_rate_3); + .mul_ceil(slash_rate_3) + .unwrap(); assert!( ((pre_stake_10 - post_stake_10).change() - exp_slashed_during_processing_9.change()) @@ -4178,22 +4182,27 @@ mod test_finalize_block { // Check that we can compute the stake at the pipeline epoch // NOTE: may be off. by 1 namnam due to rounding; let exp_pipeline_stake = (Dec::one() - slash_rate_3) - * Dec::from( + * Dec::try_from( initial_stake + del_1_amount - self_unbond_1_amount - del_unbond_1_amount + self_bond_1_amount - self_unbond_2_amount, ) - + Dec::from(del_2_amount); + .unwrap() + + Dec::try_from(del_2_amount).unwrap(); assert!( - exp_pipeline_stake.abs_diff(&Dec::from(post_stake_10)) + exp_pipeline_stake + .abs_diff(Dec::try_from(post_stake_10).unwrap()) + .unwrap() <= Dec::new(2, NATIVE_MAX_DECIMAL_PLACES).unwrap(), "Expected {}, got {} (with less than 2 err), diff {}", exp_pipeline_stake, post_stake_10.to_string_native(), - exp_pipeline_stake.abs_diff(&Dec::from(post_stake_10)), + exp_pipeline_stake + .abs_diff(Dec::try_from(post_stake_10).unwrap()) + .unwrap(), ); // Check the balance of the Slash Pool @@ -4420,6 +4429,7 @@ mod test_finalize_block { (self_details.unbonds[1].slashed_amount.unwrap().change() - (self_unbond_2_amount - self_bond_1_amount) .mul_ceil(rate) + .unwrap() .change()) .abs() <= Uint::from(1) @@ -4440,7 +4450,7 @@ mod test_finalize_block { .unwrap(); let exp_del_withdraw_slashed_amount = - del_unbond_1_amount.mul_ceil(slash_rate_3); + del_unbond_1_amount.mul_ceil(slash_rate_3).unwrap(); assert!( (del_withdraw - (del_unbond_1_amount - exp_del_withdraw_slashed_amount)) diff --git a/crates/apps/src/lib/node/ledger/shell/governance.rs b/crates/apps/src/lib/node/ledger/shell/governance.rs index 822d9f2a13..0d2dde998f 100644 --- a/crates/apps/src/lib/node/ledger/shell/governance.rs +++ b/crates/apps/src/lib/node/ledger/shell/governance.rs @@ -183,9 +183,15 @@ where // Take events that could have been emitted by PGF // over IBC, governance proposal execution, etc for event in shell.state.write_log_mut().take_ibc_events() { - events.emit(event.with(Height( - shell.state.in_mem().get_last_block_height() + 1, - ))); + events.emit( + event.with(Height( + shell + .state + .in_mem() + .get_last_block_height() + .next_height(), + )), + ); } gov_api::get_proposal_author(&shell.state, id)? diff --git a/crates/apps/src/lib/node/ledger/shell/init_chain.rs b/crates/apps/src/lib/node/ledger/shell/init_chain.rs index c75e72615f..fddfe83888 100644 --- a/crates/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/crates/apps/src/lib/node/ledger/shell/init_chain.rs @@ -535,7 +535,9 @@ where balance.amount(), ) .expect("Couldn't credit initial balance"); - total_token_balance += balance.amount(); + total_token_balance = total_token_balance + .checked_add(balance.amount()) + .expect("token total balance must not overflow"); } // Write the total amount of tokens for the ratio self.state diff --git a/crates/apps/src/lib/node/ledger/shell/testing/node.rs b/crates/apps/src/lib/node/ledger/shell/testing/node.rs index 21887b98de..5a8f0f9b32 100644 --- a/crates/apps/src/lib/node/ledger/shell/testing/node.rs +++ b/crates/apps/src/lib/node/ledger/shell/testing/node.rs @@ -288,12 +288,13 @@ impl MockNode { self.submit_txs(txs); } MockServiceAction::IncrementEthHeight => { - *self + let mut height = self .services .ethereum_oracle .next_block_to_process .write() - .await += 1.into(); + .await; + *height = height.next(); } } } diff --git a/crates/apps/src/lib/node/ledger/storage/rocksdb.rs b/crates/apps/src/lib/node/ledger/storage/rocksdb.rs index 5f852adaf5..22f4e7d44d 100644 --- a/crates/apps/src/lib/node/ledger/storage/rocksdb.rs +++ b/crates/apps/src/lib/node/ledger/storage/rocksdb.rs @@ -501,7 +501,8 @@ impl RocksDB { } let mut batch = RocksDB::batch(); - let previous_height = last_block.height.prev_height(); + let previous_height = + last_block.height.prev_height().expect("Must have a pred"); let state_cf = self.get_column_family(STATE_CF)?; // Revert the non-height-prepended metadata storage keys which get From 7c7e483df654afb26fdecec4e88ce02993588bec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 2 May 2024 18:06:26 +0200 Subject: [PATCH 26/29] benches: update to non-panicking core API --- crates/benches/native_vps.rs | 12 ++++++++---- crates/benches/txs.rs | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/crates/benches/native_vps.rs b/crates/benches/native_vps.rs index dfa2863c93..e49d3a40d9 100644 --- a/crates/benches/native_vps.rs +++ b/crates/benches/native_vps.rs @@ -134,8 +134,10 @@ fn governance(c: &mut Criterion) { author: defaults::albert_address(), r#type: ProposalType::Default, voting_start_epoch, - voting_end_epoch: voting_start_epoch + 3_u64, - activation_epoch: voting_start_epoch + 9_u64, + voting_end_epoch: voting_start_epoch + .unchecked_add(3_u64), + activation_epoch: voting_start_epoch + .unchecked_add(9_u64), }, None, Some(vec![content_section]), @@ -187,8 +189,10 @@ fn governance(c: &mut Criterion) { wasm_code_section.get_hash(), ), voting_start_epoch, - voting_end_epoch: voting_start_epoch + 3_u64, - activation_epoch: voting_start_epoch + 9_u64, + voting_end_epoch: voting_start_epoch + .unchecked_add(3_u64), + activation_epoch: voting_start_epoch + .unchecked_add(9_u64), }, None, Some(vec![content_section, wasm_code_section]), diff --git a/crates/benches/txs.rs b/crates/benches/txs.rs index 881aa16010..4f20955a9b 100644 --- a/crates/benches/txs.rs +++ b/crates/benches/txs.rs @@ -862,7 +862,7 @@ fn unjail_validator(c: &mut Criterion) { // Jail the validator let pos_params = read_pos_params(&shell.state).unwrap(); let current_epoch = shell.state.in_mem().block.epoch; - let evidence_epoch = current_epoch.prev(); + let evidence_epoch = current_epoch.prev().unwrap(); proof_of_stake::slashing::slash( &mut shell.state, &pos_params, From 2394478c64d4fc9b28e49f95a024d32274d23316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 2 May 2024 18:06:41 +0200 Subject: [PATCH 27/29] wasm: update to non-panicking core API --- wasm/Cargo.lock | 6 ++++++ wasm/vp_implicit/src/lib.rs | 3 ++- wasm/vp_user/src/lib.rs | 3 ++- wasm_for_tests/Cargo.lock | 6 ++++++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index d2c2ef6ae7..ee5ab622a9 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -3551,6 +3551,7 @@ dependencies = [ "serde_json", "sha2 0.9.9", "slip10_ed25519", + "smooth-operator", "tempfile", "thiserror", "tiny-bip39", @@ -3590,6 +3591,8 @@ name = "namada_controller" version = "0.34.0" dependencies = [ "namada_core", + "smooth-operator", + "thiserror", ] [[package]] @@ -3698,6 +3701,7 @@ dependencies = [ "proptest", "serde", "serde_json", + "smooth-operator", "thiserror", "tracing", ] @@ -3799,6 +3803,7 @@ dependencies = [ "once_cell", "proptest", "serde", + "smooth-operator", "thiserror", "tracing", ] @@ -3864,6 +3869,7 @@ dependencies = [ "serde_json", "sha2 0.9.9", "slip10_ed25519", + "smooth-operator", "tendermint-config", "tendermint-rpc", "thiserror", diff --git a/wasm/vp_implicit/src/lib.rs b/wasm/vp_implicit/src/lib.rs index 2b876e2072..3f3c57fbc0 100644 --- a/wasm/vp_implicit/src/lib.rs +++ b/wasm/vp_implicit/src/lib.rs @@ -151,7 +151,8 @@ fn validate_tx( ctx.read_pre(key).into_vp_error()?.unwrap_or_default(); let post: token::Amount = ctx.read_post(key).into_vp_error()?.unwrap_or_default(); - let change = post.change() - pre.change(); + let change = + post.change().checked_sub(pre.change()).unwrap(); gadget.verify_signatures_when( // NB: debit has to signed, credit doesn't || change.is_negative(), diff --git a/wasm/vp_user/src/lib.rs b/wasm/vp_user/src/lib.rs index 9cee74a04d..7e90186a9b 100644 --- a/wasm/vp_user/src/lib.rs +++ b/wasm/vp_user/src/lib.rs @@ -112,7 +112,8 @@ fn validate_tx( ctx.read_pre(key).into_vp_error()?.unwrap_or_default(); let post: token::Amount = ctx.read_post(key).into_vp_error()?.unwrap_or_default(); - let change = post.change() - pre.change(); + let change = + post.change().checked_sub(pre.change()).unwrap(); gadget.verify_signatures_when( // NB: debit has to signed, credit doesn't || change.is_negative(), diff --git a/wasm_for_tests/Cargo.lock b/wasm_for_tests/Cargo.lock index 67ec32df07..adeda6f95e 100644 --- a/wasm_for_tests/Cargo.lock +++ b/wasm_for_tests/Cargo.lock @@ -3529,6 +3529,7 @@ dependencies = [ "serde_json", "sha2 0.9.9", "slip10_ed25519", + "smooth-operator", "tempfile", "thiserror", "tiny-bip39", @@ -3566,6 +3567,8 @@ name = "namada_controller" version = "0.34.0" dependencies = [ "namada_core", + "smooth-operator", + "thiserror", ] [[package]] @@ -3668,6 +3671,7 @@ dependencies = [ "proptest", "serde", "serde_json", + "smooth-operator", "thiserror", "tracing", ] @@ -3755,6 +3759,7 @@ dependencies = [ "once_cell", "proptest", "serde", + "smooth-operator", "thiserror", "tracing", ] @@ -3818,6 +3823,7 @@ dependencies = [ "serde_json", "sha2 0.9.9", "slip10_ed25519", + "smooth-operator", "tendermint-config", "tendermint-rpc", "thiserror", From e53a3cce482c85abf81d3b49b27259da5db7d022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Fri, 3 May 2024 14:57:48 +0200 Subject: [PATCH 28/29] namada/pos: re-use into_tm_voting_power from pos crate --- crates/namada/src/ledger/pos/mod.rs | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/crates/namada/src/ledger/pos/mod.rs b/crates/namada/src/ledger/pos/mod.rs index 897b444ee0..35015cb5c9 100644 --- a/crates/namada/src/ledger/pos/mod.rs +++ b/crates/namada/src/ledger/pos/mod.rs @@ -10,6 +10,7 @@ pub use namada_proof_of_stake::pos_queries::*; pub use namada_proof_of_stake::storage::*; #[cfg(any(test, feature = "testing"))] pub use namada_proof_of_stake::test_utils; +pub use namada_proof_of_stake::types::into_tm_voting_power; pub use namada_proof_of_stake::{staking_token_address, types}; pub use vp::PosVP; pub use {namada_proof_of_stake, namada_state}; @@ -24,20 +25,6 @@ pub const ADDRESS: Address = address::POS; pub const SLASH_POOL_ADDRESS: Address = Address::Internal(InternalAddress::PosSlashPool); -/// 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: token::Amount, -) -> i64 { - let tokens = tokens.change(); - let prod = tokens - .mul_ceil(votes_per_token) - .expect("Must be able to convert tokens to TM votes"); - let res = i128::try_from(prod).expect("Failed conversion to i128"); - i64::try_from(res).expect("Invalid validator voting power (i64)") -} - /// Alias for a PoS type with the same name with concrete type parameters pub type BondId = namada_proof_of_stake::types::BondId; From a78ec98a4e16dd6735b4732195ad15f43681d1c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Fri, 3 May 2024 17:23:04 +0200 Subject: [PATCH 29/29] changelog: add #3074 --- .changelog/unreleased/improvements/3074-checked-arith.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/improvements/3074-checked-arith.md diff --git a/.changelog/unreleased/improvements/3074-checked-arith.md b/.changelog/unreleased/improvements/3074-checked-arith.md new file mode 100644 index 0000000000..835cb0c722 --- /dev/null +++ b/.changelog/unreleased/improvements/3074-checked-arith.md @@ -0,0 +1,2 @@ +- Prohibit unchecked arithmetics and conversions in the core crate. + ([\#3074](https://github.com/anoma/namada/pull/3074)) \ No newline at end of file