Skip to content

Commit a7f9bbe

Browse files
committed
Merge bitcoin/bitcoin#32821: rpc: Handle -named argument parsing where '=' character is used
f53dbbc test: Add functional tests for named argument parsing (zaidmstrr) 694f04e rpc: Handle -named argument parsing where '=' character is used (zaidmstrr) Pull request description: Addresses [comment](bitcoin/bitcoin#31375 (comment)) and [this](bitcoin/bitcoin#31375 (comment)). The [PR #31375](bitcoin/bitcoin#31375) got merged and enables `-named` by default in the `bitcoin rpc` interface; `bitcoin rpc` corresponds to `bitcoin-cli -named` as it's just a wrapper. Now, the problem arises when we try to parse the positional paramater which might contain "=" character. This splits the parameter into two parts first, before the "=" character, which treats this as the parameter name, but the other half is mostly passed as an empty string. Here, the first part of the string is an unknown parameter name; thus, an error is thrown. These types of errors are only applicable to those RPCs which might contain the `=` character as a parameter. Some examples are `finalizepsbt`, `decodepsbt`, `verifymessage` etc. This is the one example of the error in `finalizepsbt` RPC: ``` ./bitcoin-cli -named -regtest finalizepsbt cHNidP8BAJoCAAAAAqvNEjSrzRI0q80SNKvNEjSrzRI0q80SNKvNEjSrzRI0AAAAAAD9////NBLNqzQSzas0Es2rNBLNqzQSzas0Es2rNBLNqzQSzasBAAAAAP3///8CoIYBAAAAAAAWABQVQBGVs/sqFAmC8HZ8O+g1htqivkANAwAAAAAAFgAUir7MzgyzDnRMjdkVa7d+Dwr07jsAAAAAAAAAAAA= error code: -8 error message: Unknown named parameter cHNidP8BAJoCAAAAAqvNEjSrzRI0q80SNKvNEjSrzRI0q80SNKvNEjSrzRI0AAAAAAD9////NBLNqzQSzas0Es2rNBLNqzQSzas0Es2rNBLNqzQSzasBAAAAAP3///8CoIYBAAAAAAAWABQVQBGVs/sqFAmC8HZ8O+g1htqivkANAwAAAAAAFgAUir7MzgyzDnRMjdkVa7d+Dwr07jsAAAAAAAAAAAA ``` This PR fixes this by updating the `vRPCConvertParams` table that identifies parameters that need special handling in `-named` parameter mode. The parser now recognises these parameters and handles strings with "=" char correctly, preventing them from being incorrectly split as parameter assignments. ACKs for top commit: ryanofsky: Code review ACK f53dbbc. Just applied comment & test suggestions since last review kannapoix: Code review ACK: f53dbbc achow101: ACK f53dbbc Tree-SHA512: 1b517144efeff45a4c4256c27a39ddf187f1d6189d133402a45171678214a10ff2925c31edcfd556d67f85bd26d42f63c528b941b68c9880eab443f2c883e681
2 parents 2444488 + f53dbbc commit a7f9bbe

File tree

6 files changed

+279
-63
lines changed

6 files changed

+279
-63
lines changed

src/bitcoin.cpp

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,8 @@ int main(int argc, char* argv[])
9191
// Since "bitcoin rpc" is a new interface that doesn't need to be
9292
// backward compatible, enable -named by default so it is convenient
9393
// for callers to use a mix of named and unnamed parameters. Callers
94-
// can override this by specifying -nonamed, but should not need to
95-
// unless they are passing string values containing '=' characters
96-
// as unnamed parameters.
94+
// can override this by specifying -nonamed, but it handles parameters
95+
// that contain '=' characters, so -nonamed should rarely be needed.
9796
args.emplace_back("-named");
9897
} else if (cmd.command == "wallet") {
9998
args.emplace_back("bitcoin-wallet");

src/rpc/client.cpp

Lines changed: 149 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -12,26 +12,53 @@
1212
#include <string>
1313
#include <string_view>
1414

15+
//! Specify whether parameter should be parsed by bitcoin-cli as a JSON value,
16+
//! or passed unchanged as a string, or a combination of both.
17+
enum ParamFormat { JSON, STRING, JSON_OR_STRING };
18+
1519
class CRPCConvertParam
1620
{
1721
public:
1822
std::string methodName; //!< method whose params want conversion
1923
int paramIdx; //!< 0-based idx of param to convert
2024
std::string paramName; //!< parameter name
21-
bool also_string{false}; //!< The parameter is also a string
25+
ParamFormat format{ParamFormat::JSON}; //!< parameter format
2226
};
2327

2428
// clang-format off
2529
/**
26-
* Specify a (method, idx, name) here if the argument is a non-string RPC
27-
* argument and needs to be converted from JSON.
30+
* Specify a (method, idx, name, format) here if the argument is a non-string RPC
31+
* argument and needs to be converted from JSON, or if it is a string argument
32+
* passed to a method that accepts '=' characters in any string arguments.
33+
*
34+
* JSON parameters need to be listed here to make bitcoin-cli parse command line
35+
* arguments as JSON, instead of passing them as raw strings. `JSON` and
36+
* `JSON_OR_STRING` formats both make `bitcoin-cli` attempt to parse the
37+
* argument as JSON. But if parsing fails, the former triggers an error while
38+
* the latter falls back to passing the argument as a raw string. This is
39+
* useful for arguments like hash_or_height, allowing invocations such as
40+
* `bitcoin-cli getblockstats <hash>` without needing to quote the hash string
41+
* as JSON (`'"<hash>"'`).
42+
*
43+
* String parameters that may contain an '=' character (e.g. base64 strings,
44+
* filenames, or labels) need to be listed here with format `ParamFormat::STRING`
45+
* to make bitcoin-cli treat them as positional parameters when `-named` is used.
46+
* This prevents `bitcoin-cli` from splitting strings like "my=wallet" into a named
47+
* argument "my" and value "wallet" when the whole string is intended to be a
48+
* single positional argument. And if one string parameter is listed for a method,
49+
* other string parameters for that method need to be listed as well so bitcoin-cli
50+
* does not make the opposite mistake and pass other arguments by position instead of
51+
* name because it does not recognize their names. See \ref RPCConvertNamedValues
52+
* for more information on how named and positional arguments are distinguished with
53+
* -named.
2854
*
2955
* @note Parameter indexes start from 0.
3056
*/
3157
static const CRPCConvertParam vRPCConvertParams[] =
3258
{
3359
{ "setmocktime", 0, "timestamp" },
3460
{ "mockscheduler", 0, "delta_time" },
61+
{ "utxoupdatepsbt", 0, "psbt", ParamFormat::STRING },
3562
{ "utxoupdatepsbt", 1, "descriptors" },
3663
{ "generatetoaddress", 0, "nblocks" },
3764
{ "generatetoaddress", 2, "maxtries" },
@@ -41,16 +68,21 @@ static const CRPCConvertParam vRPCConvertParams[] =
4168
{ "generateblock", 2, "submit" },
4269
{ "getnetworkhashps", 0, "nblocks" },
4370
{ "getnetworkhashps", 1, "height" },
71+
{ "sendtoaddress", 0, "address", ParamFormat::STRING },
4472
{ "sendtoaddress", 1, "amount" },
73+
{ "sendtoaddress", 2, "comment", ParamFormat::STRING },
74+
{ "sendtoaddress", 3, "comment_to", ParamFormat::STRING },
4575
{ "sendtoaddress", 4, "subtractfeefromamount" },
4676
{ "sendtoaddress", 5 , "replaceable" },
4777
{ "sendtoaddress", 6 , "conf_target" },
78+
{ "sendtoaddress", 7, "estimate_mode", ParamFormat::STRING },
4879
{ "sendtoaddress", 8, "avoid_reuse" },
4980
{ "sendtoaddress", 9, "fee_rate"},
5081
{ "sendtoaddress", 10, "verbose"},
5182
{ "settxfee", 0, "amount" },
5283
{ "getreceivedbyaddress", 1, "minconf" },
5384
{ "getreceivedbyaddress", 2, "include_immature_coinbase" },
85+
{ "getreceivedbylabel", 0, "label", ParamFormat::STRING },
5486
{ "getreceivedbylabel", 1, "minconf" },
5587
{ "getreceivedbylabel", 2, "include_immature_coinbase" },
5688
{ "listreceivedbyaddress", 0, "minconf" },
@@ -70,20 +102,27 @@ static const CRPCConvertParam vRPCConvertParams[] =
70102
{ "waitforblockheight", 1, "timeout" },
71103
{ "waitforblock", 1, "timeout" },
72104
{ "waitfornewblock", 0, "timeout" },
105+
{ "listtransactions", 0, "label", ParamFormat::STRING },
73106
{ "listtransactions", 1, "count" },
74107
{ "listtransactions", 2, "skip" },
75108
{ "listtransactions", 3, "include_watchonly" },
109+
{ "walletpassphrase", 0, "passphrase", ParamFormat::STRING },
76110
{ "walletpassphrase", 1, "timeout" },
77111
{ "getblocktemplate", 0, "template_request" },
112+
{ "listsinceblock", 0, "blockhash", ParamFormat::STRING },
78113
{ "listsinceblock", 1, "target_confirmations" },
79114
{ "listsinceblock", 2, "include_watchonly" },
80115
{ "listsinceblock", 3, "include_removed" },
81116
{ "listsinceblock", 4, "include_change" },
117+
{ "listsinceblock", 5, "label", ParamFormat::STRING },
118+
{ "sendmany", 0, "dummy", ParamFormat::STRING },
82119
{ "sendmany", 1, "amounts" },
83120
{ "sendmany", 2, "minconf" },
121+
{ "sendmany", 3, "comment", ParamFormat::STRING },
84122
{ "sendmany", 4, "subtractfeefrom" },
85123
{ "sendmany", 5 , "replaceable" },
86124
{ "sendmany", 6 , "conf_target" },
125+
{ "sendmany", 7, "estimate_mode", ParamFormat::STRING },
87126
{ "sendmany", 8, "fee_rate"},
88127
{ "sendmany", 9, "verbose" },
89128
{ "deriveaddresses", 1, "range" },
@@ -170,10 +209,14 @@ static const CRPCConvertParam vRPCConvertParams[] =
170209
{ "walletcreatefundedpsbt", 3, "max_tx_weight"},
171210
{ "walletcreatefundedpsbt", 4, "bip32derivs" },
172211
{ "walletcreatefundedpsbt", 5, "version" },
212+
{ "walletprocesspsbt", 0, "psbt", ParamFormat::STRING },
173213
{ "walletprocesspsbt", 1, "sign" },
214+
{ "walletprocesspsbt", 2, "sighashtype", ParamFormat::STRING },
174215
{ "walletprocesspsbt", 3, "bip32derivs" },
175216
{ "walletprocesspsbt", 4, "finalize" },
217+
{ "descriptorprocesspsbt", 0, "psbt", ParamFormat::STRING },
176218
{ "descriptorprocesspsbt", 1, "descriptors"},
219+
{ "descriptorprocesspsbt", 2, "sighashtype", ParamFormat::STRING },
177220
{ "descriptorprocesspsbt", 3, "bip32derivs" },
178221
{ "descriptorprocesspsbt", 4, "finalize" },
179222
{ "createpsbt", 0, "inputs" },
@@ -183,16 +226,19 @@ static const CRPCConvertParam vRPCConvertParams[] =
183226
{ "createpsbt", 4, "version" },
184227
{ "combinepsbt", 0, "txs"},
185228
{ "joinpsbts", 0, "txs"},
229+
{ "finalizepsbt", 0, "psbt", ParamFormat::STRING },
186230
{ "finalizepsbt", 1, "extract"},
187231
{ "converttopsbt", 1, "permitsigdata"},
188232
{ "converttopsbt", 2, "iswitness"},
189233
{ "gettxout", 1, "n" },
190234
{ "gettxout", 2, "include_mempool" },
191235
{ "gettxoutproof", 0, "txids" },
192-
{ "gettxoutsetinfo", 1, "hash_or_height", /*also_string=*/true },
236+
{ "gettxoutsetinfo", 1, "hash_or_height", ParamFormat::JSON_OR_STRING },
193237
{ "gettxoutsetinfo", 2, "use_index"},
238+
{ "dumptxoutset", 0, "path", ParamFormat::STRING },
239+
{ "dumptxoutset", 1, "type", ParamFormat::STRING },
194240
{ "dumptxoutset", 2, "options" },
195-
{ "dumptxoutset", 2, "rollback", /*also_string=*/true },
241+
{ "dumptxoutset", 2, "rollback", ParamFormat::JSON_OR_STRING },
196242
{ "lockunspent", 0, "unlock" },
197243
{ "lockunspent", 1, "transactions" },
198244
{ "lockunspent", 2, "persistent" },
@@ -239,6 +285,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
239285
{ "simulaterawtransaction", 0, "rawtxs" },
240286
{ "simulaterawtransaction", 1, "options" },
241287
{ "simulaterawtransaction", 1, "include_watchonly"},
288+
{ "importmempool", 0, "filepath", ParamFormat::STRING },
242289
{ "importmempool", 1, "options" },
243290
{ "importmempool", 1, "apply_fee_delta_priority" },
244291
{ "importmempool", 1, "use_current_time" },
@@ -247,7 +294,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
247294
{ "listdescriptors", 0, "private" },
248295
{ "verifychain", 0, "checklevel" },
249296
{ "verifychain", 1, "nblocks" },
250-
{ "getblockstats", 0, "hash_or_height", /*also_string=*/true },
297+
{ "getblockstats", 0, "hash_or_height", ParamFormat::JSON_OR_STRING },
251298
{ "getblockstats", 1, "stats" },
252299
{ "pruneblockchain", 0, "height" },
253300
{ "keypoolrefill", 0, "newsize" },
@@ -299,14 +346,20 @@ static const CRPCConvertParam vRPCConvertParams[] =
299346
{ "echojson", 9, "arg9" },
300347
{ "rescanblockchain", 0, "start_height"},
301348
{ "rescanblockchain", 1, "stop_height"},
349+
{ "createwallet", 0, "wallet_name", ParamFormat::STRING },
302350
{ "createwallet", 1, "disable_private_keys"},
303351
{ "createwallet", 2, "blank"},
352+
{ "createwallet", 3, "passphrase", ParamFormat::STRING },
304353
{ "createwallet", 4, "avoid_reuse"},
305354
{ "createwallet", 5, "descriptors"},
306355
{ "createwallet", 6, "load_on_startup"},
307356
{ "createwallet", 7, "external_signer"},
357+
{ "restorewallet", 0, "wallet_name", ParamFormat::STRING },
358+
{ "restorewallet", 1, "backup_file", ParamFormat::STRING },
308359
{ "restorewallet", 2, "load_on_startup"},
360+
{ "loadwallet", 0, "filename", ParamFormat::STRING },
309361
{ "loadwallet", 1, "load_on_startup"},
362+
{ "unloadwallet", 0, "wallet_name", ParamFormat::STRING },
310363
{ "unloadwallet", 1, "load_on_startup"},
311364
{ "getnodeaddresses", 0, "count"},
312365
{ "addpeeraddress", 1, "port"},
@@ -315,91 +368,140 @@ static const CRPCConvertParam vRPCConvertParams[] =
315368
{ "stop", 0, "wait" },
316369
{ "addnode", 2, "v2transport" },
317370
{ "addconnection", 2, "v2transport" },
371+
{ "decodepsbt", 0, "psbt", ParamFormat::STRING },
372+
{ "analyzepsbt", 0, "psbt", ParamFormat::STRING},
373+
{ "verifymessage", 1, "signature", ParamFormat::STRING },
374+
{ "verifymessage", 2, "message", ParamFormat::STRING },
375+
{ "getnewaddress", 0, "label", ParamFormat::STRING },
376+
{ "getnewaddress", 1, "address_type", ParamFormat::STRING },
377+
{ "backupwallet", 0, "destination", ParamFormat::STRING },
378+
{ "echoipc", 0, "arg", ParamFormat::STRING },
379+
{ "encryptwallet", 0, "passphrase", ParamFormat::STRING },
380+
{ "getaddressesbylabel", 0, "label", ParamFormat::STRING },
381+
{ "loadtxoutset", 0, "path", ParamFormat::STRING },
382+
{ "migratewallet", 0, "wallet_name", ParamFormat::STRING },
383+
{ "migratewallet", 1, "passphrase", ParamFormat::STRING },
384+
{ "setlabel", 1, "label", ParamFormat::STRING },
385+
{ "signmessage", 1, "message", ParamFormat::STRING },
386+
{ "signmessagewithprivkey", 1, "message", ParamFormat::STRING },
387+
{ "walletpassphrasechange", 0, "oldpassphrase", ParamFormat::STRING },
388+
{ "walletpassphrasechange", 1, "newpassphrase", ParamFormat::STRING },
318389
};
319390
// clang-format on
320391

321392
/** Parse string to UniValue or throw runtime_error if string contains invalid JSON */
322-
static UniValue Parse(std::string_view raw, bool also_string)
393+
static UniValue Parse(std::string_view raw, ParamFormat format = ParamFormat::JSON)
323394
{
324395
UniValue parsed;
325396
if (!parsed.read(raw)) {
326-
if (!also_string) throw std::runtime_error(tfm::format("Error parsing JSON: %s", raw));
327-
return raw;
397+
if (format != ParamFormat::JSON_OR_STRING) throw std::runtime_error(tfm::format("Error parsing JSON: %s", raw));
398+
return UniValue(std::string(raw));
328399
}
329400
return parsed;
330401
}
331402

332-
class CRPCConvertTable
403+
namespace rpc_convert
404+
{
405+
const CRPCConvertParam* FromPosition(std::string_view method, size_t pos)
333406
{
334-
private:
335-
std::map<std::pair<std::string, int>, bool> members;
336-
std::map<std::pair<std::string, std::string>, bool> membersByName;
407+
auto it = std::ranges::find_if(vRPCConvertParams, [&](const auto& p) {
408+
return p.methodName == method && p.paramIdx == static_cast<int>(pos);
409+
});
337410

338-
public:
339-
CRPCConvertTable();
411+
return it == std::end(vRPCConvertParams) ? nullptr : &*it;
412+
}
340413

341-
/** Return arg_value as UniValue, and first parse it if it is a non-string parameter */
342-
UniValue ArgToUniValue(std::string_view arg_value, const std::string& method, int param_idx)
343-
{
344-
const auto& it = members.find({method, param_idx});
345-
if (it != members.end()) {
346-
return Parse(arg_value, it->second);
347-
}
348-
return arg_value;
349-
}
414+
const CRPCConvertParam* FromName(std::string_view method, std::string_view name)
415+
{
416+
auto it = std::ranges::find_if(vRPCConvertParams, [&](const auto& p) {
417+
return p.methodName == method && p.paramName == name;
418+
});
350419

351-
/** Return arg_value as UniValue, and first parse it if it is a non-string parameter */
352-
UniValue ArgToUniValue(std::string_view arg_value, const std::string& method, const std::string& param_name)
353-
{
354-
const auto& it = membersByName.find({method, param_name});
355-
if (it != membersByName.end()) {
356-
return Parse(arg_value, it->second);
357-
}
358-
return arg_value;
359-
}
360-
};
420+
return it == std::end(vRPCConvertParams) ? nullptr : &*it;
421+
}
422+
} // namespace rpc_convert
361423

362-
CRPCConvertTable::CRPCConvertTable()
424+
static UniValue ParseParam(const CRPCConvertParam* param, std::string_view raw)
363425
{
364-
for (const auto& cp : vRPCConvertParams) {
365-
members.emplace(std::make_pair(cp.methodName, cp.paramIdx), cp.also_string);
366-
membersByName.emplace(std::make_pair(cp.methodName, cp.paramName), cp.also_string);
367-
}
426+
// Only parse parameters which have the JSON or JSON_OR_STRING format; otherwise, treat them as strings.
427+
return (param && (param->format == ParamFormat::JSON || param->format == ParamFormat::JSON_OR_STRING)) ? Parse(raw, param->format) : UniValue(std::string(raw));
368428
}
369429

370-
static CRPCConvertTable rpcCvtTable;
371-
430+
/**
431+
* Convert command lines arguments to params object when -named is disabled.
432+
*/
372433
UniValue RPCConvertValues(const std::string &strMethod, const std::vector<std::string> &strParams)
373434
{
374435
UniValue params(UniValue::VARR);
375436

376-
for (unsigned int idx = 0; idx < strParams.size(); idx++) {
377-
std::string_view value{strParams[idx]};
378-
params.push_back(rpcCvtTable.ArgToUniValue(value, strMethod, idx));
437+
for (std::string_view s : strParams) {
438+
params.push_back(ParseParam(rpc_convert::FromPosition(strMethod, params.size()), s));
379439
}
380440

381441
return params;
382442
}
383443

444+
/**
445+
* Convert command line arguments to params object when -named is enabled.
446+
*
447+
* The -named syntax accepts named arguments in NAME=VALUE format, as well as
448+
* positional arguments without names. The syntax is inherently ambiguous if
449+
* names are omitted and values contain '=', so a heuristic is used to
450+
* disambiguate:
451+
*
452+
* - Arguments that do not contain '=' are treated as positional parameters.
453+
*
454+
* - Arguments that do contain '=' are assumed to be named parameters in
455+
* NAME=VALUE format except for two special cases:
456+
*
457+
* 1. The case where NAME is not a known parameter name, and the next
458+
* positional parameter requires a JSON value, and the argument parses as
459+
* JSON. E.g. ["list", "with", "="].
460+
*
461+
* 2. The case where NAME is not a known parameter name and the next
462+
* positional parameter requires a string value. E.g. "my=wallet".
463+
*
464+
* For example, the command `bitcoin-cli -named createwallet "my=wallet"`,
465+
* the parser initially sees "my=wallet" and attempts to process it as a
466+
* parameter named "my". When it finds that "my" is not a valid named parameter
467+
* parameter for this method, it falls back to checking the rule for the
468+
* next available positional parameter (index 0). Because it finds the rule
469+
* that this parameter is a ParamFormat::STRING, it correctly treats the entire
470+
* "my=wallet" as a single positional string, successfully creating a
471+
* wallet with that literal name.
472+
*/
384473
UniValue RPCConvertNamedValues(const std::string &strMethod, const std::vector<std::string> &strParams)
385474
{
386475
UniValue params(UniValue::VOBJ);
387476
UniValue positional_args{UniValue::VARR};
388477

389478
for (std::string_view s: strParams) {
390479
size_t pos = s.find('=');
391-
if (pos == std::string::npos) {
392-
positional_args.push_back(rpcCvtTable.ArgToUniValue(s, strMethod, positional_args.size()));
480+
if (pos == std::string_view::npos) {
481+
positional_args.push_back(ParseParam(rpc_convert::FromPosition(strMethod, positional_args.size()), s));
393482
continue;
394483
}
395484

396485
std::string name{s.substr(0, pos)};
397486
std::string_view value{s.substr(pos+1)};
398487

488+
const CRPCConvertParam* named_param{rpc_convert::FromName(strMethod, name)};
489+
if (!named_param) {
490+
const CRPCConvertParam* positional_param = rpc_convert::FromPosition(strMethod, positional_args.size());
491+
UniValue parsed_value;
492+
if (positional_param && positional_param->format == ParamFormat::JSON && parsed_value.read(s)) {
493+
positional_args.push_back(std::move(parsed_value));
494+
continue;
495+
} else if (positional_param && positional_param->format == ParamFormat::STRING) {
496+
positional_args.push_back(s);
497+
continue;
498+
}
499+
}
500+
399501
// Intentionally overwrite earlier named values with later ones as a
400502
// convenience for scripts and command line users that want to merge
401503
// options.
402-
params.pushKV(name, rpcCvtTable.ArgToUniValue(value, strMethod, name));
504+
params.pushKV(name, ParseParam(named_param, value));
403505
}
404506

405507
if (!positional_args.empty()) {

0 commit comments

Comments
 (0)