Skip to content

Conversation

@rflechtner
Copy link
Contributor

@rflechtner rflechtner commented Nov 13, 2024

I realised that scaling may be done more elegantly and less likely to overflow by using a U256 to implement what essentially amounts to direct division on a fixed point number with >= 128 integer bits. This way the only overflow we can get is when the result (the scaled value) is too large to fit into the target type.

In other words, the scaling logic is infallible, with the only error case being that the scaling factor is too small, so that the result cannot be represented.

I wanted to ask for your opinion on this solution before I apply it to my pending PR.

Checklist:

  • I have verified that the code works
    • No panics! (checked arithmetic ops, no indexing array[3] use get(3), ...)
  • I have verified that the code is easy to understand
    • If not, I have left a well-balanced amount of inline comments
  • I have left the code in a better state
  • I have documented the changes (where applicable)
    • Either PR or Ticket to update the Docs
    • Link the PR/Ticket here

Comment on lines +113 to +114
let decimals = U256::from(10)
.checked_pow(denomination.into())
Copy link
Contributor Author

Choose a reason for hiding this comment

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

denomination is now cast to a U256, which seems a little dumb. We could also stick to using a u128 to perform this calculation (raising to the power of denomination) and then convert the result to U256. Not sure what's more efficient though.

@rflechtner rflechtner requested review from Ad96el and ntn-x2 and removed request for ntn-x2 November 13, 2024 13:11
let mut x_u256 = U256::from(x);
// Shift left to produce the representation that our fixed type would have (but
// with extra integer bits that would potentially not fit in the fixed type).
x_u256.shl_assign(CurveParameterTypeOf::<T>::frac_nbits());
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Any idea if this can panic? I didn't find a checked_ variant of shl, so probably not?

Copy link
Contributor

Choose a reason for hiding this comment

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

The macro is implementing the trait:

		impl<T> $crate::core_::ops::ShlAssign<T> for $name where T: Into<$name> {
			fn shl_assign(&mut self, shift: T) {
				*self = *self << shift;
			}
		}

the << operation can panic

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok but the panic would occur if the shift is larger than the size of the number, right? Which would be an insane misconfiguration. Should we just prevent against misconfiguration the way @ntn-x2 suggested here and not check this at runtime?

Copy link
Contributor

Choose a reason for hiding this comment

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

I am against removing the checks from the runtime. Anything that the type system does not guarantee against should be checked, and having it in the runtime does not imply removing the static tests for the config itself.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok because there is no checked implementation, I would then implement a check to be run at each invocation of the function determining if the types have been chosen correctly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually as long as we don't have this configurable this cannot panic, because the U256 is large enough to work with any Fixed type that exists

Comment on lines 109 to 111
pub(crate) fn convert_to_fixed<T: Config>(x: u128, denomination: u8) -> Result<CurveParameterTypeOf<T>, ArithmeticError>
where
<CurveParameterTypeOf<T> as Fixed>::Bits: TryFrom<u128>,
<CurveParameterTypeOf<T> as Fixed>::Bits: TryFrom<U256>,
Copy link
Contributor Author

@rflechtner rflechtner Nov 13, 2024

Choose a reason for hiding this comment

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

We could in principle make this more generic by not fixing the type of x (an Into<U256> trait bound or similar is enough) and by making the type of the big integer configurable in the runtime, so you can match it to the chosen balance and fixed types.

The big integer only needs to be the size of the balance type plus the size of the fractional bits of the fixed, so for a u64 balance and a I75F53 fixed a u128 would be sufficient for conversion.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added a TODO in cffd4bd

where
<CurveParameterTypeOf<T> as Fixed>::Bits: Copy + ToFixed + AddAssign + BitOrAssign + ShlAssign + TryFrom<u128>,
<CurveParameterTypeOf<T> as Fixed>::Bits: Copy + ToFixed + AddAssign + BitOrAssign + ShlAssign + TryFrom<U256>,
CollateralCurrenciesBalanceOf<T>: Into<U256> + TryFrom<U256>,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Using bigger integers is also a better fix for the overflow issues we had when computing the refund amounts. This second trait bound is for this fix.

Copy link
Contributor Author

@rflechtner rflechtner Nov 13, 2024

Choose a reason for hiding this comment

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

We can also make this large integer type configurable, it just needs to have twice the size of the balance type.

Copy link
Contributor

Choose a reason for hiding this comment

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

No strong opinion on this. I don’t think any other project is using a balance type larger than u128.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good thing is you can also configure it to be smaller, avoiding blowing it up to a U256 when it's completely unnecessary, as it would be if your balance is just a u64 (where a u128 would be enough)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added a TODO in cffd4bd

Comment on lines +689 to +690
.fold(U256::from(0), |sum, id| {
sum.saturating_add(T::Fungibles::total_issuance(id).into())
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this can now also no longer overflow

Comment on lines 693 to 696
defensive_assert!(
sum_of_issuances >= burnt,
"burnt amount exceeds the total supply of all bonded currencies"
);
Copy link
Contributor

Choose a reason for hiding this comment

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

This can panic or not?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this does not panic in production - it does though in non-optimised builds

where
<CurveParameterTypeOf<T> as Fixed>::Bits: Copy + ToFixed + AddAssign + BitOrAssign + ShlAssign + TryFrom<u128>,
<CurveParameterTypeOf<T> as Fixed>::Bits: Copy + ToFixed + AddAssign + BitOrAssign + ShlAssign + TryFrom<U256>,
CollateralCurrenciesBalanceOf<T>: Into<U256> + TryFrom<U256>,
Copy link
Contributor

Choose a reason for hiding this comment

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

No strong opinion on this. I don’t think any other project is using a balance type larger than u128.

let mut x_u256 = U256::from(x);
// Shift left to produce the representation that our fixed type would have (but
// with extra integer bits that would potentially not fit in the fixed type).
x_u256.shl_assign(CurveParameterTypeOf::<T>::frac_nbits());
Copy link
Contributor

Choose a reason for hiding this comment

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

The macro is implementing the trait:

		impl<T> $crate::core_::ops::ShlAssign<T> for $name where T: Into<$name> {
			fn shl_assign(&mut self, shift: T) {
				*self = *self << shift;
			}
		}

the << operation can panic

Ad96el and others added 2 commits November 14, 2024 10:02
Little bug in the total sum calculation
@rflechtner rflechtner marked this pull request as ready for review November 14, 2024 12:06
@rflechtner rflechtner merged commit 08b71c4 into fix-denomination-arithmetic Nov 14, 2024
1 of 7 checks passed
@rflechtner rflechtner deleted the rf-U256-math branch November 14, 2024 12:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants