Skip to content
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

Add methods to UInt128 for explicit overflow control #853

Merged
merged 3 commits into from
Apr 1, 2021

Conversation

yihuang
Copy link
Contributor

@yihuang yihuang commented Mar 29, 2021

Fixes #850

@yihuang yihuang changed the title Add methods to Uint128 for explicit overflow control Add methods to UInt128 for explicit overflow control Mar 29, 2021
Copy link
Member

@webmaster128 webmaster128 left a comment

Choose a reason for hiding this comment

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

Nice start. Could you add tests for the newly added functions?

I think the best way to handle the operator implementation is deperate it and ask users to use the explicit methods.

operand2: String,
#[cfg(feature = "backtraces")]
backtrace: Backtrace,
},
Copy link
Member

Choose a reason for hiding this comment

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

We should generalize this and remove the term "underflow" because:

Storing values that are too low in an integer variable (e.g., attempting to store −1 in an unsigned integer) is properly referred to as integer overflow, or more broadly, integer wraparound. The term underflow normally refers to floating point numbers only, which is a separate issue. It is not possible in most floating-point designs to store a too-low value, as usually they are signed and have a negative infinity value.

https://en.wikipedia.org/wiki/Arithmetic_underflow

Copy link
Contributor Author

@yihuang yihuang Mar 30, 2021

Choose a reason for hiding this comment

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

done ;D only OverflowError and DivideByZeroError now.

self.0
.checked_add(other.0)
.map(Self)
.ok_or_else(|| StdError::overflow(self, other))
Copy link
Member

Choose a reason for hiding this comment

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

Can you create a specific

struct Uint128Overflow {
   // all the error information we need
}

and implement From<Uint128Overflow> for StdError?

This would make clear there is only one error type that can happen (as expressed by the Option API from Rust) but at the same time make it easy for contract developers to just use ?.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done ;D

@yihuang yihuang force-pushed the fix-overflow branch 3 times, most recently from 53df44b to 0a99ba2 Compare March 30, 2021 08:56
@yihuang
Copy link
Contributor Author

yihuang commented Mar 30, 2021

Nice start. Could you add tests for the newly added functions?

I think the best way to handle the operator implementation is deperate it and ask users to use the explicit methods.

  • added tests
  • change the Add operator to return Result.

Copy link
Member

@ethanfrey ethanfrey left a comment

Choose a reason for hiding this comment

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

Nice to add these extra helpers.
I would just keep the default behavior of Add and Sum as they were.

@@ -180,6 +180,80 @@ impl Uint128 {
pub fn is_zero(&self) -> bool {
self.0 == 0
}

pub fn checked_add(self, other: Self) -> Result<Self, OverflowError> {
Copy link
Member

Choose a reason for hiding this comment

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

Nice to see these all added here

self.0 += rhs.u128();
fn add(self, rhs: &'a Uint128) -> Self::Output {
let (op1, op2) = (self.u128(), rhs.u128());
op1.checked_add(op2)
Copy link
Member

Choose a reason for hiding this comment

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

Why change the behavior here?

There is the checked_add operator (and more), I would keep the same public API of + (even if it is something like self.checked_add(rhs).unwrap() to make "panic on overflow" explicit)

Also, why calling checked_add on the .u128() and not the Uint128 helper method.

Copy link
Contributor Author

@yihuang yihuang Mar 30, 2021

Choose a reason for hiding this comment

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

But why Sub is "checked" by default, and Add is panic by default?

Copy link
Member

Choose a reason for hiding this comment

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

Because Sub will error in many normal cases (sending 13 tokens when I have 5), where Add will only error in some extreme case (entire ETH supply in wei can safely be added far from the Uint128 overflow)

Copy link
Contributor Author

@yihuang yihuang Mar 30, 2021

Choose a reason for hiding this comment

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

The initial motivation for this change is for the use case here(https://github.com/yihuang/cosmwasm-plus/blob/cw1155-impl/contracts/cw1155-base/src/contract.rs#L110).
When the from is None(which means mint new tokens), the user inputted amount get passed to the + operator directly, I thought it's a security vulnerability at first. If we always make sure it panic at overflow, then it's not a security vulnerability, but still I find it a little bit confused why it's different from the Sub operator.

Copy link
Member

@webmaster128 webmaster128 Mar 30, 2021

Choose a reason for hiding this comment

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

Let's keep the panic for add by default. This the behaviour of all Rust types as well and the only reason why Uint128 exists is JSON serialization/deserialization of u128. It should be almost identical to u128.

To me the behaviour of impl ops::Sub<Uint128> for Uint128 is what should get attention. Ideally we make it panic. However, I am a bit scared to just change the behaviour. This is why I suggested deprecating it.

Copy link
Member

Choose a reason for hiding this comment

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

ok, I'll revert the Add change.

Thanks!

Do you want to put deprecated attribute on both Add and Sub? there's maybe also legit use case for that too? How about keep it as it for now?

What about this: impl ops::Sub<Uint128> for Uint128 gets the following deprecate note: "The behaviour of the minus operator for Uint128 is likely to change in the future. Please consider using Uint128::checked_sub, Uint128::wrapping_sub or Uint128::saturating_sub."

Copy link
Member

Choose a reason for hiding this comment

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

When the from is None(which means mint new tokens), the user inputted amount get passed to the + operator directly, I thought it's a security vulnerability at first. If we always make sure it panic at overflow, then it's not a security vulnerability, but still I find it a little bit confused why it's different from the Sub operator.

Good point. And relying on compiler flags for such security critical behavior is sub-optimal.

I think a reimplementation using checked_add internally, but then panic on error (.unwrap()) will have the same external behavior but be very explicit and stable about how it handles overflow.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think a reimplementation using checked_add internally, but then panic on error (.unwrap()) will have the same external behavior but be very explicit and stable about how it handles overflow.

Sounds good to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The behaviour of the minus operator for Uint128 is likely to change in the future. Please consider using Uint128::checked_sub, Uint128::wrapping_sub or Uint128::saturating_sub.

it seems unable to add deprecated annotation to trait impl: rust-lang/rust#39935

Copy link
Member

Choose a reason for hiding this comment

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

We could also remove the Sub trait for now and just force everyone to checked_sub.
v0.14.0 will have plenty of breaking changes, I'd rather do this sooner than later if we want to do it.

impl Sum<Uint128> for Uint128 {
fn sum<I: Iterator<Item = Uint128>>(iter: I) -> Self {
iter.fold(Uint128::zero(), ops::Add::add)
impl Sum<Uint128> for Result<Uint128, OverflowError> {
Copy link
Member

Choose a reason for hiding this comment

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

Also seems more unwieldy to me. I agree with allowing opt-in, but why as default?
It makes the whole API more complex and doesn't give more security in this extreme edge case (contract panic will halt), just a better error message.

Copy link
Contributor Author

@yihuang yihuang Mar 30, 2021

Choose a reason for hiding this comment

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

It makes the interface consistent with Sub, also make user aware of the overflow error case.

Copy link
Member

Choose a reason for hiding this comment

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

I see it as a case that is never hit in normal usage and impedes the usage experience for normal user.

If I am doing math for a bonding curve and squaring amounts and adding them, it is very important to use checked_add, and great to expose them as you do in this PR. But in this case, I assume the developer will use the methods not + (or quickly realize that on the first tests that overflows and panics)

Copy link
Member

@ethanfrey ethanfrey Mar 30, 2021

Choose a reason for hiding this comment

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

API changes are @webmaster128 judgement, I just find it to be a bit more annoying to enforce checked add constantly. But if you both prefer it, I will use it.

@yihuang
Copy link
Contributor Author

yihuang commented Mar 31, 2021

All suggestions are fixed, other than deprecating Sub operator, which seems don't work: rust-lang/rust#39935

Copy link
Member

@ethanfrey ethanfrey left a comment

Choose a reason for hiding this comment

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

Looks good to me.
Only open question seems to be whether to remove Sub or leave it. I'm fine with both, but lean towards removing it.

I'll let Simon make the final call and merge this.

@webmaster128
Copy link
Member

Thank you.

Regarding

All suggestions are fixed, other than deprecating Sub operator, which seems don't work: rust-lang/rust#39935

and

Only open question seems to be whether to remove Sub or leave it. I'm fine with both, but lean towards removing it.

Let's remove the implementation. It is better to break the code from compiling than changing the implementation. Then we can wait a couple of releases and re-introduce a panicking - operator.

@yihuang
Copy link
Contributor Author

yihuang commented Apr 1, 2021

Let's remove the implementation. It is better to break the code from compiling than changing the implementation. Then we can wait a couple of releases and re-introduce a panicking - operator.

Done

@webmaster128 webmaster128 merged commit 53a7833 into CosmWasm:main Apr 1, 2021
}
}

#[derive(Error, Debug, PartialEq, Eq)]
Copy link
Member

Choose a reason for hiding this comment

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

Ah, I just see this: This derive makes two error instances unequal if they have different backtraces. For StdError we avoided that using the manual PartialEq implementation.

But no worries, I'll fix this now.

Copy link
Member

Choose a reason for hiding this comment

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

@webmaster128
Copy link
Member

I pushed a CHANGELOG entry for this PR to main: fd4f9f4

Thank you!

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.

Inconsistency in Uint128 api for overflow/underflow behaviour
3 participants