wallet, rpc: Disallow import of unused() if key already exists
What changed, and why it matters
This Bitcoin Core change prevents users from importing a special 'unused()' wallet descriptor if the private key it refers to is already present in the wallet. The patch adds a check during descriptor import and a test to confirm the new behavior. The commit message does not describe this as a security fix, and no external security references are provided, so its security relevance is uncertain. It appears to be a defensive correctness fix that could prevent confusion or wallet-state inconsistencies rather than a fix for an active exploit.
Treat as a low-risk defensive fix. Reviewers may want to confirm whether duplicate unused() descriptors previously caused any exploitable behavior, such as unexpected key reuse, balance reporting issues, or backup/restore inconsistencies. If no such behavior is identified, no urgent action is needed beyond normal review and testing.
Security signals we found
Prevents duplicate private key state via descriptor import
Adds explicit RPC error for conflicting unused() descriptor import
Includes functional regression test for the new restriction
Evidence from the diff
In src/wallet/rpc/backup.cpp, ProcessDescriptorImport now detects descriptors of the form unused(KEY). It extracts the single public key, derives its key ID, and calls wallet.GetKey() to see if the corresponding private key already exists in the wallet. If so, it throws RPC_WALLET_ERROR with the message ‘Cannot import an unused() descriptor when its private key is already in the wallet’. A functional test in wallet_importdescriptors.py verifies that importing unused(xprv) fails when the wallet already holds that extended private key. The change is purely additive and does not alter existing valid imports.
Changed components
Bitcoin Core wallet RPCsrc/wallet/rpc/backup.cppimportdescriptors RPCDescriptor wallet import logicInspect captured patch +34 / −0
diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp
index 4b87ba23..cb417a6e 100644
--- a/src/wallet/rpc/backup.cpp
+++ b/src/wallet/rpc/backup.cpp
@@ -265,6 +265,23 @@ static UniValue ProcessDescriptorImport(CWallet& wallet, const UniValue& data, c
}
}
+ // If this is an unused(KEY) descriptor, check that the wallet doesn't already have other descriptors with this key
+ if (!parsed_desc->HasScripts()) {
+ // Unused descriptors must contain a single key.
+ // Earlier checks will have enforced that this key is either a private key when private keys are enabled,
+ // or that this key is a public key when private keys are disabled.
+ // If we can retrieve the corresponding private key from the wallet, then this key is already in the wallet
+ // and we should not import it.
+ std::set<CPubKey> pubkeys;
+ std::set<CExtPubKey> extpubs;
+ parsed_desc->GetPubKeys(pubkeys, extpubs);
+ std::transform(extpubs.begin(), extpubs.end(), std::inserter(pubkeys, pubkeys.begin()), [](const CExtPubKey& xpub) { return xpub.pubkey; });
+ CHECK_NONFATAL(pubkeys.size() == 1);
+ if (wallet.GetKey(pubkeys.begin()->GetID())) {
+ throw JSONRPCError(RPC_WALLET_ERROR, "Cannot import an unused() descriptor when its private key is already in the wallet");
+ }
+ }
+
WalletDescriptor w_desc(std::move(parsed_desc), timestamp, range_start, range_end, next_index);
// Add descriptor to the wallet
diff --git a/test/functional/wallet_importdescriptors.py b/test/functional/wallet_importdescriptors.py
index c22bf103..23bb0cf6 100755
--- a/test/functional/wallet_importdescriptors.py
+++ b/test/functional/wallet_importdescriptors.py
@@ -88,6 +88,22 @@ class ImportDescriptorsTest(BitcoinTestFramework):
assert_equal(hdkeys[0]["xpub"], xpub)
wallet.unloadwallet()
+ def test_import_unused_key_existing(self):
+ self.log.info("Test import of unused(KEY) with existing KEY")
+ self.nodes[0].createwallet(wallet_name="import_existing_unused")
+ wallet = self.nodes[0].get_wallet_rpc("import_existing_unused")
+
+ hdkeys = wallet.gethdkeys(private=True)
+ assert_equal(len(hdkeys), 1)
+ xprv = hdkeys[0]["xprv"]
+
+ self.test_importdesc({"timestamp": "now", "desc": descsum_create(f"unused({xprv})")},
+ success=False,
+ error_code=-4,
+ error_message="Cannot import an unused() descriptor when its private key is already in the wallet",
+ wallet=wallet)
+ wallet.unloadwallet()
+
def run_test(self):
self.log.info('Setting up wallets')
self.nodes[0].createwallet(wallet_name='w0', disable_private_keys=False)
@@ -846,6 +862,7 @@ class ImportDescriptorsTest(BitcoinTestFramework):
self.test_import_unused_key()
+ self.test_import_unused_key_existing()
if __name__ == '__main__':
ImportDescriptorsTest(__file__).main()
Why this scored 33/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.