Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions doc/release-notes-25122.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

Wallet
------

- RPC `getreceivedbylabel` now returns an error, "Label not found
in wallet" (-4), if the label is not in the address book. (dash#7550)

6 changes: 6 additions & 0 deletions doc/release-notes-25504.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Updated RPCs
------------

- The `listsinceblock`, `listtransactions` and `gettransaction` output now contain a new
`parent_descs` field for every "receive" entry.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- A new optional `include_change` parameter was added to the `listsinceblock` command.
3 changes: 2 additions & 1 deletion src/Makefile.test.include
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,8 @@ BITCOIN_TESTS += \
wallet/test/init_tests.cpp \
wallet/test/ismine_tests.cpp \
wallet/test/rpc_util_tests.cpp \
wallet/test/scriptpubkeyman_tests.cpp
wallet/test/scriptpubkeyman_tests.cpp \
wallet/test/walletload_tests.cpp

FUZZ_SUITE_LD_COMMON +=\
$(SQLITE_LIBS) \
Expand Down
1 change: 1 addition & 0 deletions src/rpc/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "listsinceblock", 1, "target_confirmations" },
{ "listsinceblock", 2, "include_watchonly" },
{ "listsinceblock", 3, "include_removed" },
{ "listsinceblock", 4, "include_change" },
{ "sendmany", 1, "amounts" },
{ "sendmany", 2, "minconf" },
{ "sendmany", 3, "addlocked" },
Expand Down
63 changes: 56 additions & 7 deletions src/wallet/coinselection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ util::Result<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool
for (const size_t& i : best_selection) {
result.AddInput(utxo_pool.at(i));
}
result.ComputeAndSetWaste(CAmount{0});
result.ComputeAndSetWaste(cost_of_change, cost_of_change, CAmount{0});
assert(best_waste == result.GetWaste());

return result;
Expand All @@ -206,13 +206,21 @@ class MinOutputGroupComparator
};

util::Result<SelectionResult> SelectCoinsSRD(const std::vector<OutputGroup>& utxo_pool, CAmount target_value, FastRandomContext& rng,
int max_weight)
int max_weight, bool only_fully_mixed)
{
if (utxo_pool.empty()) return util::Error();

SelectionResult result(target_value, SelectionAlgorithm::SRD);
std::priority_queue<OutputGroup, std::vector<OutputGroup>, MinOutputGroupComparator> heap;

// Include change for SRD as we want to avoid making really small change if the selection just
// barely meets the target. Just use the lower bound change target instead of the randomly
// generated one, since SRD will result in a random change amount anyway; avoid making the
// target needlessly large.
if (!only_fully_mixed) {
target_value += CHANGE_LOWER;
}

std::vector<size_t> indexes;
indexes.resize(utxo_pool.size());
std::iota(indexes.begin(), indexes.end(), 0);
Expand Down Expand Up @@ -530,20 +538,26 @@ CAmount GetSelectionWaste(const std::set<COutput>& inputs, CAmount change_cost,
return waste;
}

CAmount GenerateChangeTarget(CAmount payment_value, FastRandomContext& rng)
CAmount GenerateChangeTarget(const CAmount payment_value, const CAmount change_fee, FastRandomContext& rng)
{
if (payment_value <= CHANGE_LOWER / 2) {
return CHANGE_LOWER;
return change_fee + CHANGE_LOWER;
} else {
// random value between 50ksat and min (payment_value * 2, 1milsat)
const auto upper_bound = std::min(payment_value * 2, CHANGE_UPPER);
return rng.randrange(upper_bound - CHANGE_LOWER) + CHANGE_LOWER;
return change_fee + rng.randrange(upper_bound - CHANGE_LOWER) + CHANGE_LOWER;
}
}

void SelectionResult::ComputeAndSetWaste(CAmount change_cost)
void SelectionResult::ComputeAndSetWaste(const CAmount min_viable_change, const CAmount change_cost, const CAmount change_fee)
{
m_waste = GetSelectionWaste(m_selected_inputs, change_cost, m_target, m_use_effective);
const CAmount change = GetChange(min_viable_change, change_fee);

if (change > 0) {
m_waste = GetSelectionWaste(m_selected_inputs, change_cost, m_target, m_use_effective);
} else {
m_waste = GetSelectionWaste(m_selected_inputs, 0, m_target, m_use_effective);
}
}

CAmount SelectionResult::GetWaste() const
Expand All @@ -556,6 +570,11 @@ CAmount SelectionResult::GetSelectedValue() const
return std::accumulate(m_selected_inputs.cbegin(), m_selected_inputs.cend(), CAmount{0}, [](CAmount sum, const auto& coin) { return sum + coin.txout.nValue; });
}

CAmount SelectionResult::GetSelectedEffectiveValue() const
{
return std::accumulate(m_selected_inputs.cbegin(), m_selected_inputs.cend(), CAmount{0}, [](CAmount sum, const auto& coin) { return sum + coin.GetEffectiveValue(); });
}

void SelectionResult::Clear()
{
m_selected_inputs.clear();
Expand All @@ -568,6 +587,16 @@ void SelectionResult::AddInput(const OutputGroup& group)
m_use_effective = !group.m_subtract_fee_outputs;
}

void SelectionResult::Merge(const SelectionResult& other)
{
m_target += other.m_target;
m_use_effective |= other.m_use_effective;
if (m_algo == SelectionAlgorithm::MANUAL) {
m_algo = other.m_algo;
}
util::insert(m_selected_inputs, other.m_selected_inputs);
}

const std::set<COutput>& SelectionResult::GetInputSet() const
{
return m_selected_inputs;
Expand Down Expand Up @@ -605,4 +634,24 @@ std::string GetAlgorithmName(const SelectionAlgorithm algo)
}
assert(false);
}

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;
}

} // namespace wallet
48 changes: 40 additions & 8 deletions src/wallet/coinselection.h
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ struct CoinSelectionParams {
/** Mininmum change to target in Knapsack solver: select coins to cover the payment and
* at least this value of change. */
CAmount m_min_change_target{0};
/** Minimum amount for creating a change output.
* If change budget is smaller than min_change then we forgo creation of change output.
*/
CAmount min_viable_change{0};
/** Cost of creating the change output. */
CAmount m_change_fee{0};
/** Cost of creating the change output + cost of spending the change output in the future. */
Expand Down Expand Up @@ -251,6 +255,7 @@ struct OutputGroup

/** Choose a random change target for each transaction to make it harder to fingerprint the Core
* wallet based on the change output values of transactions it creates.
* Change target covers at least change fees and adds a random value on top of it.
* The random value is between 50ksat and min(2 * payment_value, 1milsat)
* When payment_value <= 25ksat, the value is just 50ksat.
*
Expand All @@ -260,8 +265,9 @@ struct OutputGroup
* coins selected are just sufficient to cover the payment amount ("unnecessary input" heuristic).
*
* @param[in] payment_value Average payment value of the transaction output(s).
* @param[in] change_fee Fee for creating a change output.
*/
[[nodiscard]] CAmount GenerateChangeTarget(CAmount payment_value, FastRandomContext& rng);
[[nodiscard]] CAmount GenerateChangeTarget(const CAmount payment_value, const CAmount change_fee, FastRandomContext& rng);

enum class SelectionAlgorithm : uint8_t
{
Expand All @@ -278,17 +284,16 @@ struct SelectionResult
private:
/** Set of inputs selected by the algorithm to use in the transaction */
std::set<COutput> m_selected_inputs;
/** The target the algorithm selected for. Equal to the recipient amount plus non-input fees */
CAmount m_target;
/** The algorithm used to produce this result */
SelectionAlgorithm m_algo;
/** Whether the input values for calculations should be the effective value (true) or normal value (false) */
bool m_use_effective{false};
/** The computed waste */
std::optional<CAmount> m_waste;

public:
/** The target the algorithm selected for. Note that this may not be equal to the recipient amount as it can include non-input fees */
const CAmount m_target;
/** The algorithm used to produce this result */
const SelectionAlgorithm m_algo;

explicit SelectionResult(const CAmount target, SelectionAlgorithm algo)
: m_target(target), m_algo(algo) {}

Expand All @@ -297,20 +302,47 @@ struct SelectionResult
/** Get the sum of the input values */
[[nodiscard]] CAmount GetSelectedValue() const;

[[nodiscard]] CAmount GetSelectedEffectiveValue() const;

void Clear();

void AddInput(const OutputGroup& group);

/** Calculates and stores the waste for this selection via GetSelectionWaste */
void ComputeAndSetWaste(CAmount change_cost);
void ComputeAndSetWaste(const CAmount min_viable_change, const CAmount change_cost, const CAmount change_fee);
[[nodiscard]] CAmount GetWaste() const;

void Merge(const SelectionResult& other);

/** Get m_selected_inputs */
const std::set<COutput>& GetInputSet() const;
/** Get the vector of COutputs that will be used to fill in a CTransaction's vin */
std::vector<COutput> GetShuffledInputVector() const;

bool operator<(SelectionResult other) const;

/** Get the amount for the change output after paying needed fees.
*
* The change amount is not 100% precise due to discrepancies in fee calculation.
* The final change amount (if any) should be corrected after calculating the final tx fees.
* When there is a discrepancy, most of the time the final change would be slightly bigger than estimated.
*
* Following are the possible factors of discrepancy:
* + non-input fees always include segwit flags
* + input fee estimation always include segwit stack size
* + input fees are rounded individually and not collectively, which leads to small rounding errors
* - input counter size is always assumed to be 1vbyte
*
* @param[in] min_viable_change Minimum amount for change output, if change would be less then we forgo change
* @param[in] change_fee Fees to include change output in the tx
* @returns Amount for change output, 0 when there is no change.
*
*/
CAmount GetChange(const CAmount min_viable_change, const CAmount change_fee) const;

CAmount GetTarget() const { return m_target; }

SelectionAlgorithm GetAlgo() const { return m_algo; }
};

util::Result<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change,
Expand All @@ -326,7 +358,7 @@ util::Result<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool
* @returns If successful, a valid SelectionResult, otherwise, util::Error
*/
util::Result<SelectionResult> SelectCoinsSRD(const std::vector<OutputGroup>& utxo_pool, CAmount target_value, FastRandomContext& rng,
int max_weight);
int max_weight, bool only_fully_mixed);

// Original coin selection algorithm as a fallback
util::Result<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, const CAmount& nTargetValue,
Expand Down
6 changes: 3 additions & 3 deletions src/wallet/receive.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,8 @@ CAmount CachedTxGetAvailableCredit(const CWallet& wallet, const CWalletTx& wtx,

void CachedTxGetAmounts(const CWallet& wallet, const CWalletTx& wtx,
std::list<COutputEntry>& listReceived,
std::list<COutputEntry>& listSent, CAmount& nFee, const isminefilter& filter)
std::list<COutputEntry>& listSent, CAmount& nFee, const isminefilter& filter,
bool include_change)
{
nFee = 0;
listReceived.clear();
Expand All @@ -219,8 +220,7 @@ void CachedTxGetAmounts(const CWallet& wallet, const CWalletTx& wtx,
// 2) the output is to us (received)
if (nDebit > 0)
{
// Don't report 'change' txouts
if (OutputIsChange(wallet, txout))
if (!include_change && OutputIsChange(wallet, txout))
continue;
}
else if (!(fIsMine & filter))
Expand Down
3 changes: 2 additions & 1 deletion src/wallet/receive.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ struct COutputEntry
void CachedTxGetAmounts(const CWallet& wallet, const CWalletTx& wtx,
std::list<COutputEntry>& listReceived,
std::list<COutputEntry>& listSent,
CAmount& nFee, const isminefilter& filter);
CAmount& nFee, const isminefilter& filter,
bool include_change);
bool CachedTxIsFromMe(const CWallet& wallet, const CWalletTx& wtx, const isminefilter& filter);
bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx, std::set<uint256>& trusted_parents) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx);
Expand Down
28 changes: 8 additions & 20 deletions src/wallet/rpc/backup.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ RPCHelpMan importprivkey()
"\nNote: This call can take over an hour to complete if rescan is true, during that time, other rpc calls\n"
"may report that the imported key exists but related transactions are still missing, leading to temporarily incorrect/bogus balances and unspent outputs until rescan completes.\n"
"The rescan parameter can be set to false if the key was never used to create transactions. If it is set to false,\n"
"but the key was used to create transactions, rescanwallet needs to be called with the appropriate block range.\n"
"but the key was used to create transactions, rescanblockchain needs to be called with the appropriate block range.\n"
"Note: This command is only compatible with legacy wallets. Use \"importdescriptors\" with \"combo(X)\" for descriptor wallets.\n",
{
{"privkey", RPCArg::Type::STR, RPCArg::Optional::NO, "The private key (see dumpprivkey)"},
Expand Down Expand Up @@ -211,7 +211,7 @@ RPCHelpMan importaddress()
"\nNote: This call can take over an hour to complete if rescan is true, during that time, other rpc calls\n"
"may report that the imported address exists but related transactions are still missing, leading to temporarily incorrect/bogus balances and unspent outputs until rescan completes.\n"
"The rescan parameter can be set to false if the key was never used to create transactions. If it is set to false,\n"
"but the key was used to create transactions, rescanwallet needs to be called with the appropriate block range.\n"
"but the key was used to create transactions, rescanblockchain needs to be called with the appropriate block range.\n"
"If you have the full public key, you should call importpubkey instead of this.\n"
"Hint: use importmulti to import more than one address.\n"
"\nNote: If you import a non-standard raw script in hex form, outputs sending to it will be treated\n"
Expand Down Expand Up @@ -296,10 +296,7 @@ RPCHelpMan importaddress()
if (fRescan)
{
RescanWallet(*pwallet, reserver);
{
LOCK(pwallet->cs_wallet);
pwallet->ReacceptWalletTransactions();
}
pwallet->ResubmitWalletTransactions(/*relay=*/false, /*force=*/true);
}

return UniValue::VNULL;
Expand Down Expand Up @@ -409,7 +406,7 @@ RPCHelpMan importpubkey()
"\nNote: This call can take over an hour to complete if rescan is true, during that time, other rpc calls\n"
"may report that the imported pubkey exists but related transactions are still missing, leading to temporarily incorrect/bogus balances and unspent outputs until rescan completes.\n"
"The rescan parameter can be set to false if the key was never used to create transactions. If it is set to false,\n"
"but the key was used to create transactions, rescanwallet needs to be called with the appropriate block range.\n"
"but the key was used to create transactions, rescanblockchain needs to be called with the appropriate block range.\n"
"Note: This command is only compatible with legacy wallets. Use \"importdescriptors\" with \"combo(X)\" for descriptor wallets.\n",
{
{"pubkey", RPCArg::Type::STR, RPCArg::Optional::NO, "The hex-encoded public key"},
Expand Down Expand Up @@ -475,10 +472,7 @@ RPCHelpMan importpubkey()
if (fRescan)
{
RescanWallet(*pwallet, reserver);
{
LOCK(pwallet->cs_wallet);
pwallet->ReacceptWalletTransactions();
}
pwallet->ResubmitWalletTransactions(/*relay=*/false, /*force=*/true);
}

return UniValue::VNULL;
Expand Down Expand Up @@ -1490,7 +1484,7 @@ RPCHelpMan importmulti()
"\nNote: This call can take over an hour to complete if rescan is true, during that time, other rpc calls\n"
"may report that the imported keys, addresses or scripts exists but related transactions are still missing.\n"
"The rescan parameter can be set to false if the key was never used to create transactions. If it is set to false,\n"
"but the key was used to create transactions, rescanwallet needs to be called with the appropriate block range.\n"
"but the key was used to create transactions, rescanblockchain needs to be called with the appropriate block range.\n"
"Note: This command is only compatible with legacy wallets. Use \"importdescriptors\" for descriptor wallets.\n",
{
{"requests", RPCArg::Type::ARR, RPCArg::Optional::NO, "Data to be imported",
Expand Down Expand Up @@ -1638,10 +1632,7 @@ RPCHelpMan importmulti()
}
if (fRescan && fRunScan && requests.size()) {
int64_t scannedTime = pwallet->RescanFromTime(nLowestTimestamp, reserver, true /* update */);
{
LOCK(pwallet->cs_wallet);
pwallet->ReacceptWalletTransactions();
}
pwallet->ResubmitWalletTransactions(/*relay=*/false, /*force=*/true);

if (pwallet->IsAbortingRescan()) {
throw JSONRPCError(RPC_MISC_ERROR, "Rescan aborted by user.");
Expand Down Expand Up @@ -1933,10 +1924,7 @@ RPCHelpMan importdescriptors() {
// Rescan the blockchain using the lowest timestamp
if (rescan) {
int64_t scanned_time = pwallet->RescanFromTime(lowest_timestamp, reserver, true /* update */);
{
LOCK(pwallet->cs_wallet);
pwallet->ReacceptWalletTransactions();
}
pwallet->ResubmitWalletTransactions(/*relay=*/false, /*force=*/true);

if (pwallet->IsAbortingRescan()) {
throw JSONRPCError(RPC_MISC_ERROR, "Rescan aborted by user.");
Expand Down
Loading