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

Create cosmwasm-math #1186

Closed
6 of 11 tasks
webmaster128 opened this issue Dec 15, 2021 · 45 comments
Closed
6 of 11 tasks

Create cosmwasm-math #1186

webmaster128 opened this issue Dec 15, 2021 · 45 comments

Comments

@webmaster128
Copy link
Member

webmaster128 commented Dec 15, 2021

This is a new package with the following goals:

  • Versioning independent of cosmwasm-std, such that CosmWasm 0.16 and 1.0 developers can use the same library
  • Add features required by defi users and implement them in a correct and heavility tested way
  • Ensure a small footprint in terms of code size and performance
  • Ensure we don't pull in float operations

TODOs:

  • Implement Sum for all types consistently (Use the same Sum implementation for all number types #1187)
  • Create checked_multiply_ratio returning an Option allowing to recover from overflow
  • Implement floor/ceil for all decimals
  • Copy over Uint64, Uint128, Uint256, Uint512, Decimal and Decimal256
  • Let all the checked_* functions returns Option instead of StdResult for better stdard library complience and avoiding performance hit (see Mark more Uint128, Uint64 and Decimal operations as const #1157).
  • Consider using macros to ensure API and implementation consistency across types
  • Signed integers
  • Signed decimals?
  • constify all the things
  • Find a consistent way to convert Uint64/Uin128 to the primitives u64 and u128. Are we happy with the current API?
  • Deprecate cosmwasm_std:{Decimal, Decimal256, Uint128, Uint256, Uint512, Uint64} and point the users to the math library
Feature Uint64 Uint128 Uint256 Uint512 Decimal Decimal256
const zero
const one ✅ (#1333) ✅ (#1333) ✅ (#1333) ✅ (#1333)
Sum
Add
AddAssign ✅ (#1253) ✅ (#1253)
Sub ✅ (#1242)
SubAssign ✅ (#1242) ✅ (#1253) ✅ (#1253)
Mul ✅ (#1245)
MulAssign ✅ (#1245) ✅ (#1255) ✅ (#1255)
Div ✅ (#1279) ✅ (#1279)
DivAssign ✅ (#1279) ✅ (#1279)
Rem ✅ (#1340 ) ✅ (#1340)
RemAssign ✅ (#1250) ✅ (#1250) ✅ (#1250) ✅ (#1250) ✅ (#1340 ) ✅ (#1340)
floor/ceil N/A N/A N/A N/A ✅ (#1347 ) ✅ (#1347)
pow ✅ (#1251) ✅ (#1251) ✅ (#1251) ✅ (#1342 ) ✅ (#1342)
checked_add ✅ (#1341 ) ✅ (#1341)
checked_sub ✅ (#1341 ) ✅ (#1341)
checked_rem ✅ (#1341 ) ✅ (#1341)
checked_mul
checked_div ✅ (#1341 ) ✅ (#1341)
checked_div_euclid (#1376) (#1376) N/A N/A
checked_pow
saturating_add ✅ (#1343 ) ✅ (#1343)
saturating_sub ✅ (#1343 ) ✅ (#1343)
saturating_mul ✅ (#1343 ) ✅ (#1343)
saturating_pow ✅ (#1343 ) ✅ (#1343) ✅ (#1342 ) ✅ (#1342)
wrapping_add ✅ (#1459) ✅ (#1459)
wrapping_sub ✅ (#1459) ✅ (#1459)
wrapping_mul ✅ (#1459) ✅ (#1459)
wrapping_pow ✅ (#1459) ✅ (#1459)
checked_from_ratio N/A N/A N/A N/A ✅ (#1281) ✅ (#1281)
checked_multiply_ratio ✅ (#1280) ✅ (#1280) ✅ (#1280) N/A N/A N/A
A const constructor ✅ (#1256) ✅ (#1264) ✅ (#1264)
const is_zero ✅ (#1256) ✅ (#1256) ✅ (#1256) ✅ (#1256) ✅ (#1256) ✅ (#1256)
const fn atomics N/A N/A N/A N/A ✅ (#1256) ✅ (#1256)
const fn decimal_places N/A N/A N/A N/A ✅ (#1256) ✅ (#1256)
pub const MAX: Self
pub const MIN: Self ✅ (#1366) ✅ (#1366) ✅ (#1366) ✅ (#1366) ✅ (#1366) ✅ (#1366)
@hashedone
Copy link
Contributor

hashedone commented Dec 15, 2021

Consider using macros to ensure API and implementation consistency across types

I have another proposal. Use num-traits crate for that, so Uint64 and Uint128 can be just represented using generics (it would give Uint8 up to Uint128 for free, and their I equivalents).

Also consider having all operations defined for native equivalent [u/i]8..[u/i]128 so it would be easier to use literals on calculations with those numbers.

Also I have another idea which might be part of this. Something I called the "safemath". Basically create the type, let call it Safe<T>(T) which behaves as type T, except that all operations performed by it are checked. How would it work? Basically every operation on Safe<T> return the type of SafeProxy(Option<T>) propagating the none upwards (so behavior is exactly as you would use checked_ for everything). At the end the result could be converted back to Option<T> with function like fn native(self) -> Option<T>. Additionally both Safe<T> and SafeProxy would have operators defined with their underlying types (so SafeProxy<T> can be added to T. Also SafeProxy would have operations defined for U where T: num_traits::AsPrimitive<T> so it can be used transparently with literals. The cherry on the top would be extension trait on anything implementing the num_traits::num which would provide checked function wrapping the stuff with Safe type.

Why such thing? Consider:

let reg = a.checked_mul(b)?.checked_add(c.checked_mull(d)?)?;

Kind of difficult to read, and this is only the linear regression. Now with "safemath":

let reg = (c.checked() * b + c.checked() * d).native()?;

Shorter, but what is more important - more natural (basically some checked() and single native() calls even for bigger contexts. The need of this is very much dependent on the fact if we actually want to care about the edge cases overflow. Basically - do we actually need to use U128/256/512, or do we do it as over-protection so we never face the case when it is relevant. It may be also important, that all those checked operations are not for free - they generate gas cost obviously. But guarantee not to overflow. And they are 100% opt-in (you use it only if you need this behavior).

Another step further may be having similar wrappers defaulting to wrapping_ operations (which are basically native types not panicking in debug mode), and saturating_ ones - but I didn't see much usecases for them (I think I've seen literally single use of wrapping_ in contracts and no uses of saturating_).

@dakom
Copy link
Contributor

dakom commented Dec 15, 2021

Signed decimals?

Yes, definitely! I'd suggest something like:

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Number {
    value: Decimal256, // or Uint256
    sign: NumberSign,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NumberSign {
  Positive,
  Negative
}

I implemented something like this with all the ops, cmp implementations you'd expect, as well as helpers like .invert_sign()... very happy to ditch it in favor of a PR here when the building blocks are in place :)

@webmaster128
Copy link
Member Author

I have another proposal. Use num-traits crate for that, so Uint64 and Uint128 can be just represented using generics (it would give Uint8 up to Uint128 for free, and their I equivalents).

I made bad experience with the num-traits crate in the past becasue it pulls in float operations.

Using generics, hmm, maybe. We only need Uint64 and Uint128. Then for Uint256 and Uint512 we need a separate implementation anyways. How would documentation look like? Everything should be rolled out completely for the specific types. The users should not see the extra abstraction.

@archseer
Copy link

I made bad experience with the num-traits crate in the past becasue it pulls in float operations.

I had some success with num-bigint if no_std was enabled, after using rust-num/num-bigint#225, but I'd prefer not using these crates if possible.

@hashedone
Copy link
Contributor

I just had a talk with friend and it turns out decimals are not so trivial, and common rust crates actually makes it wrong (it has overhead of computational error). There is very strong and heavily documented C library doing it properly - https://github.com/dnotq/decNumber. Probably we can use it as example how to do it properly.

@dakom
Copy link
Contributor

dakom commented Dec 22, 2021

common rust crates actually makes it wrong

That's a pretty heavy statement... can you be more specific with an example of an arithmetic error?

@dakom
Copy link
Contributor

dakom commented Dec 22, 2021

@hashedone - if you want to discuss something, please quote and reply. Do not edit the history of other people's comments!!!

@dakom
Copy link
Contributor

dakom commented Dec 22, 2021

To address what you said in regard to #1186 (comment):

Why not to use u2 for proper arithmetic? The u1 (what you efectively proposed) is problematic on everything starting from addition, not mentioning more difficult operations...

I am not sure what you mean by u2 vs. u1... but assuming you mean to pack it in the available 256 bits instead of storing the sign flag separately:

  1. Keeping the sign flag separate allows for deferring to the underlying value for fixed-point arithmetic, which keeps the complexity isolated in one place.
  2. Keeping the sign flag separate allows TryFrom/TryInto in both directions and From/Into from the underlying value type (always positive).
  3. I haven't run benchmarks, but it's almost definitely negligible in terms of performance cost (we're already going through jumps and extracting parts of byte arrays... a bool won't make much of an impact, and if we're really concerned about cache misses or something it could be packed with repr, but I wouldn't go down that road personally)
  4. I've implemented all the ops and cmp things. It's very easy, you just pattern match on the sign and then treat positive and negative separately. Like I said, I'm happy to make a PR once this crate has the basic Decimal256/Uint256 stuff available

@webmaster128
Copy link
Member Author

I just had a talk with friend and it turns out decimals are not so trivial, and common rust crates actually makes it wrong (it has overhead of computational error). There is very strong and heavily documented C library doing it properly - https://github.com/dnotq/decNumber. Probably we can use it as example how to do it properly.

This talks about "Decimal Floating Point", which is hard to get right for sure. But we don't need floating point types. We can simply use fixed point decimal types, which makes all the things very easy because those are just integers with a fixed decimal point. Most operations are implemented using integer arithmetic.

@dakom
Copy link
Contributor

dakom commented Jan 24, 2022

@webmaster128 - it would be really nice to also have npm wasm-packed lib that exported a typescript interface. Ideally a full class wrapper around the structs with all the methods, but for starters even pure functions could work.

That way frontend/cli code could work with the full Decimal256 and do math on the frontend, without having to worry about floating point errors or compatibility issues from some other third-party JS fixed-point lib

Of course even if this idea is accepted, it's low-priority and depends on the main crate existing first

@webmaster128
Copy link
Member Author

That way frontend/cli code could work with the full Decimal256 and do math on the frontend, without having to worry about floating point errors or compatibility issues from some other third-party JS fixed-point lib

You can use import { Decimal } from "@cosmjs/math" which does that. You have to pass 18 as fractional digits to it to make it compatible. It can store decimals of arbitrary length, so it works with both cosmwasm_std::Decimal and cosmwasm_std::Decimal256.

@dakom
Copy link
Contributor

dakom commented Jan 24, 2022

I see.. and that is great, thanks!

That's even better, suggestion could just be a drop-in replacement to that package, maybe even without breaking anything much downstream, i.e. to use the underlying wasm and just pass strings around directly, instead of going through bn.js.

Would also make it easier to implement more methods like abs(), pow(), etc.

@webmaster128
Copy link
Member Author

I think what you are bringing up there (compiling cosmwasm-math to a client lib) is a valid and valuable aproach. It will just not happy anytime soon due to reasource limits and priorities. Compiling Wasm for JS environments is not as simple as it sounds. Embedding them in a way that works for Node, bundlers and the web requires you to inline the Wasm as text in .js files. When it comes to bundling Wasm as part of JS libraries it gets very tricky. In many places we work towards getting rid of Wasm dependencies because they create big bundles and you cannot optimize for bundle size using tree shaking when you have Wasm.

@dakom
Copy link
Contributor

dakom commented Jan 24, 2022

Fair enough, though this crate, isolate from cosmwasm_std, should make the bundle pretty small!

Happy to lend a hand when the time is right (I have done a fair amount of browser-targetted wasm dev, e.g. https://github.com/dakom/awsm-web)

@webmaster128
Copy link
Member Author

I added wrapping_add/wrapping_sub/wrapping_mul/wrapping_pow to the list which are only implemented for Uint64 and Uint128.

@dakom
Copy link
Contributor

dakom commented Sep 14, 2022

how about an EPSILON const and approx_eq() for comparisons that may lose precision, even with the 18 decimal places?

@webmaster128
Copy link
Member Author

Seems like the Machine epsilon is well defined for decimal types, so we could add it. But what would be the use case exactly? Should approx_eq only accept a 1 EPSILON difference? Or some wider range?

@dakom
Copy link
Contributor

dakom commented Sep 14, 2022

yeah, I think 1 difference is the right thing to do here, i.e. (self - other).abs() < Self::EPSILON ... if someone wants a more relaxed test they could always add it with an extension trait.

@webmaster128
Copy link
Member Author

(self - other).abs() < Self::EPSILON would be the same as self == other then. We want <=, right?

@dakom
Copy link
Contributor

dakom commented Sep 14, 2022

anecdotal, but I usually see < as the cutoff, not <=. I don't know if there's a hard mathematical reason or it's just sortof the way this is usually implemented.

(< is not the same as ==, the point is that the difference is non-zero but smaller than epsilon)

@webmaster128
Copy link
Member Author

(< is not the same as ==, the point is that the difference is non-zero but smaller than epsilon)

This might be the case for floats. But Decimals have 18 digits and there is no value between 1.987654321987654321 and 1.987654321987654322. You have epsilon steps between all decimals.

@dakom
Copy link
Contributor

dakom commented Sep 15, 2022

Hmm... okay, so maybe this shouldn't be part of the type, and we'll just use extension traits if we need approx_eq with a greater threshhold than the properly defined epsilon.

Fwiw the use case I've run into might be due to losing some precision when going between, say, Uint128 and Uint256, for example.

@webmaster128
Copy link
Member Author

More wrapping stuff here: #1459 (comment)

@ewoolsey
Copy link

ewoolsey commented Jan 6, 2023

Any updates here on progress towards SignedDecimals and SignedInts? I'm currently relying on a custom built type for these things (internally using a bool), but it would be really nice to have official support for this.

@webmaster128
Copy link
Member Author

Thanks for the nudge, @ewoolsey. Could you share your version to help us designing the API we need?

I wonder how to best implement this. There can be an enum with Negative and NonNegative cases or we build it bitwise on top of a Uint* storage. The first one gives us a +0 and -0, which can be annoying.

@ewoolsey
Copy link

ewoolsey commented Jan 9, 2023

Thanks for the nudge, @ewoolsey. Could you share your version to help us designing the API we need?

Yeah I'd love to share it! I'll create a prototype repo and you can have a look! I found using an enum a little clunky so chose to use a struct with a bool instead. I consider what I currently have to be "quick and dirty" but it's passing all the tests I've written for it. My gut says that the correct way to do this is representing the sign as the most significant bit. That's definitely the most work, but probably produces the best performance.

@ewoolsey
Copy link

ewoolsey commented Jan 9, 2023

Thanks for the nudge, @ewoolsey. Could you share your version to help us designing the API we need?

Here you are!

@webmaster128
Copy link
Member Author

Thank you!

This is the part I worry about:

impl std::cmp::PartialEq for SignedInt {
    fn eq(&self, other: &Self) -> bool {
        self.value == other.value && self.is_positive == other.is_positive
    }
}

which leads to +0 != -0. Implementation can change obviously but ideally we avoid having two states for 0.

@ewoolsey
Copy link

ewoolsey commented Jan 9, 2023

So in this prototype I attempt to forbid -0. It should be impossible to ever get that value. There are some tests at the bottom that confirm this as well. (ps please pull latest I added some updates there)

@webmaster128
Copy link
Member Author

The remaining issues can be answered as follows.

Signed integers
Signed decimals?

Have been extracted to #1710 and #1711.

Let all the checked_* functions returns Option instead of StdResult for better stdard library complience and avoiding performance hit (see #1157).

Is not going to happen because sometimes we have different error cases. In cosmwasm-std 2.0 we'll make sure to make math errors cheap to create and handle (i.e. no String allocations).

Find a consistent way to convert Uint64/Uin128 to the primitives u64 and u128. Are we happy with the current API?

Maybe, one day, let's see.

Deprecate cosmwasm_std:{Decimal, Decimal256, Uint128, Uint256, Uint512, Uint64} and point the users to the math library

This is not planned anymore. Having everything here and cleaning up on major version bumps should be more practical. Also some cosmwasm-std types like Timestamp depend on math types.

Thanks everyone

@maurolacy
Copy link
Contributor

maurolacy commented Feb 15, 2024

Deprecate cosmwasm_std:{Decimal, Decimal256, Uint128, Uint256, Uint512, Uint64} and point the users to the math library

This is not planned anymore. Having everything here and cleaning up on major version bumps should be more practical.

I think having a (relatively) lightweight independent math library would be a nice thing. By example, for packages / crates that need numerical types, and don't need / use the CosmWasm API.

Also some cosmwasm-std types like Timestamp depend on math types.

Just make cosmwasm-std depend on cosmwasm-math, what's the problem?

@webmaster128
Copy link
Member Author

Just make cosmwasm-std depend on cosmwasm-math, what's the problem?

This might work. I was initially thinking the other way around (cosmwasm-math being an opt-in library for contract developers). What would be the use case for such a split? More crates leads to more maintenance cost so we better have a good reason.

@maurolacy
Copy link
Contributor

maurolacy commented Feb 23, 2024

What would be the use case for such a split? More crates leads to more maintenance cost so we better have a good reason.

As mentioned above, packages and modules that require math types / utils.

I recently needed an u256 type for a package, and ended up adding cosmwasm-std to the project, just to use UInt256 from it.

Not a big deal, because that package will end up being used in contracts development anyway, but it felt inelegant, and a little overkill to add the entire CosmWasm standard library just to have access to UInt256.

@webmaster128
Copy link
Member Author

Let's continue this discussion in #1956

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

No branches or pull requests

8 participants