Skip to content

Commit 3db746b

Browse files
committed
Introduce a Shuffle for FastRandomContext and use it in wallet and coinselection
1 parent 8098379 commit 3db746b

File tree

4 files changed

+60
-3
lines changed

4 files changed

+60
-3
lines changed

src/random.h

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,29 @@ class FastRandomContext {
130130
inline uint64_t operator()() { return rand64(); }
131131
};
132132

133+
/** More efficient than using std::shuffle on a FastRandomContext.
134+
*
135+
* This is more efficient as std::shuffle will consume entropy in groups of
136+
* 64 bits at the time and throw away most.
137+
*
138+
* This also works around a bug in libstdc++ std::shuffle that may cause
139+
* type::operator=(type&&) to be invoked on itself, which the library's
140+
* debug mode detects and panics on. This is a known issue, see
141+
* https://stackoverflow.com/questions/22915325/avoiding-self-assignment-in-stdshuffle
142+
*/
143+
template<typename I, typename R>
144+
void Shuffle(I first, I last, R&& rng)
145+
{
146+
while (first != last) {
147+
size_t j = rng.randrange(last - first);
148+
if (j) {
149+
using std::swap;
150+
swap(*first, *(first + j));
151+
}
152+
++first;
153+
}
154+
}
155+
133156
/* Number of random bytes returned by GetOSRand.
134157
* When changing this constant make sure to change all call sites, and make
135158
* sure that the underlying OS APIs for all platforms support the number.

src/test/random_tests.cpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,42 @@ BOOST_AUTO_TEST_CASE(stdrandom_test)
7575
for (int j = 1; j <= 10; ++j) {
7676
BOOST_CHECK(std::find(test.begin(), test.end(), j) != test.end());
7777
}
78+
Shuffle(test.begin(), test.end(), ctx);
79+
for (int j = 1; j <= 10; ++j) {
80+
BOOST_CHECK(std::find(test.begin(), test.end(), j) != test.end());
81+
}
7882
}
7983

8084
}
8185

86+
/** Test that Shuffle reaches every permutation with equal probability. */
87+
BOOST_AUTO_TEST_CASE(shuffle_stat_test)
88+
{
89+
FastRandomContext ctx(true);
90+
uint32_t counts[5 * 5 * 5 * 5 * 5] = {0};
91+
for (int i = 0; i < 12000; ++i) {
92+
int data[5] = {0, 1, 2, 3, 4};
93+
Shuffle(std::begin(data), std::end(data), ctx);
94+
int pos = data[0] + data[1] * 5 + data[2] * 25 + data[3] * 125 + data[4] * 625;
95+
++counts[pos];
96+
}
97+
unsigned int sum = 0;
98+
double chi_score = 0.0;
99+
for (int i = 0; i < 5 * 5 * 5 * 5 * 5; ++i) {
100+
int i1 = i % 5, i2 = (i / 5) % 5, i3 = (i / 25) % 5, i4 = (i / 125) % 5, i5 = i / 625;
101+
uint32_t count = counts[i];
102+
if (i1 == i2 || i1 == i3 || i1 == i4 || i1 == i5 || i2 == i3 || i2 == i4 || i2 == i5 || i3 == i4 || i3 == i5 || i4 == i5) {
103+
BOOST_CHECK(count == 0);
104+
} else {
105+
chi_score += ((count - 100.0) * (count - 100.0)) / 100.0;
106+
BOOST_CHECK(count > 50);
107+
BOOST_CHECK(count < 150);
108+
sum += count;
109+
}
110+
}
111+
BOOST_CHECK(chi_score > 58.1411); // 99.9999% confidence interval
112+
BOOST_CHECK(chi_score < 210.275);
113+
BOOST_CHECK_EQUAL(sum, 12000);
114+
}
115+
82116
BOOST_AUTO_TEST_SUITE_END()

src/wallet/coinselection.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ bool KnapsackSolver(const CAmount& nTargetValue, std::vector<OutputGroup>& group
223223
std::vector<OutputGroup> applicable_groups;
224224
CAmount nTotalLower = 0;
225225

226-
random_shuffle(groups.begin(), groups.end(), GetRandInt);
226+
Shuffle(groups.begin(), groups.end(), FastRandomContext());
227227

228228
for (const OutputGroup& group : groups) {
229229
if (group.m_value == nTargetValue) {

src/wallet/wallet.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2462,7 +2462,7 @@ bool CWallet::SelectCoins(const std::vector<COutput>& vAvailableCoins, const CAm
24622462
// Cases where we have 11+ outputs all pointing to the same destination may result in
24632463
// privacy leaks as they will potentially be deterministically sorted. We solve that by
24642464
// explicitly shuffling the outputs before processing
2465-
std::shuffle(vCoins.begin(), vCoins.end(), FastRandomContext());
2465+
Shuffle(vCoins.begin(), vCoins.end(), FastRandomContext());
24662466
}
24672467
std::vector<OutputGroup> groups = GroupOutputs(vCoins, !coin_control.m_avoid_partial_spends);
24682468

@@ -2922,7 +2922,7 @@ bool CWallet::CreateTransaction(interfaces::Chain::Lock& locked_chain, const std
29222922
// Shuffle selected coins and fill in final vin
29232923
txNew.vin.clear();
29242924
std::vector<CInputCoin> selected_coins(setCoins.begin(), setCoins.end());
2925-
std::shuffle(selected_coins.begin(), selected_coins.end(), FastRandomContext());
2925+
Shuffle(selected_coins.begin(), selected_coins.end(), FastRandomContext());
29262926

29272927
// Note how the sequence number is set to non-maxint so that
29282928
// the nLockTime set above actually works.

0 commit comments

Comments
 (0)