wallet: Have GetBalance report used amount directly without two calls
What changed, and why it matters
This is a small internal cleanup in how Bitcoin Core wallets report balances for reused addresses. It does not fix a vulnerability or change user-facing totals; it just computes the 'used' balance in one pass instead of calling the balance function twice and subtracting. There is no security issue visible in the change.
No action required. Treat as ordinary code-quality refactor.
Security signals we found
No security-relevant signals detected in the diff
Refactor only: single-pass balance computation replacing two-pass subtraction
No changes to validation, networking, scripting, or wallet encryption
Evidence from the diff
The commit refactors GetBalance to track a new m_mine_used bucket directly, and exposes it through WalletBalances/used_balance. Previously getbalances computed ‘used’ by subtracting the avoid_reuse balance from the full balance. The new code avoids a second GetBalance call and makes the accounting explicit. No cryptographic, consensus, or authorization changes are present.
Changed components
src/wallet/receive.cppsrc/wallet/receive.hsrc/wallet/rpc/coins.cppsrc/wallet/interfaces.cppsrc/interfaces/wallet.hInspect captured patch +22 / −12
diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h
index 2ccd514d..118f9a2e 100644
--- a/src/interfaces/wallet.h
+++ b/src/interfaces/wallet.h
@@ -369,11 +369,13 @@ struct WalletBalances
CAmount balance = 0;
CAmount unconfirmed_balance = 0;
CAmount immature_balance = 0;
+ CAmount used_balance = 0;
bool balanceChanged(const WalletBalances& prev) const
{
return balance != prev.balance || unconfirmed_balance != prev.unconfirmed_balance ||
- immature_balance != prev.immature_balance;
+ immature_balance != prev.immature_balance ||
+ used_balance != prev.used_balance;
}
};
diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp
index 4dc87334..7fa0d8a1 100644
--- a/src/wallet/interfaces.cpp
+++ b/src/wallet/interfaces.cpp
@@ -388,6 +388,7 @@ public:
result.balance = bal.m_mine_trusted;
result.unconfirmed_balance = bal.m_mine_untrusted_pending;
result.immature_balance = bal.m_mine_immature;
+ result.used_balance = bal.m_mine_used;
return result;
}
bool tryGetBalances(WalletBalances& balances, uint256& block_hash) override
diff --git a/src/wallet/receive.cpp b/src/wallet/receive.cpp
index 13a25b36..8832ddb6 100644
--- a/src/wallet/receive.cpp
+++ b/src/wallet/receive.cpp
@@ -255,17 +255,25 @@ Balance GetBalance(const CWallet& wallet, const int min_depth, bool avoid_reuse)
const bool is_trusted{CachedTxIsTrusted(wallet, wtx, trusted_parents)};
const int tx_depth{wallet.GetTxDepthInMainChain(wtx)};
- if (!wallet.IsSpent(outpoint) && (allow_used_addresses || !wallet.IsSpentKey(txo.GetTxOut().scriptPubKey))) {
- // Get the amounts for mine
- CAmount credit_mine = txo.GetTxOut().nValue;
+ if (!wallet.IsSpent(outpoint)) {
+ CAmount* bucket = nullptr;
// Set the amounts in the return object
if (wallet.IsTxImmatureCoinBase(wtx) && wtx.isConfirmed()) {
- ret.m_mine_immature += credit_mine;
+ bucket = &ret.m_mine_immature;
} else if (is_trusted && tx_depth >= min_depth) {
- ret.m_mine_trusted += credit_mine;
+ bucket = &ret.m_mine_trusted;
} else if (!is_trusted && wtx.InMempool()) {
- ret.m_mine_untrusted_pending += credit_mine;
+ bucket = &ret.m_mine_untrusted_pending;
+ }
+ if (bucket) {
+ // Get the amounts for mine
+ CAmount credit_mine = txo.GetTxOut().nValue;
+
+ if (!allow_used_addresses && wallet.IsSpentKey(txo.GetTxOut().scriptPubKey)) {
+ bucket = &ret.m_mine_used;
+ }
+ *bucket += credit_mine;
}
}
}
diff --git a/src/wallet/receive.h b/src/wallet/receive.h
index f6c55a45..5bc0545b 100644
--- a/src/wallet/receive.h
+++ b/src/wallet/receive.h
@@ -47,6 +47,7 @@ struct Balance {
CAmount m_mine_trusted{0}; //!< Trusted, at depth=GetBalance.min_depth or more
CAmount m_mine_untrusted_pending{0}; //!< Untrusted, but in mempool (pending)
CAmount m_mine_immature{0}; //!< Immature coinbases in the main chain
+ CAmount m_mine_used{0}; //!< Trusted/untrusted/immature funds in utxos that have already been spent from (only populated if AVOID REUSE wallet flag is set)
};
Balance GetBalance(const CWallet& wallet, int min_depth = 0, bool avoid_reuse = true);
diff --git a/src/wallet/rpc/coins.cpp b/src/wallet/rpc/coins.cpp
index 4aef0dda..c76d5e3c 100644
--- a/src/wallet/rpc/coins.cpp
+++ b/src/wallet/rpc/coins.cpp
@@ -433,7 +433,8 @@ RPCHelpMan getbalances()
LOCK(wallet.cs_wallet);
- const auto bal = GetBalance(wallet);
+ const auto bal = GetBalance(wallet, /*min_depth=*/0, /*avoid_reuse=*/true);
+
UniValue balances{UniValue::VOBJ};
{
UniValue balances_mine{UniValue::VOBJ};
@@ -441,10 +442,7 @@ RPCHelpMan getbalances()
balances_mine.pushKV("untrusted_pending", ValueFromAmount(bal.m_mine_untrusted_pending));
balances_mine.pushKV("immature", ValueFromAmount(bal.m_mine_immature));
if (wallet.IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE)) {
- // If the AVOID_REUSE flag is set, bal has been set to just the un-reused address balance. Get
- // the total balance, and then subtract bal to get the reused address balance.
- const auto full_bal = GetBalance(wallet, 0, false);
- balances_mine.pushKV("used", ValueFromAmount(full_bal.m_mine_trusted + full_bal.m_mine_untrusted_pending - bal.m_mine_trusted - bal.m_mine_untrusted_pending));
+ balances_mine.pushKV("used", ValueFromAmount(bal.m_mine_used));
}
balances.pushKV("mine", std::move(balances_mine));
}
Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.