Skip to content
This repository was archived by the owner on Nov 15, 2023. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 45 additions & 3 deletions primitives/arithmetic/src/helpers_128bit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ mod double128 {
}

/// Returns 2^128 / a
/// `checked_div()` could be used here but is not possible to `unwrap()` inside a `const fn`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can use a match or if in const functions.

But for this function i would probably make more sense to add a checked_div128 function instead of changing it.

const fn div128(a: u128) -> u128 {
(neg128(a) / a).wrapping_add(1)
}
Expand Down Expand Up @@ -182,6 +183,19 @@ mod double128 {
}
}

pub const fn checked_multiply_by_rational_with_rounding(
a: u128,
b: u128,
c: u128,
r: Rounding,
) -> Result<Option<u128>, &'static str> {
if c == 0 {
return Err("Division by zero")
}

Ok(multiply_by_rational_with_rounding(a, b, c, r))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a general consideration of design:
I think its easier to put the actual logic inside the checked_ function such that the unchecked variant is just checked_function().expect() or checked_function.unwrap_or(whatever) since otherwise you have the case like now, where the checked variant does not have any logic and can therefore not ensure that it wont ever panic.

}

/// Returns `a * b / c` (wrapping to 128 bits) or `None` in the case of
/// overflow.
pub const fn multiply_by_rational_with_rounding(
Expand All @@ -191,9 +205,6 @@ pub const fn multiply_by_rational_with_rounding(
r: Rounding,
) -> Option<u128> {
use double128::Double128;
if c == 0 {
return None
}
Comment on lines -194 to -196
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually making it less secure, which is why i would rather put the logic in the checked variant (basically same comment as above).

let (result, remainder) = Double128::product_of(a, b).div(c);
let mut result: u128 = match result.try_into_u128() {
Ok(v) => v,
Expand Down Expand Up @@ -246,6 +257,7 @@ mod tests {
use super::*;
use codec::{Decode, Encode};
use multiply_by_rational_with_rounding as mulrat;
use checked_multiply_by_rational_with_rounding as checked_mulrat;
use Rounding::*;

const MAX: u128 = u128::max_value();
Expand Down Expand Up @@ -278,6 +290,36 @@ mod tests {
assert_eq!(mulrat(1, MAX / 2 + 1, MAX, NearestPrefUp), Some(1));
}

#[test]
fn rational_checked_multiply_basic_rounding_works() {
assert_eq!(checked_mulrat(1, 1, 1, Up), Ok(Some(1)));
assert_eq!(checked_mulrat(3, 1, 3, Up), Ok(Some(1)));
assert_eq!(checked_mulrat(1, 1, 3, Up), Ok(Some(1)));
assert_eq!(checked_mulrat(1, 2, 3, Down), Ok(Some(0)));
assert_eq!(checked_mulrat(1, 1, 3, NearestPrefDown), Ok(Some(0)));
assert_eq!(checked_mulrat(1, 1, 2, NearestPrefDown), Ok(Some(0)));
assert_eq!(checked_mulrat(1, 2, 3, NearestPrefDown), Ok(Some(1)));
assert_eq!(checked_mulrat(1, 1, 3, NearestPrefUp), Ok(Some(0)));
assert_eq!(checked_mulrat(1, 1, 2, NearestPrefUp), Ok(Some(1)));
assert_eq!(checked_mulrat(1, 2, 3, NearestPrefUp), Ok(Some(1)));
assert_eq!(checked_mulrat(3, 1, 0, Up), Err("Division by zero"));
}

#[test]
fn rational_checked_multiply_big_number_works() {
assert_eq!(checked_mulrat(MAX, MAX - 1, MAX, Down), Ok(Some(MAX - 1)));
assert_eq!(checked_mulrat(MAX, 1, MAX, Down), Ok(Some(1)));
assert_eq!(checked_mulrat(MAX, MAX - 1, MAX, Up), Ok(Some(MAX - 1)));
assert_eq!(checked_mulrat(MAX, 1, MAX, Up), Ok(Some(1)));
assert_eq!(checked_mulrat(1, MAX - 1, MAX, Down), Ok(Some(0)));
assert_eq!(checked_mulrat(1, 1, MAX, Up), Ok(Some(1)));
assert_eq!(checked_mulrat(1, MAX / 2, MAX, NearestPrefDown), Ok(Some(0)));
assert_eq!(checked_mulrat(1, MAX / 2 + 1, MAX, NearestPrefDown), Ok(Some(1)));
assert_eq!(checked_mulrat(1, MAX / 2, MAX, NearestPrefUp), Ok(Some(0)));
assert_eq!(checked_mulrat(1, MAX / 2 + 1, MAX, NearestPrefUp), Ok(Some(1)));
assert_eq!(checked_mulrat(1, MAX / 2 + 1, 0, NearestPrefUp), Err("Division by zero"));
}

#[test]
fn sqrt_works() {
for i in 0..100_000u32 {
Expand Down
131 changes: 128 additions & 3 deletions primitives/arithmetic/src/per_things.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
use serde::{Deserialize, Serialize};

use crate::traits::{
BaseArithmetic, Bounded, CheckedAdd, CheckedMul, CheckedSub, One, SaturatedConversion,
Saturating, UniqueSaturatedInto, Unsigned, Zero,
BaseArithmetic, Bounded, CheckedAdd, CheckedDiv, CheckedMul, CheckedSub, One,
SaturatedConversion, Saturating, UniqueSaturatedInto, Unsigned, Zero,
};
use codec::{CompactAs, Encode};
use num_traits::{Pow, SaturatingAdd, SaturatingSub};
Expand Down Expand Up @@ -496,11 +496,34 @@ where
(x / maximum) * part_n + c
}

/// Checked compute of the error due to integer division in the expression `x / denom * numer`.
fn checked_rational_mul_correction<N, P>(
x: N,
numer: P::Inner,
denom: P::Inner,
rounding: Rounding,
) -> Result<N, &'static str>
where
N: MultiplyArg + UniqueSaturatedInto<P::Inner>,
P: PerThing,
P::Inner: Into<N>,
{
if P::Inner::is_zero(&denom) {
return Err("Division by zero")
}
Ok(rational_mul_correction::<N, P>(x, numer, denom, rounding))
Comment on lines +511 to +514
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now you have the implication that the underlying function can only error iff denom is null, which is an implementation detail. Again i would rather move the logic into the checked variant and then call it from the unchecked one.

}

/// Compute the error due to integer division in the expression `x / denom * numer`.
///
/// Take the remainder of `x / denom` and multiply by `numer / denom`. The result can be added
/// to `x / denom * numer` for an accurate result.
fn rational_mul_correction<N, P>(x: N, numer: P::Inner, denom: P::Inner, rounding: Rounding) -> N
fn rational_mul_correction<N, P>(
x: N,
numer: P::Inner,
denom: P::Inner,
rounding: Rounding,
) -> N
where
N: MultiplyArg + UniqueSaturatedInto<P::Inner>,
P: PerThing,
Expand Down Expand Up @@ -1021,6 +1044,21 @@ macro_rules! implement_per_thing {
}
}

/// # Note
/// I am not sure if this is the best way to div safely for PerThings
impl CheckedDiv for $name {
#[inline]
fn checked_div(&self, rhs: &Self) -> Option<Self> {
let q = rhs.0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there should be an rhs.is_zero()


if q.is_zero() {
return None
}

Some(*self / *rhs)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably want to use the deconstruct() function here to get the inner value.

}
}

impl $crate::traits::Zero for $name {
fn zero() -> Self {
Self::zero()
Expand Down Expand Up @@ -1544,6 +1582,77 @@ macro_rules! implement_per_thing {
}
}

#[test]
fn checked_rational_mul_correction_works() {
assert_eq!(
super::checked_rational_mul_correction::<$type, $name>(
<$type>::max_value(),
<$type>::max_value(),
<$type>::max_value(),
super::Rounding::NearestPrefDown,
),
Ok(0),
);
assert_eq!(
super::checked_rational_mul_correction::<$type, $name>(
<$type>::max_value() - 1,
<$type>::max_value(),
<$type>::max_value(),
super::Rounding::NearestPrefDown,
),
Ok(<$type>::max_value() - 1),
);
assert_eq!(
super::checked_rational_mul_correction::<$upper_type, $name>(
((<$type>::max_value() - 1) as $upper_type).pow(2),
<$type>::max_value(),
<$type>::max_value(),
super::Rounding::NearestPrefDown,
),
Ok(1),
);
// ((max^2 - 1) % max) * max / max == max - 1
assert_eq!(
super::checked_rational_mul_correction::<$upper_type, $name>(
(<$type>::max_value() as $upper_type).pow(2) - 1,
<$type>::max_value(),
<$type>::max_value(),
super::Rounding::NearestPrefDown,
),
Ok(<$upper_type>::from((<$type>::max_value() - 1))),
);
// (max % 2) * max / 2 == max / 2
assert_eq!(
super::checked_rational_mul_correction::<$upper_type, $name>(
(<$type>::max_value() as $upper_type).pow(2),
<$type>::max_value(),
2 as $type,
super::Rounding::NearestPrefDown,
),
Ok(<$type>::max_value() as $upper_type / 2),
);
// ((max^2 - 1) % max) * 2 / max == 2 (rounded up)
assert_eq!(
super::checked_rational_mul_correction::<$upper_type, $name>(
(<$type>::max_value() as $upper_type).pow(2) - 1,
2 as $type,
<$type>::max_value(),
super::Rounding::NearestPrefDown,
),
Ok(2),
);
// ((max^2 - 1) % max) * 2 / max == 1 (rounded down)
assert_eq!(
super::checked_rational_mul_correction::<$upper_type, $name>(
(<$type>::max_value() as $upper_type).pow(2) - 1,
2 as $type,
<$type>::max_value(),
super::Rounding::Down,
),
Ok(1),
);
}

#[test]
fn rational_mul_correction_works() {
assert_eq!(
Expand Down Expand Up @@ -1737,6 +1846,22 @@ macro_rules! implement_per_thing {
Some($name::from_percent(0))
);
}

#[test]
fn test_basic_checked_div() {
assert_eq!(
$name::from_parts($max).checked_div(&$name::from_parts($max)),
Some($name::from_percent(100))
);
assert_eq!(
$name::from_percent(100).checked_div(&$name::from_percent(100)),
Some($name::from_percent(100))
);
assert_eq!(
$name::from_percent(0).checked_div(&$name::from_percent(0)),
None
);
}
}
};
}
Expand Down