Instead of "randomly" fuzzing min_viable_change and change_output_size, and since they're correlated, this PR changes the approach to fuzz them according to the logic in CreateTransactionInternal.
fuzz: coinselection, improve `min_viable_change`/`change_output_size` #28372
pull brunoerg wants to merge 1 commits into bitcoin:master from brunoerg:2023-08-fuzz-coinselection-min-viable-change changing 1 files +7 −5-
brunoerg commented at 6:48 PM on August 30, 2023: contributor
-
DrahtBot commented at 6:49 PM on August 30, 2023: contributor
<!--e57a25ab6845829454e8d69fc972939a-->
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
<!--006a51241073e994b41acfe9ec718e94-->
Code Coverage
For detailed information about the code coverage, see the test coverage report.
<!--021abf342d371248e50ceaed478a90ca-->
Reviews
See the guideline for information on the review process.
Type Reviewers ACK furszy, murchandamus, achow101 If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.
<!--174a7506f384e20aa4161008e828411d-->
Conflicts
No conflicts as of last run.
- DrahtBot added the label Tests on Aug 30, 2023
-
in src/wallet/test/fuzz/coinselection.cpp:105 in bb7b9e1f79 outdated
110 | coin_params.change_spend_size = fuzzed_data_provider.ConsumeIntegralInRange<int>(41, 1000); 111 | - coin_params.m_cost_of_change = coin_params.m_change_fee + coin_params.m_discard_feerate.GetFee(coin_params.change_spend_size); 112 | + const auto change_spend_fee{coin_params.m_discard_feerate.GetFee(coin_params.change_spend_size)}; 113 | + coin_params.m_cost_of_change = coin_params.m_change_fee + change_spend_fee; 114 | + const auto dust{GetDustThreshold(change_prototype_txout, coin_params.m_discard_feerate)}; 115 | + coin_params.min_viable_change = std::max(change_spend_fee + 1, dust);
murchandamus commented at 7:57 PM on August 30, 2023:I was pondering whether the input size being rolled randomly and the dust being only based on the output type could lead to
min_viable_changebeing smaller thancost_of_change - change_fee, but this seems right to me now. :thinking:Approach ACK, will throw into my fuzzer for a while.
in src/wallet/test/fuzz/coinselection.cpp:82 in bb7b9e1f79 outdated
73 | @@ -74,6 +74,13 @@ static SelectionResult ManualSelection(std::vector<COutput>& utxos, const CAmoun 74 | return result; 75 | } 76 | 77 | +static CTxOut GetChangePrototypeTxout(FuzzedDataProvider& fuzzed_data_provider) 78 | +{ 79 | + const CTxDestination tx_destination{ConsumeTxDestination(fuzzed_data_provider)}; 80 | + CScript script_change{GetScriptForDestination(tx_destination)}; 81 | + return CTxOut(0, script_change); 82 | +}
murchandamus commented at 8:01 PM on August 30, 2023:This will narrow down the range of output sizes being fuzzed.
An alternative would perhaps be to randomly roll change output size and change input size, while calculating all the other derived values from those, but my gut says that there shouldn’t be all that much exciting happening anyway. Will revisit this question when I’ve fuzzed a bit.
brunoerg commented at 4:32 PM on October 5, 2023:Did you revisit it?
furszy commented at 7:28 PM on December 14, 2023:This will narrow down the range of output sizes being fuzzed.
Agree. You don't need to create valid known destinations, we only care about the change output size. All yours:
diff --git a/src/wallet/test/fuzz/coinselection.cpp b/src/wallet/test/fuzz/coinselection.cpp --- a/src/wallet/test/fuzz/coinselection.cpp (revision ccb25a1ddc1f79f432a6718604f01ed97dced15f) +++ b/src/wallet/test/fuzz/coinselection.cpp (date 1702581796902) @@ -11,6 +11,7 @@ #include <test/util/setup_common.h> #include <wallet/coinselection.h> +#include <algorithm> #include <vector> namespace wallet { @@ -74,13 +75,6 @@ return result; } -static CTxOut GetChangePrototypeTxout(FuzzedDataProvider& fuzzed_data_provider) -{ - const CTxDestination tx_destination{ConsumeTxDestination(fuzzed_data_provider)}; - CScript script_change{GetScriptForDestination(tx_destination)}; - return CTxOut(0, script_change); -} - // Returns true if the result contains an error and the message is not empty static bool HasErrorMsg(const util::Result<SelectionResult>& res) { return !util::ErrorString(res).empty(); } @@ -101,14 +95,16 @@ coin_params.m_subtract_fee_outputs = subtract_fee_outputs; coin_params.m_long_term_feerate = long_term_fee_rate; coin_params.m_effective_feerate = effective_fee_rate; - auto change_prototype_txout{GetChangePrototypeTxout(fuzzed_data_provider)}; - coin_params.change_output_size = GetSerializeSize(change_prototype_txout); + coin_params.change_output_size = fuzzed_data_provider.ConsumeIntegralInRange(1, MAX_SCRIPT_SIZE); coin_params.m_change_fee = effective_fee_rate.GetFee(coin_params.change_output_size); coin_params.m_discard_feerate = discard_fee_rate; coin_params.change_spend_size = fuzzed_data_provider.ConsumeIntegralInRange<int>(41, 1000); const auto change_spend_fee{coin_params.m_discard_feerate.GetFee(coin_params.change_spend_size)}; coin_params.m_cost_of_change = coin_params.m_change_fee + change_spend_fee; - const auto dust{GetDustThreshold(change_prototype_txout, coin_params.m_discard_feerate)}; + // We only care about the change output size + CScript change_out_script; + std::fill_n(change_out_script.begin(), coin_params.change_output_size, OP_TRUE); + const auto dust{GetDustThreshold(CTxOut{/*nValueIn=*/0, change_out_script}, coin_params.m_discard_feerate)}; coin_params.min_viable_change = std::max(change_spend_fee + 1, dust); int next_locktime{0};maflcko added this to the milestone 26.0 on Aug 31, 2023maflcko commented at 8:00 AM on August 31, 2023: memberAdded milestone, because this is a fuzz blocker
murchandamus commented at 7:04 PM on September 1, 2023: contributorI found a crash:
fuzz: ../../src/wallet/test/fuzz/coinselection.cpp:130: void wallet::coinselection_fuzz_target(FuzzBufferType): Assertion `result_bnb->GetChange(coin_params.m_cost_of_change, CAmount{0}) == 0' failed.
You can run the crash seed using:
$ echo "FP93w8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8N3d1h3d4h3d3cUAjsTsTsTsTOOd3d3d5t3d3d3WHd3iHd3d3d3d///yZkblPEYAAB3d3d3d3cKJ3d3d////wAyd3d3d3c093d3d3d3dwonAAD/kiEA" | base64 -d > crash.input $ FUZZ=coinselection src/test/fuzz/fuzz crash.inputbrunoerg commented at 6:46 PM on September 2, 2023: contributor@murchandamus The crash happens because
GetChangeis returning exactly thecost_of_change, instead of 0.In that crash the
m_subtract_fee_outputsisTrue. Som_use_effectiveinGetChangeisFalse.See:
CAmount SelectionResult::GetChange(const CAmount min_viable_change, const CAmount change_fee) const { // change = SUM(inputs) - SUM(outputs) - fees // 1) With SFFO we don't pay any fees // 2) Otherwise we pay all the fees: // - input fees are covered by GetSelectedEffectiveValue() // - non_input_fee is included in m_target // - change_fee const CAmount change = m_use_effective ? GetSelectedEffectiveValue() - m_target - change_fee : GetSelectedValue() - m_target; if (change < min_viable_change) { return 0; } return change; }I tested
m_use_effectivewith True and the change is 0.DrahtBot added the label CI failed on Sep 3, 2023DrahtBot removed the label CI failed on Sep 5, 2023DrahtBot added the label CI failed on Sep 5, 2023DrahtBot removed the label CI failed on Sep 5, 2023maflcko commented at 4:03 PM on October 5, 2023: memberAre you still working on this? What is the current status here?
brunoerg commented at 4:31 PM on October 5, 2023: contributorAre you still working on this? What is the current status here?
This is just an improvement in the logic and it's not an attempt to fix the crash since it's a real bug in BnB.
maflcko removed this from the milestone 26.0 on Oct 5, 2023maflcko commented at 4:36 PM on October 5, 2023: memberOk, removed milestone for now.
Looks like this crashes in any case? #28372 (comment)
So maybe mark as draft for now, while this is not yet ready for review.
brunoerg marked this as a draft on Oct 16, 2023brunoerg commented at 6:14 PM on October 16, 2023: contributorDraft while waiting for a fix in BnB
brunoerg force-pushed on Dec 13, 2023brunoerg marked this as ready for review on Dec 13, 2023brunoerg commented at 3:10 PM on December 13, 2023: contributorRebased
brunoerg commented at 4:57 PM on December 13, 2023: contributorCI error seems unrelated:
2023-12-13T15:26:02.5983445Z 54/286 - rpc_signer.py failed, Duration: 3 s 2023-12-13T15:26:02.5983915Z 2023-12-13T15:26:02.5984128Z stdout: 2023-12-13T15:26:02.5985467Z 2023-12-13T15:25:59.249000Z TestFramework (INFO): PRNG seed is: 6112864330657271854 2023-12-13T15:26:02.5986266Z 2023-12-13T15:26:02.5994148Z 2023-12-13T15:25:59.249000Z TestFramework (INFO): Initializing test directory D:\a\_temp\test_runner_₿_🏃_20231213_151925\rpc_signer_229 2023-12-13T15:26:02.5995453Z 2023-12-13T15:26:02.5996137Z 2023-12-13T15:26:00.333000Z TestFramework (ERROR): Assertion failed 2023-12-13T15:26:02.5996821Z 2023-12-13T15:26:02.5997101Z Traceback (most recent call last): 2023-12-13T15:26:02.5997578Z 2023-12-13T15:26:02.5998332Z File "D:\a\bitcoin\bitcoin\test\functional\test_framework\test_framework.py", line 556, in start_nodes 2023-12-13T15:26:02.5999355Z 2023-12-13T15:26:02.5999728Z node.wait_for_rpc_connection() 2023-12-13T15:26:02.6000194Z 2023-12-13T15:26:02.6001027Z File "D:\a\bitcoin\bitcoin\test\functional\test_framework\test_node.py", line 249, in wait_for_rpc_connection 2023-12-13T15:26:02.6002062Z 2023-12-13T15:26:02.6002391Z raise FailedToStartError(self._node_msg( 2023-12-13T15:26:02.6002931Z 2023-12-13T15:26:02.6005067Z test_framework.test_node.FailedToStartError: [node 1] bitcoind exited with status 1 during initialization. Error: Error parsing command line arguments: Invalid parameter -signer=py -3 D:\a\bitcoin\bitcoin\test\functional\mocks\signer.pymurchandamus commented at 3:09 AM on December 14, 2023: contributorJust kicked off 10×10h of fuzzing, will take a look tomorrow
murchandamus commented at 2:06 PM on December 14, 2023: contributorMost of them, but not all crashed on this:
[#626233](/bitcoin-bitcoin/626233/) REDUCE cov: 1289 ft: 13823 corp: 4183/16Mb lim: 66076 exec/s: 206 rss: 88Mb L: 157/65635 MS: 1 EraseBytes- fuzz: ../../src/wallet/test/fuzz/coinselection.cpp:131: void wallet::coinselection_fuzz_target(FuzzBufferType): Assertion `result_bnb->GetChange(coin_params.m_cost_of_change, CAmount{0}) == 0' failed.I think we might have misdiagnosed the issue, when we thought it could only happen when SFFO is active
furszy commented at 2:11 PM on December 14, 2023: memberI think we might have misdiagnosed the issue, when we thought it could only happen when SFFO is active @murchandamus. The code here has not fixed the issue I mentioned #28918 (comment). Need to provide
min_viable_changeto the BnB resultGetChange()function, notcost_of_change. @brunoergfurszy commented at 2:22 PM on December 14, 2023: memberYes, I'm addressing it.
Before pushing the changes, can verify them against the issue #28918 (comment).
brunoerg commented at 2:31 PM on December 14, 2023: contributorAlso, I think it would be more appropriate if we don't set
change_spend_sizerandomly. Perhaps we could useCalculateMaximumSignedInputSizeand the same approach inCreateTransactionInternal.murchandamus commented at 2:49 PM on December 14, 2023: contributorI think it would be fine to roll input and output sizes randomly as long as they remain greater than zero and all the related values are derived from them. E.g. it would be an issue if
cost_of_changeormin_viable_changewere independent ofchange_spend_sizebrunoerg commented at 6:10 PM on December 14, 2023: contributorNeed to provide min_viable_change to the BnB result GetChange() function, not cost_of_change. @brunoerg
Got same crash with it.
diff --git a/src/wallet/test/fuzz/coinselection.cpp b/src/wallet/test/fuzz/coinselection.cpp index b35bf34db3..5a3250bdd7 100644 --- a/src/wallet/test/fuzz/coinselection.cpp +++ b/src/wallet/test/fuzz/coinselection.cpp @@ -128,7 +128,7 @@ FUZZ_TARGET(coinselection) auto result_bnb = coin_params.m_subtract_fee_outputs ? util::Error{Untranslated("BnB disabled when SFFO is enabled")} : SelectCoinsBnB(group_pos, target, coin_params.m_cost_of_change, MAX_STANDARD_TX_WEIGHT); if (result_bnb) { - assert(result_bnb->GetChange(coin_params.m_cost_of_change, CAmount{0}) == 0); + assert(result_bnb->GetChange(coin_params.min_viable_change, CAmount{0}) == 0); assert(result_bnb->GetSelectedValue() >= target); (void)result_bnb->GetShuffledInputVector(); (void)result_bnb->GetInputSet();echo "//////////////////8AKH4pAAAsICkpAAAAAAgARSwLCwv//////////wsLCwsL Cwt+MPILCwsLCwsLCwsLCwEAAAsHCwsLTJV4CwsLCwsLCwsBAAALCwsLCwsLCwsL CwsA+f8nAAsLCwsLAf//JgImJtAB/w==" | base64 --decode > coinselection.crashmurchandamus commented at 6:41 PM on December 14, 2023: contributorIt seems to me that the check in line 131 is simply wrong:
assert(result_bnb->GetChange(coin_params.m_cost_of_change, CAmount{0}) == 0);The function definition is
CAmount SelectionResult::GetChange(const CAmount min_viable_change, const CAmount change_fee) constSo, instead of passing
coin_params.m_cost_of_changeand0, we should be passingcoin_params.min_viable_changeandcoin_params.m_change_fee. Just fixing one or the other was insufficient, but when I replace both all my fuzz crashes pass.@@ -128,7 +128,7 @@ FUZZ_TARGET(coinselection) auto result_bnb = coin_params.m_subtract_fee_outputs ? util::Error{Untranslated("BnB disabled when SFFO is enabled")} : SelectCoinsBnB(group_pos, target, coin_params.m_cost_of_change, MAX_STANDARD_TX_WEIGHT); if (result_bnb) { - assert(result_bnb->GetChange(coin_params.m_cost_of_change, CAmount{0}) == 0); + assert(result_bnb->GetChange(coin_params.min_viable_change, coin_params.m_change_fee) == 0); assert(result_bnb->GetSelectedValue() >= target); (void)result_bnb->GetShuffledInputVector(); (void)result_bnb->GetInputSet();furszy commented at 7:04 PM on December 14, 2023: memberSo, instead of passing coin_params.m_cost_of_change and 0, we should be passing coin_params.min_viable_change and coin_params.m_change_fee. Just fixing one or the other was insufficient, but when I replace both all my fuzz crashes pass.
Yeah, thats good. It is because
cost_of_change, the BnB upper bound, includeschange_feewhilemin_viable_changedoes not.furszy commented at 7:29 PM on December 14, 2023: memberLeft a comment
murchandamus commented at 8:42 PM on December 14, 2023: contributor@@ -128,7 +128,7 @@ FUZZ_TARGET(coinselection) auto result_bnb = coin_params.m_subtract_fee_outputs ? util::Error{Untranslated("BnB disabled when SFFO is enabled")} : SelectCoinsBnB(group_pos, target, coin_params.m_cost_of_change, MAX_STANDARD_TX_WEIGHT); if (result_bnb) { - assert(result_bnb->GetChange(coin_params.m_cost_of_change, CAmount{0}) == 0); + assert(result_bnb->GetChange(coin_params.min_viable_change, coin_params.m_change_fee) == 0); assert(result_bnb->GetSelectedValue() >= target); (void)result_bnb->GetShuffledInputVector(); (void)result_bnb->GetInputSet();Fuzzed for 1h with that change and got no new crash.
brunoerg commented at 9:18 PM on December 14, 2023: contributorSo, instead of passing coin_params.m_cost_of_change and 0, we should be passing coin_params.min_viable_change and coin_params.m_change_fee. Just fixing one or the other was insufficient, but when I replace both all my fuzz crashes pass.
Cool, I think the 0 came from the way we call
ComputeAndSetWasteinternally inSelectCoinsBnB.brunoerg commented at 9:19 PM on December 14, 2023: contributorThanks, @furszy and @murchandamus. I will address the suggestions and leave it running for some time before pushing.
Just an observation on @furszy's suggestion - I think it will cause
stack-buffer-overflow:- const auto dust{GetDustThreshold(change_prototype_txout, coin_params.m_discard_feerate)}; + // We only care about the change output size + CScript change_out_script; + std::fill_n(change_out_script.begin(), coin_params.change_output_size, OP_TRUE); + const auto dust{GetDustThreshold(CTxOut{/*nValueIn=*/0, change_out_script}, coin_params.m_discard_feerate)};furszy commented at 9:46 PM on December 14, 2023: memberJust an observation on @furszy's suggestion - I think it will cause
stack-buffer-overflow:- const auto dust{GetDustThreshold(change_prototype_txout, coin_params.m_discard_feerate)}; + // We only care about the change output size + CScript change_out_script; + std::fill_n(change_out_script.begin(), coin_params.change_output_size, OP_TRUE); + const auto dust{GetDustThreshold(CTxOut{/*nValueIn=*/0, change_out_script}, coin_params.m_discard_feerate)};I coded it on the fly. You can just push the opcodes manually if
std::fill_n()doesn't work or correct it in some way. The rationale is to not construct only known destinations as you are doing here. Coin selection doesn't care about them, it just uses the script size.cd810075edfuzz: coinselection, improve `min_viable_change`/`change_output_size`
Change it to use same approach from `CreateTransactionInternal`.
brunoerg force-pushed on Dec 15, 2023brunoerg commented at 9:30 AM on December 15, 2023: contributorForce-pushed addressing #28372 (comment) and #28372 (review). Thanks @murchandamus and @furszy for the review!
Left it running for 10h before pushing and did not crash.
furszy approvedfurszy commented at 1:14 PM on December 15, 2023: memberCode ACK cd810075eddd
murchandamus commented at 1:33 PM on December 19, 2023: contributorACK cd810075eddd8b1a7139559b475b56126f70a93d
Passes all my fuzzing crashes and no new crashes with some light additional fuzzing, code LGTM.
achow101 commented at 12:38 AM on December 21, 2023: memberACK cd810075eddd8b1a7139559b475b56126f70a93d
achow101 merged this on Dec 21, 2023achow101 closed this on Dec 21, 2023luke-jr referenced this in commit 462f2be2ca on Mar 15, 2024luke-jr referenced this in commit 51251355eb on Mar 18, 2024bitcoin locked this on Dec 20, 2024
This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-04-28 03:13 UTC
More mirrored repositories can be found on mirror.b10c.me