Skip to content

Change SignedAmount MAX and MIN to equal +/- MAX_MONEY#3719

Merged
apoelstra merged 1 commit intorust-bitcoin:masterfrom
jamillambert:1210-Signedamount-max
Dec 12, 2024
Merged

Change SignedAmount MAX and MIN to equal +/- MAX_MONEY#3719
apoelstra merged 1 commit intorust-bitcoin:masterfrom
jamillambert:1210-Signedamount-max

Conversation

@jamillambert
Copy link
Copy Markdown
Contributor

To prevent rounding errors converting to and from f64 change SignedAmount MAX and MIN to +/- MAX_MONEY which are within the limit in f64 that has issues.

Add checks to from_str_in, checked_add, checked_sub and checked_mul that the result is within MIN and MAX.

Modify tests to work with new MIN and MAX.

Discussed in #3688 and #3691

@github-actions github-actions bot added the C-units PRs modifying the units crate label Dec 10, 2024
@jamillambert jamillambert changed the title Change SignedAmount MAX and MIN to equal MAX_MONEY` Change SignedAmount MAX and MIN to equal +/- MAX_MONEY Dec 10, 2024
@coveralls
Copy link
Copy Markdown

coveralls commented Dec 10, 2024

Pull Request Test Coverage Report for Build 12272289434

Details

  • 18 of 18 (100.0%) changed or added relevant lines in 2 files are covered.
  • 1 unchanged line in 1 file lost coverage.
  • Overall coverage increased (+0.01%) to 82.925%

Files with Coverage Reduction New Missed Lines %
units/src/amount/signed.rs 1 71.51%
Totals Coverage Status
Change from base Build 12269599764: 0.01%
Covered Lines: 20295
Relevant Lines: 24474

💛 - Coveralls

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think a more consistent name is from_sats_checked like I added here: #3703

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think from_sats_checked is related to addressing #3696. This is just a private function created so the code is not repeated for the min/max check in the various other functions. But if the public function is created it could be used for the check as well I guess.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

But if we add a check for MIN to from_sats_checked, then we don't need check_min_max.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

As a private helper function this looks fine to me. Since its private I'd shorten the docs though. Also checking min first seems a bit cleaner to me

    /// Checks the amount is within the allowed range.
    const fn check_min_max(self) -> Option<SignedAmount> {
        if self.0 < Self::MIN.0 || self.0 > Self::MAX.0 {
            None
        } else {
            Some(self)
        }
    }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If we are going to enforce a lower bound that's not i64::MIN , should that be MIN_MONEY? Not sure since core doesn't.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I have no strong opinion either way on adding a MIN_MONEY or leaving as it is. But lean towards not adding one since there was already discussion of removing MAX_MONEY.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

-1 to adding MIN_MONEY

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yeah I'm inclined to agree

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think this makes sense to check twice. There's a way to do this where we don't need to check self.0.checked_add(rhs.0) as well as .check_min_max(). right? same for the other operations.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In other words, do a checked add, and if the results exceed MAX_MONEY, then NONE else Some(signed_amount)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't understand what you mean by check twice. The first checked_add() will only fail if it overflows, it has nothing to do with our MAX invariant. There is only one check for MIN and MAX, the .check_min_max() private function. I made it this way so that the check for all the public functions are not repeated and are done by one function which returns the Option.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The first checked_add() will only fail if it overflows

Right. This check doesn't need to happen is what I'm saying. If two numbers are added together and the result is less than MAX_MONEY like what you do in check_min_max then you don't also need to check that the two numbers when added together don't overflow. IE less than MAX_MONEY implies not overflow.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

But I still need to handle the case where the addition overflows. I can't check if the sum is greater than MAX if adding the numbers overflows, therefore I use i64::checked_add

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this could be verified with an assertion added to a test. My point is that checked_add should check that adding two numbers is not greater than MAX_MONEY, then if is not greater than MAX_MONEY you don't need to check that it overflows do you?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This change looks correct to me.

Copy link
Copy Markdown
Contributor

@yancyribbens yancyribbens Dec 10, 2024

Choose a reason for hiding this comment

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

It's not incorrect to check both that it is not greater than MAX_MONEY and not greater than i64::MAX just unnecessary.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

\me says a little prayer "please lord give me patience"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

same here, if checked_sub is greater than MIN, then return Some else None.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ditto

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I had already put in a PR for this, if that commits first you'll need to rebase.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Rebased to remove conflict. I kept it as MAX.0 and not .to_sats() to be consistent with the other changes

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This change is unnecessary.

@yancyribbens
Copy link
Copy Markdown
Contributor

This PR conflicts with #3703 and #3697 which I've been waiting to get feedback on. It would be appreciated if you don't agree with those PRs to comment on them with your suggestions. I do agree that MIN should probably be redefined at some point as you have done in this PR.

@jamillambert
Copy link
Copy Markdown
Contributor Author

This PR conflicts with #3703 and #3697 which I've been waiting to get feedback on. It would be appreciated if you don't agree with those PRs to comment on them with your suggestions. I do agree that MIN should probably be redefined at some point as you have done in this PR.

I have rebased to remove the conflict with #3697. #3703 deals with the issue #3696 which we decided to deal with separately to changing the MAX value to the lower MAX_MONEY. This PR is just applying what was done in #3693 to SignedAmount.

@yancyribbens
Copy link
Copy Markdown
Contributor

This PR is just applying what was done in #3693 to SignedAmount.

What issue is this addressing then if not issue 3696? The reason I point out the conflict with 3703 is that I don't think we need both from_sats_checked as introduced in 3703 and check_min_max here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

-1 to adding MIN_MONEY

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This change is unnecessary.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Perhaps this better expresses what this match is doing

            (true, sat) if sat > SignedAmount::MIN.to_sat().unsigned_abs() => Err(ParseAmountError(
                ParseAmountErrorInner::OutOfRange(OutOfRangeError::too_small()),
            )),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, thanks that is clearer. It did seem odd to check a min against a max even though it's the same thing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This change looks correct to me.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

As a private helper function this looks fine to me. Since its private I'd shorten the docs though. Also checking min first seems a bit cleaner to me

    /// Checks the amount is within the allowed range.
    const fn check_min_max(self) -> Option<SignedAmount> {
        if self.0 < Self::MIN.0 || self.0 > Self::MAX.0 {
            None
        } else {
            Some(self)
        }
    }

To prevent rounding errors converting to and from f64 change
`SignedAmount` `MAX` and `MIN` to +/- `MAX_MONEY` which are within the
limit in f64 that has issues.

Add checks to `from_str_in`, `checked_add`,  `checked_sub` and
`checked_mul` that the result is within MIN and MAX.

Modify tests to work with new `MIN` and `MAX`
@jamillambert
Copy link
Copy Markdown
Contributor Author

Thanks for the reviews. I have made the suggested changes.

@jamillambert
Copy link
Copy Markdown
Contributor Author

Needs #3721

Copy link
Copy Markdown
Member

@tcharding tcharding left a comment

Choose a reason for hiding this comment

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

ACK 4b926e1

Copy link
Copy Markdown
Member

@apoelstra apoelstra left a comment

Choose a reason for hiding this comment

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

ACK 4b926e1; successfully ran local tests

@apoelstra apoelstra merged commit 157d5bf into rust-bitcoin:master Dec 12, 2024
@jamillambert jamillambert deleted the 1210-Signedamount-max branch December 29, 2025 14:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

C-units PRs modifying the units crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants