Compare commits

...

4 Commits

Author SHA1 Message Date
Bart
b3cc014398 Merge remote-tracking branch 'origin/develop' into bthomee/rpc-method-name-constants
# Conflicts:
#	src/test/basics/PerfLog_test.cpp
#	src/xrpld/perflog/detail/PerfLogImp.cpp
2026-08-12 14:25:04 -04:00
Bart
f0900d4f49 refactor: Name the null-termination check
Two places now ask whether a string_view can be handed on as a C
string, and both had to spell it as a data() round-trip with a NOLINT
for the clang-tidy check that flags exactly that. Naming it once, as
isNullTerminated in StringUtilities.h, keeps the suppression and the
reasoning for it in one place instead of inline at each use, where a
reformat could separate a NOLINTNEXTLINE from the line it applies to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 17:47:28 -04:00
Bart
7a269a33d5 fix: Make the compile-time table checks portable
The static_assert validating RPCParser's command table sat inside the
class body, where a pointer to a member of RPCParser is not yet a
constant expression, since a static_assert expression is not one of the
complete-class contexts. Clang accepts it anyway; GCC rejects
`command.parse == nullptr`, so the coverage build failed to compile.
The check moves into a constexpr commandsValid() that a static_assert
just past the closing brace calls, where the class is complete.

Both null-termination checks tested `name.data()[name.size()]`, which
clang-tidy asks be written `name[name.size()]` -- correct advice for
std::string, but undefined for string_view, whose operator[] does not
reach the terminator. Instead of suppressing the check, both now assert
what the code actually relies on: that rebuilding the view from data()
as a C string, which is what json::StaticString goes on to do, yields
the same view. A slice of a literal loses its tail that way and an
unterminated one is not a constant expression at all. The two
StaticString sites borrow the C string deliberately, so they keep the
NOLINT that CurrentThreadName.cpp already uses for this.

The dispatch table's size was `std::size(kHandlerArray) + 2`, the 2
being the handlers that carry their name as a static member and so
cannot live in that array. They move into an array of their own and
both sizes come from std::size, so adding to either needs no edit to
the concatenation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:52:23 -04:00
Bart
fb36b0adc0 refactor: Build the RPC dispatch tables at compile time
A new MethodNames.h gives every RPC method a string_view constant,
shared by the handler table and the command-line parser table so the
two cannot drift apart. That lets a handler carry its name as a
string_view, and both tables become sorted constexpr arrays found by
binary search, replacing the HandlerTable singleton and its multimap
and RPCParser's runtime table. Their invariants -- a unique name, a
method, and a valid API version range -- are now a static_assert rather
than a logicError on the first request of the process. Handler::Method
becomes a plain function pointer: every method is a free function known
at compile time, so the std::function never captured anything, and
dropping it makes Handler a literal type, which is what a constexpr
table needs; it also makes doCommand's null-method branch dead, since
an empty std::function is no longer representable.

The names being program-lifetime constants removes work throughout.
getHandlerNames and commandLineMethodNames return a span over a
constexpr array instead of building a fresh std::set per call, one of
those on the startup path; PerfLogImp's rpc map keys on string_view, so
it needs neither a transparent hasher nor a string copy per key, and
reports counters through json::StaticString rather than duplicating
each key into the object. Command parameter counts become unsigned with
a named kUnlimitedParams; as ints with -1 for unlimited they were
compared against an unsigned size, so `count < minParams` promoted -1
and was always true, and only the `>= 0` guards kept that from being
reachable.

PerfLog no longer reaches into the RPC layer to learn which methods to
count: makePerfLog takes the names, so xrpld.perflog stops depending on
xrpld.rpc, and PerfLog_test uses five made-up labels, which drops
test.basics > xrpld.rpc as well. Both edges are gone from levelization.
Handler_test's benchmark now asks for kApiMinimumSupportedVersion
instead of a hardcoded 1 and fails if a lookup misses -- once API
versions 1 and 2 retire, getHandler(1, ...) would return at its bounds
check and the benchmark would have silently reported timings for that
check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 15:20:53 -04:00
20 changed files with 884 additions and 612 deletions

View File

@@ -82,7 +82,6 @@ test.app > xrpl.tx
test.basics > test.jtx
test.basics > xrpl.basics
test.basics > xrpl.core
test.basics > xrpld.rpc
test.basics > xrpl.json
test.basics > xrpl.protocol
test.beast > xrpl.basics
@@ -286,10 +285,10 @@ xrpld.perflog > xrpl.basics
xrpld.perflog > xrpl.config
xrpld.perflog > xrpl.core
xrpld.perflog > xrpld.app
xrpld.perflog > xrpld.rpc
xrpld.perflog > xrpl.json
xrpld.perflog > xrpl.nodestore
xrpld.perflog > xrpl.protocol
xrpld.perflog > xrpl.server
xrpld.rpc > xrpl.basics
xrpld.rpc > xrpl.config
xrpld.rpc > xrpl.core

View File

@@ -163,4 +163,31 @@ toUInt64(std::string const& s);
bool
isProperlyFormedTomlDomain(std::string_view domain);
/**
* Whether a view can be passed on as a C string.
*
* A view is only usable that way if it covers a whole null-terminated string
* rather than a slice of one, since a reader given just the pointer stops at
* the first null. This asks exactly that question: rebuild the view from its
* data() as a C string, as such a reader would, and see if it comes back
* unchanged. A slice comes back longer, having run past its own end.
*
* The byte after a view is not part of it, so this is only well defined when
* @p str points into storage that is known to hold a null somewhere at or
* after its end -- a string literal, or the buffer of a std::string. For a
* view built from a bare pointer and length it reads out of bounds, and in a
* constant expression that is a compile error rather than undefined behaviour.
*
* @param str The view to test.
* @return Whether @p str is null-terminated.
*/
constexpr bool
isNullTerminated(std::string_view str)
{
// Reading past the view is the point, so the usual warning about data() not
// being null-terminated does not apply.
// NOLINTNEXTLINE(bugprone-suspicious-stringview-data-usage)
return std::string_view{str.data()} == str;
}
} // namespace xrpl

View File

@@ -9,7 +9,8 @@
#include <filesystem>
#include <functional>
#include <memory>
#include <string>
#include <span>
#include <string_view>
namespace beast {
class Journal;
@@ -67,7 +68,7 @@ public:
* @param requestId Unique identifier to track command
*/
virtual void
rpcStart(std::string const& method, std::uint64_t requestId) = 0;
rpcStart(std::string_view method, std::uint64_t requestId) = 0;
/**
* Log successful finish of RPC call
@@ -76,7 +77,7 @@ public:
* @param requestId Unique identifier to track command
*/
virtual void
rpcFinish(std::string const& method, std::uint64_t requestId) = 0;
rpcFinish(std::string_view method, std::uint64_t requestId) = 0;
/**
* Log errored RPC call
@@ -85,7 +86,7 @@ public:
* @param requestId Unique identifier to track command
*/
virtual void
rpcError(std::string const& method, std::uint64_t requestId) = 0;
rpcError(std::string_view method, std::uint64_t requestId) = 0;
/**
* Log queued job
@@ -150,10 +151,20 @@ public:
PerfLog::Setup
setupPerfLog(Section const& section, std::filesystem::path const& configDir);
/**
* @param methodNames The RPC methods to count, one counter per name. Each name
* must be a view of a whole, null-terminated string literal rather than
* a slice of one, because the counters are reported as JSON keys that
* borrow the name and read it as a C string. The names must outlive the
* returned object, which holds views of them. Callers pass
* rpc::getHandlerNames(); it is an argument so that this layer needs to
* know nothing about the RPC dispatch table.
*/
std::unique_ptr<PerfLog>
makePerfLog(
PerfLog::Setup const& setup,
Application& app,
std::span<std::string_view const> methodNames,
beast::Journal journal,
std::function<void()>&& signalStop);
@@ -161,7 +172,7 @@ template <typename Func, class Rep, class Period>
auto
measureDurationAndLog(
Func&& func,
std::string const& actionDescription,
std::string_view actionDescription,
std::chrono::duration<Rep, Period> maxDelay,
beast::Journal const& journal)
{

View File

@@ -1,9 +1,6 @@
#include <test/jtx/Env.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/envconfig.h>
#include <xrpld/rpc/detail/Handler.h>
#include <xrpl/basics/random.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Journal.h>
@@ -16,6 +13,7 @@
#include <xrpl/protocol/jss.h>
#include <algorithm>
#include <array>
#include <chrono>
#include <cstdint>
#include <filesystem>
@@ -26,7 +24,9 @@
#include <memory>
#include <ostream>
#include <random>
#include <ranges>
#include <string>
#include <string_view>
#include <system_error>
#include <thread>
#include <utility>
@@ -42,6 +42,21 @@ class PerfLog_test : public beast::unit_test::Suite
using path = std::filesystem::path;
// The method names to count. PerfLog treats these as opaque keys, so these
// are made up rather than taken from the RPC dispatch table: this test then
// needs to know nothing about the RPC layer, and does not silently change
// shape when a method is added or removed.
//
// They are string literals because PerfLog stores views of these names and
// reads them back as C strings, so they must be null-terminated and outlive
// the PerfLog. Sorted, because the counters are reported in sorted order.
static constexpr std::array kMethodNames{
std::string_view{"method_a"},
std::string_view{"method_b"},
std::string_view{"method_c"},
std::string_view{"method_d"},
std::string_view{"method_e"}};
// We're only using Env for its Journal. That Journal gives better
// coverage in unit tests.
test::jtx::Env env_{*this, test::jtx::envconfig(), nullptr, beast::Severity::Disabled};
@@ -114,7 +129,7 @@ class PerfLog_test : public beast::unit_test::Suite
{
perf::PerfLog::Setup const setup{
.perfLog = withFile == WithFile::No ? "" : logFile(), .logInterval = logInterval()};
return perf::makePerfLog(setup, app, j, [this]() {
return perf::makePerfLog(setup, app, kMethodNames, j, [this]() {
signalStop();
return;
});
@@ -310,9 +325,11 @@ public:
auto perfLog{fixture.perfLog(withFile)};
perfLog->start();
// Get the all the labels we can use for RPC interfaces without
// causing an assert.
std::vector<char const*> labels = test::jtx::makeVector(xrpl::rpc::getHandlerNames());
// The labels we can use for RPC interfaces without causing an assert:
// exactly those the PerfLog was constructed with. Copied into a vector
// because they are shuffled below and then paired positionally with the
// request ids.
auto labels = std::ranges::to<std::vector>(kMethodNames);
std::shuffle(labels.begin(), labels.end(), defaultPrng());
// Get two IDs to associate with each label. Errors tend to happen at
@@ -347,7 +364,7 @@ public:
for (auto& label : labels)
{
// Expect every label in labels to have the same contents.
json::Value const& counter{countersJson[label]};
json::Value const& counter{countersJson[std::string{label}]};
BEAST_EXPECT(counter[jss::duration_us] == "0");
BEAST_EXPECT(counter[jss::errored] == "0");
BEAST_EXPECT(counter[jss::finished] == "0");
@@ -404,7 +421,7 @@ public:
// their durations with the appropriate labels.
{
// The first label is special. It should have "errored" : "0".
json::Value const& first = rpc[labels[0]];
json::Value const& first = rpc[std::string{labels[0]}];
BEAST_EXPECT(first[jss::duration_us] != "0");
BEAST_EXPECT(first[jss::errored] == "0");
BEAST_EXPECT(first[jss::finished] == "1");
@@ -415,7 +432,7 @@ public:
std::uint64_t prevDur = std::numeric_limits<std::uint64_t>::max();
for (int i = 1; i < labels.size(); ++i)
{
json::Value const& counter{rpc[labels[i]]};
json::Value const& counter{rpc[std::string{labels[i]}]};
std::uint64_t const dur{jsonToUInt64(counter[jss::duration_us])};
BEAST_EXPECT(dur != 0 && dur < prevDur);
prevDur = dur;
@@ -447,7 +464,7 @@ public:
BEAST_EXPECT(only.size() == 2);
BEAST_EXPECT(only.isObject());
BEAST_EXPECT(only[jss::duration_us] != "0");
BEAST_EXPECT(only[jss::method] == labels[0]);
BEAST_EXPECT(only[jss::method] == std::string{labels[0]});
};
// Validate the final state of the PerfLog.

View File

@@ -9,6 +9,7 @@
#include <memory>
#include <mutex>
#include <string>
#include <string_view>
namespace xrpl {
@@ -21,17 +22,17 @@ namespace perf {
class PerfLogTest : public PerfLog
{
void
rpcStart(std::string const& method, std::uint64_t requestId) override
rpcStart(std::string_view method, std::uint64_t requestId) override
{
}
void
rpcFinish(std::string const& method, std::uint64_t requestId) override
rpcFinish(std::string_view method, std::uint64_t requestId) override
{
}
void
rpcError(std::string const& method, std::uint64_t dur) override
rpcError(std::string_view method, std::uint64_t requestId) override
{
}

View File

@@ -43,7 +43,6 @@
#include <memory>
#include <mutex>
#include <optional>
#include <ranges>
#include <source_location>
#include <string>
#include <tuple>
@@ -316,13 +315,6 @@ auto const kData = JTxFieldWrapper<BlobField>(sfData);
auto const kAmount = JTxFieldWrapper<StAmountField>(sfAmount);
template <std::ranges::range Input>
auto
makeVector(Input const& input)
{
return std::vector(std::ranges::begin(input), std::ranges::end(input));
}
// Functions used in debugging
json::Value
getAccountOffers(Env& env, AccountID const& acct, bool current = false);

View File

@@ -1,9 +1,8 @@
#include <test/jtx/TestHelpers.h>
#include <xrpld/rpc/detail/Handler.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/protocol/ApiVersion.h>
#include <algorithm>
#include <array>
@@ -13,8 +12,8 @@
#include <cstddef>
#include <iostream>
#include <random>
#include <string>
#include <tuple>
#include <vector>
// cspell: words stdev
namespace xrpl::test {
@@ -88,21 +87,41 @@ class Handler_test : public beast::unit_test::Suite
std::random_device dev;
std::ranlux48 prng(dev());
std::vector<char const*> names = test::jtx::makeVector(xrpl::rpc::getHandlerNames());
// Contiguous, so the timed loop's pick-a-name-by-index costs nothing
// and the measurement reflects getHandler() alone.
auto const names = xrpl::rpc::getHandlerNames();
std::uniform_int_distribution<std::size_t> distr{0, names.size() - 1};
// The lowest version still served. Asking for one outside the supported
// range would make getHandler() return at its bounds check, without
// searching, and the benchmark would then be timing that check.
constexpr unsigned kVersion = rpc::kApiMinimumSupportedVersion;
std::size_t dummy = 0;
std::size_t misses = 0;
auto const [mean, stdev, n] = time(
1'000'000,
[&](std::size_t i) {
auto const d = rpc::getHandler(1, false, names[i]);
auto const d = rpc::getHandler(kVersion, false, names[i]);
if (d == nullptr)
{
++misses;
return;
}
dummy = dummy + i + (int)d->role;
},
[&]() -> std::size_t { return distr(prng); });
std::cout << "mean=" << mean << " stdev=" << stdev << " N=" << n << '\n';
// A miss means the timed call did no lookup, so the figure above is not
// a measurement of one. Every name comes from getHandlerNames(), so a
// handler answering at kVersion is the only way this holds.
BEAST_EXPECTS(
misses == 0,
std::to_string(misses) + " of " + std::to_string(n) + " lookups at API version " +
std::to_string(kVersion) + " found no handler, so nothing was measured");
BEAST_EXPECT(dummy != 0);
}

View File

@@ -3,6 +3,9 @@
#include <test/jtx/utility.h>
#include <xrpld/core/Config.h>
#include <xrpld/rpc/MethodNames.h>
#include <xrpld/rpc/RPCCall.h>
#include <xrpld/rpc/detail/Handler.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/json/json_reader.h>
@@ -12,6 +15,8 @@
#include <boost/algorithm/string/replace.hpp>
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstring>
#include <initializer_list>
@@ -5923,10 +5928,38 @@ public:
}
}
// Every name in the command-line table must also name a server method, so
// that a command accepted on the command line can actually be dispatched.
//
// The reverse does not hold: a server method needs no command-line form.
// Three command-line names are also exempt because they are wrappers that
// forward a caller-supplied method rather than naming one themselves.
void
testCommandLineNamesAreDispatchable()
{
testcase("Command-line methods are dispatchable");
static constexpr std::array kWrappers{
rpc::method::kInternal, rpc::method::kJson, rpc::method::kJson2};
auto const dispatchable = rpc::getHandlerNames();
BEAST_EXPECT(!dispatchable.empty());
for (auto const& name : commandLineMethodNames())
{
if (std::ranges::find(kWrappers, name) != kWrappers.end())
continue;
// Both name lists are sorted, so a binary search suffices.
BEAST_EXPECTS(std::ranges::binary_search(dispatchable, name), std::string{name});
}
}
void
run() override
{
forAllApiVersions([this](unsigned apiVersion) { testRPCCall(apiVersion); });
testCommandLineNamesAreDispatchable();
}
};

View File

@@ -35,6 +35,7 @@
#include <xrpld/rpc/RPCHandler.h>
#include <xrpld/rpc/Role.h>
#include <xrpld/rpc/ServerHandler.h>
#include <xrpld/rpc/detail/Handler.h>
#include <xrpld/rpc/detail/PathRequestManager.h>
#include <xrpld/rpc/detail/Pathfinder.h>
#include <xrpld/shamap/NodeFamily.h>
@@ -320,6 +321,7 @@ public:
perf::makePerfLog(
perf::setupPerfLog(config_->section(Sections::kPerf), config_->configDir),
*this,
rpc::getHandlerNames(),
logs_->journal("PerfLog"),
[this] { signalStop("PerfLog"); }))
, txMaster_(*this)

View File

@@ -3,6 +3,7 @@
#include <xrpld/app/main/Application.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/core/CurrentThreadName.h>
#include <xrpl/beast/utility/Journal.h>
@@ -16,6 +17,7 @@
#include <xrpl/json/json_writer.h>
#include <xrpl/nodestore/Database.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/server/NetworkOPs.h>
#include <chrono>
#include <cstdint>
@@ -25,8 +27,9 @@
#include <memory>
#include <mutex>
#include <ostream>
#include <set>
#include <span>
#include <string>
#include <string_view>
#include <system_error>
#include <unordered_map>
#include <utility>
@@ -34,14 +37,25 @@
namespace xrpl::perf {
PerfLogImp::Counters::Counters(std::set<char const*> const& labels, JobTypes const& jobTypes)
PerfLogImp::Counters::Counters(std::span<std::string_view const> labels, JobTypes const& jobTypes)
{
{
// populateRpc
rpc.reserve(labels.size());
for (std::string const label : labels)
for (auto const& label : labels)
{
auto const inserted = rpc.emplace(label, Rpc()).second;
// countersJson() reports these through json::StaticString, which
// reads them as C strings, so each must be a view of a whole string
// literal rather than a slice of one. Checked here, where the names
// arrive, rather than on every report. Callers pass a compile-time
// constant list and this runs once, before any thread starts, so a
// bad name faults every startup rather than some later request.
XRPL_ASSERT(
isNullTerminated(label),
"xrpl::perf::PerfLogImp::Counters::Counters : label is "
"null-terminated");
auto const inserted = rpc.try_emplace(label).second;
if (!inserted)
{
// Ensure that no other function populates this entry.
@@ -100,7 +114,11 @@ PerfLogImp::Counters::countersJson() const
totalRpc.errored += value.errored;
p[jss::duration_us] = std::to_string(value.duration.count());
totalRpc.duration += value.duration;
rpcobj[proc.first] = p;
// The key outlives the program and, per the constructor's assert, is
// null-terminated, so it can be borrowed as a C string rather than
// duplicated into the object.
// NOLINTNEXTLINE(bugprone-suspicious-stringview-data-usage)
rpcobj[json::StaticString{proc.first.data()}] = p;
}
if (totalRpc.started != 0u)
@@ -195,7 +213,9 @@ PerfLogImp::Counters::currentJson() const
for (auto m : methods)
{
json::Value methodobj(json::ValueType::Object);
methodobj[jss::method] = m.first;
// Borrowed as a C string, as in countersJson() above.
// NOLINTNEXTLINE(bugprone-suspicious-stringview-data-usage)
methodobj[jss::method] = json::StaticString{m.first.data()};
methodobj[jss::duration_us] =
std::to_string(std::chrono::duration_cast<microseconds>(present - m.second).count());
methodsArray.append(methodobj);
@@ -299,9 +319,14 @@ PerfLogImp::report()
PerfLogImp::PerfLogImp(
Setup setup,
Application& app,
std::span<std::string_view const> methodNames,
beast::Journal journal,
std::function<void()>&& signalStop)
: setup_(std::move(setup)), app_(app), j_(journal), signalStop_(std::move(signalStop))
: setup_(std::move(setup))
, app_(app)
, j_(journal)
, signalStop_(std::move(signalStop))
, counters_(methodNames, JobTypes::instance())
{
openLog();
}
@@ -312,7 +337,7 @@ PerfLogImp::~PerfLogImp()
}
void
PerfLogImp::rpcStart(std::string const& method, std::uint64_t const requestId)
PerfLogImp::rpcStart(std::string_view method, std::uint64_t const requestId)
{
auto counter = counters_.rpc.find(method);
if (counter == counters_.rpc.end())
@@ -328,11 +353,11 @@ PerfLogImp::rpcStart(std::string const& method, std::uint64_t const requestId)
++counter->second.value.started;
}
std::scoped_lock const lock(counters_.methodsMutex);
counters_.methods[requestId] = {counter->first.c_str(), steady_clock::now()};
counters_.methods[requestId] = {counter->first, steady_clock::now()};
}
void
PerfLogImp::rpcEnd(std::string const& method, std::uint64_t const requestId, bool finish)
PerfLogImp::rpcEnd(std::string_view method, std::uint64_t const requestId, bool finish)
{
auto counter = counters_.rpc.find(method);
if (counter == counters_.rpc.end())
@@ -501,10 +526,11 @@ std::unique_ptr<PerfLog>
makePerfLog(
PerfLog::Setup const& setup,
Application& app,
std::span<std::string_view const> methodNames,
beast::Journal journal,
std::function<void()>&& signalStop)
{
return std::make_unique<PerfLogImp>(setup, app, journal, std::move(signalStop));
return std::make_unique<PerfLogImp>(setup, app, methodNames, journal, std::move(signalStop));
}
} // namespace xrpl::perf

View File

@@ -1,7 +1,5 @@
#pragma once
#include <xrpld/rpc/detail/Handler.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/core/Job.h>
#include <xrpl/core/JobTypes.h>
@@ -15,8 +13,9 @@
#include <fstream>
#include <functional>
#include <mutex>
#include <set>
#include <span>
#include <string>
#include <string_view>
#include <thread>
#include <unordered_map>
#include <utility>
@@ -59,7 +58,8 @@ class PerfLogImp : public PerfLog
struct Counters
{
public:
using MethodStart = std::pair<char const*, steady_time_point>;
using MethodStart = std::pair<std::string_view, steady_time_point>;
/**
* RPC performance counters.
*/
@@ -91,14 +91,18 @@ class PerfLogImp : public PerfLog
// rpc and jq do not need mutex protection because all
// keys and values are created before more threads are started.
std::unordered_map<std::string, Locked<Rpc>> rpc;
//
// Every key is a view of a method name constant, which outlives the
// program, so the map neither copies a name to store it nor needs one
// materialized to look it up.
std::unordered_map<std::string_view, Locked<Rpc>> rpc;
std::unordered_map<JobType, Locked<Jq>> jq;
std::vector<std::pair<JobType, steady_time_point>> jobs;
mutable std::mutex jobsMutex;
std::unordered_map<std::uint64_t, MethodStart> methods;
mutable std::mutex methodsMutex;
Counters(std::set<char const*> const& labels, JobTypes const& jobTypes);
Counters(std::span<std::string_view const> labels, JobTypes const& jobTypes);
json::Value
countersJson() const;
json::Value
@@ -109,7 +113,7 @@ class PerfLogImp : public PerfLog
Application& app_;
beast::Journal const j_;
std::function<void()> const signalStop_;
Counters counters_{xrpl::rpc::getHandlerNames(), JobTypes::instance()};
Counters counters_;
std::ofstream logFile_;
std::thread thread_;
std::mutex mutex_;
@@ -126,28 +130,35 @@ class PerfLogImp : public PerfLog
void
report();
void
rpcEnd(std::string const& method, std::uint64_t const requestId, bool finish);
rpcEnd(std::string_view method, std::uint64_t const requestId, bool finish);
public:
/**
* @param methodNames The RPC methods to count, one counter per name. The
* names must outlive this object, which holds views of them. Passed
* in rather than looked up here so that this layer needs to know
* nothing about the RPC dispatch table.
*/
PerfLogImp(
Setup setup,
Application& app,
std::span<std::string_view const> methodNames,
beast::Journal journal,
std::function<void()>&& signalStop);
~PerfLogImp() override;
void
rpcStart(std::string const& method, std::uint64_t const requestId) override;
rpcStart(std::string_view method, std::uint64_t const requestId) override;
void
rpcFinish(std::string const& method, std::uint64_t const requestId) override
rpcFinish(std::string_view method, std::uint64_t const requestId) override
{
rpcEnd(method, requestId, true);
}
void
rpcError(std::string const& method, std::uint64_t const requestId) override
rpcError(std::string_view method, std::uint64_t const requestId) override
{
rpcEnd(method, requestId, false);
}

View File

@@ -0,0 +1,93 @@
#pragma once
#include <string_view>
namespace xrpl::rpc::method {
/**
* Names of the RPC methods the server accepts.
*
* Defined here so the dispatch table in Handler.cpp and the command-line parser
* table in RPCCall.cpp name each method through the same constant, and cannot
* drift apart.
*/
inline constexpr std::string_view kAccountChannels{"account_channels"};
inline constexpr std::string_view kAccountCurrencies{"account_currencies"};
inline constexpr std::string_view kAccountInfo{"account_info"};
inline constexpr std::string_view kAccountLines{"account_lines"};
inline constexpr std::string_view kAccountNfts{"account_nfts"};
inline constexpr std::string_view kAccountObjects{"account_objects"};
inline constexpr std::string_view kAccountOffers{"account_offers"};
inline constexpr std::string_view kAccountTx{"account_tx"};
inline constexpr std::string_view kAmmInfo{"amm_info"};
inline constexpr std::string_view kBlacklist{"blacklist"}; // no command-line form
inline constexpr std::string_view kBookChanges{"book_changes"};
inline constexpr std::string_view kBookOffers{"book_offers"};
inline constexpr std::string_view kCanDelete{"can_delete"};
inline constexpr std::string_view kChannelAuthorize{"channel_authorize"};
inline constexpr std::string_view kChannelVerify{"channel_verify"};
inline constexpr std::string_view kConnect{"connect"};
inline constexpr std::string_view kConsensusInfo{"consensus_info"};
inline constexpr std::string_view kDepositAuthorized{"deposit_authorized"};
inline constexpr std::string_view kFeature{"feature"};
inline constexpr std::string_view kFee{"fee"}; // no command-line form
inline constexpr std::string_view kFetchInfo{"fetch_info"};
inline constexpr std::string_view kGatewayBalances{"gateway_balances"};
inline constexpr std::string_view kGetAggregatePrice{
"get_aggregate_price"}; // no command-line form
inline constexpr std::string_view kGetCounts{"get_counts"};
inline constexpr std::string_view kInternal{"internal"}; // command-line wrapper
inline constexpr std::string_view kJson{"json"}; // command-line wrapper
inline constexpr std::string_view kJson2{"json2"}; // command-line wrapper
inline constexpr std::string_view kLedger{"ledger"};
inline constexpr std::string_view kLedgerAccept{"ledger_accept"};
inline constexpr std::string_view kLedgerCleaner{"ledger_cleaner"}; // no command-line form
inline constexpr std::string_view kLedgerClosed{"ledger_closed"};
inline constexpr std::string_view kLedgerCurrent{"ledger_current"};
inline constexpr std::string_view kLedgerData{"ledger_data"}; // no command-line form
inline constexpr std::string_view kLedgerEntry{"ledger_entry"};
inline constexpr std::string_view kLedgerHeader{"ledger_header"};
inline constexpr std::string_view kLedgerRequest{"ledger_request"};
inline constexpr std::string_view kLogLevel{"log_level"};
inline constexpr std::string_view kLogrotate{"logrotate"};
inline constexpr std::string_view kManifest{"manifest"};
inline constexpr std::string_view kNftBuyOffers{"nft_buy_offers"}; // no command-line form
inline constexpr std::string_view kNftSellOffers{"nft_sell_offers"}; // no command-line form
inline constexpr std::string_view kNorippleCheck{"noripple_check"}; // no command-line form
inline constexpr std::string_view kOwnerInfo{"owner_info"};
inline constexpr std::string_view kPathFind{"path_find"};
inline constexpr std::string_view kPeerReservationsAdd{"peer_reservations_add"};
inline constexpr std::string_view kPeerReservationsDel{"peer_reservations_del"};
inline constexpr std::string_view kPeerReservationsList{"peer_reservations_list"};
inline constexpr std::string_view kPeers{"peers"};
inline constexpr std::string_view kPing{"ping"};
inline constexpr std::string_view kPrint{"print"};
inline constexpr std::string_view kRandom{"random"};
inline constexpr std::string_view kRipplePathFind{"ripple_path_find"};
inline constexpr std::string_view kServerDefinitions{"server_definitions"};
inline constexpr std::string_view kServerInfo{"server_info"};
inline constexpr std::string_view kServerState{"server_state"};
inline constexpr std::string_view kSign{"sign"};
inline constexpr std::string_view kSignFor{"sign_for"};
inline constexpr std::string_view kSimulate{"simulate"};
inline constexpr std::string_view kStop{"stop"};
inline constexpr std::string_view kSubmit{"submit"};
inline constexpr std::string_view kSubmitMultisigned{"submit_multisigned"};
inline constexpr std::string_view kSubscribe{"subscribe"};
inline constexpr std::string_view kTransactionEntry{"transaction_entry"};
inline constexpr std::string_view kTx{"tx"};
inline constexpr std::string_view kTxHistory{"tx_history"};
inline constexpr std::string_view kTxReduceRelay{"tx_reduce_relay"}; // no command-line form
inline constexpr std::string_view kUnlList{"unl_list"};
inline constexpr std::string_view kUnsubscribe{"unsubscribe"};
inline constexpr std::string_view kValidationCreate{"validation_create"};
inline constexpr std::string_view kValidatorInfo{"validator_info"};
inline constexpr std::string_view kValidatorListSites{
"validator_list_sites"}; // no command-line form
inline constexpr std::string_view kValidators{"validators"}; // no command-line form
inline constexpr std::string_view kVaultInfo{"vault_info"};
inline constexpr std::string_view kVersion{"version"};
inline constexpr std::string_view kWalletPropose{"wallet_propose"};
} // namespace xrpl::rpc::method

View File

@@ -10,7 +10,9 @@
#include <cstdint>
#include <functional>
#include <span>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>
@@ -56,6 +58,15 @@ rpcCmdToJson(
unsigned int apiVersion,
beast::Journal j);
/**
* Return the names of all methods accepted on the command line.
*
* The names view refers to storage that outlives the program, so it is safe to
* hold on to.
*/
std::span<std::string_view const>
commandLineMethodNames();
/**
* Internal invocation of RPC client.
* Used by both xrpld command line as well as xrpld unit tests

View File

@@ -6,7 +6,7 @@
#include <xrpl/json/json_value.h>
#include <string>
#include <string_view>
namespace xrpl::rpc {
@@ -19,6 +19,6 @@ Status
doCommand(rpc::JsonContext&, json::Value&);
Role
roleRequired(unsigned int version, bool betaEnabled, std::string const& method);
roleRequired(unsigned int version, bool betaEnabled, std::string_view method);
} // namespace xrpl::rpc

View File

@@ -1,50 +1,53 @@
#include <xrpld/rpc/detail/Handler.h>
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/MethodNames.h>
#include <xrpld/rpc/Role.h>
#include <xrpld/rpc/Status.h>
#include <xrpld/rpc/handlers/Handlers.h>
#include <xrpld/rpc/handlers/ledger/Ledger.h>
#include <xrpld/rpc/handlers/server_info/Version.h>
#include <xrpl/basics/contract.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/ApiVersion.h>
#include <algorithm>
#include <array>
#include <cstddef>
#include <map>
#include <set>
#include <string>
#include <utility>
#include <iterator>
#include <span>
#include <string_view>
namespace xrpl::rpc {
namespace {
/**
* Adjust an old-style handler to be call-by-reference.
*
* The handler is a template parameter rather than an argument, so that byRef
* names a plain function instead of returning a closure over it.
*/
template <typename Function>
Handler::Method<json::Value>
byRef(Function const& f)
template <json::Value (*Function)(JsonContext&)>
Status
byRef(JsonContext& context, json::Value& result)
{
return [f](JsonContext& context, json::Value& result) {
result = f(context);
if (result.type() != json::ValueType::Object)
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::rpc::byRef : result is object");
result = rpc::makeObjectValue(result);
// LCOV_EXCL_STOP
}
result = Function(context);
if (result.type() != json::ValueType::Object)
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::rpc::byRef : result is object");
result = rpc::makeObjectValue(result);
// LCOV_EXCL_STOP
}
return Status();
};
return Status();
}
template <class Object, class HandlerImpl>
template <class HandlerImpl>
Status
handle(JsonContext& context, Object& object)
handle(JsonContext& context, json::Value& object)
{
XRPL_ASSERT(
context.apiVersion >= HandlerImpl::minApiVer &&
@@ -65,427 +68,399 @@ handle(JsonContext& context, Object& object)
}
template <typename HandlerImpl>
Handler
constexpr Handler
handlerFrom()
{
static_assert(HandlerImpl::minApiVer <= HandlerImpl::maxApiVer);
static_assert(HandlerImpl::maxApiVer <= rpc::kApiMaximumValidVersion);
static_assert(rpc::kApiMinimumSupportedVersion <= HandlerImpl::minApiVer);
return {
HandlerImpl::name,
&handle<json::Value, HandlerImpl>,
&handle<HandlerImpl>,
HandlerImpl::role,
HandlerImpl::condition,
HandlerImpl::minApiVer,
HandlerImpl::maxApiVer};
}
Handler const kHandlerArray[]{
// Some handlers not specified here are added to the table via addHandler()
// The handlers, in whatever order reads best. getHandler() searches kHandlers
// below, which is this array sorted; the order here carries no meaning.
constexpr Handler kHandlerArray[]{
// Request-response methods
{.name = "account_info",
.valueMethod = byRef(&doAccountInfo),
{.name = method::kAccountInfo,
.valueMethod = &byRef<&doAccountInfo>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "account_currencies",
.valueMethod = byRef(&doAccountCurrencies),
{.name = method::kAccountCurrencies,
.valueMethod = &byRef<&doAccountCurrencies>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "account_lines",
.valueMethod = byRef(&doAccountLines),
{.name = method::kAccountLines,
.valueMethod = &byRef<&doAccountLines>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "account_channels",
.valueMethod = byRef(&doAccountChannels),
{.name = method::kAccountChannels,
.valueMethod = &byRef<&doAccountChannels>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "account_nfts",
.valueMethod = byRef(&doAccountNFTs),
{.name = method::kAccountNfts,
.valueMethod = &byRef<&doAccountNFTs>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "account_objects",
.valueMethod = byRef(&doAccountObjects),
{.name = method::kAccountObjects,
.valueMethod = &byRef<&doAccountObjects>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "account_offers",
.valueMethod = byRef(&doAccountOffers),
{.name = method::kAccountOffers,
.valueMethod = &byRef<&doAccountOffers>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "account_tx",
.valueMethod = byRef(&doAccountTx),
{.name = method::kAccountTx,
.valueMethod = &byRef<&doAccountTx>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "amm_info",
.valueMethod = byRef(&doAMMInfo),
{.name = method::kAmmInfo,
.valueMethod = &byRef<&doAMMInfo>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "blacklist",
.valueMethod = byRef(&doBlackList),
{.name = method::kBlacklist,
.valueMethod = &byRef<&doBlackList>,
.role = Role::ADMIN,
.condition = Condition::NoCondition},
{.name = "book_changes",
.valueMethod = byRef(&doBookChanges),
{.name = method::kBookChanges,
.valueMethod = &byRef<&doBookChanges>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "book_offers",
.valueMethod = byRef(&doBookOffers),
{.name = method::kBookOffers,
.valueMethod = &byRef<&doBookOffers>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "can_delete",
.valueMethod = byRef(&doCanDelete),
{.name = method::kCanDelete,
.valueMethod = &byRef<&doCanDelete>,
.role = Role::ADMIN,
.condition = Condition::NoCondition},
{.name = "channel_authorize",
.valueMethod = byRef(&doChannelAuthorize),
{.name = method::kChannelAuthorize,
.valueMethod = &byRef<&doChannelAuthorize>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "channel_verify",
.valueMethod = byRef(&doChannelVerify),
{.name = method::kChannelVerify,
.valueMethod = &byRef<&doChannelVerify>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "connect",
.valueMethod = byRef(&doConnect),
{.name = method::kConnect,
.valueMethod = &byRef<&doConnect>,
.role = Role::ADMIN,
.condition = Condition::NoCondition},
{.name = "consensus_info",
.valueMethod = byRef(&doConsensusInfo),
{.name = method::kConsensusInfo,
.valueMethod = &byRef<&doConsensusInfo>,
.role = Role::ADMIN,
.condition = Condition::NoCondition},
{.name = "deposit_authorized",
.valueMethod = byRef(&doDepositAuthorized),
{.name = method::kDepositAuthorized,
.valueMethod = &byRef<&doDepositAuthorized>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "feature",
.valueMethod = byRef(&doFeature),
{.name = method::kFeature,
.valueMethod = &byRef<&doFeature>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "fee",
.valueMethod = byRef(&doFee),
{.name = method::kFee,
.valueMethod = &byRef<&doFee>,
.role = Role::USER,
.condition = Condition::NeedsCurrentLedger},
{.name = "fetch_info",
.valueMethod = byRef(&doFetchInfo),
{.name = method::kFetchInfo,
.valueMethod = &byRef<&doFetchInfo>,
.role = Role::ADMIN,
.condition = Condition::NoCondition},
{.name = "gateway_balances",
.valueMethod = byRef(&doGatewayBalances),
{.name = method::kGatewayBalances,
.valueMethod = &byRef<&doGatewayBalances>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "get_counts",
.valueMethod = byRef(&doGetCounts),
{.name = method::kGetCounts,
.valueMethod = &byRef<&doGetCounts>,
.role = Role::ADMIN,
.condition = Condition::NoCondition},
{.name = "get_aggregate_price",
.valueMethod = byRef(&doGetAggregatePrice),
{.name = method::kGetAggregatePrice,
.valueMethod = &byRef<&doGetAggregatePrice>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "ledger_accept",
.valueMethod = byRef(&doLedgerAccept),
{.name = method::kLedgerAccept,
.valueMethod = &byRef<&doLedgerAccept>,
.role = Role::ADMIN,
.condition = Condition::NeedsCurrentLedger},
{.name = "ledger_cleaner",
.valueMethod = byRef(&doLedgerCleaner),
{.name = method::kLedgerCleaner,
.valueMethod = &byRef<&doLedgerCleaner>,
.role = Role::ADMIN,
.condition = Condition::NeedsNetworkConnection},
{.name = "ledger_closed",
.valueMethod = byRef(&doLedgerClosed),
{.name = method::kLedgerClosed,
.valueMethod = &byRef<&doLedgerClosed>,
.role = Role::USER,
.condition = Condition::NeedsClosedLedger},
{.name = "ledger_current",
.valueMethod = byRef(&doLedgerCurrent),
{.name = method::kLedgerCurrent,
.valueMethod = &byRef<&doLedgerCurrent>,
.role = Role::USER,
.condition = Condition::NeedsCurrentLedger},
{.name = "ledger_data",
.valueMethod = byRef(&doLedgerData),
{.name = method::kLedgerData,
.valueMethod = &byRef<&doLedgerData>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "ledger_entry",
.valueMethod = byRef(&doLedgerEntry),
{.name = method::kLedgerEntry,
.valueMethod = &byRef<&doLedgerEntry>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "ledger_header",
.valueMethod = byRef(&doLedgerHeader),
{.name = method::kLedgerHeader,
.valueMethod = &byRef<&doLedgerHeader>,
.role = Role::USER,
.condition = Condition::NoCondition,
.minApiVer = 1,
.maxApiVer = 1},
{.name = "ledger_request",
.valueMethod = byRef(&doLedgerRequest),
{.name = method::kLedgerRequest,
.valueMethod = &byRef<&doLedgerRequest>,
.role = Role::ADMIN,
.condition = Condition::NoCondition},
{.name = "log_level",
.valueMethod = byRef(&doLogLevel),
{.name = method::kLogLevel,
.valueMethod = &byRef<&doLogLevel>,
.role = Role::ADMIN,
.condition = Condition::NoCondition},
{.name = "logrotate",
.valueMethod = byRef(&doLogRotate),
{.name = method::kLogrotate,
.valueMethod = &byRef<&doLogRotate>,
.role = Role::ADMIN,
.condition = Condition::NoCondition},
{.name = "manifest",
.valueMethod = byRef(&doManifest),
{.name = method::kManifest,
.valueMethod = &byRef<&doManifest>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "nft_buy_offers",
.valueMethod = byRef(&doNFTBuyOffers),
{.name = method::kNftBuyOffers,
.valueMethod = &byRef<&doNFTBuyOffers>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "nft_sell_offers",
.valueMethod = byRef(&doNFTSellOffers),
{.name = method::kNftSellOffers,
.valueMethod = &byRef<&doNFTSellOffers>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "noripple_check",
.valueMethod = byRef(&doNoRippleCheck),
{.name = method::kNorippleCheck,
.valueMethod = &byRef<&doNoRippleCheck>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "owner_info",
.valueMethod = byRef(&doOwnerInfo),
{.name = method::kOwnerInfo,
.valueMethod = &byRef<&doOwnerInfo>,
.role = Role::USER,
.condition = Condition::NeedsCurrentLedger},
{.name = "peers",
.valueMethod = byRef(&doPeers),
{.name = method::kPeers,
.valueMethod = &byRef<&doPeers>,
.role = Role::ADMIN,
.condition = Condition::NoCondition},
{.name = "path_find",
.valueMethod = byRef(&doPathFind),
{.name = method::kPathFind,
.valueMethod = &byRef<&doPathFind>,
.role = Role::USER,
.condition = Condition::NeedsCurrentLedger},
{.name = "ping",
.valueMethod = byRef(&doPing),
{.name = method::kPing,
.valueMethod = &byRef<&doPing>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "print",
.valueMethod = byRef(&doPrint),
{.name = method::kPrint,
.valueMethod = &byRef<&doPrint>,
.role = Role::ADMIN,
.condition = Condition::NoCondition},
// { "profile", byRef (&doProfile), Role::USER,
// NEEDS_CURRENT_LEDGER },
{.name = "random",
.valueMethod = byRef(&doRandom),
{.name = method::kRandom,
.valueMethod = &byRef<&doRandom>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "peer_reservations_add",
.valueMethod = byRef(&doPeerReservationsAdd),
{.name = method::kPeerReservationsAdd,
.valueMethod = &byRef<&doPeerReservationsAdd>,
.role = Role::ADMIN,
.condition = Condition::NoCondition},
{.name = "peer_reservations_del",
.valueMethod = byRef(&doPeerReservationsDel),
{.name = method::kPeerReservationsDel,
.valueMethod = &byRef<&doPeerReservationsDel>,
.role = Role::ADMIN,
.condition = Condition::NoCondition},
{.name = "peer_reservations_list",
.valueMethod = byRef(&doPeerReservationsList),
{.name = method::kPeerReservationsList,
.valueMethod = &byRef<&doPeerReservationsList>,
.role = Role::ADMIN,
.condition = Condition::NoCondition},
{.name = "ripple_path_find",
.valueMethod = byRef(&doRipplePathFind),
{.name = method::kRipplePathFind,
.valueMethod = &byRef<&doRipplePathFind>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "server_definitions",
.valueMethod = byRef(&doServerDefinitions),
{.name = method::kServerDefinitions,
.valueMethod = &byRef<&doServerDefinitions>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "server_info",
.valueMethod = byRef(&doServerInfo),
{.name = method::kServerInfo,
.valueMethod = &byRef<&doServerInfo>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "server_state",
.valueMethod = byRef(&doServerState),
{.name = method::kServerState,
.valueMethod = &byRef<&doServerState>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "sign",
.valueMethod = byRef(&doSign),
{.name = method::kSign,
.valueMethod = &byRef<&doSign>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "sign_for",
.valueMethod = byRef(&doSignFor),
{.name = method::kSignFor,
.valueMethod = &byRef<&doSignFor>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "simulate",
.valueMethod = byRef(&doSimulate),
{.name = method::kSimulate,
.valueMethod = &byRef<&doSimulate>,
.role = Role::USER,
.condition = Condition::NeedsCurrentLedger},
{.name = "stop",
.valueMethod = byRef(&doStop),
{.name = method::kStop,
.valueMethod = &byRef<&doStop>,
.role = Role::ADMIN,
.condition = Condition::NoCondition},
{.name = "submit",
.valueMethod = byRef(&doSubmit),
{.name = method::kSubmit,
.valueMethod = &byRef<&doSubmit>,
.role = Role::USER,
.condition = Condition::NeedsCurrentLedger},
{.name = "submit_multisigned",
.valueMethod = byRef(&doSubmitMultiSigned),
{.name = method::kSubmitMultisigned,
.valueMethod = &byRef<&doSubmitMultiSigned>,
.role = Role::USER,
.condition = Condition::NeedsCurrentLedger},
{.name = "transaction_entry",
.valueMethod = byRef(&doTransactionEntry),
{.name = method::kTransactionEntry,
.valueMethod = &byRef<&doTransactionEntry>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "tx",
.valueMethod = byRef(&doTxJson),
{.name = method::kTx,
.valueMethod = &byRef<&doTxJson>,
.role = Role::USER,
.condition = Condition::NeedsNetworkConnection},
{.name = "tx_history",
.valueMethod = byRef(&doTxHistory),
{.name = method::kTxHistory,
.valueMethod = &byRef<&doTxHistory>,
.role = Role::USER,
.condition = Condition::NoCondition,
.minApiVer = 1,
.maxApiVer = 1},
{.name = "tx_reduce_relay",
.valueMethod = byRef(&doTxReduceRelay),
{.name = method::kTxReduceRelay,
.valueMethod = &byRef<&doTxReduceRelay>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "unl_list",
.valueMethod = byRef(&doUnlList),
{.name = method::kUnlList,
.valueMethod = &byRef<&doUnlList>,
.role = Role::ADMIN,
.condition = Condition::NoCondition},
{.name = "validation_create",
.valueMethod = byRef(&doValidationCreate),
{.name = method::kValidationCreate,
.valueMethod = &byRef<&doValidationCreate>,
.role = Role::ADMIN,
.condition = Condition::NoCondition},
{.name = "validators",
.valueMethod = byRef(&doValidators),
{.name = method::kValidators,
.valueMethod = &byRef<&doValidators>,
.role = Role::ADMIN,
.condition = Condition::NoCondition},
{.name = "validator_list_sites",
.valueMethod = byRef(&doValidatorListSites),
{.name = method::kValidatorListSites,
.valueMethod = &byRef<&doValidatorListSites>,
.role = Role::ADMIN,
.condition = Condition::NoCondition},
{.name = "validator_info",
.valueMethod = byRef(&doValidatorInfo),
{.name = method::kValidatorInfo,
.valueMethod = &byRef<&doValidatorInfo>,
.role = Role::ADMIN,
.condition = Condition::NoCondition},
{.name = "vault_info",
.valueMethod = byRef(&doVaultInfo),
{.name = method::kVaultInfo,
.valueMethod = &byRef<&doVaultInfo>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "wallet_propose",
.valueMethod = byRef(&doWalletPropose),
{.name = method::kWalletPropose,
.valueMethod = &byRef<&doWalletPropose>,
.role = Role::ADMIN,
.condition = Condition::NoCondition},
// Event methods
{.name = "subscribe",
.valueMethod = byRef(&doSubscribe),
{.name = method::kSubscribe,
.valueMethod = &byRef<&doSubscribe>,
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "unsubscribe",
.valueMethod = byRef(&doUnsubscribe),
{.name = method::kUnsubscribe,
.valueMethod = &byRef<&doUnsubscribe>,
.role = Role::USER,
.condition = Condition::NoCondition},
};
class HandlerTable
{
private:
using handler_table_t = std::multimap<std::string, Handler>;
// Use with equal_range to enforce that API range of a newly added handler
// does not overlap with API range of an existing handler with same name
[[nodiscard]] static bool
overlappingApiVersion(
std::pair<handler_table_t::iterator, handler_table_t::iterator> range,
unsigned minVer,
unsigned maxVer)
{
XRPL_ASSERT(minVer <= maxVer, "xrpl::rpc::HandlerTable : valid API version range");
XRPL_ASSERT(
maxVer <= rpc::kApiMaximumValidVersion,
"xrpl::rpc::HandlerTable : valid max API version");
return std::any_of(
range.first,
range.second, //
[minVer, maxVer](auto const& item) {
return item.second.minApiVer <= maxVer && item.second.maxApiVer >= minVer;
});
}
template <std::size_t N>
explicit HandlerTable(Handler const (&entries)[N])
{
for (auto const& entry : entries)
{
if (overlappingApiVersion(
table_.equal_range(entry.name), entry.minApiVer, entry.maxApiVer))
{
logicError(
std::string("Handler for ") + entry.name +
" overlaps with an existing handler");
}
table_.insert({entry.name, entry});
}
// This is where the new-style handlers are added.
addHandler<LedgerHandler>();
addHandler<VersionHandler>();
}
public:
static HandlerTable const&
instance()
{
static HandlerTable const kHandlerTable(kHandlerArray);
return kHandlerTable;
}
[[nodiscard]] Handler const*
getHandler(unsigned version, bool betaEnabled, std::string const& name) const
{
if (version < rpc::kApiMinimumSupportedVersion ||
version > (betaEnabled ? rpc::kApiBetaVersion : rpc::kApiMaximumSupportedVersion))
return nullptr;
auto const range = table_.equal_range(name);
auto const i = std::find_if(range.first, range.second, [version](auto const& entry) {
return entry.second.minApiVer <= version && version <= entry.second.maxApiVer;
});
return i == range.second ? nullptr : &i->second;
}
[[nodiscard]] std::set<char const*>
getHandlerNames() const
{
std::set<char const*> ret;
for (auto const& i : table_)
ret.insert(i.second.name);
return ret;
}
private:
handler_table_t table_;
template <class HandlerImpl>
void
addHandler()
{
static_assert(HandlerImpl::minApiVer <= HandlerImpl::maxApiVer);
static_assert(HandlerImpl::maxApiVer <= rpc::kApiMaximumValidVersion);
static_assert(rpc::kApiMinimumSupportedVersion <= HandlerImpl::minApiVer);
if (overlappingApiVersion(
table_.equal_range(HandlerImpl::name),
HandlerImpl::minApiVer,
HandlerImpl::maxApiVer))
{
logicError(
std::string("Handler for ") + HandlerImpl::name +
" overlaps with an existing handler");
}
table_.insert({HandlerImpl::name, handlerFrom<HandlerImpl>()});
}
// The class-based handlers, which carry their name and API range as static
// members rather than as a table entry, so they cannot go in the array above.
constexpr Handler kClassHandlerArray[]{
handlerFrom<LedgerHandler>(),
handlerFrom<VersionHandler>(),
};
// The whole dispatch table: the two arrays above, concatenated. Their sizes are
// taken from the arrays so that adding a handler to either needs no change here.
constexpr auto kHandlers = [] {
std::array<Handler, std::size(kHandlerArray) + std::size(kClassHandlerArray)> all{};
auto const out = std::ranges::copy(kHandlerArray, all.begin()).out;
std::ranges::copy(kClassHandlerArray, out);
// Sorted by name, so a handler can be found by binary search.
std::ranges::sort(all, {}, &Handler::name);
return all;
}();
// getHandler() relies on this being sorted to binary search it, and
// kHandlerNames below inherits the order.
static_assert(
std::ranges::is_sorted(kHandlers, {}, &Handler::name),
"xrpl::rpc : kHandlers must be sorted by name");
// A name must select exactly one handler, otherwise a request would have two
// answers. Where a method's behaviour differs by API version, the handler
// branches on context.apiVersion rather than being registered once per range.
// Checked here, at compile time, rather than on the first dispatch.
static_assert(
[]() {
for (std::size_t i = 0; i < kHandlers.size(); ++i)
{
auto const& h = kHandlers[i];
if (h.name.empty() || h.valueMethod == nullptr || h.minApiVer > h.maxApiVer ||
h.maxApiVer > rpc::kApiMaximumValidVersion ||
h.minApiVer < rpc::kApiMinimumSupportedVersion)
return false;
// Sorted, so a repeat can only be of the preceding entry.
if (i > 0 && kHandlers[i - 1].name == h.name)
return false;
}
return true;
}(),
"xrpl::rpc : every handler needs a unique name, a method, and a valid API "
"version range");
// The names are handed to json::StaticString, and read as C strings from there,
// so each must be a view of a whole string literal rather than a slice of one.
static_assert(
std::ranges::all_of(kHandlers, isNullTerminated, &Handler::name),
"xrpl::rpc : every handler name must be null-terminated");
// The handler names, which are already distinct and sorted.
constexpr auto kHandlerNames = [] {
std::array<std::string_view, kHandlers.size()> names{};
std::ranges::transform(kHandlers, names.begin(), &Handler::name);
return names;
}();
} // namespace
Handler const*
getHandler(unsigned version, bool betaEnabled, std::string const& name)
getHandler(unsigned version, bool betaEnabled, std::string_view name)
{
return HandlerTable::instance().getHandler(version, betaEnabled, name);
if (version < rpc::kApiMinimumSupportedVersion ||
version > (betaEnabled ? rpc::kApiBetaVersion : rpc::kApiMaximumSupportedVersion))
return nullptr;
// Names are unique, so the binary search finds the only candidate; it then
// answers this request only if it serves this version.
auto const i = std::ranges::lower_bound(kHandlers, name, {}, &Handler::name);
if (i == kHandlers.end() || i->name != name)
return nullptr;
if (i->minApiVer <= version && version <= i->maxApiVer)
return &*i;
return nullptr;
}
std::set<char const*>
std::span<std::string_view const>
getHandlerNames()
{
return HandlerTable::instance().getHandlerNames();
return kHandlerNames;
}
} // namespace xrpl::rpc

View File

@@ -12,13 +12,8 @@
#include <xrpl/protocol/jss.h>
#include <xrpl/server/NetworkOPs.h>
#include <functional>
#include <set>
#include <string>
namespace json {
class Object;
} // namespace json
#include <span>
#include <string_view>
namespace xrpl::rpc {
@@ -32,11 +27,14 @@ enum class Condition {
struct Handler
{
template <class JsonValue>
using Method = std::function<Status(JsonContext&, JsonValue&)>;
// A plain function pointer, not a std::function: every method is a free
// function known at compile time, so nothing needs to be captured. This
// also keeps Handler a literal type, letting the dispatch table be built
// and checked at compile time.
using Method = Status (*)(JsonContext&, json::Value&);
char const* name;
Method<json::Value> valueMethod;
std::string_view name;
Method valueMethod;
Role role;
rpc::Condition condition;
@@ -45,7 +43,7 @@ struct Handler
};
Handler const*
getHandler(unsigned int version, bool betaEnabled, std::string const&);
getHandler(unsigned int version, bool betaEnabled, std::string_view name);
/**
* Return a json::ValueType::Object with a single entry.
@@ -60,9 +58,12 @@ makeObjectValue(Value const& value, json::StaticString const& field = jss::messa
}
/**
* Return names of all methods.
* Return the names of all methods, sorted and without duplicates.
*
* The names view refers to storage that outlives the program, so it is safe to
* hold on to.
*/
std::set<char const*>
std::span<std::string_view const>
getHandlerNames();
template <class T>

View File

@@ -1,6 +1,7 @@
#include <xrpld/rpc/RPCCall.h>
#include <xrpld/core/Config.h>
#include <xrpld/rpc/MethodNames.h>
#include <xrpld/rpc/ServerHandler.h>
#include <xrpl/basics/ByteUtilities.h>
@@ -44,10 +45,13 @@
#include <exception>
#include <functional>
#include <iostream>
#include <limits>
#include <optional>
#include <span>
#include <sstream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>
@@ -56,6 +60,27 @@ namespace xrpl {
class RPCParser;
namespace {
// One command the command line accepts: the method it names, the parser that
// turns arguments into a request, and how many arguments that parser needs.
//
// Declared out here, rather than nested in RPCParser, so that the defaults
// below can be used: a default member initializer is not available while the
// enclosing class is still incomplete, which is when the table is built.
struct Command
{
// For a command that accepts any number of parameters.
static constexpr unsigned kUnlimitedParams = std::numeric_limits<unsigned>::max();
std::string_view name;
json::Value (RPCParser::*parse)(json::Value const& jvParams);
unsigned minParams = 0;
unsigned maxParams = kUnlimitedParams;
};
} // namespace
//
// HTTP protocol
//
@@ -1239,7 +1264,249 @@ private:
return jvRequest;
}
// An omitted minParams means the command takes no arguments; an omitted
// maxParams means it takes any number. See Command.
//
// The commands, in whatever order reads best. parseCommand() searches
// kSortedCommands below, so the order here carries no meaning.
static constexpr auto kCommandArray = std::to_array<Command>({
// Request-response methods
// - Returns an error, or the request.
// - To modify the method, provide a new method in the request.
{.name = rpc::method::kAccountCurrencies,
.parse = &RPCParser::parseAccountCurrencies,
.minParams = 1,
.maxParams = 3},
{.name = rpc::method::kAccountInfo,
.parse = &RPCParser::parseAccountItems,
.minParams = 1,
.maxParams = 3},
{.name = rpc::method::kAccountLines,
.parse = &RPCParser::parseAccountLines,
.minParams = 1,
.maxParams = 5},
{.name = rpc::method::kAccountChannels,
.parse = &RPCParser::parseAccountChannels,
.minParams = 1,
.maxParams = 3},
{.name = rpc::method::kAccountNfts,
.parse = &RPCParser::parseAccountItems,
.minParams = 1,
.maxParams = 5},
{.name = rpc::method::kAccountObjects,
.parse = &RPCParser::parseAccountItems,
.minParams = 1,
.maxParams = 5},
{.name = rpc::method::kAccountOffers,
.parse = &RPCParser::parseAccountItems,
.minParams = 1,
.maxParams = 4},
{.name = rpc::method::kAccountTx,
.parse = &RPCParser::parseAccountTransactions,
.minParams = 1,
.maxParams = 8},
{.name = rpc::method::kAmmInfo,
.parse = &RPCParser::parseAsIs,
.minParams = 1,
.maxParams = 2},
{.name = rpc::method::kVaultInfo,
.parse = &RPCParser::parseVault,
.minParams = 1,
.maxParams = 2},
{.name = rpc::method::kBookChanges,
.parse = &RPCParser::parseLedgerId,
.minParams = 1,
.maxParams = 1},
{.name = rpc::method::kBookOffers,
.parse = &RPCParser::parseBookOffers,
.minParams = 2,
.maxParams = 7},
{.name = rpc::method::kCanDelete, .parse = &RPCParser::parseCanDelete, .maxParams = 1},
{.name = rpc::method::kChannelAuthorize,
.parse = &RPCParser::parseChannelAuthorize,
.minParams = 3,
.maxParams = 4},
{.name = rpc::method::kChannelVerify,
.parse = &RPCParser::parseChannelVerify,
.minParams = 4,
.maxParams = 4},
{.name = rpc::method::kConnect,
.parse = &RPCParser::parseConnect,
.minParams = 1,
.maxParams = 2},
{.name = rpc::method::kConsensusInfo, .parse = &RPCParser::parseAsIs, .maxParams = 0},
{.name = rpc::method::kDepositAuthorized,
.parse = &RPCParser::parseDepositAuthorized,
.minParams = 2,
.maxParams = 11},
{.name = rpc::method::kFeature, .parse = &RPCParser::parseFeature, .maxParams = 2},
{.name = rpc::method::kFetchInfo, .parse = &RPCParser::parseFetchInfo, .maxParams = 1},
{.name = rpc::method::kGatewayBalances,
.parse = &RPCParser::parseGatewayBalances,
.minParams = 1},
{.name = rpc::method::kGetCounts, .parse = &RPCParser::parseGetCounts, .maxParams = 1},
{.name = rpc::method::kJson,
.parse = &RPCParser::parseJson,
.minParams = 2,
.maxParams = 2},
{.name = rpc::method::kJson2,
.parse = &RPCParser::parseJson2,
.minParams = 1,
.maxParams = 1},
{.name = rpc::method::kLedger, .parse = &RPCParser::parseLedger, .maxParams = 2},
{.name = rpc::method::kLedgerAccept, .parse = &RPCParser::parseAsIs, .maxParams = 0},
{.name = rpc::method::kLedgerClosed, .parse = &RPCParser::parseAsIs, .maxParams = 0},
{.name = rpc::method::kLedgerCurrent, .parse = &RPCParser::parseAsIs, .maxParams = 0},
{.name = rpc::method::kLedgerEntry,
.parse = &RPCParser::parseLedgerEntry,
.minParams = 1,
.maxParams = 2},
{.name = rpc::method::kLedgerHeader,
.parse = &RPCParser::parseLedgerId,
.minParams = 1,
.maxParams = 1},
{.name = rpc::method::kLedgerRequest,
.parse = &RPCParser::parseLedgerId,
.minParams = 1,
.maxParams = 1},
{.name = rpc::method::kLogLevel, .parse = &RPCParser::parseLogLevel, .maxParams = 2},
{.name = rpc::method::kLogrotate, .parse = &RPCParser::parseAsIs, .maxParams = 0},
{.name = rpc::method::kManifest,
.parse = &RPCParser::parseManifest,
.minParams = 1,
.maxParams = 1},
{.name = rpc::method::kOwnerInfo,
.parse = &RPCParser::parseAccountItems,
.minParams = 1,
.maxParams = 3},
{.name = rpc::method::kPeers, .parse = &RPCParser::parseAsIs, .maxParams = 0},
{.name = rpc::method::kPing, .parse = &RPCParser::parseAsIs, .maxParams = 0},
{.name = rpc::method::kPrint, .parse = &RPCParser::parseAsIs, .maxParams = 1},
{.name = rpc::method::kRandom, .parse = &RPCParser::parseAsIs, .maxParams = 0},
{.name = rpc::method::kPeerReservationsAdd,
.parse = &RPCParser::parsePeerReservationsAdd,
.minParams = 1,
.maxParams = 2},
{.name = rpc::method::kPeerReservationsDel,
.parse = &RPCParser::parsePeerReservationsDel,
.minParams = 1,
.maxParams = 1},
{.name = rpc::method::kPeerReservationsList,
.parse = &RPCParser::parseAsIs,
.maxParams = 0},
{.name = rpc::method::kRipplePathFind,
.parse = &RPCParser::parseRipplePathFind,
.minParams = 1,
.maxParams = 2},
{.name = rpc::method::kServerDefinitions,
.parse = &RPCParser::parseServerDefinitions,
.maxParams = 1},
{.name = rpc::method::kServerInfo, .parse = &RPCParser::parseServerInfo, .maxParams = 1},
{.name = rpc::method::kServerState, .parse = &RPCParser::parseServerInfo, .maxParams = 1},
{.name = rpc::method::kSign,
.parse = &RPCParser::parseSignSubmit,
.minParams = 2,
.maxParams = 4},
{.name = rpc::method::kSignFor,
.parse = &RPCParser::parseSignFor,
.minParams = 3,
.maxParams = 4},
{.name = rpc::method::kStop, .parse = &RPCParser::parseAsIs, .maxParams = 0},
{.name = rpc::method::kSimulate,
.parse = &RPCParser::parseSimulate,
.minParams = 1,
.maxParams = 2},
{.name = rpc::method::kSubmit,
.parse = &RPCParser::parseSignSubmit,
.minParams = 1,
.maxParams = 4},
{.name = rpc::method::kSubmitMultisigned,
.parse = &RPCParser::parseSubmitMultiSigned,
.minParams = 1,
.maxParams = 1},
{.name = rpc::method::kTransactionEntry,
.parse = &RPCParser::parseTransactionEntry,
.minParams = 2,
.maxParams = 2},
{.name = rpc::method::kTx, .parse = &RPCParser::parseTx, .minParams = 1, .maxParams = 4},
{.name = rpc::method::kTxHistory,
.parse = &RPCParser::parseTxHistory,
.minParams = 1,
.maxParams = 1},
{.name = rpc::method::kUnlList, .parse = &RPCParser::parseAsIs, .maxParams = 0},
{.name = rpc::method::kValidationCreate,
.parse = &RPCParser::parseValidationCreate,
.maxParams = 1},
{.name = rpc::method::kValidatorInfo, .parse = &RPCParser::parseAsIs, .maxParams = 0},
{.name = rpc::method::kVersion, .parse = &RPCParser::parseAsIs, .maxParams = 0},
{.name = rpc::method::kWalletPropose,
.parse = &RPCParser::parseWalletPropose,
.maxParams = 1},
{.name = rpc::method::kInternal, .parse = &RPCParser::parseInternal, .minParams = 1},
// Event methods -- rejected outright below, so any parameters will do
{.name = rpc::method::kPathFind, .parse = &RPCParser::parseEvented},
{.name = rpc::method::kSubscribe, .parse = &RPCParser::parseEvented},
{.name = rpc::method::kUnsubscribe, .parse = &RPCParser::parseEvented},
});
// kCommandArray sorted by name, so a command can be found by binary search.
static constexpr auto kSortedCommands = [] {
auto commands = kCommandArray;
std::ranges::sort(commands, {}, &Command::name);
return commands;
}();
// parseCommand() relies on this being sorted to binary search it, and
// kCommandNames below inherits the order.
static_assert(
std::ranges::is_sorted(kSortedCommands, {}, &Command::name),
"xrpl::RPCParser : kSortedCommands must be sorted");
// The command names, which are already distinct and sorted.
static constexpr auto kCommandNames = [] {
std::array<std::string_view, kSortedCommands.size()> names{};
std::ranges::transform(kSortedCommands, names.begin(), &Command::name);
return names;
}();
public:
/**
* Names of every method the command line accepts.
*/
static std::span<std::string_view const>
methodNames()
{
return kCommandNames;
}
/**
* Whether the command table is well formed.
*
* A name must select exactly one command, and must name a method the server
* can dispatch -- otherwise the command line would accept something that
* then goes nowhere. The latter is checked by RPCCall_test, which can see
* the handler table; here we can at least rule out duplicates and gaps.
*
* This is a function the static_assert below the class calls, rather than
* the assert itself, because reading a pointer to a member of RPCParser is
* only a constant expression once RPCParser is complete.
*/
static constexpr bool
commandsValid()
{
for (std::size_t i = 0; i < kSortedCommands.size(); ++i)
{
auto const& command = kSortedCommands[i];
if (command.name.empty() || command.parse == nullptr ||
command.minParams > command.maxParams)
return false;
if (i > 0 && kSortedCommands[i - 1].name == command.name)
return false;
}
return true;
}
//--------------------------------------------------------------------------
explicit RPCParser(unsigned apiVersion, beast::Journal j) : apiVersion_(apiVersion), j_(j)
@@ -1251,7 +1518,7 @@ public:
// Convert a rpc method and params to a request.
// <-- { method: xyz, params: [... ] } or { error: ..., ... }
json::Value
parseCommand(std::string strMethod, json::Value jvParams, bool allowAnyCommand)
parseCommand(std::string_view strMethod, json::Value const& jvParams, bool allowAnyCommand)
{
if (auto stream = j_.trace())
{
@@ -1259,254 +1526,37 @@ public:
stream << "Params: " << jvParams;
}
struct Command
auto const found = std::ranges::lower_bound(kSortedCommands, strMethod, {}, &Command::name);
if (found == kSortedCommands.end() || found->name != strMethod)
{
char const* name;
parseFuncPtr parse;
int minParams;
int maxParams;
};
// The command could not be found
if (!allowAnyCommand)
return rpcError(RpcUnknownCommand);
static constexpr Command kCommands[] = {
// Request-response methods
// - Returns an error, or the request.
// - To modify the method, provide a new method in the request.
{.name = "account_currencies",
.parse = &RPCParser::parseAccountCurrencies,
.minParams = 1,
.maxParams = 3},
{.name = "account_info",
.parse = &RPCParser::parseAccountItems,
.minParams = 1,
.maxParams = 3},
{.name = "account_lines",
.parse = &RPCParser::parseAccountLines,
.minParams = 1,
.maxParams = 5},
{.name = "account_channels",
.parse = &RPCParser::parseAccountChannels,
.minParams = 1,
.maxParams = 3},
{.name = "account_nfts",
.parse = &RPCParser::parseAccountItems,
.minParams = 1,
.maxParams = 5},
{.name = "account_objects",
.parse = &RPCParser::parseAccountItems,
.minParams = 1,
.maxParams = 5},
{.name = "account_offers",
.parse = &RPCParser::parseAccountItems,
.minParams = 1,
.maxParams = 4},
{.name = "account_tx",
.parse = &RPCParser::parseAccountTransactions,
.minParams = 1,
.maxParams = 8},
{.name = "amm_info", .parse = &RPCParser::parseAsIs, .minParams = 1, .maxParams = 2},
{.name = "vault_info", .parse = &RPCParser::parseVault, .minParams = 1, .maxParams = 2},
{.name = "book_changes",
.parse = &RPCParser::parseLedgerId,
.minParams = 1,
.maxParams = 1},
{.name = "book_offers",
.parse = &RPCParser::parseBookOffers,
.minParams = 2,
.maxParams = 7},
{.name = "can_delete",
.parse = &RPCParser::parseCanDelete,
.minParams = 0,
.maxParams = 1},
{.name = "channel_authorize",
.parse = &RPCParser::parseChannelAuthorize,
.minParams = 3,
.maxParams = 4},
{.name = "channel_verify",
.parse = &RPCParser::parseChannelVerify,
.minParams = 4,
.maxParams = 4},
{.name = "connect", .parse = &RPCParser::parseConnect, .minParams = 1, .maxParams = 2},
{.name = "consensus_info",
.parse = &RPCParser::parseAsIs,
.minParams = 0,
.maxParams = 0},
{.name = "deposit_authorized",
.parse = &RPCParser::parseDepositAuthorized,
.minParams = 2,
.maxParams = 11},
{.name = "feature", .parse = &RPCParser::parseFeature, .minParams = 0, .maxParams = 2},
{.name = "fetch_info",
.parse = &RPCParser::parseFetchInfo,
.minParams = 0,
.maxParams = 1},
{.name = "gateway_balances",
.parse = &RPCParser::parseGatewayBalances,
.minParams = 1,
.maxParams = -1},
{.name = "get_counts",
.parse = &RPCParser::parseGetCounts,
.minParams = 0,
.maxParams = 1},
{.name = "json", .parse = &RPCParser::parseJson, .minParams = 2, .maxParams = 2},
{.name = "json2", .parse = &RPCParser::parseJson2, .minParams = 1, .maxParams = 1},
{.name = "ledger", .parse = &RPCParser::parseLedger, .minParams = 0, .maxParams = 2},
{.name = "ledger_accept",
.parse = &RPCParser::parseAsIs,
.minParams = 0,
.maxParams = 0},
{.name = "ledger_closed",
.parse = &RPCParser::parseAsIs,
.minParams = 0,
.maxParams = 0},
{.name = "ledger_current",
.parse = &RPCParser::parseAsIs,
.minParams = 0,
.maxParams = 0},
{.name = "ledger_entry",
.parse = &RPCParser::parseLedgerEntry,
.minParams = 1,
.maxParams = 2},
{.name = "ledger_header",
.parse = &RPCParser::parseLedgerId,
.minParams = 1,
.maxParams = 1},
{.name = "ledger_request",
.parse = &RPCParser::parseLedgerId,
.minParams = 1,
.maxParams = 1},
{.name = "log_level",
.parse = &RPCParser::parseLogLevel,
.minParams = 0,
.maxParams = 2},
{.name = "logrotate", .parse = &RPCParser::parseAsIs, .minParams = 0, .maxParams = 0},
{.name = "manifest",
.parse = &RPCParser::parseManifest,
.minParams = 1,
.maxParams = 1},
{.name = "owner_info",
.parse = &RPCParser::parseAccountItems,
.minParams = 1,
.maxParams = 3},
{.name = "peers", .parse = &RPCParser::parseAsIs, .minParams = 0, .maxParams = 0},
{.name = "ping", .parse = &RPCParser::parseAsIs, .minParams = 0, .maxParams = 0},
{.name = "print", .parse = &RPCParser::parseAsIs, .minParams = 0, .maxParams = 1},
// { "profile", &RPCParser::parseProfile, 1, 9
// },
{.name = "random", .parse = &RPCParser::parseAsIs, .minParams = 0, .maxParams = 0},
{.name = "peer_reservations_add",
.parse = &RPCParser::parsePeerReservationsAdd,
.minParams = 1,
.maxParams = 2},
{.name = "peer_reservations_del",
.parse = &RPCParser::parsePeerReservationsDel,
.minParams = 1,
.maxParams = 1},
{.name = "peer_reservations_list",
.parse = &RPCParser::parseAsIs,
.minParams = 0,
.maxParams = 0},
{.name = "ripple_path_find",
.parse = &RPCParser::parseRipplePathFind,
.minParams = 1,
.maxParams = 2},
{.name = "server_definitions",
.parse = &RPCParser::parseServerDefinitions,
.minParams = 0,
.maxParams = 1},
{.name = "server_info",
.parse = &RPCParser::parseServerInfo,
.minParams = 0,
.maxParams = 1},
{.name = "server_state",
.parse = &RPCParser::parseServerInfo,
.minParams = 0,
.maxParams = 1},
{.name = "sign", .parse = &RPCParser::parseSignSubmit, .minParams = 2, .maxParams = 4},
{.name = "sign_for", .parse = &RPCParser::parseSignFor, .minParams = 3, .maxParams = 4},
{.name = "stop", .parse = &RPCParser::parseAsIs, .minParams = 0, .maxParams = 0},
{.name = "simulate",
.parse = &RPCParser::parseSimulate,
.minParams = 1,
.maxParams = 2},
{.name = "submit",
.parse = &RPCParser::parseSignSubmit,
.minParams = 1,
.maxParams = 4},
{.name = "submit_multisigned",
.parse = &RPCParser::parseSubmitMultiSigned,
.minParams = 1,
.maxParams = 1},
{.name = "transaction_entry",
.parse = &RPCParser::parseTransactionEntry,
.minParams = 2,
.maxParams = 2},
{.name = "tx", .parse = &RPCParser::parseTx, .minParams = 1, .maxParams = 4},
{.name = "tx_history",
.parse = &RPCParser::parseTxHistory,
.minParams = 1,
.maxParams = 1},
{.name = "unl_list", .parse = &RPCParser::parseAsIs, .minParams = 0, .maxParams = 0},
{.name = "validation_create",
.parse = &RPCParser::parseValidationCreate,
.minParams = 0,
.maxParams = 1},
{.name = "validator_info",
.parse = &RPCParser::parseAsIs,
.minParams = 0,
.maxParams = 0},
{.name = "version", .parse = &RPCParser::parseAsIs, .minParams = 0, .maxParams = 0},
{.name = "wallet_propose",
.parse = &RPCParser::parseWalletPropose,
.minParams = 0,
.maxParams = 1},
{.name = "internal",
.parse = &RPCParser::parseInternal,
.minParams = 1,
.maxParams = -1},
// Event methods
{.name = "path_find",
.parse = &RPCParser::parseEvented,
.minParams = -1,
.maxParams = -1},
{.name = "subscribe",
.parse = &RPCParser::parseEvented,
.minParams = -1,
.maxParams = -1},
{.name = "unsubscribe",
.parse = &RPCParser::parseEvented,
.minParams = -1,
.maxParams = -1},
};
auto const count = jvParams.size();
for (auto const& command : kCommands)
{
if (strMethod == command.name)
{
if ((command.minParams >= 0 && count < command.minParams) ||
(command.maxParams >= 0 && count > command.maxParams))
{
JLOG(j_.debug()) << "Wrong number of parameters for " << command.name
<< " minimum=" << command.minParams
<< " maximum=" << command.maxParams << " actual=" << count;
return rpcError(RpcBadSyntax);
}
return (this->*(command.parse))(jvParams);
}
return parseAsIs(jvParams);
}
// The command could not be found
if (!allowAnyCommand)
return rpcError(RpcUnknownCommand);
auto const count = jvParams.size();
if (count < found->minParams || count > found->maxParams)
{
JLOG(j_.debug()) << "Wrong number of parameters for " << found->name
<< " minimum=" << found->minParams << " maximum=" << found->maxParams
<< " actual=" << count;
return parseAsIs(jvParams);
return rpcError(RpcBadSyntax);
}
return (this->*(found->parse))(jvParams);
}
};
// See the comment on commandsValid() for why this is out here.
static_assert(
RPCParser::commandsValid(),
"xrpl::RPCParser : every command needs a unique name, a parser, and a valid "
"parameter count range");
//------------------------------------------------------------------------------
//
@@ -1614,6 +1664,12 @@ struct RPCCallImp
//------------------------------------------------------------------------------
std::span<std::string_view const>
commandLineMethodNames()
{
return RPCParser::methodNames();
}
// Used internally by rpcClient.
json::Value
rpcCmdToJson(

View File

@@ -23,6 +23,7 @@
#include <cstdint>
#include <exception>
#include <string>
#include <string_view>
namespace xrpl::rpc {
@@ -153,9 +154,8 @@ fillHandler(JsonContext& context, Handler const*& result)
return RpcSuccess;
}
template <class Object, class Method>
Status
callMethod(JsonContext& context, Method method, std::string const& name, Object& result)
callMethod(JsonContext& context, Handler::Method method, std::string_view name, json::Value& result)
{
static std::atomic<std::uint64_t> kRequestId{0};
auto& perfLog = context.app.getPerfLog();
@@ -163,7 +163,8 @@ callMethod(JsonContext& context, Method method, std::string const& name, Object&
try
{
perfLog.rpcStart(name, curId);
auto v = context.app.getJobQueue().makeLoadEvent(JtGeneric, "cmd:" + name);
auto v =
context.app.getJobQueue().makeLoadEvent(JtGeneric, std::string{"cmd:"}.append(name));
auto start = std::chrono::system_clock::now();
auto ret = method(context, result);
@@ -199,32 +200,28 @@ doCommand(rpc::JsonContext& context, json::Value& result)
return error;
}
if (auto method = handler->valueMethod)
// No null check on the method: every entry in the dispatch table carries
// one, which a static_assert there enforces at compile time.
if (!context.headers.user.empty() || !context.headers.forwardedFor.empty())
{
if (!context.headers.user.empty() || !context.headers.forwardedFor.empty())
{
JLOG(context.j.debug())
<< "start command: " << handler->name << ", user: " << context.headers.user
<< ", forwarded for: " << context.headers.forwardedFor;
JLOG(context.j.debug()) << "start command: " << handler->name
<< ", user: " << context.headers.user
<< ", forwarded for: " << context.headers.forwardedFor;
auto ret = callMethod(context, method, handler->name, result);
auto const ret = callMethod(context, handler->valueMethod, handler->name, result);
JLOG(context.j.debug())
<< "finish command: " << handler->name << ", user: " << context.headers.user
<< ", forwarded for: " << context.headers.forwardedFor;
JLOG(context.j.debug()) << "finish command: " << handler->name
<< ", user: " << context.headers.user
<< ", forwarded for: " << context.headers.forwardedFor;
return ret;
}
auto ret = callMethod(context, method, handler->name, result);
return ret;
}
return RpcUnknownCommand;
return callMethod(context, handler->valueMethod, handler->name, result);
}
Role
roleRequired(unsigned int version, bool betaEnabled, std::string const& method)
roleRequired(unsigned int version, bool betaEnabled, std::string_view method)
{
auto handler = rpc::getHandler(version, betaEnabled, method);

View File

@@ -3,6 +3,7 @@
#include <xrpld/app/main/Application.h>
#include <xrpld/app/misc/TxQ.h> // IWYU pragma: keep
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/MethodNames.h>
#include <xrpld/rpc/Role.h>
#include <xrpld/rpc/Status.h>
#include <xrpld/rpc/detail/Handler.h>
@@ -12,12 +13,9 @@
#include <xrpl/protocol/ApiVersion.h>
#include <memory>
#include <string_view>
#include <vector>
namespace json {
class Object;
} // namespace json
namespace xrpl::rpc {
struct JsonContext;
@@ -40,7 +38,7 @@ public:
writeResult(json::Value&);
// NOLINTBEGIN(readability-identifier-naming)
static constexpr char name[] = "ledger";
static constexpr std::string_view name = method::kLedger;
static constexpr unsigned minApiVer = rpc::kApiMinimumSupportedVersion;

View File

@@ -2,6 +2,7 @@
#include <xrpld/app/main/Application.h> // IWYU pragma: keep
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/MethodNames.h>
#include <xrpld/rpc/Role.h>
#include <xrpld/rpc/Status.h>
#include <xrpld/rpc/detail/Handler.h>
@@ -9,6 +10,8 @@
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/ApiVersion.h>
#include <string_view>
namespace xrpl::rpc {
class VersionHandler
@@ -32,7 +35,7 @@ public:
}
// NOLINTBEGIN(readability-identifier-naming)
static constexpr char const* name = "version";
static constexpr std::string_view name = method::kVersion;
static constexpr unsigned minApiVer = rpc::kApiMinimumSupportedVersion;