walletrpc: reject listdes with priv key on w-only wallets
What changed, and why it matters
This commit tightens a Bitcoin Core wallet RPC command so that watch-only wallets (which intentionally never store private keys) can no longer be asked to export private-key versions of their descriptors. Previously, the command would attempt to produce a private descriptor string and fail with a generic error; now it rejects the request up front with a clear message. It also replaces an error-handling path with an internal consistency check. The change is defensive and reduces the chance of misleading behavior or future bugs, but it does not appear to expose funds on its own.
No immediate action required beyond normal review and backport consideration. The change is a hardening fix; operators should ensure they are running a version that includes it if they rely on watch-only descriptor wallets.
Security signals we found
Adds explicit access-control guard for private descriptor export on watch-only wallets
Replaces recoverable RPC error with non-fatal assertion (CHECK_NONFATAL) after guard makes failure unreachable
Includes functional test update asserting the new rejection behavior
Evidence from the diff
In listdescriptors, when the caller passes private=true, the code now first checks WALLET_FLAG_DISABLE_PRIVATE_KEYS and throws RPC_WALLET_ERROR: "Can't get private descriptor string for watch-only wallets" before attempting descriptor string generation. The subsequent GetDescriptorString(descriptor, priv) call is wrapped in CHECK_NONFATAL, turning a previously user-facing error into an internal assertion that the call cannot fail at that point. A functional test is updated to expect the new error message.
Changed components
src/wallet/rpc/backup.cpptest/functional/wallet_listdescriptors.pyInspect captured patch +7 / −5
diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp
index 5cb40304..ebc678aa 100644
--- a/src/wallet/rpc/backup.cpp
+++ b/src/wallet/rpc/backup.cpp
@@ -17,6 +17,7 @@
#include <sync.h>
#include <uint256.h>
#include <util/bip32.h>
+#include <util/check.h>
#include <util/fs.h>
#include <util/time.h>
#include <util/translation.h>
@@ -493,6 +494,9 @@ RPCHelpMan listdescriptors()
if (!wallet) return UniValue::VNULL;
const bool priv = !request.params[0].isNull() && request.params[0].get_bool();
+ if (wallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) && priv) {
+ throw JSONRPCError(RPC_WALLET_ERROR, "Can't get private descriptor string for watch-only wallets");
+ }
if (priv) {
EnsureWalletIsUnlocked(*wallet);
}
@@ -519,9 +523,7 @@ RPCHelpMan listdescriptors()
LOCK(desc_spk_man->cs_desc_man);
const auto& wallet_descriptor = desc_spk_man->GetWalletDescriptor();
std::string descriptor;
- if (!desc_spk_man->GetDescriptorString(descriptor, priv)) {
- throw JSONRPCError(RPC_WALLET_ERROR, "Can't get descriptor string.");
- }
+ CHECK_NONFATAL(desc_spk_man->GetDescriptorString(descriptor, priv));
const bool is_range = wallet_descriptor.descriptor->IsRange();
wallet_descriptors.push_back({
descriptor,
diff --git a/test/functional/wallet_listdescriptors.py b/test/functional/wallet_listdescriptors.py
index aaa5daac..da73d5ee 100755
--- a/test/functional/wallet_listdescriptors.py
+++ b/test/functional/wallet_listdescriptors.py
@@ -107,7 +107,7 @@ class ListDescriptorsTest(BitcoinTestFramework):
'desc': descsum_create('wpkh(' + xpub_acc + ')'),
'timestamp': TIME_GENESIS_BLOCK,
}])
- assert_raises_rpc_error(-4, 'Can\'t get descriptor string', watch_only_wallet.listdescriptors, True)
+ assert_raises_rpc_error(-4, 'Can\'t get private descriptor string for watch-only wallets', watch_only_wallet.listdescriptors, True)
self.log.info('Test non-active non-range combo descriptor')
node.createwallet(wallet_name='w4', blank=True)
@@ -122,7 +122,7 @@ class ListDescriptorsTest(BitcoinTestFramework):
{'active': False,
'desc': 'combo(0227d85ba011276cf25b51df6a188b75e604b38770a462b2d0e9fb2fc839ef5d3f)#np574htj',
'timestamp': TIME_GENESIS_BLOCK},
- ]
+ ],
}
assert_equal(expected, wallet.listdescriptors())
Why this scored 34/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.