Skip to content

Commit 3ab96f2

Browse files
committed
Merge bitcoin/bitcoin#24560: wallet: Use single FastRandomContext when creating a wallet tx
fa7deaa wallet: Pass FastRandomContext& to coin selection (MarcoFalke) 77773b0 wallet: Pass FastRandomContext& to DiscourageFeeSniping (MarcoFalke) Pull request description: Passing around a single randomness context shouldn't come with any downsides, but documents better where randomness is used and allows the unit test to be deterministic, if they wish to be so. ACKs for top commit: achow101: ACK fa7deaa promag: Code review ACK fa7deaa. glozow: light code review ACK fa7deaa Tree-SHA512: c16287708cc82ce58311710595d0127af42fb156c93fbcaa5bde634ce323d325f4d8c99a74af24423ab22b5ad58163dd771e8b1a0e7d6bff39c9fb2a1cb21bc7
2 parents f9ed0ae + fa7deaa commit 3ab96f2

File tree

5 files changed

+86
-52
lines changed

5 files changed

+86
-52
lines changed

src/bench/coin_selection.cpp

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,17 @@ static void CoinSelection(benchmark::Bench& bench)
6262
}
6363

6464
const CoinEligibilityFilter filter_standard(1, 6, 0);
65-
const CoinSelectionParams coin_selection_params(/* change_output_size= */ 34,
66-
/* change_spend_size= */ 148, /* effective_feerate= */ CFeeRate(0),
67-
/* long_term_feerate= */ CFeeRate(0), /* discard_feerate= */ CFeeRate(0),
68-
/* tx_noinputs_size= */ 0, /* avoid_partial= */ false);
65+
FastRandomContext rand{};
66+
const CoinSelectionParams coin_selection_params{
67+
rand,
68+
/* change_output_size= */ 34,
69+
/* change_spend_size= */ 148,
70+
/* effective_feerate= */ CFeeRate(0),
71+
/* long_term_feerate= */ CFeeRate(0),
72+
/* discard_feerate= */ CFeeRate(0),
73+
/* tx_noinputs_size= */ 0,
74+
/* avoid_partial= */ false,
75+
};
6976
bench.run([&] {
7077
auto result = AttemptSelection(wallet, 1003 * COIN, filter_standard, coins, coin_selection_params);
7178
assert(result);

src/wallet/coinselection.cpp

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -165,14 +165,14 @@ std::optional<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_poo
165165
return result;
166166
}
167167

168-
std::optional<SelectionResult> SelectCoinsSRD(const std::vector<OutputGroup>& utxo_pool, CAmount target_value)
168+
std::optional<SelectionResult> SelectCoinsSRD(const std::vector<OutputGroup>& utxo_pool, CAmount target_value, FastRandomContext& rng)
169169
{
170170
SelectionResult result(target_value);
171171

172172
std::vector<size_t> indexes;
173173
indexes.resize(utxo_pool.size());
174174
std::iota(indexes.begin(), indexes.end(), 0);
175-
Shuffle(indexes.begin(), indexes.end(), FastRandomContext());
175+
Shuffle(indexes.begin(), indexes.end(), rng);
176176

177177
CAmount selected_eff_value = 0;
178178
for (const size_t i : indexes) {
@@ -187,16 +187,14 @@ std::optional<SelectionResult> SelectCoinsSRD(const std::vector<OutputGroup>& ut
187187
return std::nullopt;
188188
}
189189

190-
static void ApproximateBestSubset(const std::vector<OutputGroup>& groups, const CAmount& nTotalLower, const CAmount& nTargetValue,
190+
static void ApproximateBestSubset(FastRandomContext& insecure_rand, const std::vector<OutputGroup>& groups, const CAmount& nTotalLower, const CAmount& nTargetValue,
191191
std::vector<char>& vfBest, CAmount& nBest, int iterations = 1000)
192192
{
193193
std::vector<char> vfIncluded;
194194

195195
vfBest.assign(groups.size(), true);
196196
nBest = nTotalLower;
197197

198-
FastRandomContext insecure_rand;
199-
200198
for (int nRep = 0; nRep < iterations && nBest != nTargetValue; nRep++)
201199
{
202200
vfIncluded.assign(groups.size(), false);
@@ -233,7 +231,7 @@ static void ApproximateBestSubset(const std::vector<OutputGroup>& groups, const
233231
}
234232
}
235233

236-
std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, const CAmount& nTargetValue)
234+
std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, const CAmount& nTargetValue, FastRandomContext& rng)
237235
{
238236
SelectionResult result(nTargetValue);
239237

@@ -242,7 +240,7 @@ std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups,
242240
std::vector<OutputGroup> applicable_groups;
243241
CAmount nTotalLower = 0;
244242

245-
Shuffle(groups.begin(), groups.end(), FastRandomContext());
243+
Shuffle(groups.begin(), groups.end(), rng);
246244

247245
for (const OutputGroup& group : groups) {
248246
if (group.GetSelectionAmount() == nTargetValue) {
@@ -274,9 +272,9 @@ std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups,
274272
std::vector<char> vfBest;
275273
CAmount nBest;
276274

277-
ApproximateBestSubset(applicable_groups, nTotalLower, nTargetValue, vfBest, nBest);
275+
ApproximateBestSubset(rng, applicable_groups, nTotalLower, nTargetValue, vfBest, nBest);
278276
if (nBest != nTargetValue && nTotalLower >= nTargetValue + MIN_CHANGE) {
279-
ApproximateBestSubset(applicable_groups, nTotalLower, nTargetValue + MIN_CHANGE, vfBest, nBest);
277+
ApproximateBestSubset(rng, applicable_groups, nTotalLower, nTargetValue + MIN_CHANGE, vfBest, nBest);
280278
}
281279

282280
// If we have a bigger coin and (either the stochastic approximation didn't find a good solution,

src/wallet/coinselection.h

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,9 @@ class CInputCoin {
7373
};
7474

7575
/** Parameters for one iteration of Coin Selection. */
76-
struct CoinSelectionParams
77-
{
76+
struct CoinSelectionParams {
77+
/** Randomness to use in the context of coin selection. */
78+
FastRandomContext& rng_fast;
7879
/** Size of a change output in bytes, determined by the output type. */
7980
size_t change_output_size = 0;
8081
/** Size of the input to spend a change output in virtual bytes. */
@@ -100,17 +101,20 @@ struct CoinSelectionParams
100101
* reuse. Dust outputs are not eligible to be added to output groups and thus not considered. */
101102
bool m_avoid_partial_spends = false;
102103

103-
CoinSelectionParams(size_t change_output_size, size_t change_spend_size, CFeeRate effective_feerate,
104-
CFeeRate long_term_feerate, CFeeRate discard_feerate, size_t tx_noinputs_size, bool avoid_partial) :
105-
change_output_size(change_output_size),
106-
change_spend_size(change_spend_size),
107-
m_effective_feerate(effective_feerate),
108-
m_long_term_feerate(long_term_feerate),
109-
m_discard_feerate(discard_feerate),
110-
tx_noinputs_size(tx_noinputs_size),
111-
m_avoid_partial_spends(avoid_partial)
112-
{}
113-
CoinSelectionParams() {}
104+
CoinSelectionParams(FastRandomContext& rng_fast, size_t change_output_size, size_t change_spend_size, CFeeRate effective_feerate,
105+
CFeeRate long_term_feerate, CFeeRate discard_feerate, size_t tx_noinputs_size, bool avoid_partial)
106+
: rng_fast{rng_fast},
107+
change_output_size(change_output_size),
108+
change_spend_size(change_spend_size),
109+
m_effective_feerate(effective_feerate),
110+
m_long_term_feerate(long_term_feerate),
111+
m_discard_feerate(discard_feerate),
112+
tx_noinputs_size(tx_noinputs_size),
113+
m_avoid_partial_spends(avoid_partial)
114+
{
115+
}
116+
CoinSelectionParams(FastRandomContext& rng_fast)
117+
: rng_fast{rng_fast} {}
114118
};
115119

116120
/** Parameters for filtering which OutputGroups we may use in coin selection.
@@ -246,10 +250,10 @@ std::optional<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_poo
246250
* @param[in] target_value The target value to select for
247251
* @returns If successful, a SelectionResult, otherwise, std::nullopt
248252
*/
249-
std::optional<SelectionResult> SelectCoinsSRD(const std::vector<OutputGroup>& utxo_pool, CAmount target_value);
253+
std::optional<SelectionResult> SelectCoinsSRD(const std::vector<OutputGroup>& utxo_pool, CAmount target_value, FastRandomContext& rng);
250254

251255
// Original coin selection algorithm as a fallback
252-
std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, const CAmount& nTargetValue);
256+
std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, const CAmount& nTargetValue, FastRandomContext& rng);
253257
} // namespace wallet
254258

255259
#endif // BITCOIN_WALLET_COINSELECTION_H

src/wallet/spend.cpp

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -386,15 +386,15 @@ std::optional<SelectionResult> AttemptSelection(const CWallet& wallet, const CAm
386386
std::vector<OutputGroup> all_groups = GroupOutputs(wallet, coins, coin_selection_params, eligibility_filter, false /* positive_only */);
387387
// While nTargetValue includes the transaction fees for non-input things, it does not include the fee for creating a change output.
388388
// So we need to include that for KnapsackSolver as well, as we are expecting to create a change output.
389-
if (auto knapsack_result{KnapsackSolver(all_groups, nTargetValue + coin_selection_params.m_change_fee)}) {
389+
if (auto knapsack_result{KnapsackSolver(all_groups, nTargetValue + coin_selection_params.m_change_fee, coin_selection_params.rng_fast)}) {
390390
knapsack_result->ComputeAndSetWaste(coin_selection_params.m_cost_of_change);
391391
results.push_back(*knapsack_result);
392392
}
393393

394394
// We include the minimum final change for SRD as we do want to avoid making really small change.
395395
// KnapsackSolver does not need this because it includes MIN_CHANGE internally.
396396
const CAmount srd_target = nTargetValue + coin_selection_params.m_change_fee + MIN_FINAL_CHANGE;
397-
if (auto srd_result{SelectCoinsSRD(positive_groups, srd_target)}) {
397+
if (auto srd_result{SelectCoinsSRD(positive_groups, srd_target, coin_selection_params.rng_fast)}) {
398398
srd_result->ComputeAndSetWaste(coin_selection_params.m_cost_of_change);
399399
results.push_back(*srd_result);
400400
}
@@ -501,7 +501,7 @@ std::optional<SelectionResult> SelectCoins(const CWallet& wallet, const std::vec
501501
// Cases where we have 101+ outputs all pointing to the same destination may result in
502502
// privacy leaks as they will potentially be deterministically sorted. We solve that by
503503
// explicitly shuffling the outputs before processing
504-
Shuffle(vCoins.begin(), vCoins.end(), FastRandomContext());
504+
Shuffle(vCoins.begin(), vCoins.end(), coin_selection_params.rng_fast);
505505
}
506506

507507
// Coin Selection attempts to select inputs from a pool of eligible UTXOs to fund the
@@ -585,7 +585,8 @@ static bool IsCurrentForAntiFeeSniping(interfaces::Chain& chain, const uint256&
585585
* Set a height-based locktime for new transactions (uses the height of the
586586
* current chain tip unless we are not synced with the current chain
587587
*/
588-
static void DiscourageFeeSniping(CMutableTransaction& tx, interfaces::Chain& chain, const uint256& block_hash, int block_height)
588+
static void DiscourageFeeSniping(CMutableTransaction& tx, FastRandomContext& rng_fast,
589+
interfaces::Chain& chain, const uint256& block_hash, int block_height)
589590
{
590591
// All inputs must be added by now
591592
assert(!tx.vin.empty());
@@ -616,8 +617,8 @@ static void DiscourageFeeSniping(CMutableTransaction& tx, interfaces::Chain& cha
616617
// that transactions that are delayed after signing for whatever reason,
617618
// e.g. high-latency mix networks and some CoinJoin implementations, have
618619
// better privacy.
619-
if (GetRandInt(10) == 0) {
620-
tx.nLockTime = std::max(0, int(tx.nLockTime) - GetRandInt(100));
620+
if (rng_fast.randrange(10) == 0) {
621+
tx.nLockTime = std::max(0, int(tx.nLockTime) - int(rng_fast.randrange(100)));
621622
}
622623
} else {
623624
// If our chain is lagging behind, we can't discourage fee sniping nor help
@@ -653,9 +654,10 @@ static bool CreateTransactionInternal(
653654
{
654655
AssertLockHeld(wallet.cs_wallet);
655656

657+
FastRandomContext rng_fast;
656658
CMutableTransaction txNew; // The resulting transaction that we make
657659

658-
CoinSelectionParams coin_selection_params; // Parameters for coin selection, init with dummy
660+
CoinSelectionParams coin_selection_params{rng_fast}; // Parameters for coin selection, init with dummy
659661
coin_selection_params.m_avoid_partial_spends = coin_control.m_avoid_partial_spends;
660662

661663
// Set the long term feerate estimate to the wallet's consolidate feerate
@@ -782,10 +784,9 @@ static bool CreateTransactionInternal(
782784
assert(change_and_fee >= 0);
783785
CTxOut newTxOut(change_and_fee, scriptChange);
784786

785-
if (nChangePosInOut == -1)
786-
{
787+
if (nChangePosInOut == -1) {
787788
// Insert change txn at random position:
788-
nChangePosInOut = GetRandInt(txNew.vout.size()+1);
789+
nChangePosInOut = rng_fast.randrange(txNew.vout.size() + 1);
789790
}
790791
else if ((unsigned int)nChangePosInOut > txNew.vout.size())
791792
{
@@ -811,7 +812,7 @@ static bool CreateTransactionInternal(
811812
for (const auto& coin : selected_coins) {
812813
txNew.vin.push_back(CTxIn(coin.outpoint, CScript(), nSequence));
813814
}
814-
DiscourageFeeSniping(txNew, wallet.chain(), wallet.GetLastBlockHash(), wallet.GetLastBlockHeight());
815+
DiscourageFeeSniping(txNew, rng_fast, wallet.chain(), wallet.GetLastBlockHash(), wallet.GetLastBlockHeight());
815816

816817
// Calculate the transaction fee
817818
TxSize tx_sizes = CalculateMaximumSignedTxSize(CTransaction(txNew), &wallet, &coin_control);

src/wallet/test/coinselector_tests.cpp

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -164,10 +164,17 @@ inline std::vector<OutputGroup>& GroupCoins(const std::vector<COutput>& coins)
164164

165165
inline std::vector<OutputGroup>& KnapsackGroupOutputs(const std::vector<COutput>& coins, CWallet& wallet, const CoinEligibilityFilter& filter)
166166
{
167-
CoinSelectionParams coin_selection_params(/* change_output_size= */ 0,
168-
/* change_spend_size= */ 0, /* effective_feerate= */ CFeeRate(0),
169-
/* long_term_feerate= */ CFeeRate(0), /* discard_feerate= */ CFeeRate(0),
170-
/* tx_noinputs_size= */ 0, /* avoid_partial= */ false);
167+
FastRandomContext rand{};
168+
CoinSelectionParams coin_selection_params{
169+
rand,
170+
/* change_output_size= */ 0,
171+
/* change_spend_size= */ 0,
172+
/* effective_feerate= */ CFeeRate(0),
173+
/* long_term_feerate= */ CFeeRate(0),
174+
/* discard_feerate= */ CFeeRate(0),
175+
/* tx_noinputs_size= */ 0,
176+
/* avoid_partial= */ false,
177+
};
171178
static std::vector<OutputGroup> static_groups;
172179
static_groups = GroupOutputs(wallet, coins, coin_selection_params, filter, /*positive_only=*/false);
173180
return static_groups;
@@ -176,6 +183,7 @@ inline std::vector<OutputGroup>& KnapsackGroupOutputs(const std::vector<COutput>
176183
// Branch and bound coin selection tests
177184
BOOST_AUTO_TEST_CASE(bnb_search_test)
178185
{
186+
FastRandomContext rand{};
179187
// Setup
180188
std::vector<CInputCoin> utxo_pool;
181189
SelectionResult expected_result(CAmount(0));
@@ -301,10 +309,16 @@ BOOST_AUTO_TEST_CASE(bnb_search_test)
301309
}
302310

303311
// Make sure that effective value is working in AttemptSelection when BnB is used
304-
CoinSelectionParams coin_selection_params_bnb(/* change_output_size= */ 0,
305-
/* change_spend_size= */ 0, /* effective_feerate= */ CFeeRate(3000),
306-
/* long_term_feerate= */ CFeeRate(1000), /* discard_feerate= */ CFeeRate(1000),
307-
/* tx_noinputs_size= */ 0, /* avoid_partial= */ false);
312+
CoinSelectionParams coin_selection_params_bnb{
313+
rand,
314+
/* change_output_size= */ 0,
315+
/* change_spend_size= */ 0,
316+
/* effective_feerate= */ CFeeRate(3000),
317+
/* long_term_feerate= */ CFeeRate(1000),
318+
/* discard_feerate= */ CFeeRate(1000),
319+
/* tx_noinputs_size= */ 0,
320+
/* avoid_partial= */ false,
321+
};
308322
{
309323
std::unique_ptr<CWallet> wallet = std::make_unique<CWallet>(m_node.chain.get(), "", m_args, CreateMockWalletDatabase());
310324
wallet->LoadWallet();
@@ -351,6 +365,9 @@ BOOST_AUTO_TEST_CASE(bnb_search_test)
351365

352366
BOOST_AUTO_TEST_CASE(knapsack_solver_test)
353367
{
368+
FastRandomContext rand{};
369+
const auto temp1{[&rand](std::vector<OutputGroup>& g, const CAmount& v) { return KnapsackSolver(g, v, rand); }};
370+
const auto KnapsackSolver{temp1};
354371
std::unique_ptr<CWallet> wallet = std::make_unique<CWallet>(m_node.chain.get(), "", m_args, CreateMockWalletDatabase());
355372
wallet->LoadWallet();
356373
LOCK(wallet->cs_wallet);
@@ -660,6 +677,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test)
660677

661678
BOOST_AUTO_TEST_CASE(ApproximateBestSubset)
662679
{
680+
FastRandomContext rand{};
663681
std::unique_ptr<CWallet> wallet = std::make_unique<CWallet>(m_node.chain.get(), "", m_args, CreateMockWalletDatabase());
664682
wallet->LoadWallet();
665683
LOCK(wallet->cs_wallet);
@@ -673,7 +691,7 @@ BOOST_AUTO_TEST_CASE(ApproximateBestSubset)
673691
add_coin(coins, *wallet, 1000 * COIN);
674692
add_coin(coins, *wallet, 3 * COIN);
675693

676-
const auto result = KnapsackSolver(KnapsackGroupOutputs(coins, *wallet, filter_standard), 1003 * COIN);
694+
const auto result = KnapsackSolver(KnapsackGroupOutputs(coins, *wallet, filter_standard), 1003 * COIN, rand);
677695
BOOST_CHECK(result);
678696
BOOST_CHECK_EQUAL(result->GetSelectedValue(), 1003 * COIN);
679697
BOOST_CHECK_EQUAL(result->GetInputSet().size(), 2U);
@@ -714,10 +732,16 @@ BOOST_AUTO_TEST_CASE(SelectCoins_test)
714732
CAmount target = rand.randrange(balance - 1000) + 1000;
715733

716734
// Perform selection
717-
CoinSelectionParams cs_params(/* change_output_size= */ 34,
718-
/* change_spend_size= */ 148, /* effective_feerate= */ CFeeRate(0),
719-
/* long_term_feerate= */ CFeeRate(0), /* discard_feerate= */ CFeeRate(0),
720-
/* tx_noinputs_size= */ 0, /* avoid_partial= */ false);
735+
CoinSelectionParams cs_params{
736+
rand,
737+
/* change_output_size= */ 34,
738+
/* change_spend_size= */ 148,
739+
/* effective_feerate= */ CFeeRate(0),
740+
/* long_term_feerate= */ CFeeRate(0),
741+
/* discard_feerate= */ CFeeRate(0),
742+
/* tx_noinputs_size= */ 0,
743+
/* avoid_partial= */ false,
744+
};
721745
CCoinControl cc;
722746
const auto result = SelectCoins(*wallet, coins, target, cc, cs_params);
723747
BOOST_CHECK(result);

0 commit comments

Comments
 (0)