Defensive Programming in Substrate Reference Document (#2615)

_This PR is being continued from
https://github.com/paritytech/polkadot-sdk/pull/2206, which was closed
when the developer_hub was merged._
closes https://github.com/paritytech/polkadot-sdk-docs/issues/44

---
# Description

This PR adds a reference document to the `developer-hub` crate (see
https://github.com/paritytech/polkadot-sdk/pull/2102). This specific
reference document covers defensive programming practices common within
the context of developing a runtime with Substrate.

In particular, this covers the following areas: 

- Default behavior of how Rust deals with numbers in general
- How to deal with floating point numbers in runtime / fixed point
arithmetic
- How to deal with Integer overflows
- General "safe math" / defensive programming practices for common
pallet development scenarios
- Defensive traits that exist within Substrate, i.e.,
`defensive_saturating_add `, `defensive_unwrap_or`
- More general defensive programming examples (keep it concise)
- Link to relevant examples where these practices are actually in
production / being used
- Unwrapping (or rather lack thereof) 101

todo
-- 
- [x] Apply feedback from previous PR
- [x] This may warrant a PR to append some of these docs to
`sp_arithmetic`

---------

Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>
Co-authored-by: Gonçalo Pestana <g6pestana@gmail.com>
Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>
Co-authored-by: Francisco Aguirre <franciscoaguirreperez@gmail.com>
Co-authored-by: Radha <86818441+DrW3RK@users.noreply.github.com>
This commit is contained in:
bader y
2024-03-20 09:26:59 -04:00
committed by GitHub
parent 7241a8db7b
commit b686bfefba
10 changed files with 586 additions and 32 deletions
@@ -16,6 +16,33 @@
// limitations under the License.
//! Decimal Fixed Point implementations for Substrate runtime.
//! Similar to types that implement [`PerThing`](crate::per_things), these are also
//! fixed-point types, however, they are able to represent larger fractions:
#![doc = docify::embed!("./src/lib.rs", fixed_u64)]
//!
//! ### Fixed Point Types in Practice
//!
//! If one needs to exceed the value of one (1), then
//! [`FixedU64`](FixedU64) (and its signed and `u128` counterparts) can be utilized.
//! Take for example this very rudimentary pricing mechanism, where we wish to calculate the demand
//! / supply to get a price for some on-chain compute:
#![doc = docify::embed!(
"./src/lib.rs",
fixed_u64_block_computation_example
)]
//!
//! For a much more comprehensive example, be sure to look at the source for broker (the "coretime")
//! pallet.
//!
//! #### Fixed Point Types in Practice
//!
//! Just as with [`PerThing`](PerThing), you can also perform regular mathematical
//! expressions:
#![doc = docify::embed!(
"./src/lib.rs",
fixed_u64_operation_example
)]
//!
use crate::{
helpers_128bit::{multiply_by_rational_with_rounding, sqrt},
+108 -26
View File
@@ -101,7 +101,7 @@ where
fn tcmp(&self, other: &T, threshold: T) -> Ordering {
// early exit.
if threshold.is_zero() {
return self.cmp(other)
return self.cmp(other);
}
let upper_bound = other.saturating_add(threshold);
@@ -206,12 +206,12 @@ where
// Nothing to do here.
if count.is_zero() {
return Ok(Vec::<T>::new())
return Ok(Vec::<T>::new());
}
let diff = targeted_sum.max(sum) - targeted_sum.min(sum);
if diff.is_zero() {
return Ok(input.to_vec())
return Ok(input.to_vec());
}
let needs_bump = targeted_sum > sum;
@@ -254,7 +254,7 @@ where
min_index += 1;
min_index %= count;
}
leftover -= One::one()
leftover -= One::one();
}
} else {
// must decrease the stakes a bit. decrement from the max element. index of maximum is now
@@ -288,7 +288,7 @@ where
if output_with_idx[max_index].1 <= threshold {
max_index = max_index.checked_sub(1).unwrap_or(count - 1);
}
leftover -= One::one()
leftover -= One::one();
} else {
max_index = max_index.checked_sub(1).unwrap_or(count - 1);
}
@@ -300,7 +300,7 @@ where
targeted_sum,
"sum({:?}) != {:?}",
output_with_idx,
targeted_sum,
targeted_sum
);
// sort again based on the original index.
@@ -356,7 +356,7 @@ mod normalize_tests {
vec![
Perbill::from_parts(333333334),
Perbill::from_parts(333333333),
Perbill::from_parts(333333333),
Perbill::from_parts(333333333)
]
);
@@ -367,7 +367,7 @@ mod normalize_tests {
vec![
Perbill::from_parts(316666668),
Perbill::from_parts(383333332),
Perbill::from_parts(300000000),
Perbill::from_parts(300000000)
]
);
}
@@ -378,13 +378,13 @@ mod normalize_tests {
// could have a situation where the sum cannot be calculated in the inner type. Calculating
// using the upper type of the per_thing should assure this to be okay.
assert_eq!(
vec![PerU16::from_percent(40), PerU16::from_percent(40), PerU16::from_percent(40),]
vec![PerU16::from_percent(40), PerU16::from_percent(40), PerU16::from_percent(40)]
.normalize(PerU16::one())
.unwrap(),
vec![
PerU16::from_parts(21845), // 33%
PerU16::from_parts(21845), // 33%
PerU16::from_parts(21845), // 33%
PerU16::from_parts(21845) // 33%
]
);
}
@@ -428,6 +428,88 @@ mod normalize_tests {
}
}
#[cfg(test)]
mod per_and_fixed_examples {
use super::*;
#[docify::export]
#[test]
fn percent_mult() {
let percent = Percent::from_rational(5u32, 100u32); // aka, 5%
let five_percent_of_100 = percent * 100u32; // 5% of 100 is 5.
assert_eq!(five_percent_of_100, 5)
}
#[docify::export]
#[test]
fn perbill_example() {
let p = Perbill::from_percent(80);
// 800000000 bil, or a representative of 0.800000000.
// Precision is in the billions place.
assert_eq!(p.deconstruct(), 800000000);
}
#[docify::export]
#[test]
fn percent_example() {
let percent = Percent::from_rational(190u32, 400u32);
assert_eq!(percent.deconstruct(), 47);
}
#[docify::export]
#[test]
fn fixed_u64_block_computation_example() {
// Calculate a very rudimentary on-chain price from supply / demand
// Supply: Cores available per block
// Demand: Cores being ordered per block
let price = FixedU64::from_rational(5u128, 10u128);
// 0.5 DOT per core
assert_eq!(price, FixedU64::from_float(0.5));
// Now, the story has changed - lots of demand means we buy as many cores as there
// available. This also means that price goes up! For the sake of simplicity, we don't care
// about who gets a core - just about our very simple price model
// Calculate a very rudimentary on-chain price from supply / demand
// Supply: Cores available per block
// Demand: Cores being ordered per block
let price = FixedU64::from_rational(19u128, 10u128);
// 1.9 DOT per core
assert_eq!(price, FixedU64::from_float(1.9));
}
#[docify::export]
#[test]
fn fixed_u64() {
// The difference between this and perthings is perthings operates within the relam of [0,
// 1] In cases where we need > 1, we can used fixed types such as FixedU64
let rational_1 = FixedU64::from_rational(10, 5); //" 200%" aka 2.
let rational_2 = FixedU64::from_rational_with_rounding(5, 10, Rounding::Down); // "50%" aka 0.50...
assert_eq!(rational_1, (2u64).into());
assert_eq!(rational_2.into_perbill(), Perbill::from_float(0.5));
}
#[docify::export]
#[test]
fn fixed_u64_operation_example() {
let rational_1 = FixedU64::from_rational(10, 5); // "200%" aka 2.
let rational_2 = FixedU64::from_rational(8, 5); // "160%" aka 1.6.
let addition = rational_1 + rational_2;
let multiplication = rational_1 * rational_2;
let division = rational_1 / rational_2;
let subtraction = rational_1 - rational_2;
assert_eq!(addition, FixedU64::from_float(3.6));
assert_eq!(multiplication, FixedU64::from_float(3.2));
assert_eq!(division, FixedU64::from_float(1.25));
assert_eq!(subtraction, FixedU64::from_float(0.4));
}
}
#[cfg(test)]
mod threshold_compare_tests {
use super::*;
@@ -440,15 +522,15 @@ mod threshold_compare_tests {
let e = Perbill::from_percent(10).mul_ceil(b);
// [115 - 11,5 (103,5), 115 + 11,5 (126,5)] is all equal
assert_eq!(103u32.tcmp(&b, e), Ordering::Equal);
assert_eq!(104u32.tcmp(&b, e), Ordering::Equal);
assert_eq!(115u32.tcmp(&b, e), Ordering::Equal);
assert_eq!(120u32.tcmp(&b, e), Ordering::Equal);
assert_eq!(126u32.tcmp(&b, e), Ordering::Equal);
assert_eq!(127u32.tcmp(&b, e), Ordering::Equal);
assert_eq!((103u32).tcmp(&b, e), Ordering::Equal);
assert_eq!((104u32).tcmp(&b, e), Ordering::Equal);
assert_eq!((115u32).tcmp(&b, e), Ordering::Equal);
assert_eq!((120u32).tcmp(&b, e), Ordering::Equal);
assert_eq!((126u32).tcmp(&b, e), Ordering::Equal);
assert_eq!((127u32).tcmp(&b, e), Ordering::Equal);
assert_eq!(128u32.tcmp(&b, e), Ordering::Greater);
assert_eq!(102u32.tcmp(&b, e), Ordering::Less);
assert_eq!((128u32).tcmp(&b, e), Ordering::Greater);
assert_eq!((102u32).tcmp(&b, e), Ordering::Less);
}
#[test]
@@ -458,15 +540,15 @@ mod threshold_compare_tests {
let e = Perbill::from_parts(100) * b;
// [115 - 11,5 (103,5), 115 + 11,5 (126,5)] is all equal
assert_eq!(103u32.tcmp(&b, e), 103u32.cmp(&b));
assert_eq!(104u32.tcmp(&b, e), 104u32.cmp(&b));
assert_eq!(115u32.tcmp(&b, e), 115u32.cmp(&b));
assert_eq!(120u32.tcmp(&b, e), 120u32.cmp(&b));
assert_eq!(126u32.tcmp(&b, e), 126u32.cmp(&b));
assert_eq!(127u32.tcmp(&b, e), 127u32.cmp(&b));
assert_eq!((103u32).tcmp(&b, e), (103u32).cmp(&b));
assert_eq!((104u32).tcmp(&b, e), (104u32).cmp(&b));
assert_eq!((115u32).tcmp(&b, e), (115u32).cmp(&b));
assert_eq!((120u32).tcmp(&b, e), (120u32).cmp(&b));
assert_eq!((126u32).tcmp(&b, e), (126u32).cmp(&b));
assert_eq!((127u32).tcmp(&b, e), (127u32).cmp(&b));
assert_eq!(128u32.tcmp(&b, e), 128u32.cmp(&b));
assert_eq!(102u32.tcmp(&b, e), 102u32.cmp(&b));
assert_eq!((128u32).tcmp(&b, e), (128u32).cmp(&b));
assert_eq!((102u32).tcmp(&b, e), (102u32).cmp(&b));
}
#[test]
@@ -15,6 +15,42 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//! Types that implement [`PerThing`](PerThing) can be used as a floating-point alternative for
//! numbers that operate within the realm of `[0, 1]`. The primary types may you encounter in
//! Substrate would be the following:
//! - [`Percent`](Percent) - parts of one hundred.
//! - [`Permill`](Permill) - parts of a million.
//! - [`Perbill`](Perbill) - parts of a billion.
//!
//! In use, you may see them being used as follows:
//!
//! > **[`Perbill`](Perbill), parts of a billion**
#![doc = docify::embed!("./src/lib.rs", perbill_example)]
//! > **[`Percent`](Percent), parts of a hundred**
#![doc = docify::embed!("./src/lib.rs", percent_example)]
//!
//! Note that `Percent` is represented as a _rounded down_, fixed point
//! number (see the example above). Unlike primitive types, types that implement
//! [`PerThing`](PerThing) will also not overflow, and are therefore safe to use.
//! They adopt the same behavior that a saturated calculation would provide, meaning that if one is
//! to go over "100%", it wouldn't overflow, but simply stop at the upper or lower bound.
//!
//! For use cases which require precision beyond the range of `[0, 1]`, there are fixed-point types
//! which can be used.
//!
//! Each of these can be used to construct and represent ratios within our runtime.
//! You will find types like [`Perbill`](Perbill) being used often in pallet
//! development. `pallet_referenda` is a good example of a pallet which makes good use of fixed
//! point arithmetic, as it relies on representing various curves and thresholds relating to
//! governance.
//!
//! #### Fixed Point Arithmetic with [`PerThing`](PerThing)
//!
//! As stated, one can also perform mathematics using these types directly. For example, finding the
//! percentage of a particular item:
#![doc = docify::embed!("./src/lib.rs", percent_mult)]
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};