wallet: reject sendtoaddress and sendmany for external signers
What changed, and why it matters
This change makes Bitcoin Core refuse two older wallet commands (sendtoaddress and sendmany) when the wallet is controlled by an external signer, such as a hardware wallet. Previously these commands could be called but would fail in a confusing way because they expect to sign transactions internally. Now they return a clear message telling the user to use the newer send command instead. It is a usability and safety improvement, not a fix for an active attack.
No urgent action required. Users with external signer wallets should use the send RPC as instructed. Operators should ensure they are running a version that includes this change if they want clearer error handling for hardware-wallet wallets.
Security signals we found
Prevents misuse of RPCs that cannot complete signing for external-signer wallets
Adds explicit error path instead of relying on downstream failure
Adds functional test coverage for the new error behavior
Evidence from the diff
In src/wallet/rpc/spend.cpp, SendMoney() now checks WALLET_FLAG_EXTERNAL_SIGNER before checking WALLET_FLAG_DISABLE_PRIVATE_KEYS and throws a specific RPC_WALLET_ERROR: sendtoaddress and sendmany are not supported for wallets with external signers; use send instead. Functional tests in wallet_signer.py verify both RPCs raise this error. The change prevents external-signer wallets from entering a code path that cannot succeed, directing users to the PSBT-based send flow.
Changed components
src/wallet/rpc/spend.cpptest/functional/wallet_signer.pysendtoaddress RPCsendmany RPCExternal signer (hardware wallet) walletsInspect captured patch +20 / −1
diff --git a/src/wallet/rpc/spend.cpp b/src/wallet/rpc/spend.cpp
index b6cdc860..d1f838c9 100644
--- a/src/wallet/rpc/spend.cpp
+++ b/src/wallet/rpc/spend.cpp
@@ -173,7 +173,11 @@ UniValue SendMoney(CWallet& wallet, const CCoinControl &coin_control, std::vecto
EnsureWalletIsUnlocked(wallet);
// This function is only used by sendtoaddress and sendmany.
- // This should always try to sign, if we don't have private keys, don't try to do anything here.
+ // This should always try to sign, if we don't have (all) private keys, don't
+ // try to do anything here.
+ if (wallet.IsWalletFlagSet(WALLET_FLAG_EXTERNAL_SIGNER)) {
+ throw JSONRPCError(RPC_WALLET_ERROR, "Error: sendtoaddress and sendmany are not supported for wallets with external signers; use send instead");
+ }
if (wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
throw JSONRPCError(RPC_WALLET_ERROR, "Error: Private keys are disabled for this wallet");
}
diff --git a/test/functional/wallet_signer.py b/test/functional/wallet_signer.py
index 4b8e6388..896169d4 100755
--- a/test/functional/wallet_signer.py
+++ b/test/functional/wallet_signer.py
@@ -120,6 +120,21 @@ class WalletSignerTest(BitcoinTestFramework):
result = hww.walletdisplayaddress(address)
assert_equal(result, {"address": address})
+ assert_raises_rpc_error(
+ -4,
+ "Error: sendtoaddress and sendmany are not supported for wallets with external signers; use send instead",
+ hww.sendtoaddress,
+ self.nodes[0].getnewaddress(),
+ 0.01,
+ )
+ assert_raises_rpc_error(
+ -4,
+ "Error: sendtoaddress and sendmany are not supported for wallets with external signers; use send instead",
+ hww.sendmany,
+ "",
+ {self.nodes[0].getnewaddress(): 0.01},
+ )
+
# Handle error thrown by script
self.set_mock_result(self.nodes[1], "2")
assert_raises_rpc_error(-1, 'RunCommandParseJSON error',
Why this scored 24/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.