This PR refactors satoshi_round to accept different rounding modes and make rounding a required argument.
Continuation of #23225
<!--e57a25ab6845829454e8d69fc972939a-->
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
<!--006a51241073e994b41acfe9ec718e94-->
For detailed information about the code coverage, see the test coverage report.
<!--021abf342d371248e50ceaed478a90ca-->
See the guideline for information on the review process.
| Type | Reviewers |
|---|---|
| ACK | AngusP, maflcko, achow101 |
| Concept ACK | BrandonOdiwuor, kristapsk |
| Stale ACK | rkrux, ismaelsadeeq |
If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.
Concept ACK
We generally prevent fee calculation with floats to maintain precision as I have seen in a few places.
Using Decimal/Integer will be more predictable than using float which are prone to rounding errors.
If this is the scope of this PR, there are still places where we use float to represent fee rate and output values, you can refactor them to use Decimal or Integer but I am not sure how to maintain this and prevent adding fee/feerate representation or calculation in floats.
lgtm. Seems fine to force the call site to specify the rounding when turning a random float into a satoshi amout.
Note that you are not really "removing" satoshi_round, as it is still used, just inlined.
An alternative could be to make rounding a required argument.
lgtm. Seems fine to force the call site to specify the rounding when turning a random float into a satoshi amout.
Note that you are not really "removing"
satoshi_round, as it is still used, just inlined.An alternative could be to make
roundinga required argument.
I agree with the alternative suggestion, I'll restore the satoshi_round function and make rounding a requirement.
If this is the scope of this PR, there are still places where we use
floatto represent fee rate and output values, you can refactor them to use Decimal or Integer but I am not sure how to maintain this and prevent adding fee/feerate representation or calculation infloats.
I'll look through the tests and enforce the use of Decimal or Integer types. I am not sure if we can explicitly prevent floats in fee calculations within tests, but a good place to start would be type hinting
Concept ACK moving from float to Decimal
82 | + feerate = Decimal(e["feerate"]) 83 | assert_greater_than(feerate, 0) 84 | 85 | - if feerate + delta < min(fees_seen) or feerate - delta > max(fees_seen): 86 | + min_fees_seen = Decimal(min(fees_seen)).quantize(Decimal('0.00000001')) 87 | + max_fees_seen = Decimal(max(fees_seen)).quantize(Decimal('0.00000001'))
We can't use satoshi_round() here because it will by default round down and that's not what's needed here?
For this function, I wasn't sure which rounding type to use. That's why I opted to keep the implementation and just change to Decimal
Default Decimal rounding is ROUND_HALF_EVEN -- "Round to nearest with ties going to nearest even integer.":
>>> from decimal import getcontext
>>> getcontext().rounding
ROUND_HALF_EVEN
I wasn't super sure what you are trying to do here, but this example helps:
>>> Decimal(0.004).quantize(Decimal('0.01'))
Decimal('0.00')
>>> Decimal(0.008).quantize(Decimal('0.01'))
Decimal('0.01')
So you're still getting 'some' rounding behaviour, but it's not explicit which one...
I think it would be clearer if you did use satoshi_round:
min_fees_seen = satoshi_round(min(fees_seen), rounding=ROUND_DOWN)
max_fees_seen = satoshi_round(max(fees_seen), rounding=ROUND_UP)
ROUND_DOWN for min and ROUND_UP for max feels intuitively right to me, but you could also use e.g. ROUND_HALF_UP for both (as a bit of a satoshi is more than no satoshis)?
Thanks for the breakdown, seems ideal to also use satoshi_round here and to use round down for min and round up for max, helps with greater precision
tACK b7a4a81
Make successful, all functional tests successful.
To address this, the satoshi_round function is removed, and Decimal values are utilized in its place.
satoshi_round is back, let's update the PR description to reflect that because it will be picked up by the bot on merge?37 | @@ -38,9 +38,9 @@ def small_txpuzzle_randfee( 38 | # It's best to exponentially distribute our random fees 39 | # because the buckets are exponentially spaced. 40 | # Exponentially distributed from 1-128 * fee_increment 41 | - rand_fee = float(fee_increment) * (1.1892 ** random.randint(0, 28)) 42 | + rand_fee = satoshi_round(fee_increment * Decimal(1.1892 ** random.randint(0, 28)), rounding=ROUND_DOWN)
rand_fee = satoshi_round(fee_increment * (1.1892 ** random.randint(0, 28)), rounding=ROUND_DOWN)
Why the decimal?
Because multiplying a Decimal object (fee_increment) with a float (the result of 1.1892 ** random.randint(0, 28))
results in an error TypeError: unsupported operand type(s) for *: 'decimal.Decimal' and 'float'
@naiyoma thats because 1.1892 is in float, I think you should instead convert it to decimal and do the exponentiation in decimal?
rand_fee = satoshi_round(fee_increment * (Decimal('1.1892') ** random.randint(0, 28)), rounding=ROUND_DOWN)
why round down?
@ismaelsadeeq for consistency since the initial implementation uses ROUND_DOWN implicitly within the satoshi_round function. https://github.com/bitcoin/bitcoin/blob/master/test/functional/test_framework/util.py#L265
thanks, makes sense to convert (Decimal('1.1892') first.
236 | @@ -237,8 +237,9 @@ def get_fee(tx_size, feerate_btc_kvb): 237 | return target_fee_sat / Decimal(1e8) # Return result in BTC 238 | 239 | 240 | -def satoshi_round(amount): 241 | - return Decimal(amount).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN) 242 | +def satoshi_round(amount, rounding=ROUND_DOWN) -> Decimal:
def satoshi_round(amount, *, rounding) -> Decimal:
why the default, if the goal is to not use a default?
tACK b7a4a81
Make successful, all functional tests successful.
To address this, the satoshi_round function is removed, and Decimal values are utilized in its place.
- As per the second commit, the
satoshi_roundis back, let's update the PR description to reflect that because it will be picked up by the bot on merge?- Should we consider squashing the first 2 commits into one because after the review the function was put back?
Thanks for the review I have updated the PR Description and will squash commits as suggested.
<!--85328a0da195eb286784d51f73fa0af9-->
🚧 At least one of the CI tasks failed. Make sure to run all tests locally, according to the documentation.
Possibly this is due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.
Leave a comment here, if you need help tracking down a confusing failure.
<sub>Debug: https://github.com/bitcoin/bitcoin/runs/23898692372</sub>
The C.I failure here is due to unused import @naiyoma
{'-zmqpubhashblock', '-zmqpubsequencehwm', '-zmqpubsequence', '-zmqpubrawblock', '-zmqpubhashtxhwm', '-zmqpubhashblockhwm', '-includeconf', '-zmqpubhashtx', '-zmqpubrawtx', '-testdatadir', '-zmqpubrawblockhwm', '-zmqpubrawtxhwm'}
test/functional/test_framework/util.py:8:1: F401 'decimal.ROUND_DOWN' imported but unused
^---- failure generated from lint-python.py
^---- ⚠️ Failure generated from lint-*.py scripts!
The C.I failure here is due to unused import @naiyoma
{'-zmqpubhashblock', '-zmqpubsequencehwm', '-zmqpubsequence', '-zmqpubrawblock', '-zmqpubhashtxhwm', '-zmqpubhashblockhwm', '-includeconf', '-zmqpubhashtx', '-zmqpubrawtx', '-testdatadir', '-zmqpubrawblockhwm', '-zmqpubrawtxhwm'} test/functional/test_framework/util.py:8:1: F401 'decimal.ROUND_DOWN' imported but unused ^---- failure generated from lint-python.py ^---- ⚠️ Failure generated from lint-*.py scripts!
Deleted the Unused import, thanks
utACK 870e71cb8e66232c98b821e38b472e3b8ab9c1c9 🛸
Should also update the PR Title!
3 | @@ -4,7 +4,7 @@ 4 | # file COPYING or http://www.opensource.org/licenses/mit-license.php. 5 | """Test fee estimation code.""" 6 | from copy import deepcopy 7 | -from decimal import Decimal 8 | +from decimal import Decimal,ROUND_DOWN
Nit:
from decimal import Decimal, ROUND_DOWN
resolved
236 | @@ -237,8 +237,9 @@ def get_fee(tx_size, feerate_btc_kvb): 237 | return target_fee_sat / Decimal(1e8) # Return result in BTC 238 | 239 | 240 | -def satoshi_round(amount): 241 | - return Decimal(amount).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN) 242 | +def satoshi_round(amount, *, rounding) -> Decimal: 243 | + """Rounds a Decimal amount to the nearest satoshi using the specified rounding mode.""" 244 | + return Decimal(amount).quantize(Decimal('0.00000001'), rounding=rounding)
nit:
SATS_PRECISION = Decimal('0.00000001')
...
def satoshi_round(amount: Union[int, float, str], *, rounding: str) -> Decimal:
"""Rounds a Decimal amount to the nearest satoshi using the specified rounding mode."""
return Decimal(amount).quantize(SATS_PRECISION, rounding=rounding)
The Decimal('0.00000001') appears in a few places, would be good to extract it as a constant SATS_PRECISION (or whatever name you prefer) for readability and to make it less likely someone accidentally sets the wrong number of zeroes when making a change
Concept ACK
ACK aa46f0ec82c65fc8bb67c544c6b16296940d96dd
3 | @@ -4,7 +4,7 @@
4 | # file COPYING or http://www.opensource.org/licenses/mit-license.php.
5 | """Test fee estimation code."""
nit: this commit message and PR title are a bit long see https://chris.beams.io/posts/git-commit/ that was recommended in https://github.com/bitcoin/bitcoin/blob/master/CONTRIBUTING.md#committing-patches
3 | @@ -4,7 +4,7 @@
4 | # file COPYING or http://www.opensource.org/licenses/mit-license.php.
5 | """Test fee estimation code."""
6 | from copy import deepcopy
There is still a single fee calculation in feature_fee_estimation.py done with float in transact_and_mine method.
added it here c96568e4e812ad59ae7c922dac3cea0ee880341c I think this suffices, though I could also use satoshi_round function and round to the nearest half.
re-ACK c96568e4e812ad59ae7c922dac3cea0ee880341c via diff
The recent review comments were all addressed.
72 | @@ -73,36 +73,38 @@ def small_txpuzzle_randfee( 73 | 74 | def check_raw_estimates(node, fees_seen): 75 | """Call estimaterawfee and verify that the estimates meet certain invariants.""" 76 | - 77 | - delta = 1.0e-6 # account for rounding error 78 | + delta = Decimal('0.000001') # account for rounding error
not sure this needs to change. Fee estimation uses double internally, so it can't be 100% exact. Seems odd to change test code to imply that setting the rounding error needs to be exact somehow.
It isn't wrong, but it seems a bit confusing.
I think, the earlier version of this pull where the only change was to make the rounding mode of satoshi_round a mandatory keyword argument made sense and looked good.
Agreed might be an overkill because check_raw_estimates function tests for a range, ensuring the returned fee rate is sane.
Sounds good to me to limit this to @maflcko suggestion
Slight +1 on not making delta a Decimal, which suggests there's something important about its precision?
I was aiming for exact precision because of this comment #23225 (comment), but I get your point. Maybe if fee estimation changes to decimals internally in future, the tests can be modified accordingly.
Yes, sounds good to leave the functions completely untouched in this pull. When internally double is changed to something else, the Python code can also use something other than float (which has double precision). Thanks for providing context.
not sure on the latest changes
<!--85328a0da195eb286784d51f73fa0af9-->
🚧 At least one of the CI tasks failed. <sub>Debug: https://github.com/bitcoin/bitcoin/runs/28457526653</sub>
<details><summary>Hints</summary>
Make sure to run all tests locally, according to the documentation.
The failure may happen due to a number of reasons, for example:
Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.
A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.
An intermittent issue.
Leave a comment here, if you need help tracking down a confusing failure.
</details>
review ACK 7d7671ad5c83c3d1220d84110259c63b5ed7405d
The pull request description will need to be updated to reflect the current state.
Refactor satoshi_round function to accept different rounding modes.
Updated call site to use the revised `satoshi_round` function.
Co-authored-by: Kate Salazar <52637275+katesalazar@users.noreply.github.com>
The pull request description will need to be updated to reflect the current state. @maflcko Description updated. Also, reverted
fee_incrementto a float and then applied satoshi_round with ROUND_DOWN at the call site. This is much cleaner and more readable.
ACK ec317bc44b0cb089419d809b5fef38ecb9423051
review ACK ec317bc44b0cb089419d809b5fef38ecb9423051
ACK ec317bc44b0cb089419d809b5fef38ecb9423051