Skip to content

Make FeeRate use MvB internally - #4534

Merged
apoelstra merged 15 commits into
rust-bitcoin:masterfrom
tcharding:05-21-fee-rate
Jun 4, 2025
Merged

Make FeeRate use MvB internally#4534
apoelstra merged 15 commits into
rust-bitcoin:masterfrom
tcharding:05-21-fee-rate

Conversation

@tcharding

@tcharding tcharding commented May 21, 2025

Copy link
Copy Markdown
Member

The FeeRate is a bit entangled with amount and weight. Also we have an off-by-one bug caused by rounding errors and the fact that we use kwu internally.

We can get more precision in the fee rate by internally using per million virtual bytes.

@github-actions github-actions Bot added C-bitcoin PRs modifying the bitcoin crate C-units PRs modifying the units crate test labels May 21, 2025
@tcharding

Copy link
Copy Markdown
Member Author

Question, do the checked functions in fee.rs need to be const?

@github-actions

Copy link
Copy Markdown

🚨 API BREAKING CHANGE DETECTED

To see the changes click details on "Check semver breaks / PR Semver - stable toolchain" job then expand "Run semver checker script" and scroll to the end of the section.

@github-actions github-actions Bot added the API break This PR requires a version bump for the next release label May 21, 2025
@github-actions

Copy link
Copy Markdown

🚨 API BREAKING CHANGE DETECTED

To see the changes click details on "Check semver breaks / PR Semver - stable toolchain" job then expand "Run semver checker script" and scroll to the end of the section.

1 similar comment
@github-actions

Copy link
Copy Markdown

🚨 API BREAKING CHANGE DETECTED

To see the changes click details on "Check semver breaks / PR Semver - stable toolchain" job then expand "Run semver checker script" and scroll to the end of the section.

@github-actions

Copy link
Copy Markdown

🚨 API BREAKING CHANGE DETECTED

To see the changes click details on "Check semver breaks / PR Semver - stable toolchain" job then expand "Run semver checker script" and scroll to the end of the section.

@github-actions

Copy link
Copy Markdown

🚨 API BREAKING CHANGE DETECTED

To see the changes click details on "Check semver breaks / PR Semver - stable toolchain" job then expand "Run semver checker script" and scroll to the end of the section.

@github-actions

Copy link
Copy Markdown

🚨 API BREAKING CHANGE DETECTED

To see the changes click details on "Check semver breaks / PR Semver - stable toolchain" job then expand "Run semver checker script" and scroll to the end of the section.

@github-actions

Copy link
Copy Markdown

🚨 API BREAKING CHANGE DETECTED

To see the changes click details on "Check semver breaks / PR Semver - stable toolchain" job then expand "Run semver checker script" and scroll to the end of the section.

@apoelstra

Copy link
Copy Markdown
Member

Question, do the checked functions in fee.rs need to be const?

I'd like to get there eventually. But I think we'd be fine to release without it.

@tcharding

Copy link
Copy Markdown
Member Author

No worries, we can either do it straight after this or as part of the RC cycle. We'll need to go over fee and fee_rate again with a fine tooth comb anyway. This PR gets us a fair way though.

@apoelstra

apoelstra commented May 21, 2025

Copy link
Copy Markdown
Member

The first two commits fa69cde and e38b5ef are unobjectionable and can be PR'd independently to shrink this one. The next on removes an encapsulate module so I guess needs to be in the big PR.

Same with a6062b2 (reorders functions).

Same with fc52783 (changes signatures of checked_add and checked_sub)

Same with 9575521 (drop dumb From impl) and 252ec87 (reorders tests)

/// [`minimal_non_dust`]: Script::minimal_non_dust
fn minimal_non_dust_custom(&self, dust_relay_fee: FeeRate) -> Option<Amount> {
self.minimal_non_dust_internal(dust_relay_fee.to_sat_per_kwu() * 4)
self.minimal_non_dust_internal(dust_relay_fee.to_sat_per_kwu_floor() * 4)

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.

In 4831689:

This one should be _ceil since we're trying to put a floor on the set of feerates that'll get broadcast.

BTW the commit message says that you add FIXME comments but actually you don't.

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.

Also IMO this commit could be moved outside of the "remove encapsulate" commit so we could PR it separately.

Comment thread units/src/fee.rs Outdated
pub const fn checked_div_by_fee_rate_floor(self, fee_rate: FeeRate) -> Option<Weight> {
match self.to_sat().checked_mul(1000) {
Some(amount_msats) => match amount_msats.checked_div(fee_rate.to_sat_per_kwu()) {
Some(amount_msats) => match amount_msats.checked_div(fee_rate.to_sat_per_kwu_floor()) {

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.

In 4831689:

This one should be _ceil (somewhat weirdly, since the function name has _floor in it, but that's the nature of division).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Mad. Thanks for putting in the thought required to review this PR.

@tcharding tcharding May 26, 2025

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

For the same reason should checked_div_by_fee_rate_ceil also use _ceil?

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.

It should use _floor, for the same reason.

Comment thread units/src/fee.rs Outdated
/// Returns [`NumOpResult::Error`] if overflow occurred.
pub const fn checked_mul_by_weight(self, weight: Weight) -> NumOpResult<Amount> {
if let Some(fee) = self.to_sat_per_kwu().checked_mul(weight.to_wu()) {
if let Some(fee) = self.to_sat_per_kwu_floor().checked_mul(weight.to_wu()) {

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.

In 4831689:

This one should be ceil.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think it should rather work on the internal representation whatever it is.

Comment thread units/src/fee_rate/mod.rs Outdated
/// Converts to sat/vB rounding up.
pub const fn to_sat_per_vb_ceil(self) -> u64 {
(self.to_sat_per_kwu() + (1000 / 4 - 1)) / (1000 / 4)
(self.to_sat_per_kwu_floor() + (1000 / 4 - 1)) / (1000 / 4)

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.

In 4831689:

This one should be ceil.

Comment thread units/src/fee_rate/mod.rs
@@ -81,7 +84,7 @@ impl FeeRate {
#[must_use]
pub const fn checked_mul(self, rhs: u64) -> Option<Self> {

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.

In 4831689:

I'm gonna OK these for now, but they need to be changed to do some sort of infallible conversion. Arguably they should be moved inside the encapsulate boundary.

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.

Same with Sum.

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.

And the NonZeroU64 ops.

Comment thread units/src/fee_rate/mod.rs Outdated
}
}

impl From<FeeRate> for u64 {

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.

In 4831689:

Can we just delete this impl? Not to channel Kix too hard but it's really unclear what it should mean.

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.

Ah, done in 9575521

@apoelstra

Copy link
Copy Markdown
Member

Done reviewing everything except 3603b50 (the one that makes the actual change).

I identified 6 out of these 17 commits which are independent and unobjectionable. Can you PR those separately so we can reduce the size of this PR?

You can take or leave my comments about changing _floors to _ceils. I see that you replace many of them of with mvb conversions anyway.

@apoelstra

Copy link
Copy Markdown
Member

Also would be fine to PR separately (or alongside the other unobjectionable commits) to remove consts. We should file an issue to put them back. It's not hard, just annoying.

@tcharding

Copy link
Copy Markdown
Member Author

are unobjectionable and can be PR'd independently to shrink this one

PR storm incomiiiiiiiiiiiiiing!

@tcharding

Copy link
Copy Markdown
Member Author

Changes in force push:

  • Added rustdocs as part of 1ee5754
  • Added additional patch with code comment

@github-actions

Copy link
Copy Markdown

🚨 API BREAKING CHANGE DETECTED

To see the changes click details on "Check semver breaks / PR Semver - stable toolchain" job then expand "Run semver checker script" and scroll to the end of the section.

@tcharding

tcharding commented May 29, 2025

Copy link
Copy Markdown
Member Author

Changes in force push are a typo found by linter, backticked code in rustdocs incorrectly.

tcharding added 15 commits May 31, 2025 07:51
This module is a PITA to work on, just remove it until the dust settles
on fee rate.

While we are at it make the rustdocs on the getter more terse.
In preparation for changing the inner representation of `FeeRate` add
floor and ceil versions of the getter function `to_sat_per_kwu`.

For now both functions return the same thing but still call the
correct one so that when we change the representation we do not need
to re-visit them.
Code that works with `const` is annoying to use and hard to reason
about. Just remove all the consts for now so we can hack on `FeeRate`.

Introduces two lint warnings about manual implementation of `map` but
they will go away later.
We document the other to arguments already, add the missing one.
The `FeeRate::checked_mul_by_weight` function currently returns a
`NumOpResult` but all other `checked_` functions return an `Option`.
This is surprising and adds no additional information.

Change `checked_mul_by_weight` to return `None` on overflow. But in
`to_fee` saturate to `Amount::MAX` because doing so makes a few APIs
better without any risk since a fee must be checked anyway so
`Amount::MAX` as a fee is equally descriptive in the error case.

This leads to removing the `NumOpResult` from `effective_value` also.

Note that sadly we remove the very nice docs on `NumOpResult::map`
because they no longer work.

Fix: rust-bitcoin#4497
In preparation for changing the internal representation of `FeeRate` to
use MvB reduce the max value by 4_000.

Done separately to make the change explicit.
Currently we get the fee_rate per kwu then multiply it by 4. Instead
lets add a per_kvb function to `FeeRate`. We are about to change the
internal representation of `FeeRate` to use MvB so for now just panic on
ovelflow.

Also these are fee _rates_ - we already have suboptimal names from Core
in the consts in `policy`, no need to let them infect other identifiers.
To get more precision use sats per million virtual bytes.

To make review easier keep most calls in tests using
`FeeRate::from_sats_per_kwu` and just unwrap. These can likely be
cleaned up later on if we want to.

For `serde` just change the module to `_floor` and leave it at that. The
serde stuff likely needs re-visiting before release anyways.
Now we have the `fee_rate` module clened up re-introduce the
`encapsulate` module using MvB.
Remove the unit from associated consts and then make all the rustdocs on
the various consts use 'The'.
We temporarily removed `const` in the `fee` module to make patching and
reviewing easier. Now add it back in.
During dev I introduced a pancic, remove it.
The `fee` functions are a bit convoluted because of the usage of
`const`. Refactor a couple of them to be easier to read.

Internal change only.
Its not immediately obvious that x - y cannot overflow. Add a code
comment to explain it.
@tcharding

Copy link
Copy Markdown
Member Author

Rebase only, no changes.

@github-actions

Copy link
Copy Markdown

🚨 API BREAKING CHANGE DETECTED

To see the changes click details on "Check semver breaks / PR Semver - stable toolchain" job then expand "Run semver checker script" and scroll to the end of the section.

@tcharding

Copy link
Copy Markdown
Member Author

Any movement on this one @apoelstra?

@apoelstra

Copy link
Copy Markdown
Member

Nice, thanks! I'm going to ACK and merge this.

@Kixunil I still feel that the checked_ methods should return an Option. If you feel strongly about it we can revert it, but as I said above, if we do, we should go through all the checked_ methods in the units crate and do the same thing.

Meanwhile I do not want to hold up this 15-commit PR any further about it.

@apoelstra apoelstra left a comment

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.

ACK 5651675; successfully ran local tests

@apoelstra
apoelstra merged commit a13ba99 into rust-bitcoin:master Jun 4, 2025
@tcharding
tcharding deleted the 05-21-fee-rate branch June 18, 2025 00:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

API break This PR requires a version bump for the next release C-bitcoin PRs modifying the bitcoin crate C-units PRs modifying the units crate test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FeeRate range and precision issue Maybe FeeRate::fee should be saturating or another method should exist. Off-by-one error in fee calculation

5 participants