Use sensible maths for from_rational (#13660)

* Use sensible maths for from_rational

* Fixes

* Fixes

* More fixes

* Remove debugging

* Add fuzzer tests

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Prevent panics

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* docs

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Clean up old code

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Test all rounding modes of from_rational

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Clean up code

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Revert "Prevent panics"

This reverts commit 7e88ac76138a1b590e68b68318505b69efe1e1f6.

* fix imports

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* cleanup

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Fuzz test multiply_rational

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Fix import

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* fmt

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Return None in multiply_rational on zero div

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

---------

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>
Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>
This commit is contained in:
Gavin Wood
2023-03-24 14:48:37 +00:00
committed by GitHub
parent 40e1704e1c
commit 2c77f8f4ca
12 changed files with 372 additions and 227 deletions
@@ -29,52 +29,79 @@
//! More information about `honggfuzz` can be found
//! [here](https://docs.rs/honggfuzz/).
use fraction::prelude::BigFraction as Fraction;
use honggfuzz::fuzz;
use sp_arithmetic::{helpers_128bit::multiply_by_rational_with_rounding, traits::Zero, Rounding};
use sp_arithmetic::{MultiplyRational, Rounding, Rounding::*};
/// Tries to demonstrate that `multiply_by_rational_with_rounding` is incorrect.
fn main() {
loop {
fuzz!(|data: ([u8; 16], [u8; 16], [u8; 16])| {
let (a_bytes, b_bytes, c_bytes) = data;
let (a, b, c) = (
u128::from_be_bytes(a_bytes),
u128::from_be_bytes(b_bytes),
u128::from_be_bytes(c_bytes),
);
fuzz!(|data: (u128, u128, u128, ArbitraryRounding)| {
let (f, n, d, r) = (data.0, data.1, data.2, data.3 .0);
println!("++ Equation: {} * {} / {}", a, b, c);
// The point of this fuzzing is to make sure that `multiply_by_rational_with_rounding`
// is 100% accurate as long as the value fits in a u128.
if let Some(result) = multiply_by_rational_with_rounding(a, b, c, Rounding::Down) {
let truth = mul_div(a, b, c);
if result != truth && result != truth + 1 {
println!("++ Expected {}", truth);
println!("+++++++ Got {}", result);
panic!();
}
}
check::<u8>(f as u8, n as u8, d as u8, r);
check::<u16>(f as u16, n as u16, d as u16, r);
check::<u32>(f as u32, n as u32, d as u32, r);
check::<u64>(f as u64, n as u64, d as u64, r);
check::<u128>(f, n, d, r);
})
}
}
fn mul_div(a: u128, b: u128, c: u128) -> u128 {
use primitive_types::U256;
if a.is_zero() {
return Zero::zero()
}
let c = c.max(1);
fn check<N>(f: N, n: N, d: N, r: Rounding)
where
N: MultiplyRational + Into<u128> + Copy + core::fmt::Debug,
{
let Some(got) = f.multiply_rational(n, d, r) else {
return;
};
// e for extended
let ae: U256 = a.into();
let be: U256 = b.into();
let ce: U256 = c.into();
let (ae, be, ce) =
(Fraction::from(f.into()), Fraction::from(n.into()), Fraction::from(d.into()));
let want = round(ae * be / ce, r);
let r = ae * be / ce;
if r > u128::MAX.into() {
a
} else {
r.as_u128()
assert_eq!(
Fraction::from(got.into()),
want,
"{:?} * {:?} / {:?} = {:?} != {:?}",
f,
n,
d,
got,
want
);
}
/// Round a `Fraction` according to the given mode.
fn round(f: Fraction, r: Rounding) -> Fraction {
match r {
Up => f.ceil(),
NearestPrefUp =>
if f.fract() < Fraction::from(0.5) {
f.floor()
} else {
f.ceil()
},
Down => f.floor(),
NearestPrefDown =>
if f.fract() > Fraction::from(0.5) {
f.ceil()
} else {
f.floor()
},
}
}
/// An [`arbitrary::Arbitrary`] [`Rounding`] mode.
struct ArbitraryRounding(Rounding);
impl arbitrary::Arbitrary<'_> for ArbitraryRounding {
fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
Ok(Self(match u.int_in_range(0..=3).unwrap() {
0 => Up,
1 => NearestPrefUp,
2 => Down,
3 => NearestPrefDown,
_ => unreachable!(),
}))
}
}
@@ -0,0 +1,105 @@
// This file is part of Substrate.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! # Running
//! Running this fuzzer can be done with `cargo hfuzz run per_thing_from_rational`. `honggfuzz` CLI
//! options can be used by setting `HFUZZ_RUN_ARGS`, such as `-n 4` to use 4 threads.
//!
//! # Debugging a panic
//! Once a panic is found, it can be debugged with
//! `cargo hfuzz run-debug per_thing_from_rational hfuzz_workspace/per_thing_from_rational/*.fuzz`.
use fraction::prelude::BigFraction as Fraction;
use honggfuzz::fuzz;
use sp_arithmetic::{
traits::SaturatedConversion, PerThing, Perbill, Percent, Perquintill, Rounding::*, *,
};
/// Tries to demonstrate that `from_rational` is incorrect for any rounding modes.
///
/// NOTE: This `Fraction` library is really slow. Using f128/f256 does not work for the large
/// numbers. But an optimization could be done do use either floats or Fraction depending on the
/// size of the inputs.
fn main() {
loop {
fuzz!(|data: (u128, u128, ArbitraryRounding)| {
let (n, d, r) = (data.0.min(data.1), data.0.max(data.1).max(1), data.2);
check::<PerU16>(n, d, r.0);
check::<Percent>(n, d, r.0);
check::<Permill>(n, d, r.0);
check::<Perbill>(n, d, r.0);
check::<Perquintill>(n, d, r.0);
})
}
}
/// Assert that the parts of `from_rational` are correct for the given rounding mode.
fn check<Per: PerThing>(a: u128, b: u128, r: Rounding)
where
Per::Inner: Into<u128>,
{
let approx_ratio = Per::from_rational_with_rounding(a, b, r).unwrap();
let approx_parts = Fraction::from(approx_ratio.deconstruct().saturated_into::<u128>());
let perfect_ratio = if a == 0 && b == 0 {
Fraction::from(1)
} else {
Fraction::from(a) / Fraction::from(b.max(1))
};
let perfect_parts = round(perfect_ratio * Fraction::from(Per::ACCURACY.into()), r);
assert_eq!(
approx_parts, perfect_parts,
"approx_parts: {}, perfect_parts: {}, a: {}, b: {}",
approx_parts, perfect_parts, a, b
);
}
/// Round a `Fraction` according to the given mode.
fn round(f: Fraction, r: Rounding) -> Fraction {
match r {
Up => f.ceil(),
NearestPrefUp =>
if f.fract() < Fraction::from(0.5) {
f.floor()
} else {
f.ceil()
},
Down => f.floor(),
NearestPrefDown =>
if f.fract() > Fraction::from(0.5) {
f.ceil()
} else {
f.floor()
},
}
}
/// An [`arbitrary::Arbitrary`] [`Rounding`] mode.
struct ArbitraryRounding(Rounding);
impl arbitrary::Arbitrary<'_> for ArbitraryRounding {
fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
Ok(Self(match u.int_in_range(0..=3).unwrap() {
0 => Up,
1 => NearestPrefUp,
2 => Down,
3 => NearestPrefDown,
_ => unreachable!(),
}))
}
}
@@ -0,0 +1,69 @@
// This file is part of Substrate.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! # Running
//! Running this fuzzer can be done with `cargo hfuzz run per_thing_mult_fraction`. `honggfuzz` CLI
//! options can be used by setting `HFUZZ_RUN_ARGS`, such as `-n 4` to use 4 threads.
//!
//! # Debugging a panic
//! Once a panic is found, it can be debugged with
//! `cargo hfuzz run-debug per_thing_mult_fraction hfuzz_workspace/per_thing_mult_fraction/*.fuzz`.
use honggfuzz::fuzz;
use sp_arithmetic::{PerThing, Perbill, Percent, Perquintill, *};
/// Tries to disprove `(n / d) * d <= n` for any `PerThing`s.
fn main() {
loop {
fuzz!(|data: (u128, u128)| {
let (n, d) = (data.0.min(data.1), data.0.max(data.1).max(1));
check_mul::<PerU16>(n, d);
check_mul::<Percent>(n, d);
check_mul::<Perbill>(n, d);
check_mul::<Perquintill>(n, d);
check_reciprocal_mul::<PerU16>(n, d);
check_reciprocal_mul::<Percent>(n, d);
check_reciprocal_mul::<Perbill>(n, d);
check_reciprocal_mul::<Perquintill>(n, d);
})
}
}
/// Checks that `(n / d) * d <= n`.
fn check_mul<P: PerThing>(n: u128, d: u128)
where
P: PerThing + core::ops::Mul<u128, Output = u128>,
{
let q = P::from_rational_with_rounding(n, d, Rounding::Down).unwrap();
assert!(q * d <= n, "{:?} * {:?} <= {:?}", q, d, n);
}
/// Checks that `n / (n / d) >= d`.
fn check_reciprocal_mul<P: PerThing>(n: u128, d: u128)
where
P: PerThing + core::ops::Mul<u128, Output = u128>,
{
let q = P::from_rational_with_rounding(n, d, Rounding::Down).unwrap();
if q.is_zero() {
return
}
let r = q.saturating_reciprocal_mul_floor(n);
assert!(r >= d, "{} / ({} / {}) != {} but {}", n, n, d, d, r);
}
@@ -1,116 +0,0 @@
// This file is part of Substrate.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! # Running
//! Running this fuzzer can be done with `cargo hfuzz run per_thing_rational`. `honggfuzz` CLI
//! options can be used by setting `HFUZZ_RUN_ARGS`, such as `-n 4` to use 4 threads.
//!
//! # Debugging a panic
//! Once a panic is found, it can be debugged with
//! `cargo hfuzz run-debug per_thing_rational hfuzz_workspace/per_thing_rational/*.fuzz`.
use honggfuzz::fuzz;
use sp_arithmetic::{traits::SaturatedConversion, PerThing, PerU16, Perbill, Percent, Perquintill};
fn main() {
loop {
fuzz!(|data: ((u16, u16), (u32, u32), (u64, u64))| {
let (u16_pair, u32_pair, u64_pair) = data;
// peru16
let (smaller, bigger) = (u16_pair.0.min(u16_pair.1), u16_pair.0.max(u16_pair.1));
let ratio = PerU16::from_rational(smaller, bigger);
assert_per_thing_equal_error(
ratio,
PerU16::from_float(smaller as f64 / bigger.max(1) as f64),
1,
);
let (smaller, bigger) = (u32_pair.0.min(u32_pair.1), u32_pair.0.max(u32_pair.1));
let ratio = PerU16::from_rational(smaller, bigger);
assert_per_thing_equal_error(
ratio,
PerU16::from_float(smaller as f64 / bigger.max(1) as f64),
1,
);
let (smaller, bigger) = (u64_pair.0.min(u64_pair.1), u64_pair.0.max(u64_pair.1));
let ratio = PerU16::from_rational(smaller, bigger);
assert_per_thing_equal_error(
ratio,
PerU16::from_float(smaller as f64 / bigger.max(1) as f64),
1,
);
// percent
let (smaller, bigger) = (u16_pair.0.min(u16_pair.1), u16_pair.0.max(u16_pair.1));
let ratio = Percent::from_rational(smaller, bigger);
assert_per_thing_equal_error(
ratio,
Percent::from_float(smaller as f64 / bigger.max(1) as f64),
1,
);
let (smaller, bigger) = (u32_pair.0.min(u32_pair.1), u32_pair.0.max(u32_pair.1));
let ratio = Percent::from_rational(smaller, bigger);
assert_per_thing_equal_error(
ratio,
Percent::from_float(smaller as f64 / bigger.max(1) as f64),
1,
);
let (smaller, bigger) = (u64_pair.0.min(u64_pair.1), u64_pair.0.max(u64_pair.1));
let ratio = Percent::from_rational(smaller, bigger);
assert_per_thing_equal_error(
ratio,
Percent::from_float(smaller as f64 / bigger.max(1) as f64),
1,
);
// perbill
let (smaller, bigger) = (u32_pair.0.min(u32_pair.1), u32_pair.0.max(u32_pair.1));
let ratio = Perbill::from_rational(smaller, bigger);
assert_per_thing_equal_error(
ratio,
Perbill::from_float(smaller as f64 / bigger.max(1) as f64),
100,
);
let (smaller, bigger) = (u64_pair.0.min(u64_pair.1), u64_pair.0.max(u64_pair.1));
let ratio = Perbill::from_rational(smaller, bigger);
assert_per_thing_equal_error(
ratio,
Perbill::from_float(smaller as f64 / bigger.max(1) as f64),
100,
);
// perquintillion
let (smaller, bigger) = (u64_pair.0.min(u64_pair.1), u64_pair.0.max(u64_pair.1));
let ratio = Perquintill::from_rational(smaller, bigger);
assert_per_thing_equal_error(
ratio,
Perquintill::from_float(smaller as f64 / bigger.max(1) as f64),
1000,
);
})
}
}
fn assert_per_thing_equal_error<P: PerThing>(a: P, b: P, err: u128) {
let a_abs = a.deconstruct().saturated_into::<u128>();
let b_abs = b.deconstruct().saturated_into::<u128>();
let diff = a_abs.max(b_abs) - a_abs.min(b_abs);
assert!(diff <= err, "{:?} !~ {:?}", a, b);
}