Skip to content

Commit

Permalink
Defensive Programming in Substrate Reference Document (#2615)
Browse files Browse the repository at this point in the history
_This PR is being continued from
#2206, which was closed
when the developer_hub was merged._
closes paritytech/polkadot-sdk-docs#44

---
# Description

This PR adds a reference document to the `developer-hub` crate (see
#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>
  • Loading branch information
6 people committed Mar 20, 2024
1 parent 7241a8d commit b686bfe
Show file tree
Hide file tree
Showing 10 changed files with 588 additions and 34 deletions.
6 changes: 6 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions docs/sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@ sp-api = { path = "../../substrate/primitives/api" }
sp-core = { path = "../../substrate/primitives/core" }
sp-keyring = { path = "../../substrate/primitives/keyring" }
sp-runtime = { path = "../../substrate/primitives/runtime" }
sp-arithmetic = { path = "../../substrate/primitives/arithmetic" }

# Misc pallet dependencies
pallet-referenda = { path = "../../substrate/frame/referenda" }
pallet-broker = { path = "../../substrate/frame/broker" }
pallet-babe = { path = "../../substrate/frame/babe" }

sp-offchain = { path = "../../substrate/primitives/offchain" }
sp-version = { path = "../../substrate/primitives/version" }

Expand Down
6 changes: 3 additions & 3 deletions docs/sdk/src/guides/your_first_pallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@
//! This macro will call `.into()` under the hood.
#![doc = docify::embed!("./src/guides/your_first_pallet/mod.rs", transfer_better)]
//!
//! Moreover, you will learn in the [Safe Defensive Programming
//! section](crate::reference_docs::safe_defensive_programming) that it is always recommended to use
//! Moreover, you will learn in the [Defensive Programming
//! section](crate::reference_docs::defensive_programming) that it is always recommended to use
//! safe arithmetic operations in your runtime. By using [`frame::traits::CheckedSub`], we can not
//! only take a step in that direction, but also improve the error handing and make it slightly more
//! ergonomic.
Expand Down Expand Up @@ -294,7 +294,7 @@
//! The following topics where used in this guide, but not covered in depth. It is suggested to
//! study them subsequently:
//!
//! - [`crate::reference_docs::safe_defensive_programming`].
//! - [`crate::reference_docs::defensive_programming`].
//! - [`crate::reference_docs::frame_origin`].
//! - [`crate::reference_docs::frame_runtime_types`].
//! - The pallet we wrote in this guide was using `dev_mode`, learn more in
Expand Down
395 changes: 395 additions & 0 deletions docs/sdk/src/reference_docs/defensive_programming.rs

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions docs/sdk/src/reference_docs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,7 @@ pub mod signed_extensions;
pub mod frame_origin;

/// Learn about how to write safe and defensive code in your FRAME runtime.
// TODO: @CrackTheCode016 https://github.com/paritytech/polkadot-sdk-docs/issues/44
pub mod safe_defensive_programming;
pub mod defensive_programming;

/// Learn about composite enums and other runtime level types, such as "RuntimeEvent" and
/// "RuntimeCall".
Expand Down
1 change: 0 additions & 1 deletion docs/sdk/src/reference_docs/safe_defensive_programming.rs

This file was deleted.

3 changes: 3 additions & 0 deletions substrate/primitives/arithmetic/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ num-traits = { version = "0.2.17", default-features = false }
scale-info = { version = "2.10.0", default-features = false, features = ["derive"] }
serde = { features = ["alloc", "derive"], optional = true, workspace = true }
static_assertions = "1.1.0"
sp-std = { path = "../std", default-features = false }
docify = "0.2.7"

[dev-dependencies]
criterion = "0.4.0"
Expand All @@ -41,6 +43,7 @@ std = [
"scale-info/std",
"serde/std",
"sp-crypto-hashing/std",
"sp-std/std",
]
# Serde support without relying on std features.
serde = ["dep:serde", "scale-info/serde"]
Expand Down
27 changes: 27 additions & 0 deletions substrate/primitives/arithmetic/src/fixed_point.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
138 changes: 110 additions & 28 deletions substrate/primitives/arithmetic/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
Expand All @@ -300,7 +300,7 @@ where
targeted_sum,
"sum({:?}) != {:?}",
output_with_idx,
targeted_sum,
targeted_sum
);

// sort again based on the original index.
Expand Down Expand Up @@ -356,7 +356,7 @@ mod normalize_tests {
vec![
Perbill::from_parts(333333334),
Perbill::from_parts(333333333),
Perbill::from_parts(333333333),
Perbill::from_parts(333333333)
]
);

Expand All @@ -367,7 +367,7 @@ mod normalize_tests {
vec![
Perbill::from_parts(316666668),
Perbill::from_parts(383333332),
Perbill::from_parts(300000000),
Perbill::from_parts(300000000)
]
);
}
Expand All @@ -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%
]
);
}
Expand Down Expand Up @@ -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::*;
Expand All @@ -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!(128u32.tcmp(&b, e), Ordering::Greater);
assert_eq!(102u32.tcmp(&b, e), Ordering::Less);
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);
}

#[test]
Expand All @@ -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!(128u32.tcmp(&b, e), 128u32.cmp(&b));
assert_eq!(102u32.tcmp(&b, e), 102u32.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));
}

#[test]
Expand Down
Loading

0 comments on commit b686bfe

Please sign in to comment.