Skip to content

Commit b08041c

Browse files
committed
Merge bitcoin/bitcoin#32845: rpc, test: Fix JSON parsing errors in unloadwallet and getdescriptoractivity RPCs
c5c1960 doc: Add release notes for changes in RPCs (pablomartin4btc) 90fd5ac rpc, test: Fix error message in getdescriptoractivity (pablomartin4btc) 39fef1d test: Add missing logging info for each test (pablomartin4btc) 53ac704 rpc, test: Fix error message in unloadwallet (pablomartin4btc) 1fc3a8e rpc, test: Add EnsureUniqueWalletName tests (pablomartin4btc) b635bc0 rpc, util: Add EnsureUniqueWalletName (pablomartin4btc) Pull request description: Currently, `unloadwallet` RPC call fails with a JSON parsing error when no `wallet_name` argument is provided. This behavior is misleading because the error originates from a low-level JSON type mismatch, rather than clearly indicating that the wallet name or RPC endpoint (`-rpcwallet=...`) is missing. Also, found out that the [issue](bitcoin/bitcoin#13111 (comment)) was noticed during its implementation but never addressed. In addition, I've verified all RPC commands calls finding that `getdescriptoractivity` had the same problem, but related to the array input types (blockhashes & descriptors), so I've corrected that RPC as well. For consistency I've added the missing logging info for each test case in `test/functional/rpc_getdescriptoractivity.py` in preparation for the new test. **_-Before_** ``` ./build/bin/bitcoin-cli -regtest -datadir=/tmp/btc unloadwallet error code: -3 error message: JSON value of type number is not of expected type string ``` ``` ./build/bin/bitcoin-cli -regtest -datadir=/tmp/btc getdescriptoractivity error code: -3 error message: JSON value of type null is not of expected type array ``` ``` ./build/bin/bitcoin-cli -regtest -datadir=/tmp/btc getdescriptoractivity '[]' error code: -3 error message: JSON value of type null is not of expected type array ``` **_-After_** ``` ./build/bin/bitcoin-cli -regtest -datadir=/tmp/btc unloadwallet error code: -8 error message: Either the RPC endpoint wallet or the wallet name parameter must be provided ``` ``` ./build/bin/bitcoin-cli -regtest -datadir=/tmp/btc getdescriptoractivity error code: -1 error message: getdescriptoractivity ["blockhash",...] [scanobjects,...] ( include_mempool ) Get spend and receive activity associated with a set of descriptors for a set of blocks. This command pairs well with the `relevant_blocks` output of `scanblocks()`. This call may take several minutes. If you encounter timeouts, try specifying no RPC timeout (bitcoin-cli -rpcclienttimeout=0) Arguments: 1. blockhashes (json array, required) The list of blockhashes to examine for activity. Order doesn't matter. Must be along main chain or an error is thrown. [ "blockhash", (string) A valid blockhash ... ] 2. scanobjects (json array, required) Array of scan objects. Every scan object is either a string descriptor or an object: [ "descriptor", (string) An output descriptor { (json object) An object with output descriptor and metadata "desc": "str", (string, required) An output descriptor "range": n or [n,n], (numeric or array, optional, default=1000) The range of HD chain indexes to explore (either end or [begin,end]) }, ... ] 3. include_mempool (boolean, optional, default=true) Whether to include unconfirmed activity ... ``` ``` ./build/bin/bitcoin-cli -regtest -datadir=/tmp/btc getdescriptoractivity '[]' error code: -1 error message: getdescriptoractivity ["blockhash",...] [scanobjects,...] ( include_mempool ) ... ``` ACKs for top commit: achow101: ACK c5c1960 stickies-v: re-ACK c5c1960 furszy: ACK c5c1960 Tree-SHA512: e831ff1acbfd15d2ce3a69bb408cce94664c0b63b2aa2f4627a05c6c052241ae3b5cc238219ef1b30afb489a4a3f4c3030e2168b0c8f08b4d20805d050d810f5
2 parents fc16229 + c5c1960 commit b08041c

File tree

10 files changed

+118
-32
lines changed

10 files changed

+118
-32
lines changed

doc/release-notes-32845.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Updated RPCs
2+
------------
3+
4+
- `unloadwallet` - Return RPC_INVALID_PARAMETER when both the RPC wallet endpoint
5+
and wallet_name parameters are unspecified. Previously the RPC failed with a JSON
6+
parsing error.
7+
- `getdescriptoractivity` - Mark blockhashes and scanobjects arguments as required,
8+
so the user receives a clear help message when either is missing. As in `unloadwallet`,
9+
previously the RPC failed with a JSON parsing error.

src/rpc/blockchain.cpp

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2194,16 +2194,20 @@ static const auto scan_action_arg_desc = RPCArg{
21942194
"\"status\" for progress report (in %) of the current scan"
21952195
};
21962196

2197+
static const auto output_descriptor_obj = RPCArg{
2198+
"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "An object with output descriptor and metadata",
2199+
{
2200+
{"desc", RPCArg::Type::STR, RPCArg::Optional::NO, "An output descriptor"},
2201+
{"range", RPCArg::Type::RANGE, RPCArg::Default{1000}, "The range of HD chain indexes to explore (either end or [begin,end])"},
2202+
}
2203+
};
2204+
21972205
static const auto scan_objects_arg_desc = RPCArg{
21982206
"scanobjects", RPCArg::Type::ARR, RPCArg::Optional::OMITTED, "Array of scan objects. Required for \"start\" action\n"
21992207
"Every scan object is either a string descriptor or an object:",
22002208
{
22012209
{"descriptor", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "An output descriptor"},
2202-
{"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "An object with output descriptor and metadata",
2203-
{
2204-
{"desc", RPCArg::Type::STR, RPCArg::Optional::NO, "An output descriptor"},
2205-
{"range", RPCArg::Type::RANGE, RPCArg::Default{1000}, "The range of HD chain indexes to explore (either end or [begin,end])"},
2206-
}},
2210+
output_descriptor_obj,
22072211
},
22082212
RPCArgOptions{.oneline_description="[scanobjects,...]"},
22092213
};
@@ -2633,10 +2637,16 @@ static RPCHelpMan getdescriptoractivity()
26332637
"This command pairs well with the `relevant_blocks` output of `scanblocks()`.\n"
26342638
"This call may take several minutes. If you encounter timeouts, try specifying no RPC timeout (bitcoin-cli -rpcclienttimeout=0)",
26352639
{
2636-
RPCArg{"blockhashes", RPCArg::Type::ARR, RPCArg::Optional::OMITTED, "The list of blockhashes to examine for activity. Order doesn't matter. Must be along main chain or an error is thrown.\n", {
2640+
RPCArg{"blockhashes", RPCArg::Type::ARR, RPCArg::Optional::NO, "The list of blockhashes to examine for activity. Order doesn't matter. Must be along main chain or an error is thrown.\n", {
26372641
{"blockhash", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A valid blockhash"},
26382642
}},
2639-
scan_objects_arg_desc,
2643+
RPCArg{"scanobjects", RPCArg::Type::ARR, RPCArg::Optional::NO, "The list of descriptors (scan objects) to examine for activity. Every scan object is either a string descriptor or an object:",
2644+
{
2645+
{"descriptor", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "An output descriptor"},
2646+
output_descriptor_obj,
2647+
},
2648+
RPCArgOptions{.oneline_description="[scanobjects,...]"},
2649+
},
26402650
{"include_mempool", RPCArg::Type::BOOL, RPCArg::Default{true}, "Whether to include unconfirmed activity"},
26412651
},
26422652
RPCResult{

src/wallet/rpc/util.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,27 @@ bool GetAvoidReuseFlag(const CWallet& wallet, const UniValue& param) {
2929
return avoid_reuse;
3030
}
3131

32+
std::string EnsureUniqueWalletName(const JSONRPCRequest& request, const std::string* wallet_name)
33+
{
34+
std::string endpoint_wallet;
35+
if (GetWalletNameFromJSONRPCRequest(request, endpoint_wallet)) {
36+
// wallet endpoint was used
37+
if (wallet_name && *wallet_name != endpoint_wallet) {
38+
throw JSONRPCError(RPC_INVALID_PARAMETER,
39+
"The RPC endpoint wallet and the wallet name parameter specify different wallets");
40+
}
41+
return endpoint_wallet;
42+
}
43+
44+
// Not a wallet endpoint; parameter must be provided
45+
if (!wallet_name) {
46+
throw JSONRPCError(RPC_INVALID_PARAMETER,
47+
"Either the RPC endpoint wallet or the wallet name parameter must be provided");
48+
}
49+
50+
return *wallet_name;
51+
}
52+
3253
bool GetWalletNameFromJSONRPCRequest(const JSONRPCRequest& request, std::string& wallet_name)
3354
{
3455
if (request.URI.starts_with(WALLET_ENDPOINT_BASE)) {

src/wallet/rpc/util.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ static const RPCResult RESULT_LAST_PROCESSED_BLOCK { RPCResult::Type::OBJ, "last
3838
*/
3939
std::shared_ptr<CWallet> GetWalletForJSONRPCRequest(const JSONRPCRequest& request);
4040
bool GetWalletNameFromJSONRPCRequest(const JSONRPCRequest& request, std::string& wallet_name);
41+
/**
42+
* Ensures that a wallet name is specified across the endpoint and wallet_name.
43+
* Throws `RPC_INVALID_PARAMETER` if none or different wallet names are specified.
44+
*/
45+
std::string EnsureUniqueWalletName(const JSONRPCRequest& request, const std::string* wallet_name);
4146

4247
void EnsureWalletIsUnlocked(const CWallet&);
4348
WalletContext& EnsureWalletContext(const std::any& context);

src/wallet/rpc/wallet.cpp

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -438,8 +438,8 @@ static RPCHelpMan createwallet()
438438
static RPCHelpMan unloadwallet()
439439
{
440440
return RPCHelpMan{"unloadwallet",
441-
"Unloads the wallet referenced by the request endpoint, otherwise unloads the wallet specified in the argument.\n"
442-
"Specifying the wallet name on a wallet endpoint is invalid.",
441+
"Unloads the wallet referenced by the request endpoint or the wallet_name argument.\n"
442+
"If both are specified, they must be identical.",
443443
{
444444
{"wallet_name", RPCArg::Type::STR, RPCArg::DefaultHint{"the wallet name from the RPC endpoint"}, "The name of the wallet to unload. If provided both here and in the RPC endpoint, the two must be identical."},
445445
{"load_on_startup", RPCArg::Type::BOOL, RPCArg::Optional::OMITTED, "Save wallet name to persistent settings and load on startup. True to add wallet to startup list, false to remove, null to leave unchanged."},
@@ -456,14 +456,7 @@ static RPCHelpMan unloadwallet()
456456
},
457457
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
458458
{
459-
std::string wallet_name;
460-
if (GetWalletNameFromJSONRPCRequest(request, wallet_name)) {
461-
if (!(request.params[0].isNull() || request.params[0].get_str() == wallet_name)) {
462-
throw JSONRPCError(RPC_INVALID_PARAMETER, "RPC endpoint wallet and wallet_name parameter specify different wallets");
463-
}
464-
} else {
465-
wallet_name = request.params[0].get_str();
466-
}
459+
const std::string wallet_name{EnsureUniqueWalletName(request, self.MaybeArg<std::string>("wallet_name"))};
467460

468461
WalletContext& context = EnsureWalletContext(request.context);
469462
std::shared_ptr<CWallet> wallet = GetWallet(context, wallet_name);
@@ -685,17 +678,7 @@ static RPCHelpMan migratewallet()
685678
},
686679
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
687680
{
688-
std::string wallet_name;
689-
if (GetWalletNameFromJSONRPCRequest(request, wallet_name)) {
690-
if (!(request.params[0].isNull() || request.params[0].get_str() == wallet_name)) {
691-
throw JSONRPCError(RPC_INVALID_PARAMETER, "RPC endpoint wallet and wallet_name parameter specify different wallets");
692-
}
693-
} else {
694-
if (request.params[0].isNull()) {
695-
throw JSONRPCError(RPC_INVALID_PARAMETER, "Either RPC endpoint wallet or wallet_name parameter must be provided");
696-
}
697-
wallet_name = request.params[0].get_str();
698-
}
681+
const std::string wallet_name{EnsureUniqueWalletName(request, self.MaybeArg<std::string>("wallet_name"))};
699682

700683
SecureString wallet_pass;
701684
wallet_pass.reserve(100);

src/wallet/test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ target_sources(test_bitcoin
1919
scriptpubkeyman_tests.cpp
2020
spend_tests.cpp
2121
wallet_crypto_tests.cpp
22+
wallet_rpc_tests.cpp
2223
wallet_tests.cpp
2324
wallet_transaction_tests.cpp
2425
walletdb_tests.cpp

src/wallet/test/wallet_rpc_tests.cpp

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) 2025-present The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#include <rpc/request.h>
6+
#include <test/util/setup_common.h>
7+
#include <univalue.h>
8+
#include <wallet/rpc/util.h>
9+
10+
#include <boost/test/unit_test.hpp>
11+
12+
#include <optional>
13+
#include <string>
14+
15+
namespace wallet {
16+
static std::string TestWalletName(const std::string& endpoint, std::optional<std::string> parameter = std::nullopt)
17+
{
18+
JSONRPCRequest req;
19+
req.URI = endpoint;
20+
return EnsureUniqueWalletName(req, parameter ? &*parameter : nullptr);
21+
}
22+
23+
BOOST_FIXTURE_TEST_SUITE(wallet_rpc_tests, BasicTestingSetup)
24+
25+
BOOST_AUTO_TEST_CASE(ensure_unique_wallet_name)
26+
{
27+
// EnsureUniqueWalletName should only return if exactly one unique wallet name is provided
28+
BOOST_CHECK_EQUAL(TestWalletName("/wallet/foo"), "foo");
29+
BOOST_CHECK_EQUAL(TestWalletName("/wallet/foo", "foo"), "foo");
30+
BOOST_CHECK_EQUAL(TestWalletName("/", "foo"), "foo");
31+
BOOST_CHECK_EQUAL(TestWalletName("/bar", "foo"), "foo");
32+
33+
BOOST_CHECK_THROW(TestWalletName("/"), UniValue);
34+
BOOST_CHECK_THROW(TestWalletName("/foo"), UniValue);
35+
BOOST_CHECK_THROW(TestWalletName("/wallet/foo", "bar"), UniValue);
36+
BOOST_CHECK_THROW(TestWalletName("/wallet/foo", "foobar"), UniValue);
37+
BOOST_CHECK_THROW(TestWalletName("/wallet/foobar", "foo"), UniValue);
38+
}
39+
40+
BOOST_AUTO_TEST_SUITE_END()
41+
} // namespace wallet

test/functional/rpc_getdescriptoractivity.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,16 @@ def run_test(self):
3131
self.test_confirmed_and_unconfirmed(node, wallet)
3232
self.test_receive_then_spend(node, wallet)
3333
self.test_no_address(node, wallet)
34+
self.test_required_args(node)
3435

3536
def test_no_activity(self, node):
37+
self.log.info("Test that no activity is found for an unused address")
3638
_, _, addr_1 = getnewdestination()
3739
result = node.getdescriptoractivity([], [f"addr({addr_1})"], True)
3840
assert_equal(len(result['activity']), 0)
3941

4042
def test_activity_in_block(self, node, wallet):
43+
self.log.info("Test that receive activity is correctly reported in a mined block")
4144
_, spk_1, addr_1 = getnewdestination(address_type='bech32m')
4245
txid = wallet.send_to(from_node=node, scriptPubKey=spk_1, amount=1 * COIN)['txid']
4346
blockhash = self.generate(node, 1)[0]
@@ -67,6 +70,7 @@ def test_activity_in_block(self, node, wallet):
6770

6871

6972
def test_no_mempool_inclusion(self, node, wallet):
73+
self.log.info("Test that mempool transactions are not included when include_mempool argument is False")
7074
_, spk_1, addr_1 = getnewdestination()
7175
wallet.send_to(from_node=node, scriptPubKey=spk_1, amount=1 * COIN)
7276

@@ -81,6 +85,7 @@ def test_no_mempool_inclusion(self, node, wallet):
8185
assert_equal(len(result['activity']), 0)
8286

8387
def test_multiple_addresses(self, node, wallet):
88+
self.log.info("Test querying multiple addresses returns all activity correctly")
8489
_, spk_1, addr_1 = getnewdestination()
8590
_, spk_2, addr_2 = getnewdestination()
8691
wallet.send_to(from_node=node, scriptPubKey=spk_1, amount=1 * COIN)
@@ -113,6 +118,7 @@ def test_multiple_addresses(self, node, wallet):
113118
assert a2['amount'] == 2.0
114119

115120
def test_invalid_blockhash(self, node, wallet):
121+
self.log.info("Test that passing an invalid blockhash raises appropriate RPC error")
116122
self.generate(node, 20) # Generate to get more fees
117123

118124
_, spk_1, addr_1 = getnewdestination()
@@ -125,6 +131,7 @@ def test_invalid_blockhash(self, node, wallet):
125131
node.getdescriptoractivity, [invalid_blockhash], [f"addr({addr_1})"], True)
126132

127133
def test_invalid_descriptor(self, node, wallet):
134+
self.log.info("Test that an invalid descriptor raises the correct RPC error")
128135
blockhash = self.generate(node, 1)[0]
129136
_, _, addr_1 = getnewdestination()
130137

@@ -133,6 +140,7 @@ def test_invalid_descriptor(self, node, wallet):
133140
node.getdescriptoractivity, [blockhash], [f"addrx({addr_1})"], True)
134141

135142
def test_confirmed_and_unconfirmed(self, node, wallet):
143+
self.log.info("Test that both confirmed and unconfirmed transactions are reported correctly")
136144
self.generate(node, 20) # Generate to get more fees
137145

138146
_, spk_1, addr_1 = getnewdestination()
@@ -162,6 +170,7 @@ def test_confirmed_and_unconfirmed(self, node, wallet):
162170

163171
def test_receive_then_spend(self, node, wallet):
164172
"""Also important because this tests multiple blockhashes."""
173+
self.log.info("Test receive and spend activities across different blocks are reported consistently")
165174
self.generate(node, 20) # Generate to get more fees
166175

167176
sent1 = wallet.send_self_transfer(from_node=node)
@@ -189,11 +198,13 @@ def test_receive_then_spend(self, node, wallet):
189198
assert_equal(result, node.getdescriptoractivity(
190199
[blockhash_1, blockhash_2], [wallet.get_descriptor()], True))
191200

201+
self.log.info("Test that duplicated blockhash request does not report duplicated results")
192202
# Test that duplicating a blockhash yields the same result.
193203
assert_equal(result, node.getdescriptoractivity(
194204
[blockhash_1, blockhash_2, blockhash_2], [wallet.get_descriptor()], True))
195205

196206
def test_no_address(self, node, wallet):
207+
self.log.info("Test that activity is still reported for scripts without an associated address")
197208
raw_wallet = MiniWallet(self.nodes[0], mode=MiniWalletMode.RAW_P2PK)
198209
self.generate(raw_wallet, 100)
199210

@@ -221,6 +232,11 @@ def test_no_address(self, node, wallet):
221232
assert_equal(list(a2['output_spk'].keys()), ['asm', 'desc', 'hex', 'type'])
222233
assert a2['amount'] == Decimal(no_addr_tx["tx"].vout[0].nValue) / COIN
223234

235+
def test_required_args(self, node):
236+
self.log.info("Test that required arguments must be passed")
237+
assert_raises_rpc_error(-1, "getdescriptoractivity", node.getdescriptoractivity)
238+
assert_raises_rpc_error(-1, "getdescriptoractivity", node.getdescriptoractivity, [])
239+
224240

225241
if __name__ == '__main__':
226242
GetBlocksActivityTest(__file__).main()

test/functional/wallet_migration.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -524,8 +524,8 @@ def test_encrypted(self):
524524
def test_nonexistent(self):
525525
self.log.info("Check migratewallet errors for nonexistent wallets")
526526
default = self.master_node.get_wallet_rpc(self.default_wallet_name)
527-
assert_raises_rpc_error(-8, "RPC endpoint wallet and wallet_name parameter specify different wallets", default.migratewallet, "someotherwallet")
528-
assert_raises_rpc_error(-8, "Either RPC endpoint wallet or wallet_name parameter must be provided", self.master_node.migratewallet)
527+
assert_raises_rpc_error(-8, "The RPC endpoint wallet and the wallet name parameter specify different wallets", default.migratewallet, "someotherwallet")
528+
assert_raises_rpc_error(-8, "Either the RPC endpoint wallet or the wallet name parameter must be provided", self.master_node.migratewallet)
529529
assert_raises_rpc_error(-4, "Error: Wallet does not exist", self.master_node.migratewallet, "notawallet")
530530

531531
def test_unloaded_by_path(self):

test/functional/wallet_multiwallet.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,10 +343,10 @@ def wallet_file(name):
343343
self.log.info("Test dynamic wallet unloading")
344344

345345
# Test `unloadwallet` errors
346-
assert_raises_rpc_error(-3, "JSON value of type null is not of expected type string", self.nodes[0].unloadwallet)
346+
assert_raises_rpc_error(-8, "Either the RPC endpoint wallet or the wallet name parameter must be provided", self.nodes[0].unloadwallet)
347347
assert_raises_rpc_error(-18, "Requested wallet does not exist or is not loaded", self.nodes[0].unloadwallet, "dummy")
348348
assert_raises_rpc_error(-18, "Requested wallet does not exist or is not loaded", node.get_wallet_rpc("dummy").unloadwallet)
349-
assert_raises_rpc_error(-8, "RPC endpoint wallet and wallet_name parameter specify different wallets", w1.unloadwallet, "w2"),
349+
assert_raises_rpc_error(-8, "The RPC endpoint wallet and the wallet name parameter specify different wallets", w1.unloadwallet, "w2"),
350350

351351
# Successfully unload the specified wallet name
352352
self.nodes[0].unloadwallet("w1")

0 commit comments

Comments
 (0)