-
Notifications
You must be signed in to change notification settings - Fork 336
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
Conversation
There was a problem hiding this 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.
packages/std/src/errors/std_error.rs
Outdated
operand2: String, | ||
#[cfg(feature = "backtraces")] | ||
backtrace: Backtrace, | ||
}, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
packages/std/src/math.rs
Outdated
self.0 | ||
.checked_add(other.0) | ||
.map(Self) | ||
.ok_or_else(|| StdError::overflow(self, other)) |
There was a problem hiding this comment.
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 ?
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done ;D
53df44b
to
0a99ba2
Compare
|
There was a problem hiding this 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> { |
There was a problem hiding this comment.
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
packages/std/src/math.rs
Outdated
self.0 += rhs.u128(); | ||
fn add(self, rhs: &'a Uint128) -> Self::Output { | ||
let (op1, op2) = (self.u128(), rhs.u128()); | ||
op1.checked_add(op2) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 bothAdd
andSub
? 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."
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
packages/std/src/math.rs
Outdated
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> { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
restructure the error branches in StdError
All suggestions are fixed, other than deprecating |
There was a problem hiding this 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.
Thank you. Regarding
and
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 |
Done |
} | ||
} | ||
|
||
#[derive(Error, Debug, PartialEq, Eq)] |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I pushed a CHANGELOG entry for this PR to main: fd4f9f4 Thank you! |
Fixes #850