Skip to content

Commit

Permalink
rpc: Handle getinfo locally in bitcoin-cli w/ -getinfo
Browse files Browse the repository at this point in the history
Summary:
This adds the infrastructure `BaseRequestHandler` class that takes care
of converting bitcoin-cli arguments into a JSON-RPC request object, and
converting the reply into a JSON object that can be shown as result.

This is subsequently used to handle the `-getinfo` option, which sends
a JSON-RPC batch request to the RPC server with
`["getnetworkinfo", "getblockchaininfo", "getwalletinfo"]`,
and after reply combines the result into what looks like a `getinfo`
result.

There have been some requests for a client-side `getinfo` and this
is my PoC of how to do it. If this is considered a good idea
some of the logic could be moved up to rpcclient.cpp and
used in the GUI console as well.

Extra-Author: Andrew Chow <achow101@gmail.com>

Add test for bitcoin-cli -getinfo

Extra-Author: Andrew Chow <achow101@gmail.com>

Backport of PR10871
bitcoin/bitcoin#10871

Completes T586

Test Plan:
make check
test_runner.py
bitcoin-cli -getinfo

Reviewers: jasonbcox, deadalnix, Fabien, O1 Bitcoin ABC, #bitcoin_abc

Reviewed By: deadalnix, O1 Bitcoin ABC, #bitcoin_abc

Differential Revision: https://reviews.bitcoinabc.org/D2787
  • Loading branch information
laanwj authored and proteanx committed Apr 26, 2019
1 parent 0720e8d commit 9a00569
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 17 deletions.
147 changes: 130 additions & 17 deletions src/bitcoin-cli.cpp
Expand Up @@ -40,6 +40,13 @@ std::string HelpMessageCli() {
"-conf=<file>", strprintf(_("Specify configuration file (default: %s)"),
BITCOIN_CONF_FILENAME));
strUsage += HelpMessageOpt("-datadir=<dir>", _("Specify data directory"));
strUsage += HelpMessageOpt(
"-getinfo",
_("Get general information from the remote server. Note that unlike "
"server-side RPC calls, the results of -getinfo is the result of "
"multiple non-atomic requests. Some entries in the result may "
"represent results from different states (e.g. wallet balance may be "
"as of a different block from the chain state reported)"));
AppendParamsHelpMessages(strUsage);
strUsage += HelpMessageOpt(
"-named",
Expand Down Expand Up @@ -222,7 +229,113 @@ static void http_error_cb(enum evhttp_request_error err, void *ctx) {
}
#endif

static UniValue CallRPC(const std::string &strMethod, const UniValue &params) {
/**
* Class that handles the conversion from a command-line to a JSON-RPC request,
* as well as converting back to a JSON object that can be shown as result.
*/
class BaseRequestHandler {
public:
virtual UniValue PrepareRequest(const std::string &method,
const std::vector<std::string> &args) = 0;
virtual UniValue ProcessReply(const UniValue &batch_in) = 0;
};

/** Process getinfo requests */
class GetinfoRequestHandler : public BaseRequestHandler {
public:
const int ID_NETWORKINFO = 0;
const int ID_BLOCKCHAININFO = 1;
const int ID_WALLETINFO = 2;

/** Create a simulated `getinfo` request. */
UniValue PrepareRequest(const std::string &method,
const std::vector<std::string> &args) override {
UniValue result(UniValue::VARR);
result.push_back(
JSONRPCRequestObj("getnetworkinfo", NullUniValue, ID_NETWORKINFO));
result.push_back(JSONRPCRequestObj("getblockchaininfo", NullUniValue,
ID_BLOCKCHAININFO));
result.push_back(
JSONRPCRequestObj("getwalletinfo", NullUniValue, ID_WALLETINFO));
return result;
}

/** Collect values from the batch and form a simulated `getinfo` reply. */
UniValue ProcessReply(const UniValue &batch_in) override {
UniValue result(UniValue::VOBJ);
std::vector<UniValue> batch = JSONRPCProcessBatchReply(batch_in, 3);
// Errors in getnetworkinfo() and getblockchaininfo() are fatal, pass
// them on getwalletinfo() is allowed to fail in case there is no
// wallet.
if (!batch[ID_NETWORKINFO]["error"].isNull()) {
return batch[ID_NETWORKINFO];
}
if (!batch[ID_BLOCKCHAININFO]["error"].isNull()) {
return batch[ID_BLOCKCHAININFO];
}
result.pushKV("version", batch[ID_NETWORKINFO]["result"]["version"]);
result.pushKV("protocolversion",
batch[ID_NETWORKINFO]["result"]["protocolversion"]);
if (!batch[ID_WALLETINFO].isNull()) {
result.pushKV("walletversion",
batch[ID_WALLETINFO]["result"]["walletversion"]);
result.pushKV("balance", batch[ID_WALLETINFO]["result"]["balance"]);
}
result.pushKV("blocks", batch[ID_BLOCKCHAININFO]["result"]["blocks"]);
result.pushKV("timeoffset",
batch[ID_NETWORKINFO]["result"]["timeoffset"]);
result.pushKV("connections",
batch[ID_NETWORKINFO]["result"]["connections"]);
result.pushKV("proxy",
batch[ID_NETWORKINFO]["result"]["networks"][0]["proxy"]);
result.pushKV("difficulty",
batch[ID_BLOCKCHAININFO]["result"]["difficulty"]);
result.pushKV(
"testnet",
UniValue(batch[ID_BLOCKCHAININFO]["result"]["chain"].get_str() ==
"test"));
if (!batch[ID_WALLETINFO].isNull()) {
result.pushKV("walletversion",
batch[ID_WALLETINFO]["result"]["walletversion"]);
result.pushKV("balance", batch[ID_WALLETINFO]["result"]["balance"]);
result.pushKV("keypoololdest",
batch[ID_WALLETINFO]["result"]["keypoololdest"]);
result.pushKV("keypoolsize",
batch[ID_WALLETINFO]["result"]["keypoolsize"]);
if (!batch[ID_WALLETINFO]["result"]["unlocked_until"].isNull()) {
result.pushKV("unlocked_until",
batch[ID_WALLETINFO]["result"]["unlocked_until"]);
}
result.pushKV("paytxfee",
batch[ID_WALLETINFO]["result"]["paytxfee"]);
}
result.pushKV("relayfee", batch[ID_NETWORKINFO]["result"]["relayfee"]);
result.pushKV("warnings", batch[ID_NETWORKINFO]["result"]["warnings"]);
return JSONRPCReplyObj(result, NullUniValue, 1);
}
};

/** Process default single requests */
class DefaultRequestHandler : public BaseRequestHandler {
public:
UniValue PrepareRequest(const std::string &method,
const std::vector<std::string> &args) override {
UniValue params;
if (gArgs.GetBoolArg("-named", DEFAULT_NAMED)) {
params = RPCConvertNamedValues(method, args);
} else {
params = RPCConvertValues(method, args);
}
return JSONRPCRequestObj(method, params, 1);
}

UniValue ProcessReply(const UniValue &reply) override {
return reply.get_obj();
}
};

static UniValue CallRPC(BaseRequestHandler *rh, const std::string &strMethod,
const std::vector<std::string> &args) {
std::string host;
// In preference order, we choose the following for the port:
// 1. -rpcport
Expand Down Expand Up @@ -279,8 +392,7 @@ static UniValue CallRPC(const std::string &strMethod, const UniValue &params) {
(std::string("Basic ") + EncodeBase64(strRPCUserColonPass)).c_str());

// Attach request data
std::string strRequest =
JSONRPCRequestObj(strMethod, params, 1).write() + "\n";
std::string strRequest = rh->PrepareRequest(strMethod, args).write() + "\n";
struct evbuffer *output_buffer =
evhttp_request_get_output_buffer(req.get());
assert(output_buffer);
Expand Down Expand Up @@ -331,7 +443,7 @@ static UniValue CallRPC(const std::string &strMethod, const UniValue &params) {
if (!valReply.read(response.body)) {
throw std::runtime_error("couldn't parse reply from server");
}
const UniValue &reply = valReply.get_obj();
const UniValue reply = rh->ProcessReply(valReply);
if (reply.empty()) {
throw std::runtime_error(
"expected reply to have result, error and id properties");
Expand Down Expand Up @@ -365,26 +477,27 @@ int CommandLineRPC(int argc, char *argv[]) {
args.push_back(line);
}
}
if (args.size() < 1) {
throw std::runtime_error(
"too few parameters (need at least command)");
}
std::string strMethod = args[0];
// Remove trailing method name from arguments vector
args.erase(args.begin());

UniValue params;
if (gArgs.GetBoolArg("-named", DEFAULT_NAMED)) {
params = RPCConvertNamedValues(strMethod, args);
std::unique_ptr<BaseRequestHandler> rh;
std::string method;
if (gArgs.GetBoolArg("-getinfo", false)) {
rh.reset(new GetinfoRequestHandler());
method = "";
} else {
params = RPCConvertValues(strMethod, args);
rh.reset(new DefaultRequestHandler());
if (args.size() < 1) {
throw std::runtime_error(
"too few parameters (need at least command)");
}
method = args[0];
// Remove trailing method name from arguments vector
args.erase(args.begin());
}

// Execute and handle connection failures with -rpcwait
const bool fWait = gArgs.GetBoolArg("-rpcwait", false);
do {
try {
const UniValue reply = CallRPC(strMethod, params);
const UniValue reply = CallRPC(rh.get(), method, args);

// Parse reply
const UniValue &result = find_value(reply, "result");
Expand Down
19 changes: 19 additions & 0 deletions src/rpc/protocol.cpp
Expand Up @@ -118,3 +118,22 @@ void DeleteAuthCookie() {
__func__, e.what());
}
}

std::vector<UniValue> JSONRPCProcessBatchReply(const UniValue &in, size_t num) {
if (!in.isArray()) {
throw std::runtime_error("Batch must be an array");
}
std::vector<UniValue> batch(num);
for (size_t i = 0; i < in.size(); ++i) {
const UniValue &rec = in[i];
if (!rec.isObject()) {
throw std::runtime_error("Batch member must be object");
}
size_t id = rec["id"].get_int();
if (id >= num) {
throw std::runtime_error("Batch member id larger than size");
}
batch[id] = rec;
}
return batch;
}
2 changes: 2 additions & 0 deletions src/rpc/protocol.h
Expand Up @@ -131,5 +131,7 @@ bool GenerateAuthCookie(std::string *cookie_out);
bool GetAuthCookie(std::string *cookie_out);
/** Delete RPC authentication cookie from disk */
void DeleteAuthCookie();
/** Parse JSON-RPC batch reply into a vector */
std::vector<UniValue> JSONRPCProcessBatchReply(const UniValue &in, size_t num);

#endif // BITCOIN_RPCPROTOCOL_H
29 changes: 29 additions & 0 deletions test/functional/interface_bitcoin_cli.py
Expand Up @@ -46,6 +46,35 @@ def run_test(self):
assert_raises_process_error(1, "incorrect rpcuser or rpcpassword", self.nodes[0].cli(
'-rpcuser={}'.format(user), '-stdin', '-stdinrpcpass', input="foo").echo)

self.log.info(
"Compare responses from `bitcoin-cli -getinfo` and the RPCs data is retrieved from.")
cli_get_info = self.nodes[0].cli('-getinfo').help()
wallet_info = self.nodes[0].getwalletinfo()
network_info = self.nodes[0].getnetworkinfo()
blockchain_info = self.nodes[0].getblockchaininfo()

assert_equal(cli_get_info['version'], network_info['version'])
assert_equal(cli_get_info['protocolversion'],
network_info['protocolversion'])
assert_equal(cli_get_info['walletversion'],
wallet_info['walletversion'])
assert_equal(cli_get_info['balance'], wallet_info['balance'])
assert_equal(cli_get_info['blocks'], blockchain_info['blocks'])
assert_equal(cli_get_info['timeoffset'], network_info['timeoffset'])
assert_equal(cli_get_info['connections'], network_info['connections'])
assert_equal(cli_get_info['proxy'],
network_info['networks'][0]['proxy'])
assert_equal(cli_get_info['difficulty'], blockchain_info['difficulty'])
assert_equal(cli_get_info['testnet'],
blockchain_info['chain'] == "test")
assert_equal(cli_get_info['balance'], wallet_info['balance'])
assert_equal(cli_get_info['keypoololdest'],
wallet_info['keypoololdest'])
assert_equal(cli_get_info['keypoolsize'], wallet_info['keypoolsize'])
assert_equal(cli_get_info['paytxfee'], wallet_info['paytxfee'])
assert_equal(cli_get_info['relayfee'], network_info['relayfee'])
# unlocked_until is not tested because the wallet is not encrypted


if __name__ == '__main__':
TestBitcoinCli().main()

0 comments on commit 9a00569

Please sign in to comment.