Skip to content

Commit 260ede1

Browse files
committed
Merge bitcoin/bitcoin#24644: wallet: add tracepoints and algorithm information to coin selection
ab5af9c test: Add test for coinselection tracepoints (Andrew Chow) ca02b68 doc: document coin selection tracepoints (Andrew Chow) 8e3f39e wallet: Add some tracepoints for coin selection (Andrew Chow) 15b5838 wallet: compute waste for SelectionResults of preset inputs (Andrew Chow) 912f1ed wallet: track which coin selection algorithm produced a SelectionResult (Andrew Chow) Pull request description: Tracepoints can be useful for coin selection as they would allow us to observe what is being selected, selection parameters, and calculation results. So this PR adds 4 new tracepoints: 1. After `SelectCoins` returns in order to observe the `SelectionResult` 2. After the first `CreateTransactionInternal` to observe the created transaction 3. Prior to the second `CreateTransactionInternal` to notify that the optimistic avoid partial spends selection is occurring 4. After the second `CreateTransactionInternal` to observe the created transaction and inform which solution is being used. This PR also adds an algorithm enum to `SelectionResult` so that the first tracepoint will be able to report which algorithm was used to produce that result. The primary use case for these tracepoints is in running coin selection simulations. The script I use to run these simulations use these tracepoints in order to gather data on the algorithm used and the calculated waste. ACKs for top commit: jb55: crACK ab5af9c josibake: crACK bitcoin/bitcoin@ab5af9c 0xB10C: ACK ab5af9c. Code reviewed, ran the `interface_usdt_coinselection.py` test, and tested with the above bpftrace script (updated `%d` -> `%ld` where necessary, ty achow101). Tree-SHA512: a4bf7a910cdf464622f2f3b5d44c15b891f24852df6e7f8c5b177fe3d8aaa4a1164593a24c3960eb22b16544fa7140e5c745345367b9e291b78395084c0ac8ff
2 parents 833add0 + ab5af9c commit 260ede1

File tree

7 files changed

+297
-10
lines changed

7 files changed

+297
-10
lines changed

doc/tracing.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,49 @@ Arguments passed:
168168
4. Value of the coin as `int64`
169169
5. If the coin is a coinbase as `bool`
170170

171+
### Context `coin_selection`
172+
173+
#### Tracepoint `coin_selection:selected_coins`
174+
175+
Is called when `SelectCoins` completes.
176+
177+
Arguments passed:
178+
1. Wallet name as `pointer to C-style string`
179+
2. Coin selection algorithm name as `pointer to C-style string`
180+
3. Selection target value as `int64`
181+
4. Calculated waste metric of the solution as `int64`
182+
5. Total value of the selected inputs as `int64`
183+
184+
#### Tracepoint `coin_selection:normal_create_tx_internal`
185+
186+
Is called when the first `CreateTransactionInternal` completes.
187+
188+
Arguments passed:
189+
1. Wallet name as `pointer to C-style string`
190+
2. Whether `CreateTransactionInternal` succeeded as `bool`
191+
3. The expected transaction fee as an `int64`
192+
4. The position of the change output as an `int32`
193+
194+
#### Tracepoint `coin_selection:attempting_aps_create_tx`
195+
196+
Is called when `CreateTransactionInternal` is called the second time for the optimistic
197+
Avoid Partial Spends selection attempt. This is used to determine whether the next
198+
tracepoints called are for the Avoid Partial Spends solution, or a different transaction.
199+
200+
Arguments passed:
201+
1. Wallet name as `pointer to C-style string`
202+
203+
#### Tracepoint `coin_selection:aps_create_tx_internal`
204+
205+
Is called when the second `CreateTransactionInternal` with Avoid Partial Spends enabled completes.
206+
207+
Arguments passed:
208+
1. Wallet name as `pointer to C-style string`
209+
2. Whether the Avoid Partial Spends solution will be used as `bool`
210+
3. Whether `CreateTransactionInternal` succeeded as` bool`
211+
4. The expected transaction fee as an `int64`
212+
5. The position of the change output as an `int32`
213+
171214
## Adding tracepoints to Bitcoin Core
172215

173216
To add a new tracepoint, `#include <util/trace.h>` in the compilation unit where

src/wallet/coinselection.cpp

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ static const size_t TOTAL_TRIES = 100000;
6464

6565
std::optional<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change)
6666
{
67-
SelectionResult result(selection_target);
67+
SelectionResult result(selection_target, SelectionAlgorithm::BNB);
6868
CAmount curr_value = 0;
6969
std::vector<size_t> curr_selection; // selected utxo indexes
7070

@@ -167,7 +167,7 @@ std::optional<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_poo
167167

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

172172
std::vector<size_t> indexes;
173173
indexes.resize(utxo_pool.size());
@@ -249,7 +249,7 @@ static void ApproximateBestSubset(FastRandomContext& insecure_rand, const std::v
249249
std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, const CAmount& nTargetValue,
250250
CAmount change_target, FastRandomContext& rng)
251251
{
252-
SelectionResult result(nTargetValue);
252+
SelectionResult result(nTargetValue, SelectionAlgorithm::KNAPSACK);
253253

254254
// List of values less than target
255255
std::optional<OutputGroup> lowest_larger;
@@ -460,4 +460,17 @@ std::string COutput::ToString() const
460460
{
461461
return strprintf("COutput(%s, %d, %d) [%s]", outpoint.hash.ToString(), outpoint.n, depth, FormatMoney(txout.nValue));
462462
}
463+
464+
std::string GetAlgorithmName(const SelectionAlgorithm algo)
465+
{
466+
switch (algo)
467+
{
468+
case SelectionAlgorithm::BNB: return "bnb";
469+
case SelectionAlgorithm::KNAPSACK: return "knapsack";
470+
case SelectionAlgorithm::SRD: return "srd";
471+
case SelectionAlgorithm::MANUAL: return "manual";
472+
// No default case to allow for compiler to warn
473+
}
474+
assert(false);
475+
}
463476
} // namespace wallet

src/wallet/coinselection.h

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -239,21 +239,34 @@ struct OutputGroup
239239
*/
240240
[[nodiscard]] CAmount GenerateChangeTarget(CAmount payment_value, FastRandomContext& rng);
241241

242+
enum class SelectionAlgorithm : uint8_t
243+
{
244+
BNB = 0,
245+
KNAPSACK = 1,
246+
SRD = 2,
247+
MANUAL = 3,
248+
};
249+
250+
std::string GetAlgorithmName(const SelectionAlgorithm algo);
251+
242252
struct SelectionResult
243253
{
244254
private:
245255
/** Set of inputs selected by the algorithm to use in the transaction */
246256
std::set<COutput> m_selected_inputs;
247-
/** The target the algorithm selected for. Note that this may not be equal to the recipient amount as it can include non-input fees */
248-
const CAmount m_target;
249257
/** Whether the input values for calculations should be the effective value (true) or normal value (false) */
250258
bool m_use_effective{false};
251259
/** The computed waste */
252260
std::optional<CAmount> m_waste;
253261

254262
public:
255-
explicit SelectionResult(const CAmount target)
256-
: m_target(target) {}
263+
/** The target the algorithm selected for. Note that this may not be equal to the recipient amount as it can include non-input fees */
264+
const CAmount m_target;
265+
/** The algorithm used to produce this result */
266+
const SelectionAlgorithm m_algo;
267+
268+
explicit SelectionResult(const CAmount target, SelectionAlgorithm algo)
269+
: m_target(target), m_algo(algo) {}
257270

258271
SelectionResult() = delete;
259272

src/wallet/spend.cpp

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include <util/fees.h>
1212
#include <util/moneystr.h>
1313
#include <util/rbf.h>
14+
#include <util/trace.h>
1415
#include <util/translation.h>
1516
#include <wallet/coincontrol.h>
1617
#include <wallet/fees.h>
@@ -435,9 +436,10 @@ std::optional<SelectionResult> SelectCoins(const CWallet& wallet, const std::vec
435436
*/
436437
preset_inputs.Insert(out, /*ancestors=*/ 0, /*descendants=*/ 0, /*positive_only=*/ false);
437438
}
438-
SelectionResult result(nTargetValue);
439+
SelectionResult result(nTargetValue, SelectionAlgorithm::MANUAL);
439440
result.AddInput(preset_inputs);
440441
if (result.GetSelectedValue() < nTargetValue) return std::nullopt;
442+
result.ComputeAndSetWaste(coin_selection_params.m_cost_of_change);
441443
return result;
442444
}
443445

@@ -519,7 +521,7 @@ std::optional<SelectionResult> SelectCoins(const CWallet& wallet, const std::vec
519521
// permissive CoinEligibilityFilter.
520522
std::optional<SelectionResult> res = [&] {
521523
// Pre-selected inputs already cover the target amount.
522-
if (value_to_select <= 0) return std::make_optional(SelectionResult(nTargetValue));
524+
if (value_to_select <= 0) return std::make_optional(SelectionResult(nTargetValue, SelectionAlgorithm::MANUAL));
523525

524526
// If possible, fund the transaction with confirmed UTXOs only. Prefer at least six
525527
// confirmations on outputs received from other wallets and only spend confirmed change.
@@ -573,6 +575,9 @@ std::optional<SelectionResult> SelectCoins(const CWallet& wallet, const std::vec
573575

574576
// Add preset inputs to result
575577
res->AddInput(preset_inputs);
578+
if (res->m_algo == SelectionAlgorithm::MANUAL) {
579+
res->ComputeAndSetWaste(coin_selection_params.m_cost_of_change);
580+
}
576581

577582
return res;
578583
}
@@ -788,6 +793,7 @@ static bool CreateTransactionInternal(
788793
error = _("Insufficient funds");
789794
return false;
790795
}
796+
TRACE5(coin_selection, selected_coins, wallet.GetName().c_str(), GetAlgorithmName(result->m_algo).c_str(), result->m_target, result->GetWaste(), result->GetSelectedValue());
791797

792798
// Always make a change output
793799
// We will reduce the fee from this change output later, and remove the output if it is too small.
@@ -978,8 +984,10 @@ bool CreateTransaction(
978984
int nChangePosIn = nChangePosInOut;
979985
Assert(!tx); // tx is an out-param. TODO change the return type from bool to tx (or nullptr)
980986
bool res = CreateTransactionInternal(wallet, vecSend, tx, nFeeRet, nChangePosInOut, error, coin_control, fee_calc_out, sign);
987+
TRACE4(coin_selection, normal_create_tx_internal, wallet.GetName().c_str(), res, nFeeRet, nChangePosInOut);
981988
// try with avoidpartialspends unless it's enabled already
982989
if (res && nFeeRet > 0 /* 0 means non-functional fee rate estimation */ && wallet.m_max_aps_fee > -1 && !coin_control.m_avoid_partial_spends) {
990+
TRACE1(coin_selection, attempting_aps_create_tx, wallet.GetName().c_str());
983991
CCoinControl tmp_cc = coin_control;
984992
tmp_cc.m_avoid_partial_spends = true;
985993
CAmount nFeeRet2;
@@ -990,6 +998,7 @@ bool CreateTransaction(
990998
// if fee of this alternative one is within the range of the max fee, we use this one
991999
const bool use_aps = nFeeRet2 <= nFeeRet + wallet.m_max_aps_fee;
9921000
wallet.WalletLogPrintf("Fee non-grouped = %lld, grouped = %lld, using %s\n", nFeeRet, nFeeRet2, use_aps ? "grouped" : "non-grouped");
1001+
TRACE5(coin_selection, aps_create_tx_internal, wallet.GetName().c_str(), use_aps, res, nFeeRet2, nChangePosInOut2);
9931002
if (use_aps) {
9941003
tx = tx2;
9951004
nFeeRet = nFeeRet2;

src/wallet/test/coinselector_tests.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ BOOST_AUTO_TEST_CASE(bnb_search_test)
168168
FastRandomContext rand{};
169169
// Setup
170170
std::vector<COutput> utxo_pool;
171-
SelectionResult expected_result(CAmount(0));
171+
SelectionResult expected_result(CAmount(0), SelectionAlgorithm::BNB);
172172

173173
/////////////////////////
174174
// Known Outcome tests //

0 commit comments

Comments
 (0)