Skip to content

Commit

Permalink
Merge bitcoin#19386: rpc: Assert that RPCArg names are equal to CRPCC…
Browse files Browse the repository at this point in the history
…ommand ones (server)

fa7592b rpc: Update server to use new RPCHelpMan (MarcoFalke)
aaaaad5 rpc: Add option to hide RPCArg (MarcoFalke)
fa9708f rpc: Assert that passed arg names are equal to hardcoded ones (MarcoFalke)
faaeb2b rpc: Add CRPCCommand constructor which takes RPCHelpMan (MarcoFalke)
fa8ec00 rpc: Check that left section is not multiline (MarcoFalke)

Pull request description:

  This is split out from bitcoin#18531 to just touch the RPC methods in server. Description from the main pr:

  ### Motivation

  RPCArg names in the rpc help are currently only used for documentation. However, in the future they could be used to teach the server the named arguments. Named arguments are currently registered by the `CRPCCommand`s and duplicate the RPCArg names from the documentation. This redundancy is fragile, and has lead to errors in the past (despite having linters to catch those kind of errors). See section "bugs found" for a list of bugs that have been found as a result of the changes here.

  ### Changes

  The changes here add an assert in the `CRPCCommand` constructor that the RPCArg names are identical to the ones in the `CRPCCommand`.

  ### Future work

  > Here or follow up, makes sense to also assert type of returned UniValue?

  Sure, but let's not get ahead of ourselves. I am going to submit any further works as follow-ups, including:

  * Removing the CRPCCommand arguments, now that they are asserted to be equal and thus redundant
  * Removing all python regex linters on the args, now that RPCMan can be used to generate any output, including the cli.cpp table
  * Auto-formatting and sanity checking the RPCExamples with RPCMan
  * Checking passed-in json in self-check. Removing redundant checks
  * Checking returned json against documentation to avoid regressions or false documentation
  * Compile the RPC documentation at compile-time to ensure it doesn't change at runtime and is completely static

  ### Bugs found

  * The assert identified issue bitcoin#18607
  * The changes itself fixed bug bitcoin#19250

ACKs for top commit:
  laanwj:
    ACK fa7592b
  ryanofsky:
    Code review ACK fa7592b. Looks great! Just some hidden arg and Check() and comment cleanups since last review

Tree-SHA512: e64b6a212f4a3aeedeee47557559bde104d5fd40cdc1746b27eb2f3d4c8885d5e6e4dd287595ea11cdbc6a939654fe103cae765fd505875444d851f0abb11308
  • Loading branch information
MarcoFalke committed Jul 15, 2020
2 parents d626a3b + fa7592b commit 804ca26
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 45 deletions.
50 changes: 27 additions & 23 deletions src/rpc/server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,9 @@ std::string CRPCTable::help(const std::string& strCommand, const JSONRPCRequest&
return strRet;
}

UniValue help(const JSONRPCRequest& jsonRequest)
static RPCHelpMan help()
{
if (jsonRequest.fHelp || jsonRequest.params.size() > 1)
throw std::runtime_error(
RPCHelpMan{"help",
return RPCHelpMan{"help",
"\nList all commands, or get help for a specified command.\n",
{
{"command", RPCArg::Type::STR, /* default */ "all commands", "The command to get help on"},
Expand All @@ -143,44 +141,46 @@ UniValue help(const JSONRPCRequest& jsonRequest)
RPCResult::Type::STR, "", "The help text"
},
RPCExamples{""},
}.ToString()
);

[&](const RPCHelpMan& self, const JSONRPCRequest& jsonRequest) -> UniValue
{
std::string strCommand;
if (jsonRequest.params.size() > 0)
strCommand = jsonRequest.params[0].get_str();

return tableRPC.help(strCommand, jsonRequest);
},
};
}


UniValue stop(const JSONRPCRequest& jsonRequest)
static RPCHelpMan stop()
{
static const std::string RESULT{PACKAGE_NAME " stopping"};
// Accept the deprecated and ignored 'detach' boolean argument
return RPCHelpMan{"stop",
// Also accept the hidden 'wait' integer argument (milliseconds)
// For instance, 'stop 1000' makes the call wait 1 second before returning
// to the client (intended for testing)
if (jsonRequest.fHelp || jsonRequest.params.size() > 1)
throw std::runtime_error(
RPCHelpMan{"stop",
"\nRequest a graceful shutdown of " PACKAGE_NAME ".",
{},
{
{"wait", RPCArg::Type::NUM, RPCArg::Optional::OMITTED_NAMED_ARG, "how long to wait in ms", "", {}, /* hidden */ true},
},
RPCResult{RPCResult::Type::STR, "", "A string with the content '" + RESULT + "'"},
RPCExamples{""},
}.ToString());
[&](const RPCHelpMan& self, const JSONRPCRequest& jsonRequest) -> UniValue
{
// Event loop will exit after current HTTP requests have been handled, so
// this reply will get back to the client.
StartShutdown();
if (jsonRequest.params[0].isNum()) {
UninterruptibleSleep(std::chrono::milliseconds{jsonRequest.params[0].get_int()});
}
return RESULT;
},
};
}

static UniValue uptime(const JSONRPCRequest& jsonRequest)
static RPCHelpMan uptime()
{
RPCHelpMan{"uptime",
return RPCHelpMan{"uptime",
"\nReturns the total uptime of the server.\n",
{},
RPCResult{
Expand All @@ -190,14 +190,16 @@ static UniValue uptime(const JSONRPCRequest& jsonRequest)
HelpExampleCli("uptime", "")
+ HelpExampleRpc("uptime", "")
},
}.Check(jsonRequest);

[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{
return GetTime() - GetStartupTime();
}
};
}

static UniValue getrpcinfo(const JSONRPCRequest& request)
static RPCHelpMan getrpcinfo()
{
RPCHelpMan{"getrpcinfo",
return RPCHelpMan{"getrpcinfo",
"\nReturns details of the RPC server.\n",
{},
RPCResult{
Expand All @@ -217,8 +219,8 @@ static UniValue getrpcinfo(const JSONRPCRequest& request)
RPCExamples{
HelpExampleCli("getrpcinfo", "")
+ HelpExampleRpc("getrpcinfo", "")},
}.Check(request);

[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{
LOCK(g_rpc_server_info.mutex);
UniValue active_commands(UniValue::VARR);
for (const RPCCommandExecutionInfo& info : g_rpc_server_info.active_commands) {
Expand All @@ -237,6 +239,8 @@ static UniValue getrpcinfo(const JSONRPCRequest& request)

return result;
}
};
}

// clang-format off
static const CRPCCommand vRPCCommands[] =
Expand Down
17 changes: 16 additions & 1 deletion src/rpc/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#include <amount.h>
#include <rpc/request.h>
#include <rpc/util.h>

#include <functional>
#include <map>
Expand Down Expand Up @@ -85,6 +86,7 @@ void RPCUnsetTimerInterface(RPCTimerInterface *iface);
void RPCRunLater(const std::string& name, std::function<void()> func, int64_t nSeconds);

typedef UniValue(*rpcfn_type)(const JSONRPCRequest& jsonRequest);
typedef RPCHelpMan (*RpcMethodFnType)();

class CRPCCommand
{
Expand All @@ -101,6 +103,19 @@ class CRPCCommand
{
}

//! Simplified constructor taking plain RpcMethodFnType function pointer.
CRPCCommand(std::string category, std::string name_in, RpcMethodFnType fn, std::vector<std::string> args_in)
: CRPCCommand(
category,
fn().m_name,
[fn](const JSONRPCRequest& request, UniValue& result, bool) { result = fn().HandleRequest(request); return true; },
fn().GetArgNames(),
intptr_t(fn))
{
CHECK_NONFATAL(fn().m_name == name_in);
CHECK_NONFATAL(fn().GetArgNames() == args_in);
}

//! Simplified constructor taking plain rpcfn_type function pointer.
CRPCCommand(const char* category, const char* name, rpcfn_type fn, std::initializer_list<const char*> args)
: CRPCCommand(category, name,
Expand All @@ -117,7 +132,7 @@ class CRPCCommand
};

/**
* Bitcoin RPC command dispatcher.
* RPC command dispatcher.
*/
class CRPCTable
{
Expand Down
44 changes: 25 additions & 19 deletions src/rpc/util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -385,9 +385,7 @@ struct Sections {
PushSection({indent + "]" + (outer_type != OuterType::NONE ? "," : ""), ""});
break;
}

// no default case, so the compiler can warn about missing cases
}
} // no default case, so the compiler can warn about missing cases
}

/**
Expand All @@ -398,6 +396,9 @@ struct Sections {
std::string ret;
const size_t pad = m_max_pad + 4;
for (const auto& s : m_sections) {
// The left part of a section is assumed to be a single line, usually it is the name of the JSON struct or a
// brace like {, }, [, or ]
CHECK_NONFATAL(s.m_left.find('\n') == std::string::npos);
if (s.m_right.empty()) {
ret += s.m_left;
ret += "\n";
Expand Down Expand Up @@ -432,7 +433,11 @@ struct Sections {
};

RPCHelpMan::RPCHelpMan(std::string name, std::string description, std::vector<RPCArg> args, RPCResults results, RPCExamples examples)
: RPCHelpMan{std::move(name), std::move(description), std::move(args), std::move(results), std::move(examples), nullptr} {}

RPCHelpMan::RPCHelpMan(std::string name, std::string description, std::vector<RPCArg> args, RPCResults results, RPCExamples examples, RPCMethodImpl fun)
: m_name{std::move(name)},
m_fun{std::move(fun)},
m_description{std::move(description)},
m_args{std::move(args)},
m_results{std::move(results)},
Expand Down Expand Up @@ -481,6 +486,16 @@ bool RPCHelpMan::IsValidNumArgs(size_t num_args) const
}
return num_required_args <= num_args && num_args <= m_args.size();
}

std::vector<std::string> RPCHelpMan::GetArgNames() const
{
std::vector<std::string> ret;
for (const auto& arg : m_args) {
ret.emplace_back(arg.m_names);
}
return ret;
}

std::string RPCHelpMan::ToString() const
{
std::string ret;
Expand All @@ -489,6 +504,7 @@ std::string RPCHelpMan::ToString() const
ret += m_name;
bool was_optional{false};
for (const auto& arg : m_args) {
if (arg.m_hidden) continue;
const bool optional = arg.IsOptional();
ret += " ";
if (optional) {
Expand All @@ -510,6 +526,7 @@ std::string RPCHelpMan::ToString() const
Sections sections;
for (size_t i{0}; i < m_args.size(); ++i) {
const auto& arg = m_args.at(i);
if (arg.m_hidden) continue;

if (i == 0) ret += "\nArguments:\n";

Expand Down Expand Up @@ -589,9 +606,7 @@ std::string RPCArg::ToDescriptionString() const
ret += "json array";
break;
}

// no default case, so the compiler can warn about missing cases
}
} // no default case, so the compiler can warn about missing cases
}
if (m_fallback.which() == 1) {
ret += ", optional, default=" + boost::get<std::string>(m_fallback);
Expand All @@ -609,9 +624,7 @@ std::string RPCArg::ToDescriptionString() const
ret += ", required";
break;
}

// no default case, so the compiler can warn about missing cases
}
} // no default case, so the compiler can warn about missing cases
}
ret += ")";
ret += m_description.empty() ? "" : " " + m_description;
Expand Down Expand Up @@ -706,10 +719,7 @@ void RPCResult::ToSections(Sections& sections, const OuterType outer_type, const
sections.PushSection({indent + "}" + maybe_separator, ""});
return;
}

// no default case, so the compiler can warn about missing cases
}

} // no default case, so the compiler can warn about missing cases
CHECK_NONFATAL(false);
}

Expand Down Expand Up @@ -746,9 +756,7 @@ std::string RPCArg::ToStringObj(const bool oneline) const
case Type::OBJ_USER_KEYS:
// Currently unused, so avoid writing dead code
CHECK_NONFATAL(false);

// no default case, so the compiler can warn about missing cases
}
} // no default case, so the compiler can warn about missing cases
CHECK_NONFATAL(false);
}

Expand Down Expand Up @@ -783,9 +791,7 @@ std::string RPCArg::ToString(const bool oneline) const
}
return "[" + res + "...]";
}

// no default case, so the compiler can warn about missing cases
}
} // no default case, so the compiler can warn about missing cases
CHECK_NONFATAL(false);
}

Expand Down
19 changes: 17 additions & 2 deletions src/rpc/util.h
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ struct RPCArg {
using Fallback = boost::variant<Optional, /* default value for optional args */ std::string>;
const std::string m_names; //!< The name of the arg (can be empty for inner args, can contain multiple aliases separated by | for named request arguments)
const Type m_type;
const bool m_hidden;
const std::vector<RPCArg> m_inner; //!< Only used for arrays or dicts
const Fallback m_fallback;
const std::string m_description;
Expand All @@ -159,9 +160,11 @@ struct RPCArg {
const Fallback fallback,
const std::string description,
const std::string oneline_description = "",
const std::vector<std::string> type_str = {})
const std::vector<std::string> type_str = {},
const bool hidden = false)
: m_names{std::move(name)},
m_type{std::move(type)},
m_hidden{hidden},
m_fallback{std::move(fallback)},
m_description{std::move(description)},
m_oneline_description{std::move(oneline_description)},
Expand All @@ -180,6 +183,7 @@ struct RPCArg {
const std::vector<std::string> type_str = {})
: m_names{std::move(name)},
m_type{std::move(type)},
m_hidden{false},
m_inner{std::move(inner)},
m_fallback{std::move(fallback)},
m_description{std::move(description)},
Expand Down Expand Up @@ -329,8 +333,15 @@ class RPCHelpMan
{
public:
RPCHelpMan(std::string name, std::string description, std::vector<RPCArg> args, RPCResults results, RPCExamples examples);
using RPCMethodImpl = std::function<UniValue(const RPCHelpMan&, const JSONRPCRequest&)>;
RPCHelpMan(std::string name, std::string description, std::vector<RPCArg> args, RPCResults results, RPCExamples examples, RPCMethodImpl fun);

std::string ToString() const;
UniValue HandleRequest(const JSONRPCRequest& request)
{
Check(request);
return m_fun(*this, request);
}
/** If the supplied number of args is neither too small nor too high */
bool IsValidNumArgs(size_t num_args) const;
/**
Expand All @@ -343,8 +354,12 @@ class RPCHelpMan
}
}

private:
std::vector<std::string> GetArgNames() const;

const std::string m_name;

private:
const RPCMethodImpl m_fun;
const std::string m_description;
const std::vector<RPCArg> m_args;
const RPCResults m_results;
Expand Down

0 comments on commit 804ca26

Please sign in to comment.