-
Notifications
You must be signed in to change notification settings - Fork 38.7k
wallet: always do avoid partial spends if fees are within a specified range #14582
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
wallet: always do avoid partial spends if fees are within a specified range #14582
Conversation
dbdbca2 to
89e42d7
Compare
|
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. ConflictsReviewers, this pull request conflicts with the following ones:
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first. |
|
Don't the existing coin selection algorithms take into account the case where having no change output would reduce the fee? Wouldn't that have the same effect already? Or am I misunderstanding what this does / the current behaviour |
The wallet will not use output grouping unless the user turns on |
dc223c9 to
15342ad
Compare
7035fce to
c74d0e9
Compare
431bb20 to
f48a63e
Compare
f48a63e to
d14d744
Compare
d14d744 to
7e25756
Compare
4864149 to
eda938a
Compare
|
I'm super happy to see work on this! |
|
@gmaxwell Glad to hear it! I actually wrote code to turn this into a variable fee (see last commit), but was hesitant whether to push it now or wait. Your comment made me decide to push now. :) |
c022659 to
75212a5
Compare
76bad7a to
ed515f4
Compare
|
Concept ACK |
fjahr
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Concept ACK
Some suggestions, feel free to ignore the renamings. And I have expanded the test here: fjahr@aafa6c3#diff-c2a150736b92621b8db2d91b546fbf85. Please take a look and feel free use.
|
|
||
| if (gArgs.IsArgSet("-maxapsfee")) { | ||
| CAmount n = 0; | ||
| if (gArgs.GetArg("-maxapsfee", "") == "-1") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't really think of a good reason why users would want to deactivate this instead of keeping it at 0. Maybe we don't need this option?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If someone has a really big wallet with tons of UTXO:s, they may want to disable it as it effectively does coin selection twice every time, doubling the time. I don't think there are any other reasons, though.
src/wallet/init.cpp
Outdated
| gArgs.AddArg("-fallbackfee=<amt>", strprintf("A fee rate (in %s/kB) that will be used when fee estimation has insufficient data. 0 to entirely disable the fallbackfee feature. (default: %s)", | ||
| CURRENCY_UNIT, FormatMoney(DEFAULT_FALLBACK_FEE)), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET); | ||
| gArgs.AddArg("-keypool=<n>", strprintf("Set key pool size to <n> (default: %u). Warning: Smaller sizes may increase the risk of losing funds when restoring from an old backup, if none of the addresses in the original keypool have been used.", DEFAULT_KEYPOOL_SIZE), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET); | ||
| gArgs.AddArg("-maxapsfee=<n>", strprintf("Spend up to this amount in (absolute) fees (in %s) if it means prioritizing partial spend avoidance (default: %s)", CURRENCY_UNIT, FormatMoney(DEFAULT_MAX_AVOIDPARTIALSPEND_FEE)), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a wallet option maybe a percentage would be better? Personally I think I would prefer to be able to set a max % increase in the feerate for this option. An absolute value seems brittle in times of fluctuating fees and users might forget about updating it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The text here confused me at first but the way I see it works in the code and this refers to additional fees makes more sense and this is probably equally usable as a percentage for users.
| gArgs.AddArg("-maxapsfee=<n>", strprintf("Spend up to this amount in (absolute) fees (in %s) if it means prioritizing partial spend avoidance (default: %s)", CURRENCY_UNIT, FormatMoney(DEFAULT_MAX_AVOIDPARTIALSPEND_FEE)), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET); | |
| gArgs.AddArg("-maxapsfee=<n>", strprintf("Spend up to this amount in additional (absolute) fees (in %s) if it allows to use partial spend avoidance (default: %s)", CURRENCY_UNIT, FormatMoney(DEFAULT_MAX_AVOIDPARTIALSPEND_FEE)), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, tweaked a little.
src/wallet/wallet.cpp
Outdated
| CAmount maxapsfee = gArgs.GetArg("-maxapsfee", DEFAULT_MAX_AVOIDPARTIALSPEND_FEE); | ||
| int nChangePosIn = nChangePosInOut; | ||
| CTransactionRef tx2 = tx; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
| CAmount maxapsfee = gArgs.GetArg("-maxapsfee", DEFAULT_MAX_AVOIDPARTIALSPEND_FEE); | |
| int nChangePosIn = nChangePosInOut; | |
| CTransactionRef tx2 = tx; | |
| const CAmount max_aps_fee = gArgs.GetArg("-maxapsfee", DEFAULT_MAX_AVOIDPARTIALSPEND_FEE); | |
| const int change_pos_in = nChangePosInOut; | |
| CTransactionRef tx_aps = tx; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leaving most of this as is (first line is gone, 2nd is a primitive and the name is inherited, 3rd I did change the name I left name as is to match with the others).
| CAmount nFeeRet2; | ||
| int nChangePosInOut2 = nChangePosIn; | ||
| bilingual_str error2; // fired and forgotten; if an error occurs, we discard the results |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
| CAmount nFeeRet2; | |
| int nChangePosInOut2 = nChangePosIn; | |
| bilingual_str error2; // fired and forgotten; if an error occurs, we discard the results | |
| CAmount fee_ret_aps; | |
| int change_pos_in_out_aps = nChangePosIn; | |
| bilingual_str error_aps; // fired and forgotten; if an error occurs, we discard the results |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keeping these as is, as their names are inherited.
…elow threshold The threshold is defined by a new max avoid partial spends fee flag, which defaults to 0 (i.e. if fees are unchanged, use the grouped option).
a923fc3 to
8519d4e
Compare
Co-authored-by: Fabian Jahr <[email protected]>
8519d4e to
7f13dfb
Compare
|
Thanks for review, @fjahr! I believe I've addressed all your points & added your test code. |
fjahr
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tested ACK 7f13dfb
achow101
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK 7f13dfb
| assert_approx(v[0], 0.2) | ||
| assert_approx(v[1], 1.3, 0.0001) | ||
|
|
||
| # Test 'avoid partial if warranted, even if disabled' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: use self.log.info instead of a comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, will tweak if I end up needing to rebase.
|
@xekyo you might also be interested in this? |
|
Concept ACK |
jonatack
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK 7f13dfb, code review, debug build, verified the test fails with AssertionError: not(2 == 1) for the number of vouts when -maxapsfee=0.0001 is changed to 0, and verified the new logging with an added assertion.
Some feedback below that can be ignored unless need to retouch, or for a follow-up. The added tests are good. I have some ideas on increasing the coverage a bit further.
| if (gArgs.GetArg("-maxapsfee", "") == "-1") { | ||
| n = -1; | ||
| } else if (!ParseMoney(gArgs.GetArg("-maxapsfee", ""), n)) { | ||
| error = AmountErrMsg("maxapsfee", gArgs.GetArg("-maxapsfee", "")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit if you retouch, could memoize for readability and clarity
if (gArgs.IsArgSet("-maxapsfee")) {
+ const std::string max_aps_fee{gArgs.GetArg("-maxapsfee", "")};
CAmount n = 0;
- if (gArgs.GetArg("-maxapsfee", "") == "-1") {
+ if (max_aps_fee == "-1") {
n = -1;
- } else if (!ParseMoney(gArgs.GetArg("-maxapsfee", ""), n)) {
- error = AmountErrMsg("maxapsfee", gArgs.GetArg("-maxapsfee", ""));
+ } else if (!ParseMoney(max_aps_fee, n)) {
+ error = AmountErrMsg("maxapsfee", max_aps_fee);
return nullptr;
}| self.nodes[0].sendtoaddress(addr_aps, 1.0) | ||
| self.nodes[0].sendtoaddress(addr_aps, 1.0) | ||
| self.nodes[0].generate(1) | ||
| txid4 = self.nodes[3].sendtoaddress(self.nodes[0].getnewaddress(), 0.1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: test the new logging
- txid4 = self.nodes[3].sendtoaddress(self.nodes[0].getnewaddress(), 0.1)
+ with self.nodes[3].assert_debug_log(['Fee non-grouped = 2820, grouped = 4160, using grouped']):
+ txid4 = self.nodes[3].sendtoaddress(self.nodes[0].getnewaddress(), 0.1)| values = [vout["value"] for vout in tx3["vout"]] | ||
| values.sort() | ||
| assert_approx(values[0], 0.1, 0.0001) | ||
| assert_approx(values[1], 1.4) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: missing vspan arg here (iiuc it's the same value as the others, e.g. the default, but since it looks like the intention here is to make the vspans explicit, the missing one may have been an oversight)
can optionally use named args for the assert_approx calls when more than 1-2 args are passed:
- assert_approx(values[0], 0.1, 0.0001)
- assert_approx(values[1], 1.4)
+ assert_approx(values[0], vexp=0.1, vspan=0.0001)
+ assert_approx(values[1], vexp=1.4, vspan=0.0001)|
As a follow-up, will need a release note. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code review ACK 7f13dfb
Neither of my comments are critical and could be done in a followup
| } | ||
| if (n > HIGH_APS_FEE) { | ||
| warnings.push_back(AmountHighWarn("-maxapsfee") + Untranslated(" ") + | ||
| _("This is the maximum transaction fee you pay to prioritize partial spend avoidance over regular coin selection.")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should emphasize that the maxapsfee is in addition to the normal fee
| txid4 = self.nodes[3].sendtoaddress(self.nodes[0].getnewaddress(), 0.1) | ||
| tx4 = self.nodes[3].getrawtransaction(txid4, True) | ||
| # tx4 should have 2 inputs and 2 outputs although one output would | ||
| # have been enough and the transaction caused higher fees |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be good to also test here that the higher fee is still capped by the parameter provided
…re within a specified range 7f13dfb test: test the implicit avoid partial spends functionality (Karl-Johan Alm) b82067b wallet: try -avoidpartialspends mode and use its result if fees are below threshold (Karl-Johan Alm) Pull request description: The `-avoidpartialspends` feature is normally disabled, as it may affect the optimal fee for payments. This PR introduces a new parameter `-maxapsfee` (max avoid partial spends fee) which acts on the following values: * -1: disable partial spend avoidance completely (do not even try it) * 0: only do partial spend avoidance if fees are the same or better as the regular coin selection * 1..∞: use APS variant if the absolute fee difference is less than or equal to the max APS fee For values other than -1, the code will now try partial spend avoidance once, and if that gives a value within the accepted range, it will use that. Example: -maxapsfee=0.00001000 means the wallet will do regular coin select, APS coin select, and then pick AKS iff the absolute fee difference is <= 1000 satoshi. Edit: updated this to reflect the fact we are now using a max fee. ACKs for top commit: fjahr: tested ACK 7f13dfb achow101: ACK 7f13dfb jonatack: ACK 7f13dfb, code review, debug build, verified the test fails with `AssertionError: not(2 == 1)` for the number of vouts when `-maxapsfee=0.0001` is changed to 0, and verified the new logging with an added assertion. meshcollider: Code review ACK 7f13dfb Tree-SHA512: 475929df57f6191bb4e36bfbcad5a280a64bb0ecd8767b76cb2e44e2301235d0eb294a3f2fac5bbf15d35d7ecfba47acb2285feadb883c9ce31c08377e3afb3c
7e31ea9 -maxapsfee: follow-up fixes (Karl-Johan Alm) 9f77b82 doc: release notes for -maxapsfee (Karl-Johan Alm) Pull request description: Addresses feedback from jonatack and meshcollider in #14582. ACKs for top commit: jonatack: ACK 7e31ea9 meshcollider: re-ACK 7e31ea9 Tree-SHA512: 5d159d78e917e41140e1394bef05e821430dbeac585e9bd708f897041dd7104c2a6b563bfab8b2c85e6d923441160a3880d7864d768aa8e0e66860e0a2c4f56b
… range Summary: * wallet: try -avoidpartialspends mode and use its result if fees are below threshold The threshold is defined by a new max avoid partial spends fee flag, which defaults to 0 (i.e. if fees are unchanged, use the grouped option). * test: test the implicit avoid partial spends functionality Co-authored-by: Fabian Jahr <[email protected]> This is a backport of Core [[bitcoin/bitcoin#14582 | PR14582]] Test Plan: ninja all check-all Reviewers: #bitcoin_abc, majcosta Reviewed By: #bitcoin_abc, majcosta Subscribers: majcosta Differential Revision: https://reviews.bitcoinabc.org/D7807
The
-avoidpartialspendsfeature is normally disabled, as it may affect the optimal fee for payments. This PR introduces a new parameter-maxapsfee(max avoid partial spends fee) which acts on the following values:For values other than -1, the code will now try partial spend avoidance once, and if that gives a value within the accepted range, it will use that.
Example: -maxapsfee=0.00001000 means the wallet will do regular coin select, APS coin select, and then pick AKS iff the absolute fee difference is <= 1000 satoshi.
Edit: updated this to reflect the fact we are now using a max fee.