Skip to content

sp-arithmetic: add checked functions and extend fuzzer coverage #8472

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

Gioyik
Copy link

@Gioyik Gioyik commented May 8, 2025

Description

This has been in my backlog for quite a while (in a simpler form, see), Finally got it in the shape I wanted for peer review and a few pair of eyes on the TO TO/RESOLVE pieces.

In addition to the implementation and changes, I have done a Security review on the whole sp-arithmetic crate. Once this is merged, it can be marked as audited (completely) and I am fine to review future changes in the crate to do a differential security review and maintain the status of fully audited as time goes.

Closes: #1936

Integration

This PR adds more functions than it changes (removed nothing), once merged checked functions should be used instead of non checked ones when possible. The items in TO TODO/RESOLVE might introduce integration changes, I will update here once it is decided.

IMPLEMENTED

  • helpers_128bit.rs:
    • Added checked_multiply_by_rational_with_rounding.
    • Proper use of ArithmeticError.
    • Added test for introduced checked versions.
  • per_things.rs:
    • Added checked_* versions of square, mul_floor, mul_ceil, overflow_prune_mul, rational_mul_correction saturating_reciprocal_mul, saturating_reciprocal_mul_floor, saturating_reciprocal_mul_ceil and from_rational_with_rounding to the PerThing trait and their implementations within implement_per_thing the macro where it applies.
    • Added additional checked_int_div and checked_div_with_rounding.
    • Added tests for the newly checked_* additions in checked_reciprocal_mul_works, checked_from_rational_works, checked_arithmetic_works and checked_int_div_works.
    • Proper use of ArithmeticError.
    • Added test for introduced checked versions.
  • fixed_point.rs:
    • Added try_into_perbill which is a checked version of into_perbill.
    • Added checked version of from_rational_with_rounding.
    • Proper use of ArithmeticError.
  • rational.rs:
    • Added checked_to_den and checked_lcm.
    • Updated checked_add and checked_sub to follow the pattern of other checked versions.
    • Simplified import of helpers_128bit.
    • Proper use of ArithmeticError.
    • Added test for introduced checked versions.
  • fuzzing:
    • per_thing_checked_arith.rs: Covers general PerThing checked arithmetic operations (checked_mul_floor, checked_mul_ceil, reciprocal multiplications, checked_square, checked_div_with_rounding, checked_int_div).
    • fixed_utils.rs: Covers FixedPointNumber utility functions (trunc, frac, ceil, floor, round).
    • fixed_to_perbill.rs: Covers conversions of try_into_perbill
    • fixed_to_perthing.rs: Covers conversions of try_into_perthing.
    • fixed_rounding_ops.rs: Specifically fuzzes const_checked_mul_with_rounding and checked_rounding_div for FixedPointNumber types, testing all 8 SignedRounding modes.
    • per_thing_checked_from_rational.rs: Covers PerThing::checked_from_rational_with_rounding comparing against the non-checked version and validating error handling.
    • fixed_checked_ops.rs: Covers core FixedPointNumber checked arithmetic operations (add, sub, mul, div, from_integer, from_rational, mul_int, div_int, sqrt) for FixedI64, FixedU64, FixedI128, and FixedU128, using BigInt for oracle comparisons and overflow detection.
    • checked_mul_by_rational.rs: Covers helpers_128bit::checked_multiply_by_rational_with_rounding with different rounding modes and error conditions (DivisionByZero, Overflow).

Review Notes

I have introduced multiple checked functions in the main library where the was a risk/clarity of panic, as well as proper use of ArithmeticError and consistent import namespaces. The new checked functions are not complex as they should mimic the same behavior, the difference is relaying on safe functions and do not panic.

You will see in per_thing_checked_arith.rs an alternative approach I decided to try, and is the use of an "oracle" to tell what the expected outcome of an operation should be. Fuzzing is great at finding inputs that cause crashes (panics), but it doesn't inherently know if the result of a calculation is correct, especially for complex math. I think is important to be confident that they produce the correct numerical results according to mathematical rules and the specified rounding behavior.

I decided to use BigFraction / BigInt as they provide independent implementations of arbitrary-precision arithmetic, they aren't bound by the fixed precision or potential overflow limits of the u128, FixedU64, Permill, etc., types we are testing, I believe this would let compute most of the values with high-precision (I might be wrong and this backfires to me in the future :D ).

In older test, we test mathematical properties (like (a/b)*b <= a when rounding down, as seen in per_thing_mult_fraction.rs), this is also valuable but doesn't verify the exact output value like an oracle does which sometimes is important to test against. In fixed_checked_ops.rs the use of oracle paradigm is used as well, however, to properly test the behavior of each function requires more thought on the cases and vertices. I consider the tests in fixed_checked_ops.rs as a baseline and to be improved as time goes, left comments for future references.

TO TODO/RESOLVE

  • fixed_point.rs:

    • Functions like checked_from_rational, checked_mul_int, checked_div, checked_mul can be changed to checked_multiply_by_rational_with_rounding version but it means changing the return from Option<> to Result<>, or keep backwards compability and wrap the Result<> back to Option<>.
      • If doing this requires fixing other parts of the SDK where the checked functions are used, I can work on doing the migration as part of the effort for this.
  • per_things.rs:

    • Some asserts inside the checked_reciprocal_mul_works and checked_arithmetic_works fail only in PerU16 variations. I need to figure out why exactly is that happening only for this variant. This is the test output in case someone has an idea:
     	---- per_things::test_peru16::checked_arithmetic_works stdout ----
    
     	thread 'per_things::test_peru16::checked_arithmetic_works' panicked at substrate/primitives/arithmetic/src/per_things.rs:2361:1:
     	assertion `left == right` failed
     	left: Ok(4)
     	right: Ok(5)
    
     	---- per_things::test_peru16::checked_reciprocal_mul_works stdout ----
    
     	thread 'per_things::test_peru16::checked_reciprocal_mul_works' panicked at substrate/primitives/arithmetic/src/per_things.rs:2361:1:
     	assertion `left == right` failed
     	left: Ok(21)
     	right: Ok(20)
    
    
     	failures:
     		per_things::test_peru16::checked_arithmetic_works
     		per_things::test_peru16::checked_reciprocal_mul_works
    
    

Checklist

  • My PR includes a detailed description as outlined in the "Description" and its two subsections above.
  • My PR follows the labeling requirements of this project (at minimum one label for T required)
    • External contributors: ask maintainers to put the right label on your PR.
  • I have made corresponding changes to the documentation (if applicable)
  • I have added tests that prove my fix is effective or that my feature works (if applicable)

**Implemented**
- `helpers_128bit.rs`:
	- Added `checked_multiply_by_rational_with_rounding`.
	- Proper use of `ArithmeticError`.
	- Added test for introduced checked versions.
- `per_things.rs`:
	- Added `checked_*` versions of `square`, `mul_floor`, `mul_ceil`, `overflow_prune_mul`, `rational_mul_correction` `saturating_reciprocal_mul`, `saturating_reciprocal_mul_floor`, `saturating_reciprocal_mul_ceil` and `from_rational_with_rounding` to the `PerThing` trait and their implementations within `implement_per_thing` the macro where it applies.
	- Added additional `checked_int_div` and `checked_div_with_rounding`.
	- Added tests for the newly `checked_*` additions in `checked_reciprocal_mul_works`, `checked_from_rational_works`, `checked_arithmetic_works` and `checked_int_div_works`.
	- Proper use of `ArithmeticError`.
	- Added test for introduced checked versions.
- `fixed_point.rs`:
	- Added `try_into_perbill` which is a checked version of `into_perbill`.
	- Added checked version of `from_rational_with_rounding`.
	- Proper use of `ArithmeticError`.
- `rational.rs`:
	- Added `checked_to_den` and `checked_lcm`.
	- Updated `checked_add` and `checked_sub` to follow the pattern of other checked versions.
	- Simplified import of `helpers_128bit`.
	- Proper use of `ArithmeticError`.
	- Added test for introduced checked versions.
- `fuzzing`:
	- `per_thing_checked_arith.rs`: Covers general `PerThing` checked arithmetic operations (`checked_mul_floor`, `checked_mul_ceil`, reciprocal multiplications, `checked_square`, `checked_div_with_rounding`, `checked_int_div`).
	- `fixed_utils.rs`: Covers `FixedPointNumber` utility functions (`trunc`, `frac`, `ceil`, `floor`, `round`).
	- `fixed_to_perbill.rs`: Covers conversions of `try_into_perbill`
	- `fixed_to_perthing.rs`: Covers conversions of `try_into_perthing`.
	- `fixed_rounding_ops.rs`: Specifically fuzzes `const_checked_mul_with_rounding` and `checked_rounding_div` for `FixedPointNumber` types, testing all 8 `SignedRounding` modes.
	- `per_thing_checked_from_rational.rs`: Covers `PerThing::checked_from_rational_with_rounding` comparing against the non-checked version and validating error handling.
	- `fixed_checked_ops.rs`: Covers core `FixedPointNumber` checked arithmetic operations (`add`, `sub`, `mul`, `div`, `from_integer`, `from_rational`, `mul_int`, `div_int`, `sqrt`) for `FixedI64`, `FixedU64`, `FixedI128`, and `FixedU128`, using `BigInt` for oracle comparisons and overflow detection.
	- `checked_mul_by_rational.rs`: Covers `helpers_128bit::checked_multiply_by_rational_with_rounding` with different rounding modes and error conditions (DivisionByZero, Overflow).
@Gioyik Gioyik requested review from bkchr and ggwpez May 8, 2025 23:00
@Gioyik Gioyik self-assigned this May 8, 2025
@Gioyik Gioyik added T1-FRAME This PR/Issue is related to core FRAME, the framework. T10-tests This PR/Issue is related to tests. T17-primitives Changes to primitives that are not covered by any other label. labels May 8, 2025
@paritytech-workflow-stopper
Copy link

All GitHub workflows were cancelled due to failure one of the required jobs.
Failed workflow url: https://github.com/paritytech/polkadot-sdk/actions/runs/14924194076
Failed job name: test-linux-stable

@Gioyik Gioyik removed their assignment May 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T1-FRAME This PR/Issue is related to core FRAME, the framework. T10-tests This PR/Issue is related to tests. T17-primitives Changes to primitives that are not covered by any other label.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant