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 Amount type #252

Closed
wants to merge 4 commits into from
Closed

Conversation

stevenroose
Copy link
Collaborator

@stevenroose stevenroose commented Apr 3, 2019

(Retake of #192 with a simpler structure for simple satoshi-precision amounts.)

Rationale

There are a lot of places and ways in which Bitcoin amounts are used: displaying, converting denominations, arithmetic, de/serializing in different ways. It's not always clear what type to use for a Bitcoin amount: i64? u64? f64? All three of those are common choices and are confusing when used together.

The Amount type has the following functions:
functions

  • safe arithmetic: checked_* methods for checked arithmetic and std::ops trait implementations for common operations that panic on over/underflow
  • ability to display in all common denominations: BTC, mBTC, uBTC, bits, satoshi and msat
  • instantiation from satoshi (native underlying type) or any of the above named denominations
  • serde serialization as i64 satoshi or as f64 bitcoin (extensible with other options, f.e. as a string)
  • parse amount strings with above mentioned denominations

The underlying type is i64. There was a hard discussion about whether to use i64 or u64, i.e. whether or not to allow negative amounts. One of the main deciding factors was the fact that the Bitcoin Core API has several wallet-related calls where negative amounts are used. Several utility functions are added to aid those that want to avoid negative amounts: abs, is_negative and positive_sub (functions like checked_sub as if the underlying type was u64, i.e. returns None if the result or either of the operands of the subtraction would be negative).

Superseded by #270

dpc
dpc previously approved these changes Apr 3, 2019
@stevenroose
Copy link
Collaborator Author

Rebased this on top of #250 so I could use serde_json in tests.

src/util/amount.rs Outdated Show resolved Hide resolved
Copy link
Contributor

@sgeisler sgeisler left a comment

Choose a reason for hiding this comment

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

This PR feels much cleaner than the previous one. But I wonder if we could save ourselves some pain by declaring parsing and serialization of amounts with suffixes an application responsibility. That would probably avoid the custom parsing and the pressure to acommodate all kinds of weird scaling suffixes (sat, satoshi, $ as some people proposed).

///
/// Note: This only parses the value string. If you want to parse a value with denomination,
/// use `FromStr`.
pub fn parse_denom(mut s: &str, denom: Denomination) -> Result<Amount, ParseAmountError> {
Copy link
Contributor

Choose a reason for hiding this comment

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

this feels like it should be fuzz tested against a regex implementation doing the same

Copy link
Contributor

Choose a reason for hiding this comment

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

I also wonder if we couldn't use f64 here if we defined the maximum valid range as -21*10^14 sat till 21*10^14 sat. log(21*10^(6+8), 2) ≈ 50.9 and a f64 has 52 mantissa bits, that should be enough to not have any losses imo (but I'd need to test this as well, but with only 2^51 combinations even exhaustive testing seems feasible).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmm, I never touched the fuzz tests. I can try to look into this. Could also create random amounts (uniform i64 values) and string and destring for all denominations etc.

Copy link
Member

Choose a reason for hiding this comment

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

f64 in principle has enough precision to represent any bitcoin amount, yes. (This is probably one reason for Satoshi choosing the "max 21mil with 8 decimal places" limits he did.) But in practice it's hard to write a parser that doesn't do things like dividing by 10 that create rounding errors and result in wrong numbers being parsed anyway.

src/util/amount.rs Outdated Show resolved Hide resolved
src/util/amount.rs Show resolved Hide resolved
Copy link
Contributor

@jonasnick jonasnick left a comment

Choose a reason for hiding this comment

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

I'm not familiar with the history of this PR but I've rewritten some code with it and basic functionality like converting denominations and from and to f64's works fine for me.

Travis prints a few unused warnings.

src/util/amount.rs Outdated Show resolved Hide resolved
src/util/amount.rs Outdated Show resolved Hide resolved
src/util/amount.rs Show resolved Hide resolved
src/util/amount.rs Show resolved Hide resolved
src/util/amount.rs Outdated Show resolved Hide resolved
src/util/amount.rs Outdated Show resolved Hide resolved
@stevenroose stevenroose force-pushed the amount-simple branch 2 times, most recently from 4913e3c to cc8c556 Compare April 9, 2019 13:30
@stevenroose
Copy link
Collaborator Author

I added checked_* and some other arithmetic methods (one block in the end of impl Amount).

@stevenroose stevenroose force-pushed the amount-simple branch 2 times, most recently from 24ead71 to 162706b Compare April 9, 2019 14:28
src/util/amount.rs Outdated Show resolved Hide resolved
@stevenroose
Copy link
Collaborator Author

Ok so I updated the PR to use u64 instead of i64 as discussed on IRC. I also removed float arithmetic, and changed the std::ops implementation to panic on overflow, which I think is a good middle ground between safety and convenience.

The one last thing I'm not fully content with is the from_float_denom implementation. I want to return TooPrecise when the amount has some significant digits, but still avoid failing to convert 1.0 + 3.0 because internally it's 4.00000000000000012 or so. I didn't feel comfortable to introduce arbitrary constants like sats.fract() > 1000.0 * f64::EPSILON.
Suggestions welcome. I think it's valuable to have the method. But in principle I could just implement it as stringing the float and parsing it.

Copy link
Contributor

@jonasnick jonasnick left a comment

Choose a reason for hiding this comment

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

Thanks for the update. The from_float_denom doesn't seem to work, for example

assert_eq!(f(0.0001234, D::Bitcoin), Err(ParseAmountError::TooPrecise));

should be 12340 satoshi instead.

src/util/amount.rs Outdated Show resolved Hide resolved
src/util/amount.rs Outdated Show resolved Hide resolved
@dpc
Copy link
Contributor

dpc commented Apr 12, 2019

I see that you removed IntoBtc while I was commenting on it. Pasting it below, though it is probably not important now.

Nit, but "A trait used to" is redundant. Reader sees that it is a trait. :) "Can be converted to ..." is shorter and to the point.

This "into btc" is confusing. Shouldn't this be just "TryIntoAmount"? Also - shouldn't the error be associated type? Why would it always had to be "Parse"? That would imply using for just parseable stuff like strings.

TryInto etc. was just stabilized, but rust-bitcoin won't be able to use it for a while, but we could just copy that trait literally, and in the future impl<T> TryInto<Amount> for T where T: TryIntoAmount, and eventually deprecate TryIntoAmount.

@stevenroose
Copy link
Collaborator Author

Lol meta-discussion. The IntoBtc name meant that the value was supposed to be interpreted as "BTC". A TryIntoAmount for f64 wouldn't tell you how many satoshi's you would get if you used it for 4.2, f.e..

But well, it's removed because it was not super valuable and caused confusion apparently.

dpc
dpc previously approved these changes Apr 12, 2019
Copy link
Contributor

@jonasnick jonasnick left a comment

Choose a reason for hiding this comment

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

Can confirm that I can use this module in my code without issues.

src/util/amount.rs Outdated Show resolved Hide resolved
Copy link
Contributor

@sgeisler sgeisler left a comment

Choose a reason for hiding this comment

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

Some final, mostly minor, nits. Looks good otherwise.

src/util/amount.rs Outdated Show resolved Hide resolved
src/util/amount.rs Outdated Show resolved Hide resolved
src/util/amount.rs Outdated Show resolved Hide resolved
src/util/amount.rs Show resolved Hide resolved
@stevenroose
Copy link
Collaborator Author

What else is blocking this?

///
/// Note: This only parses the value string. If you want to parse a value
/// with denomination, use [FromStr].
pub fn from_str_in(mut s: &str, denom: Denomination) -> Result<Amount, ParseAmountError> {
Copy link
Contributor

@sgeisler sgeisler Apr 27, 2019

Choose a reason for hiding this comment

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

I began verifying the parsing code by trying to build a DFA from it to derive a regular expression. The regex seems ok: [0-9]*.[0-9]{0,precision_diff} the only weird case this allows is parsing . as 0.

But the precision verification is broken, e.g. the following test case in fn parsing()

assert_eq!(p("12.000", Denomination::MilliSatoshi), Err(ParseAmountError::TooPrecise));

fails with

left: `Ok(Amount(12 satoshi))`,
 right: `Err(TooPrecise)`', src/util/amount.rs:712:9

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That's a good catch! I fixed that in an extra commit. The string roundtrip test didn't catch this because the to_string_in was also broken :/

src/util/amount.rs Outdated Show resolved Hide resolved
sgeisler
sgeisler previously approved these changes Apr 29, 2019
Copy link
Contributor

@sgeisler sgeisler left a comment

Choose a reason for hiding this comment

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

If you agree feel free to address the one clarity nit I left. Other than that this seems ready to be merged to me.

src/util/amount.rs Outdated Show resolved Hide resolved
@apoelstra
Copy link
Member

concept ACK

jonasnick
jonasnick previously approved these changes May 25, 2019
Copy link
Contributor

@jonasnick jonasnick left a comment

Choose a reason for hiding this comment

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

I have not looked at the string parsing nor serde but the rest works for me. ACK adcc546

@jonasnick
Copy link
Contributor

@stevenroose serde tests fail according to travis

}

/// Get the number of satoshis in this [Amount].
pub fn as_sat(self) -> i64 {
Copy link
Member

Choose a reason for hiding this comment

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

Can we have an as_usat or something that returns a u64? I see in @jonasnick's code using this that there are zillions of casts to u64.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

One that panics on negative or one that Results? I'd personally do try_as_usat() -> Option<u64> and as_usat() -> u64.

Copy link
Member

Choose a reason for hiding this comment

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

:( in my usage negative amounts never ever exist. It's really unergonomic to have these extra error paths and also have all my amounts potentially being negative. It also prevents computing fees using checked_sub to catch unbalanced transactions as a special case.

@stevenroose
Copy link
Collaborator Author

Please see #270 for a replacement PR that has explicit u64 Amount and i64 SignedAmount.

@stevenroose stevenroose closed this Jun 5, 2019
Davidson-Souza pushed a commit to Davidson-Souza/rust-bitcoin that referenced this pull request Jul 12, 2023
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.

None yet

5 participants