From 04108a030c3b6c03080f4a08500f6f177bd12f26 Mon Sep 17 00:00:00 2001 From: Bart Date: Thu, 17 Sep 2026 10:01:28 +0000 Subject: [PATCH] refactor: Build the RPC dispatch and command-line tables at compile time (#8006) Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com> Co-authored-by: Claude Opus 5 --- .../scripts/levelization/results/ordering.txt | 3 +- include/xrpl/basics/StringUtilities.h | 85 ++ include/xrpl/core/PerfLog.h | 22 +- src/test/basics/PerfLog_test.cpp | 71 +- src/test/core/Workers_test.cpp | 7 +- src/test/jtx/TestHelpers.h | 8 - src/test/rpc/Handler_test.cpp | 159 ++- src/test/rpc/RPCCall_test.cpp | 63 ++ src/xrpld/app/main/Application.cpp | 2 + src/xrpld/perflog/detail/PerfLogImp.cpp | 105 +- src/xrpld/perflog/detail/PerfLogImp.h | 43 +- src/xrpld/rpc/MethodNames.h | 95 ++ src/xrpld/rpc/RPCCall.h | 11 + src/xrpld/rpc/RPCHandler.h | 4 +- src/xrpld/rpc/detail/Handler.cpp | 983 ++++++++++-------- src/xrpld/rpc/detail/Handler.h | 97 +- src/xrpld/rpc/detail/RPCCall.cpp | 779 +++++++++----- src/xrpld/rpc/detail/RPCHandler.cpp | 35 +- src/xrpld/rpc/handlers/ledger/Ledger.h | 8 +- src/xrpld/rpc/handlers/server_info/Version.h | 5 +- 20 files changed, 1807 insertions(+), 778 deletions(-) create mode 100644 src/xrpld/rpc/MethodNames.h diff --git a/.github/scripts/levelization/results/ordering.txt b/.github/scripts/levelization/results/ordering.txt index 5577c363fd..2b696f56f8 100644 --- a/.github/scripts/levelization/results/ordering.txt +++ b/.github/scripts/levelization/results/ordering.txt @@ -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 diff --git a/include/xrpl/basics/StringUtilities.h b/include/xrpl/basics/StringUtilities.h index e3b91c2f25..7cb67cb15b 100644 --- a/include/xrpl/basics/StringUtilities.h +++ b/include/xrpl/basics/StringUtilities.h @@ -162,4 +162,89 @@ toUInt64(std::string const& s); bool isProperlyFormedTomlDomain(std::string_view domain); +/** + * Whether a view can be passed on as a C string. + * + * A reader given only data() stops at the first null, so the view must reach the + * terminating null. The test rebuilds the view from data() and compares: a view + * that stops earlier rebuilds longer, and so compares unequal. + * + * consteval because reading the byte after the view is only defined when @p str + * points into storage holding a null at or after its end, such as a string + * literal. An unterminated view is then a compile error, not an out-of-bounds + * read. + * + * @param str The view to test. + * @return Whether @p str is null-terminated. A view with no data is not. + */ +consteval bool +isNullTerminated(std::string_view str) +{ + if (str.data() == nullptr) + return false; + + // Reading past the view is the point, so the usual data() warning does not + // apply. + // NOLINTNEXTLINE(bugprone-suspicious-stringview-data-usage) + return std::string_view{str.data()} == str; +} + +/** + * A string that is known to reach its terminating null. + * + * Converts to std::string_view, so it compares and hashes as one. Unlike a + * view, asCString() may be handed to a reader that expects a C string, such + * as json::StaticString. + * + * The only constructor is consteval and rejects a view that stops before the + * null, so the property holds by construction and no caller asserts it. + */ +class NullTerminatedView +{ +public: + /** + * Build a view from one that reaches its terminating null. + * + * Explicit, so that a plain view cannot become a proof of termination by + * accident. The conversion the other way stays implicit. + * + * @param view The string to hold. Rejected at compile time if it stops + * before its terminating null, or has no data. + */ + explicit consteval NullTerminatedView(std::string_view view) + : data_(view.data()), size_(view.size()) + { + if (!isNullTerminated(view)) + throw "xrpl::NullTerminatedView : view does not reach a null"; + } + + constexpr + operator std::string_view() const noexcept + { + return view(); + } + + /** + * @return The string as a view. + */ + [[nodiscard]] constexpr std::string_view + view() const noexcept + { + return {data_, size_}; + } + + /** + * @return The string as a C string. Never null. + */ + [[nodiscard]] constexpr char const* + asCString() const noexcept + { + return data_; + } + +private: + char const* data_; + std::size_t size_; +}; + } // namespace xrpl diff --git a/include/xrpl/core/PerfLog.h b/include/xrpl/core/PerfLog.h index dd78a8f9a6..0c544c9aa5 100644 --- a/include/xrpl/core/PerfLog.h +++ b/include/xrpl/core/PerfLog.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -9,7 +10,8 @@ #include #include #include -#include +#include +#include namespace beast { class Journal; @@ -67,7 +69,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 +78,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 +87,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 +152,20 @@ public: PerfLog::Setup setupPerfLog(Section const& section, std::filesystem::path const& configDir); +/** + * @param methodNames The RPC methods to count, one counter per name. Reported + * as JSON keys that borrow each name and read it as a C string, which is + * why the parameter type requires one that reaches its terminating null. + * The names must outlive the returned object, which holds views of them. + * The range itself need not: it is copied. + * Passed in rather than looked up here, so that this layer needs no + * knowledge of the dispatch table. + */ std::unique_ptr makePerfLog( PerfLog::Setup const& setup, Application& app, + std::span methodNames, beast::Journal journal, std::function&& signalStop); @@ -161,7 +173,7 @@ template auto measureDurationAndLog( Func&& func, - std::string const& actionDescription, + std::string_view actionDescription, std::chrono::duration maxDelay, beast::Journal const& journal) { diff --git a/src/test/basics/PerfLog_test.cpp b/src/test/basics/PerfLog_test.cpp index f7679dc488..0896de8eee 100644 --- a/src/test/basics/PerfLog_test.cpp +++ b/src/test/basics/PerfLog_test.cpp @@ -1,9 +1,7 @@ #include -#include #include -#include - +#include #include #include #include @@ -16,6 +14,7 @@ #include #include +#include #include #include #include @@ -26,7 +25,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -42,6 +43,21 @@ class PerfLog_test : public beast::unit_test::Suite using path = std::filesystem::path; + // The method names to count. PerfLog treats them as opaque keys, so these are + // made up rather than taken from the dispatch table: this test then needs no + // knowledge of the RPC layer, and does not change shape when a method is + // added or removed. + // + // String literals because PerfLog reads them back as C strings, which is what + // NullTerminatedView requires, and they must outlive the PerfLog. Sorted, + // because the counters are reported in sorted order. + static constexpr std::array kMethodNames{ + NullTerminatedView{"method_a"}, + NullTerminatedView{"method_b"}, + NullTerminatedView{"method_c"}, + NullTerminatedView{"method_d"}, + NullTerminatedView{"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 +130,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 +326,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 labels = test::jtx::makeVector(xrpl::rpc::getHandlerNames()); + // The only labels the RPC interface accepts: those the PerfLog was + // constructed with, since rpcStart() reaches UNREACHABLE for any other. + // Copied into a vector because they are shuffled below, then paired + // positionally with the request ids. + auto labels = std::ranges::to(kMethodNames); std::shuffle(labels.begin(), labels.end(), defaultPrng()); // Get two IDs to associate with each label. Errors tend to happen at @@ -347,7 +365,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"); @@ -370,7 +388,7 @@ public: std::uint64_t prevDur = std::numeric_limits::max(); for (int i = 0; i < currents.size(); ++i) { - BEAST_EXPECT(currents[i].name == labels[i / 2]); + BEAST_EXPECT(currents[i].name == labels[i / 2].view()); BEAST_EXPECT(prevDur > currents[i].dur); prevDur = currents[i].dur; } @@ -404,7 +422,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 +433,7 @@ public: std::uint64_t prevDur = std::numeric_limits::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 +465,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. @@ -1012,6 +1030,34 @@ public: } } + // makePerfLog() copies the range of names it is given, so only the names have + // to outlive the PerfLog. Here the range does not: it is destroyed before the + // counters are read. Retaining it instead is a use-after-free that a + // sanitizer build reports and this test would otherwise pass through. + void + testCallerRangeNeedNotOutlive() + { + testcase("Caller's range need not outlive the PerfLog"); + + Fixture const fixture{env_.app(), j_}; + + std::unique_ptr perfLog; + { + std::vector const names{kMethodNames.begin(), kMethodNames.end()}; + perf::PerfLog::Setup const setup{.perfLog = "", .logInterval = fixture.logInterval()}; + perfLog = perf::makePerfLog(setup, env_.app(), names, j_, []() {}); + } + + perfLog->start(); + perfLog->rpcStart(kMethodNames[0], 1); + perfLog->rpcFinish(kMethodNames[0], 1); + + // Reads the retained names, which is where a dangling range would surface. + json::Value const counters{perfLog->countersJson()[jss::rpc]}; + BEAST_EXPECT(counters.isMember(std::string{kMethodNames[0].view()})); + perfLog->stop(); + } + void run() override { @@ -1024,6 +1070,7 @@ public: testInvalidID(WithFile::Yes); testRotate(WithFile::No); testRotate(WithFile::Yes); + testCallerRangeNeedNotOutlive(); } }; diff --git a/src/test/core/Workers_test.cpp b/src/test/core/Workers_test.cpp index fe3820b84a..6824b94769 100644 --- a/src/test/core/Workers_test.cpp +++ b/src/test/core/Workers_test.cpp @@ -9,6 +9,7 @@ #include #include #include +#include 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 { } diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index 801c3627b8..382a6fe333 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -43,7 +43,6 @@ #include #include #include -#include #include #include #include @@ -316,13 +315,6 @@ auto const kData = JTxFieldWrapper(sfData); auto const kAmount = JTxFieldWrapper(sfAmount); -template -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); diff --git a/src/test/rpc/Handler_test.cpp b/src/test/rpc/Handler_test.cpp index be78864cac..16838d3f5a 100644 --- a/src/test/rpc/Handler_test.cpp +++ b/src/test/rpc/Handler_test.cpp @@ -1,9 +1,8 @@ -#include - #include #include +#include #include #include @@ -12,7 +11,11 @@ #include #include #include +#include +#include #include +#include +#include #include #include // cspell: words stdev @@ -88,21 +91,50 @@ class Handler_test : public beast::unit_test::Suite std::random_device dev; std::ranlux48 prng(dev()); - std::vector names = test::jtx::makeVector(xrpl::rpc::getHandlerNames()); + // The lowest version still served. Outside the supported range getHandler() + // returns at its bounds check without searching, so the benchmark would + // time that check instead of a lookup. + constexpr unsigned kVersion = rpc::kApiMinimumSupportedVersion; + + // Only the names that answer at kVersion, so that every timed call does a + // whole lookup: a method served from a later version only would not. + // Contiguous, so that picking one by index costs nothing. + std::vector names; + std::ranges::copy_if( + rpc::getHandlerNames(), std::back_inserter(names), [](std::string_view name) { + return rpc::getHandler(kVersion, false, name) != nullptr; + }); + + if (!BEAST_EXPECTS( + !names.empty(), + "no handler answers at API version " + std::to_string(kVersion) + + ", so there is nothing to measure")) + return; std::uniform_int_distribution distr{0, names.size() - 1}; 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'; + // Every name answered once already, so a miss here cannot happen. + 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); } @@ -114,6 +146,125 @@ public: } }; +// Manual: the suite only reports a timing, which says nothing on a CI runner. +// The table invariants are static_asserts in Handler.cpp. BEAST_DEFINE_TESTSUITE_MANUAL(Handler, rpc, xrpl); +// What getHandler() answers, as opposed to how fast it answers. A lookup needs no +// Application, so these cases run as an automatic suite. +// +// The bounds check they cover is unreachable from a request: getAPIVersionNumber() +// applies the same predicate first, and every caller rejects an invalid version +// before it asks for a handler. That is why it is checked here directly, and why +// it is worth checking at all rather than deleting as unreachable. +class HandlerLookup_test : public beast::unit_test::Suite +{ + /** + * Find a method that is served at a given API version. + * + * The name comes from the table, so a case below does not name a method that a + * later API version may retire. + * + * @param version The API version to answer at. + * @param betaEnabled Whether the beta API version is enabled. + * @return A name that answers, or nullopt if none does. + */ + static std::optional + nameServedAt(unsigned version, bool betaEnabled) + { + for (std::string_view name : rpc::getHandlerNames()) + { + if (rpc::getHandler(version, betaEnabled, name) != nullptr) + return name; + } + + return std::nullopt; + } + + void + testUnservedVersion() + { + testcase("An unserved API version has no handler"); + + // A name the table certainly holds, so that a null answer below can only + // come from the version and not from the name. + auto const name = nameServedAt(rpc::kApiMinimumSupportedVersion, false); + if (!BEAST_EXPECTS( + name.has_value(), + "no handler answers at API version " + + std::to_string(rpc::kApiMinimumSupportedVersion) + + ", so there is no name to ask about")) + return; + + // Below the minimum, which no setting serves. + BEAST_EXPECT( + rpc::getHandler(rpc::kApiMinimumSupportedVersion - 1, false, *name) == nullptr); + BEAST_EXPECT(rpc::getHandler(rpc::kApiMinimumSupportedVersion - 1, true, *name) == nullptr); + + // Above the maximum each setting serves. Both values stay outside the + // served range however the version constants move, so neither case can + // become vacuous. + BEAST_EXPECT( + rpc::getHandler(rpc::kApiMaximumSupportedVersion + 1, false, *name) == nullptr); + BEAST_EXPECT(rpc::getHandler(rpc::kApiBetaVersion + 1, true, *name) == nullptr); + } + + void + testBetaVersionGate() + { + testcase("The beta API version is served only where it is enabled"); + + // Between betas the beta version is the maximum supported one, leaving the + // two settings nothing to tell apart. Compiled out rather than asserted, so + // that the case arms itself again when a later beta version arrives. + if constexpr (rpc::kApiBetaVersion > rpc::kApiMaximumSupportedVersion) + { + auto const name = nameServedAt(rpc::kApiBetaVersion, true); + if (!BEAST_EXPECTS( + name.has_value(), + "no handler answers at API version " + std::to_string(rpc::kApiBetaVersion) + + ", so there is nothing for the gate to reject")) + return; + + // The handler serves this version, so only the server's own range can + // turn the answer into a null one. + BEAST_EXPECT(rpc::getHandler(rpc::kApiBetaVersion, true, *name) != nullptr); + BEAST_EXPECT(rpc::getHandler(rpc::kApiBetaVersion, false, *name) == nullptr); + } + else + { + log << "the beta API version is the maximum supported version, so no gate " + "separates them\n"; + pass(); + } + } + + void + testUnknownMethod() + { + testcase("An unknown method has no handler"); + + constexpr unsigned kVersion = rpc::kApiMinimumSupportedVersion; + + BEAST_EXPECT(rpc::getHandler(kVersion, false, "no such method") == nullptr); + BEAST_EXPECT(rpc::getHandler(kVersion, false, "") == nullptr); + + // A method name holds lowercase letters and underscores, so a tilde sorts + // after every entry. This runs the search off the end of the table, which + // no other case here does. + BEAST_EXPECT(rpc::getHandler(kVersion, false, "~") == nullptr); + } + +public: + void + run() override + { + testUnservedVersion(); + testBetaVersionGate(); + testUnknownMethod(); + } +}; + +BEAST_DEFINE_TESTSUITE(HandlerLookup, rpc, xrpl); + } // namespace xrpl::test diff --git a/src/test/rpc/RPCCall_test.cpp b/src/test/rpc/RPCCall_test.cpp index ef3213008c..e09d95f99a 100644 --- a/src/test/rpc/RPCCall_test.cpp +++ b/src/test/rpc/RPCCall_test.cpp @@ -3,6 +3,9 @@ #include #include +#include +#include +#include #include #include @@ -12,11 +15,14 @@ #include +#include +#include #include #include #include #include #include +#include #include #include @@ -5923,10 +5929,67 @@ public: } } + // The command-line table and the dispatch table must agree. + // + // Forwards: every name the command line accepts must reach a handler at the + // version the command-line client requests. Presence in the dispatch table is + // not enough: a handler whose API range excludes kApiCommandLineVersion parses + // the command and then answers RpcUnknownCommand. + // + // Backwards: a handler that claims a command-line form must have one, and + // one that denies it must not, so that Handler::hasCommandLineForm cannot go + // stale. + // + // Three command-line names are exempt from the forward check because they + // are wrappers that forward a caller-supplied method rather than naming one + // themselves, so they have no handler of their own. + void + testCommandLineTableMatchesHandlers() + { + testcase("Command-line and dispatch tables agree"); + + static constexpr std::array kWrappers{ + rpc::method::kInternal, rpc::method::kJson, rpc::method::kJson2}; + + auto const commandLine = commandLineMethodNames(); + auto const handlers = rpc::getHandlerNames(); + BEAST_EXPECT(!commandLine.empty()); + BEAST_EXPECT(!handlers.empty()); + + // The command-line client always requests this version, so this is the + // only version at which its commands have to be dispatchable. Beta + // methods are off: a command must work against a stock server. + auto const handlerFor = [](std::string_view name) { + return rpc::getHandler(rpc::kApiCommandLineVersion, false, name); + }; + + for (auto const& name : commandLine) + { + if (std::ranges::find(kWrappers, name) != kWrappers.end()) + continue; + + auto const* handler = handlerFor(name); + if (BEAST_EXPECTS(handler != nullptr, std::string{name})) + BEAST_EXPECTS(handler->hasCommandLineForm, std::string{name}); + } + + for (auto const& name : handlers) + { + auto const* handler = handlerFor(name); + bool const claimsCommandLine = handler != nullptr && handler->hasCommandLineForm; + + // Both name lists are sorted, so a binary search suffices. + BEAST_EXPECTS( + claimsCommandLine == std::ranges::binary_search(commandLine, name.view()), + std::string{name}); + } + } + void run() override { forAllApiVersions([this](unsigned apiVersion) { testRPCCall(apiVersion); }); + testCommandLineTableMatchesHandlers(); } }; diff --git a/src/xrpld/app/main/Application.cpp b/src/xrpld/app/main/Application.cpp index d50637d98e..775471854d 100644 --- a/src/xrpld/app/main/Application.cpp +++ b/src/xrpld/app/main/Application.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -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) diff --git a/src/xrpld/perflog/detail/PerfLogImp.cpp b/src/xrpld/perflog/detail/PerfLogImp.cpp index 2777e0dcdb..d7944589af 100644 --- a/src/xrpld/perflog/detail/PerfLogImp.cpp +++ b/src/xrpld/perflog/detail/PerfLogImp.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -16,6 +17,7 @@ #include #include #include +#include #include #include @@ -25,8 +27,9 @@ #include #include #include -#include +#include #include +#include #include #include #include @@ -34,40 +37,39 @@ namespace xrpl::perf { -PerfLogImp::Counters::Counters(std::set const& labels, JobTypes const& jobTypes) +PerfLogImp::Counters::Counters( + std::span methodNames, + JobTypes const& jobTypes) { + // Only a name that got a counter is kept, so labels and rpc hold the same set + // and countersJson() reports each counter once. Keeping a repeated name would + // add its counter to the totals twice, because the assertion below is compiled + // out of a release build. + labels.reserve(methodNames.size()); + rpc.reserve(methodNames.size()); + for (auto const& name : methodNames) { - // populateRpc - rpc.reserve(labels.size()); - for (std::string const label : labels) + auto const inserted = rpc.try_emplace(name).second; + if (!inserted) { - auto const inserted = rpc.emplace(label, Rpc()).second; - if (!inserted) - { - // Ensure that no other function populates this entry. - // LCOV_EXCL_START - UNREACHABLE( - "xrpl::perf::PerfLogImp::Counters::Counters : failed to " - "insert label"); - // LCOV_EXCL_STOP - } + // LCOV_EXCL_START + UNREACHABLE("xrpl::perf::PerfLogImp::Counters::Counters : method name is unique"); + continue; + // LCOV_EXCL_STOP } + labels.push_back(name); } + + jq.reserve(jobTypes.size()); + for (auto const& [jobType, _] : jobTypes) { - // populateJq - jq.reserve(jobTypes.size()); - for (auto const& [jobType, _] : jobTypes) + auto const inserted = jq.emplace(jobType, Jq()).second; + if (!inserted) { - auto const inserted = jq.emplace(jobType, Jq()).second; - if (!inserted) - { - // Ensure that no other function populates this entry. - // LCOV_EXCL_START - UNREACHABLE( - "xrpl::perf::PerfLogImp::Counters::Counters : failed to " - "insert job type"); - // LCOV_EXCL_STOP - } + // Nothing else inserts into jq, so a job type cannot repeat. + // LCOV_EXCL_START + UNREACHABLE("xrpl::perf::PerfLogImp::Counters::Counters : failed to insert job type"); + // LCOV_EXCL_STOP } } } @@ -78,17 +80,31 @@ PerfLogImp::Counters::countersJson() const json::Value rpcobj(json::ValueType::Object); // totalRpc represents all rpc methods. All that started, finished, etc. Rpc totalRpc; - for (auto const& proc : rpc) + // Walked by label rather than by map entry, so that each key can be reported + // as a C string. The constructor gives rpc an entry per label, so the lookup + // succeeds; it is a find rather than an at() because this runs on the logging + // thread, where a throw would end the process. + for (auto const& label : labels) { + auto const entry = rpc.find(label); + if (entry == rpc.end()) + { + // LCOV_EXCL_START + UNREACHABLE("xrpl::perf::PerfLogImp::Counters::countersJson : label has a counter"); + continue; + // LCOV_EXCL_STOP + } + auto const& counter = entry->second; + Rpc value; { - std::scoped_lock const lock(proc.second.mutex); - if ((proc.second.value.started == 0u) && (proc.second.value.finished == 0u) && - (proc.second.value.errored == 0u)) + std::scoped_lock const lock(counter.mutex); + if ((counter.value.started == 0u) && (counter.value.finished == 0u) && + (counter.value.errored == 0u)) { continue; } - value = proc.second.value; + value = counter.value; } json::Value p(json::ValueType::Object); @@ -100,7 +116,7 @@ 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; + rpcobj[json::StaticString{label.asCString()}] = p; } if (totalRpc.started != 0u) @@ -195,7 +211,9 @@ PerfLogImp::Counters::currentJson() const for (auto m : methods) { json::Value methodobj(json::ValueType::Object); - methodobj[jss::method] = m.first; + // A key of rpc, per methods' declaration, so borrowed as 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(present - m.second).count()); methodsArray.append(methodobj); @@ -299,9 +317,14 @@ PerfLogImp::report() PerfLogImp::PerfLogImp( Setup setup, Application& app, + std::span methodNames, beast::Journal journal, std::function&& 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 +335,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 +351,12 @@ 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()}; + // The key, not the method argument: what is stored has to outlive the call. + 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 +525,11 @@ std::unique_ptr makePerfLog( PerfLog::Setup const& setup, Application& app, + std::span methodNames, beast::Journal journal, std::function&& signalStop) { - return std::make_unique(setup, app, journal, std::move(signalStop)); + return std::make_unique(setup, app, methodNames, journal, std::move(signalStop)); } } // namespace xrpl::perf diff --git a/src/xrpld/perflog/detail/PerfLogImp.h b/src/xrpld/perflog/detail/PerfLogImp.h index 7efdbe1b7f..ad1f98e356 100644 --- a/src/xrpld/perflog/detail/PerfLogImp.h +++ b/src/xrpld/perflog/detail/PerfLogImp.h @@ -1,7 +1,6 @@ #pragma once -#include - +#include #include #include #include @@ -15,8 +14,9 @@ #include #include #include -#include +#include #include +#include #include #include #include @@ -59,7 +59,8 @@ class PerfLogImp : public PerfLog struct Counters { public: - using MethodStart = std::pair; + using MethodStart = std::pair; + /** * RPC performance counters. */ @@ -91,14 +92,27 @@ 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> rpc; + // + // Every key views the characters of a name in labels below, which the caller + // guarantees outlive this object, so the map copies no name to store one and + // needs no string to look one up. + std::unordered_map> rpc; + + // The same names, in the order the caller gave them, and still carrying the + // proof that each reaches its terminating null. countersJson() walks these + // rather than rpc, so that it can report a key as a C string. Held by value, + // so that a caller may build the range it passes on the fly: only the names + // have to outlive this object, not the container that carried them. + std::vector labels; std::unordered_map> jq; std::vector> jobs; mutable std::mutex jobsMutex; + // Each view is a key of rpc above, not the argument rpcStart() received, so + // currentJson() may read it as a C string. std::unordered_map methods; mutable std::mutex methodsMutex; - Counters(std::set const& labels, JobTypes const& jobTypes); + Counters(std::span labels, JobTypes const& jobTypes); json::Value countersJson() const; json::Value @@ -109,7 +123,7 @@ class PerfLogImp : public PerfLog Application& app_; beast::Journal const j_; std::function const signalStop_; - Counters counters_{xrpl::rpc::getHandlerNames(), JobTypes::instance()}; + Counters counters_; std::ofstream logFile_; std::thread thread_; std::mutex mutex_; @@ -126,28 +140,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 methodNames, beast::Journal journal, std::function&& 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); } diff --git a/src/xrpld/rpc/MethodNames.h b/src/xrpld/rpc/MethodNames.h new file mode 100644 index 0000000000..5dc2d9ff88 --- /dev/null +++ b/src/xrpld/rpc/MethodNames.h @@ -0,0 +1,95 @@ +#pragma once + +#include + +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. + * + * Not every method appears in both tables. Whether a method has a command-line + * form is recorded by Handler::hasCommandLineForm in the dispatch table, and + * checked against the command-line table by RPCCall_test. + */ + +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"}; +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"}; +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"}; +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"}; +inline constexpr std::string_view kLedgerClosed{"ledger_closed"}; +inline constexpr std::string_view kLedgerCurrent{"ledger_current"}; +inline constexpr std::string_view kLedgerData{"ledger_data"}; +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"}; +inline constexpr std::string_view kNftSellOffers{"nft_sell_offers"}; +inline constexpr std::string_view kNorippleCheck{"noripple_check"}; +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"}; +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"}; +inline constexpr std::string_view kValidators{"validators"}; +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 diff --git a/src/xrpld/rpc/RPCCall.h b/src/xrpld/rpc/RPCCall.h index 2fec78f93b..b3678efa25 100644 --- a/src/xrpld/rpc/RPCCall.h +++ b/src/xrpld/rpc/RPCCall.h @@ -10,7 +10,9 @@ #include #include +#include #include +#include #include #include #include @@ -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 +commandLineMethodNames(); + /** * Internal invocation of RPC client. * Used by both xrpld command line as well as xrpld unit tests diff --git a/src/xrpld/rpc/RPCHandler.h b/src/xrpld/rpc/RPCHandler.h index 637a492943..483e7e5baa 100644 --- a/src/xrpld/rpc/RPCHandler.h +++ b/src/xrpld/rpc/RPCHandler.h @@ -6,7 +6,7 @@ #include -#include +#include 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 diff --git a/src/xrpld/rpc/detail/Handler.cpp b/src/xrpld/rpc/detail/Handler.cpp index 326af4f4ee..3c0b41a149 100644 --- a/src/xrpld/rpc/detail/Handler.cpp +++ b/src/xrpld/rpc/detail/Handler.cpp @@ -1,50 +1,57 @@ #include #include +#include #include +#include #include #include #include -#include +#include #include #include #include #include +#include #include -#include -#include -#include +#include +#include +#include #include namespace xrpl::rpc { namespace { +// Shorthand: the tables below name this type once per entry. +using Method = Handler::Method; + /** * 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 -Handler::Method -byRef(Function const& f) +template +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 +template Status -handle(JsonContext& context, Object& object) +handle(JsonContext& context, json::Value& object) { XRPL_ASSERT( context.apiVersion >= HandlerImpl::minApiVer && @@ -65,427 +72,581 @@ handle(JsonContext& context, Object& object) } template -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, + Method::of<&handle>(), HandlerImpl::role, HandlerImpl::condition, HandlerImpl::minApiVer, - HandlerImpl::maxApiVer}; + HandlerImpl::maxApiVer, + }; } -Handler const kHandlerArray[]{ - // Some handlers not specified here are added to the table via addHandler() +// The handlers that name the function they dispatch to. The order is free: +// getHandler() searches kHandlers below, which is this array and the next one +// sorted together. +constexpr auto kFunctionHandlerArray = std::to_array({ // Request-response methods - {.name = "account_info", - .valueMethod = byRef(&doAccountInfo), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "account_currencies", - .valueMethod = byRef(&doAccountCurrencies), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "account_lines", - .valueMethod = byRef(&doAccountLines), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "account_channels", - .valueMethod = byRef(&doAccountChannels), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "account_nfts", - .valueMethod = byRef(&doAccountNFTs), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "account_objects", - .valueMethod = byRef(&doAccountObjects), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "account_offers", - .valueMethod = byRef(&doAccountOffers), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "account_tx", - .valueMethod = byRef(&doAccountTx), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "amm_info", - .valueMethod = byRef(&doAMMInfo), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "blacklist", - .valueMethod = byRef(&doBlackList), - .role = Role::ADMIN, - .condition = Condition::NoCondition}, - {.name = "book_changes", - .valueMethod = byRef(&doBookChanges), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "book_offers", - .valueMethod = byRef(&doBookOffers), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "can_delete", - .valueMethod = byRef(&doCanDelete), - .role = Role::ADMIN, - .condition = Condition::NoCondition}, - {.name = "channel_authorize", - .valueMethod = byRef(&doChannelAuthorize), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "channel_verify", - .valueMethod = byRef(&doChannelVerify), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "connect", - .valueMethod = byRef(&doConnect), - .role = Role::ADMIN, - .condition = Condition::NoCondition}, - {.name = "consensus_info", - .valueMethod = byRef(&doConsensusInfo), - .role = Role::ADMIN, - .condition = Condition::NoCondition}, - {.name = "deposit_authorized", - .valueMethod = byRef(&doDepositAuthorized), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "feature", - .valueMethod = byRef(&doFeature), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "fee", - .valueMethod = byRef(&doFee), - .role = Role::USER, - .condition = Condition::NeedsCurrentLedger}, - {.name = "fetch_info", - .valueMethod = byRef(&doFetchInfo), - .role = Role::ADMIN, - .condition = Condition::NoCondition}, - {.name = "gateway_balances", - .valueMethod = byRef(&doGatewayBalances), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "get_counts", - .valueMethod = byRef(&doGetCounts), - .role = Role::ADMIN, - .condition = Condition::NoCondition}, - {.name = "get_aggregate_price", - .valueMethod = byRef(&doGetAggregatePrice), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "ledger_accept", - .valueMethod = byRef(&doLedgerAccept), - .role = Role::ADMIN, - .condition = Condition::NeedsCurrentLedger}, - {.name = "ledger_cleaner", - .valueMethod = byRef(&doLedgerCleaner), - .role = Role::ADMIN, - .condition = Condition::NeedsNetworkConnection}, - {.name = "ledger_closed", - .valueMethod = byRef(&doLedgerClosed), - .role = Role::USER, - .condition = Condition::NeedsClosedLedger}, - {.name = "ledger_current", - .valueMethod = byRef(&doLedgerCurrent), - .role = Role::USER, - .condition = Condition::NeedsCurrentLedger}, - {.name = "ledger_data", - .valueMethod = byRef(&doLedgerData), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "ledger_entry", - .valueMethod = byRef(&doLedgerEntry), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "ledger_header", - .valueMethod = byRef(&doLedgerHeader), - .role = Role::USER, - .condition = Condition::NoCondition, - .minApiVer = 1, - .maxApiVer = 1}, - {.name = "ledger_request", - .valueMethod = byRef(&doLedgerRequest), - .role = Role::ADMIN, - .condition = Condition::NoCondition}, - {.name = "log_level", - .valueMethod = byRef(&doLogLevel), - .role = Role::ADMIN, - .condition = Condition::NoCondition}, - {.name = "logrotate", - .valueMethod = byRef(&doLogRotate), - .role = Role::ADMIN, - .condition = Condition::NoCondition}, - {.name = "manifest", - .valueMethod = byRef(&doManifest), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "nft_buy_offers", - .valueMethod = byRef(&doNFTBuyOffers), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "nft_sell_offers", - .valueMethod = byRef(&doNFTSellOffers), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "noripple_check", - .valueMethod = byRef(&doNoRippleCheck), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "owner_info", - .valueMethod = byRef(&doOwnerInfo), - .role = Role::USER, - .condition = Condition::NeedsCurrentLedger}, - {.name = "peers", - .valueMethod = byRef(&doPeers), - .role = Role::ADMIN, - .condition = Condition::NoCondition}, - {.name = "path_find", - .valueMethod = byRef(&doPathFind), - .role = Role::USER, - .condition = Condition::NeedsCurrentLedger}, - {.name = "ping", - .valueMethod = byRef(&doPing), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "print", - .valueMethod = byRef(&doPrint), - .role = Role::ADMIN, - .condition = Condition::NoCondition}, - // { "profile", byRef (&doProfile), Role::USER, - // NEEDS_CURRENT_LEDGER }, - {.name = "random", - .valueMethod = byRef(&doRandom), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "peer_reservations_add", - .valueMethod = byRef(&doPeerReservationsAdd), - .role = Role::ADMIN, - .condition = Condition::NoCondition}, - {.name = "peer_reservations_del", - .valueMethod = byRef(&doPeerReservationsDel), - .role = Role::ADMIN, - .condition = Condition::NoCondition}, - {.name = "peer_reservations_list", - .valueMethod = byRef(&doPeerReservationsList), - .role = Role::ADMIN, - .condition = Condition::NoCondition}, - {.name = "ripple_path_find", - .valueMethod = byRef(&doRipplePathFind), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "server_definitions", - .valueMethod = byRef(&doServerDefinitions), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "server_info", - .valueMethod = byRef(&doServerInfo), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "server_state", - .valueMethod = byRef(&doServerState), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "sign", - .valueMethod = byRef(&doSign), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "sign_for", - .valueMethod = byRef(&doSignFor), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "simulate", - .valueMethod = byRef(&doSimulate), - .role = Role::USER, - .condition = Condition::NeedsCurrentLedger}, - {.name = "stop", - .valueMethod = byRef(&doStop), - .role = Role::ADMIN, - .condition = Condition::NoCondition}, - {.name = "submit", - .valueMethod = byRef(&doSubmit), - .role = Role::USER, - .condition = Condition::NeedsCurrentLedger}, - {.name = "submit_multisigned", - .valueMethod = byRef(&doSubmitMultiSigned), - .role = Role::USER, - .condition = Condition::NeedsCurrentLedger}, - {.name = "transaction_entry", - .valueMethod = byRef(&doTransactionEntry), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "tx", - .valueMethod = byRef(&doTxJson), - .role = Role::USER, - .condition = Condition::NeedsNetworkConnection}, - {.name = "tx_history", - .valueMethod = byRef(&doTxHistory), - .role = Role::USER, - .condition = Condition::NoCondition, - .minApiVer = 1, - .maxApiVer = 1}, - {.name = "tx_reduce_relay", - .valueMethod = byRef(&doTxReduceRelay), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "unl_list", - .valueMethod = byRef(&doUnlList), - .role = Role::ADMIN, - .condition = Condition::NoCondition}, - {.name = "validation_create", - .valueMethod = byRef(&doValidationCreate), - .role = Role::ADMIN, - .condition = Condition::NoCondition}, - {.name = "validators", - .valueMethod = byRef(&doValidators), - .role = Role::ADMIN, - .condition = Condition::NoCondition}, - {.name = "validator_list_sites", - .valueMethod = byRef(&doValidatorListSites), - .role = Role::ADMIN, - .condition = Condition::NoCondition}, - {.name = "validator_info", - .valueMethod = byRef(&doValidatorInfo), - .role = Role::ADMIN, - .condition = Condition::NoCondition}, - {.name = "vault_info", - .valueMethod = byRef(&doVaultInfo), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "wallet_propose", - .valueMethod = byRef(&doWalletPropose), - .role = Role::ADMIN, - .condition = Condition::NoCondition}, + { + .name = method::kAccountInfo, + .valueMethod = Method::of<&byRef<&doAccountInfo>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + }, + { + .name = method::kAccountCurrencies, + .valueMethod = Method::of<&byRef<&doAccountCurrencies>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + }, + { + .name = method::kAccountLines, + .valueMethod = Method::of<&byRef<&doAccountLines>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + }, + { + .name = method::kAccountChannels, + .valueMethod = Method::of<&byRef<&doAccountChannels>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + }, + { + .name = method::kAccountNfts, + .valueMethod = Method::of<&byRef<&doAccountNFTs>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + }, + { + .name = method::kAccountObjects, + .valueMethod = Method::of<&byRef<&doAccountObjects>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + }, + { + .name = method::kAccountOffers, + .valueMethod = Method::of<&byRef<&doAccountOffers>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + }, + { + .name = method::kAccountTx, + .valueMethod = Method::of<&byRef<&doAccountTx>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + }, + { + .name = method::kAmmInfo, + .valueMethod = Method::of<&byRef<&doAMMInfo>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + }, + { + .name = method::kBlacklist, + .valueMethod = Method::of<&byRef<&doBlackList>>(), + .role = Role::ADMIN, + .condition = Condition::NoCondition, + .hasCommandLineForm = false, + }, + { + .name = method::kBookChanges, + .valueMethod = Method::of<&byRef<&doBookChanges>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + }, + { + .name = method::kBookOffers, + .valueMethod = Method::of<&byRef<&doBookOffers>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + }, + { + .name = method::kCanDelete, + .valueMethod = Method::of<&byRef<&doCanDelete>>(), + .role = Role::ADMIN, + .condition = Condition::NoCondition, + }, + { + .name = method::kChannelAuthorize, + .valueMethod = Method::of<&byRef<&doChannelAuthorize>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + }, + { + .name = method::kChannelVerify, + .valueMethod = Method::of<&byRef<&doChannelVerify>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + }, + { + .name = method::kConnect, + .valueMethod = Method::of<&byRef<&doConnect>>(), + .role = Role::ADMIN, + .condition = Condition::NoCondition, + }, + { + .name = method::kConsensusInfo, + .valueMethod = Method::of<&byRef<&doConsensusInfo>>(), + .role = Role::ADMIN, + .condition = Condition::NoCondition, + }, + { + .name = method::kDepositAuthorized, + .valueMethod = Method::of<&byRef<&doDepositAuthorized>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + }, + { + .name = method::kFeature, + .valueMethod = Method::of<&byRef<&doFeature>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + }, + { + .name = method::kFee, + .valueMethod = Method::of<&byRef<&doFee>>(), + .role = Role::USER, + .condition = Condition::NeedsCurrentLedger, + .hasCommandLineForm = false, + }, + { + .name = method::kFetchInfo, + .valueMethod = Method::of<&byRef<&doFetchInfo>>(), + .role = Role::ADMIN, + .condition = Condition::NoCondition, + }, + { + .name = method::kGatewayBalances, + .valueMethod = Method::of<&byRef<&doGatewayBalances>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + }, + { + .name = method::kGetCounts, + .valueMethod = Method::of<&byRef<&doGetCounts>>(), + .role = Role::ADMIN, + .condition = Condition::NoCondition, + }, + { + .name = method::kGetAggregatePrice, + .valueMethod = Method::of<&byRef<&doGetAggregatePrice>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + .hasCommandLineForm = false, + }, + { + .name = method::kLedgerAccept, + .valueMethod = Method::of<&byRef<&doLedgerAccept>>(), + .role = Role::ADMIN, + .condition = Condition::NeedsCurrentLedger, + }, + { + .name = method::kLedgerCleaner, + .valueMethod = Method::of<&byRef<&doLedgerCleaner>>(), + .role = Role::ADMIN, + .condition = Condition::NeedsNetworkConnection, + .hasCommandLineForm = false, + }, + { + .name = method::kLedgerClosed, + .valueMethod = Method::of<&byRef<&doLedgerClosed>>(), + .role = Role::USER, + .condition = Condition::NeedsClosedLedger, + }, + { + .name = method::kLedgerCurrent, + .valueMethod = Method::of<&byRef<&doLedgerCurrent>>(), + .role = Role::USER, + .condition = Condition::NeedsCurrentLedger, + }, + { + .name = method::kLedgerData, + .valueMethod = Method::of<&byRef<&doLedgerData>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + .hasCommandLineForm = false, + }, + { + .name = method::kLedgerEntry, + .valueMethod = Method::of<&byRef<&doLedgerEntry>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + }, + { + .name = method::kLedgerHeader, + .valueMethod = Method::of<&byRef<&doLedgerHeader>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + .minApiVer = 1, + .maxApiVer = 1, + }, + { + .name = method::kLedgerRequest, + .valueMethod = Method::of<&byRef<&doLedgerRequest>>(), + .role = Role::ADMIN, + .condition = Condition::NoCondition, + }, + { + .name = method::kLogLevel, + .valueMethod = Method::of<&byRef<&doLogLevel>>(), + .role = Role::ADMIN, + .condition = Condition::NoCondition, + }, + { + .name = method::kLogrotate, + .valueMethod = Method::of<&byRef<&doLogRotate>>(), + .role = Role::ADMIN, + .condition = Condition::NoCondition, + }, + { + .name = method::kManifest, + .valueMethod = Method::of<&byRef<&doManifest>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + }, + { + .name = method::kNftBuyOffers, + .valueMethod = Method::of<&byRef<&doNFTBuyOffers>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + .hasCommandLineForm = false, + }, + { + .name = method::kNftSellOffers, + .valueMethod = Method::of<&byRef<&doNFTSellOffers>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + .hasCommandLineForm = false, + }, + { + .name = method::kNorippleCheck, + .valueMethod = Method::of<&byRef<&doNoRippleCheck>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + .hasCommandLineForm = false, + }, + { + .name = method::kOwnerInfo, + .valueMethod = Method::of<&byRef<&doOwnerInfo>>(), + .role = Role::USER, + .condition = Condition::NeedsCurrentLedger, + }, + { + .name = method::kPeers, + .valueMethod = Method::of<&byRef<&doPeers>>(), + .role = Role::ADMIN, + .condition = Condition::NoCondition, + }, + { + .name = method::kPathFind, + .valueMethod = Method::of<&byRef<&doPathFind>>(), + .role = Role::USER, + .condition = Condition::NeedsCurrentLedger, + }, + { + .name = method::kPing, + .valueMethod = Method::of<&byRef<&doPing>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + }, + { + .name = method::kPrint, + .valueMethod = Method::of<&byRef<&doPrint>>(), + .role = Role::ADMIN, + .condition = Condition::NoCondition, + }, + { + .name = method::kRandom, + .valueMethod = Method::of<&byRef<&doRandom>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + }, + { + .name = method::kPeerReservationsAdd, + .valueMethod = Method::of<&byRef<&doPeerReservationsAdd>>(), + .role = Role::ADMIN, + .condition = Condition::NoCondition, + }, + { + .name = method::kPeerReservationsDel, + .valueMethod = Method::of<&byRef<&doPeerReservationsDel>>(), + .role = Role::ADMIN, + .condition = Condition::NoCondition, + }, + { + .name = method::kPeerReservationsList, + .valueMethod = Method::of<&byRef<&doPeerReservationsList>>(), + .role = Role::ADMIN, + .condition = Condition::NoCondition, + }, + { + .name = method::kRipplePathFind, + .valueMethod = Method::of<&byRef<&doRipplePathFind>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + }, + { + .name = method::kServerDefinitions, + .valueMethod = Method::of<&byRef<&doServerDefinitions>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + }, + { + .name = method::kServerInfo, + .valueMethod = Method::of<&byRef<&doServerInfo>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + }, + { + .name = method::kServerState, + .valueMethod = Method::of<&byRef<&doServerState>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + }, + { + .name = method::kSign, + .valueMethod = Method::of<&byRef<&doSign>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + }, + { + .name = method::kSignFor, + .valueMethod = Method::of<&byRef<&doSignFor>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + }, + { + .name = method::kSimulate, + .valueMethod = Method::of<&byRef<&doSimulate>>(), + .role = Role::USER, + .condition = Condition::NeedsCurrentLedger, + }, + { + .name = method::kStop, + .valueMethod = Method::of<&byRef<&doStop>>(), + .role = Role::ADMIN, + .condition = Condition::NoCondition, + }, + { + .name = method::kSubmit, + .valueMethod = Method::of<&byRef<&doSubmit>>(), + .role = Role::USER, + .condition = Condition::NeedsCurrentLedger, + }, + { + .name = method::kSubmitMultisigned, + .valueMethod = Method::of<&byRef<&doSubmitMultiSigned>>(), + .role = Role::USER, + .condition = Condition::NeedsCurrentLedger, + }, + { + .name = method::kTransactionEntry, + .valueMethod = Method::of<&byRef<&doTransactionEntry>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + }, + { + .name = method::kTx, + .valueMethod = Method::of<&byRef<&doTxJson>>(), + .role = Role::USER, + .condition = Condition::NeedsNetworkConnection, + }, + { + .name = method::kTxHistory, + .valueMethod = Method::of<&byRef<&doTxHistory>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + .minApiVer = 1, + .maxApiVer = 1, + }, + { + .name = method::kTxReduceRelay, + .valueMethod = Method::of<&byRef<&doTxReduceRelay>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + .hasCommandLineForm = false, + }, + { + .name = method::kUnlList, + .valueMethod = Method::of<&byRef<&doUnlList>>(), + .role = Role::ADMIN, + .condition = Condition::NoCondition, + }, + { + .name = method::kValidationCreate, + .valueMethod = Method::of<&byRef<&doValidationCreate>>(), + .role = Role::ADMIN, + .condition = Condition::NoCondition, + }, + { + .name = method::kValidators, + .valueMethod = Method::of<&byRef<&doValidators>>(), + .role = Role::ADMIN, + .condition = Condition::NoCondition, + .hasCommandLineForm = false, + }, + { + .name = method::kValidatorListSites, + .valueMethod = Method::of<&byRef<&doValidatorListSites>>(), + .role = Role::ADMIN, + .condition = Condition::NoCondition, + .hasCommandLineForm = false, + }, + { + .name = method::kValidatorInfo, + .valueMethod = Method::of<&byRef<&doValidatorInfo>>(), + .role = Role::ADMIN, + .condition = Condition::NoCondition, + }, + { + .name = method::kVaultInfo, + .valueMethod = Method::of<&byRef<&doVaultInfo>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + }, + { + .name = method::kWalletPropose, + .valueMethod = Method::of<&byRef<&doWalletPropose>>(), + .role = Role::ADMIN, + .condition = Condition::NoCondition, + }, // Event methods - {.name = "subscribe", - .valueMethod = byRef(&doSubscribe), - .role = Role::USER, - .condition = Condition::NoCondition}, - {.name = "unsubscribe", - .valueMethod = byRef(&doUnsubscribe), - .role = Role::USER, - .condition = Condition::NoCondition}, -}; + { + .name = method::kSubscribe, + .valueMethod = Method::of<&byRef<&doSubscribe>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + }, + { + .name = method::kUnsubscribe, + .valueMethod = Method::of<&byRef<&doUnsubscribe>>(), + .role = Role::USER, + .condition = Condition::NoCondition, + }, +}); -class HandlerTable +// 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 auto kClassHandlerArray = std::to_array({ + handlerFrom(), + handlerFrom(), +}); + +/** + * Join the two handler arrays above into one. + * + * Handler has no default constructor, so every entry is built in place from an + * index pack rather than the array being sized and then copied into. The packs + * come from the arrays themselves, so adding a handler to either needs no change + * here. + * + * @return kFunctionHandlerArray followed by kClassHandlerArray. + */ +constexpr auto +joinHandlers() { -private: - using handler_table_t = std::multimap; + constexpr auto kFunctionIndices = std::make_index_sequence{}; + constexpr auto kClassIndices = std::make_index_sequence{}; - // 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 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::index_sequence, std::index_sequence) { + return std::array{ + kFunctionHandlerArray[Function]..., kClassHandlerArray[Class]...}; + }(kFunctionIndices, kClassIndices); +} - return std::any_of( - range.first, - range.second, // - [minVer, maxVer](auto const& item) { - return item.second.minApiVer <= maxVer && item.second.maxApiVer >= minVer; - }); - } +// The whole dispatch table. +constexpr auto kHandlers = [] { + auto all = joinHandlers(); - template - explicit HandlerTable(Handler const (&entries)[N]) - { - for (auto const& entry : entries) + // 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. +// +// The method is not checked: Handler::Method has no default constructor, so an +// entry that omits it does not compile. +static_assert( + [] { + for (std::size_t i = 0; i < kHandlers.size(); ++i) { - if (overlappingApiVersion( - table_.equal_range(entry.name), entry.minApiVer, entry.maxApiVer)) - { - logicError( - std::string("Handler for ") + entry.name + - " overlaps with an existing handler"); - } + auto const& h = kHandlers[i]; + if (h.name.empty() || h.minApiVer > h.maxApiVer || + h.maxApiVer > rpc::kApiMaximumValidVersion || + h.minApiVer < rpc::kApiMinimumSupportedVersion) + return false; - table_.insert({entry.name, entry}); + // 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 and a valid API version range"); - // This is where the new-style handlers are added. - addHandler(); - addHandler(); - } +/** + * Convert the handler names to a form that may be read as C strings. + * + * NullTerminatedView's constructor rejects a name that does not reach its + * terminating null, so this replaces the separate assertion that used to check + * the same property. It is consteval because that constructor is. + * + * @tparam I The indices of kHandlers. + * @return The names, in the order kHandlers holds them, which is sorted. + */ +template +consteval auto +checkedHandlerNames(std::index_sequence) +{ + return std::array{NullTerminatedView{kHandlers[I].name}...}; +} -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 - getHandlerNames() const - { - std::set ret; - for (auto const& i : table_) - ret.insert(i.second.name); - - return ret; - } - -private: - handler_table_t table_; - - template - 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()}); - } -}; +// The handler names, which are already distinct and sorted. +constexpr auto kHandlerNames = checkedHandlerNames(std::make_index_sequence{}); } // 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 +std::span getHandlerNames() { - return HandlerTable::instance().getHandlerNames(); + return kHandlerNames; } } // namespace xrpl::rpc diff --git a/src/xrpld/rpc/detail/Handler.h b/src/xrpld/rpc/detail/Handler.h index 7342c5fcbf..593eafb4a4 100644 --- a/src/xrpld/rpc/detail/Handler.h +++ b/src/xrpld/rpc/detail/Handler.h @@ -6,19 +6,15 @@ #include #include +#include #include #include #include #include #include -#include -#include -#include - -namespace json { -class Object; -} // namespace json +#include +#include namespace xrpl::rpc { @@ -32,20 +28,89 @@ enum class Condition { struct Handler { - template - using Method = std::function; + /** + * The function a handler dispatches to. + * + * A plain function pointer, not a std::function: every method is a free + * function known at compile time, so nothing needs to be captured. That + * keeps Handler a literal type, letting the dispatch table be built and + * checked at compile time. + * + * of() takes the function as a template argument, and there is no default + * constructor, so a table entry that omits its method does not compile. + * + * The pointer is not also checked against null, because gcc under + * -fsanitize=undefined does not fold the address of a function template + * instantiation in a constant expression. A null check in the table + * assertion, or a requires clause on Fn, both fail to compile there. + */ + class Method + { + public: + using Function = Status (*)(JsonContext&, json::Value&); - char const* name; - Method valueMethod; + /** + * Build a Method that calls a given function. + * + * @tparam Fn The function to call. + * @return The Method. + */ + template + static constexpr Method + of() noexcept + { + return Method{Fn}; + } + + /** + * Call the function. + * + * @param context The request being served. + * @param result The object the function writes its reply into. + * @return The status the function returns. + */ + Status + operator()(JsonContext& context, json::Value& result) const + { + return fn_(context, result); + } + + private: + constexpr explicit Method(Function fn) noexcept : fn_(fn) + { + } + + Function fn_; + }; + + std::string_view name; + Method valueMethod; Role role; rpc::Condition condition; unsigned minApiVer = kApiMinimumSupportedVersion; unsigned maxApiVer = kApiMaximumValidVersion; + + // Whether the command-line client accepts this method as a command. The + // exceptions are methods whose arguments have no positional form. A field + // rather than a comment, so that RPCCall_test can check it against the + // command-line table in both directions. + bool hasCommandLineForm = true; }; +/** + * Find the handler that answers a method at an API version. + * + * @param version The API version the request asks for. + * @param betaEnabled Whether the beta API version is enabled, without which + * @p version cannot exceed kApiMaximumSupportedVersion. + * @param name The method name, matched exactly. + * @return The handler, or nullptr if the version is not served, no method has + * this name, or the method is not served at this version. The pointer is + * into the dispatch table, so it outlives every caller. + */ 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 +125,13 @@ 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 refer to storage that outlives the program, so they are safe to + * hold on to, and each reaches its terminating null, so a caller may read one + * as a C string. */ -std::set +std::span getHandlerNames(); template diff --git a/src/xrpld/rpc/detail/RPCCall.cpp b/src/xrpld/rpc/detail/RPCCall.cpp index a752858527..09d244b7e7 100644 --- a/src/xrpld/rpc/detail/RPCCall.cpp +++ b/src/xrpld/rpc/detail/RPCCall.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include @@ -44,10 +45,13 @@ #include #include #include +#include #include +#include #include #include #include +#include #include #include #include @@ -56,6 +60,77 @@ namespace xrpl { class RPCParser; +namespace { + +/** + * The member function a command dispatches to. + * + * of() takes the function as a template argument, and there is no default + * constructor, so a table entry that omits its parser does not compile. + * + * The pointer is not also checked against null, because gcc under + * -fsanitize=undefined does not fold a pointer to a member function in a + * constant expression. A null check in commandsValid(), or a requires clause on + * Fn, both fail to compile there. + */ +class Parse +{ +public: + using Function = json::Value (RPCParser::*)(json::Value const& jvParams); + + /** + * Build a Parse that calls a given member function. + * + * @tparam Fn The member function to call. + * @return The Parse. + */ + template + static constexpr Parse + of() noexcept + { + return Parse{Fn}; + } + + /** + * Call the parser. + * + * Defined below RPCParser, because calling one of its members needs the + * complete class. + * + * @param parser The parser to call the member function on. + * @param jvParams The command line arguments, as an array. + * @return The request, or an error. + */ + json::Value + operator()(RPCParser& parser, json::Value const& jvParams) const; + +private: + constexpr explicit Parse(Function fn) noexcept : fn_(fn) + { + } + + Function fn_; +}; + +// 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::max(); + + std::string_view name; + Parse parse; + unsigned minParams = 0; + unsigned maxParams = kUnlimitedParams; +}; + +} // namespace + // // HTTP protocol // @@ -1239,7 +1314,429 @@ private: return jvRequest; } + // An omitted minParams means the command takes no arguments; an omitted + // maxParams means it takes any number. See Command. + // + // The commands. The order is free: parseCommand() searches kSortedCommands + // below. + static constexpr auto kCommandArray = std::to_array({ + // 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 = Parse::of<&RPCParser::parseAccountCurrencies>(), + .minParams = 1, + .maxParams = 3, + }, + { + .name = rpc::method::kAccountInfo, + .parse = Parse::of<&RPCParser::parseAccountItems>(), + .minParams = 1, + .maxParams = 3, + }, + { + .name = rpc::method::kAccountLines, + .parse = Parse::of<&RPCParser::parseAccountLines>(), + .minParams = 1, + .maxParams = 5, + }, + { + .name = rpc::method::kAccountChannels, + .parse = Parse::of<&RPCParser::parseAccountChannels>(), + .minParams = 1, + .maxParams = 3, + }, + { + .name = rpc::method::kAccountNfts, + .parse = Parse::of<&RPCParser::parseAccountItems>(), + .minParams = 1, + .maxParams = 5, + }, + { + .name = rpc::method::kAccountObjects, + .parse = Parse::of<&RPCParser::parseAccountItems>(), + .minParams = 1, + .maxParams = 5, + }, + { + .name = rpc::method::kAccountOffers, + .parse = Parse::of<&RPCParser::parseAccountItems>(), + .minParams = 1, + .maxParams = 4, + }, + { + .name = rpc::method::kAccountTx, + .parse = Parse::of<&RPCParser::parseAccountTransactions>(), + .minParams = 1, + .maxParams = 8, + }, + { + .name = rpc::method::kAmmInfo, + .parse = Parse::of<&RPCParser::parseAsIs>(), + .minParams = 1, + .maxParams = 2, + }, + { + .name = rpc::method::kVaultInfo, + .parse = Parse::of<&RPCParser::parseVault>(), + .minParams = 1, + .maxParams = 2, + }, + { + .name = rpc::method::kBookChanges, + .parse = Parse::of<&RPCParser::parseLedgerId>(), + .minParams = 1, + .maxParams = 1, + }, + { + .name = rpc::method::kBookOffers, + .parse = Parse::of<&RPCParser::parseBookOffers>(), + .minParams = 2, + .maxParams = 7, + }, + { + .name = rpc::method::kCanDelete, + .parse = Parse::of<&RPCParser::parseCanDelete>(), + .maxParams = 1, + }, + { + .name = rpc::method::kChannelAuthorize, + .parse = Parse::of<&RPCParser::parseChannelAuthorize>(), + .minParams = 3, + .maxParams = 4, + }, + { + .name = rpc::method::kChannelVerify, + .parse = Parse::of<&RPCParser::parseChannelVerify>(), + .minParams = 4, + .maxParams = 4, + }, + { + .name = rpc::method::kConnect, + .parse = Parse::of<&RPCParser::parseConnect>(), + .minParams = 1, + .maxParams = 2, + }, + { + .name = rpc::method::kConsensusInfo, + .parse = Parse::of<&RPCParser::parseAsIs>(), + .maxParams = 0, + }, + { + .name = rpc::method::kDepositAuthorized, + .parse = Parse::of<&RPCParser::parseDepositAuthorized>(), + .minParams = 2, + .maxParams = 11, + }, + { + .name = rpc::method::kFeature, + .parse = Parse::of<&RPCParser::parseFeature>(), + .maxParams = 2, + }, + { + .name = rpc::method::kFetchInfo, + .parse = Parse::of<&RPCParser::parseFetchInfo>(), + .maxParams = 1, + }, + { + .name = rpc::method::kGatewayBalances, + .parse = Parse::of<&RPCParser::parseGatewayBalances>(), + .minParams = 1, + }, + { + .name = rpc::method::kGetCounts, + .parse = Parse::of<&RPCParser::parseGetCounts>(), + .maxParams = 1, + }, + { + .name = rpc::method::kJson, + .parse = Parse::of<&RPCParser::parseJson>(), + .minParams = 2, + .maxParams = 2, + }, + { + .name = rpc::method::kJson2, + .parse = Parse::of<&RPCParser::parseJson2>(), + .minParams = 1, + .maxParams = 1, + }, + { + .name = rpc::method::kLedger, + .parse = Parse::of<&RPCParser::parseLedger>(), + .maxParams = 2, + }, + { + .name = rpc::method::kLedgerAccept, + .parse = Parse::of<&RPCParser::parseAsIs>(), + .maxParams = 0, + }, + { + .name = rpc::method::kLedgerClosed, + .parse = Parse::of<&RPCParser::parseAsIs>(), + .maxParams = 0, + }, + { + .name = rpc::method::kLedgerCurrent, + .parse = Parse::of<&RPCParser::parseAsIs>(), + .maxParams = 0, + }, + { + .name = rpc::method::kLedgerEntry, + .parse = Parse::of<&RPCParser::parseLedgerEntry>(), + .minParams = 1, + .maxParams = 2, + }, + { + .name = rpc::method::kLedgerHeader, + .parse = Parse::of<&RPCParser::parseLedgerId>(), + .minParams = 1, + .maxParams = 1, + }, + { + .name = rpc::method::kLedgerRequest, + .parse = Parse::of<&RPCParser::parseLedgerId>(), + .minParams = 1, + .maxParams = 1, + }, + { + .name = rpc::method::kLogLevel, + .parse = Parse::of<&RPCParser::parseLogLevel>(), + .maxParams = 2, + }, + { + .name = rpc::method::kLogrotate, + .parse = Parse::of<&RPCParser::parseAsIs>(), + .maxParams = 0, + }, + { + .name = rpc::method::kManifest, + .parse = Parse::of<&RPCParser::parseManifest>(), + .minParams = 1, + .maxParams = 1, + }, + { + .name = rpc::method::kOwnerInfo, + .parse = Parse::of<&RPCParser::parseAccountItems>(), + .minParams = 1, + .maxParams = 3, + }, + { + .name = rpc::method::kPeers, + .parse = Parse::of<&RPCParser::parseAsIs>(), + .maxParams = 0, + }, + { + .name = rpc::method::kPing, + .parse = Parse::of<&RPCParser::parseAsIs>(), + .maxParams = 0, + }, + { + .name = rpc::method::kPrint, + .parse = Parse::of<&RPCParser::parseAsIs>(), + .maxParams = 1, + }, + { + .name = rpc::method::kRandom, + .parse = Parse::of<&RPCParser::parseAsIs>(), + .maxParams = 0, + }, + { + .name = rpc::method::kPeerReservationsAdd, + .parse = Parse::of<&RPCParser::parsePeerReservationsAdd>(), + .minParams = 1, + .maxParams = 2, + }, + { + .name = rpc::method::kPeerReservationsDel, + .parse = Parse::of<&RPCParser::parsePeerReservationsDel>(), + .minParams = 1, + .maxParams = 1, + }, + { + .name = rpc::method::kPeerReservationsList, + .parse = Parse::of<&RPCParser::parseAsIs>(), + .maxParams = 0, + }, + { + .name = rpc::method::kRipplePathFind, + .parse = Parse::of<&RPCParser::parseRipplePathFind>(), + .minParams = 1, + .maxParams = 2, + }, + { + .name = rpc::method::kServerDefinitions, + .parse = Parse::of<&RPCParser::parseServerDefinitions>(), + .maxParams = 1, + }, + { + .name = rpc::method::kServerInfo, + .parse = Parse::of<&RPCParser::parseServerInfo>(), + .maxParams = 1, + }, + { + .name = rpc::method::kServerState, + .parse = Parse::of<&RPCParser::parseServerInfo>(), + .maxParams = 1, + }, + { + .name = rpc::method::kSign, + .parse = Parse::of<&RPCParser::parseSignSubmit>(), + .minParams = 2, + .maxParams = 4, + }, + { + .name = rpc::method::kSignFor, + .parse = Parse::of<&RPCParser::parseSignFor>(), + .minParams = 3, + .maxParams = 4, + }, + { + .name = rpc::method::kStop, + .parse = Parse::of<&RPCParser::parseAsIs>(), + .maxParams = 0, + }, + { + .name = rpc::method::kSimulate, + .parse = Parse::of<&RPCParser::parseSimulate>(), + .minParams = 1, + .maxParams = 2, + }, + { + .name = rpc::method::kSubmit, + .parse = Parse::of<&RPCParser::parseSignSubmit>(), + .minParams = 1, + .maxParams = 4, + }, + { + .name = rpc::method::kSubmitMultisigned, + .parse = Parse::of<&RPCParser::parseSubmitMultiSigned>(), + .minParams = 1, + .maxParams = 1, + }, + { + .name = rpc::method::kTransactionEntry, + .parse = Parse::of<&RPCParser::parseTransactionEntry>(), + .minParams = 2, + .maxParams = 2, + }, + { + .name = rpc::method::kTx, + .parse = Parse::of<&RPCParser::parseTx>(), + .minParams = 1, + .maxParams = 4, + }, + { + .name = rpc::method::kTxHistory, + .parse = Parse::of<&RPCParser::parseTxHistory>(), + .minParams = 1, + .maxParams = 1, + }, + { + .name = rpc::method::kUnlList, + .parse = Parse::of<&RPCParser::parseAsIs>(), + .maxParams = 0, + }, + { + .name = rpc::method::kValidationCreate, + .parse = Parse::of<&RPCParser::parseValidationCreate>(), + .maxParams = 1, + }, + { + .name = rpc::method::kValidatorInfo, + .parse = Parse::of<&RPCParser::parseAsIs>(), + .maxParams = 0, + }, + { + .name = rpc::method::kVersion, + .parse = Parse::of<&RPCParser::parseAsIs>(), + .maxParams = 0, + }, + { + .name = rpc::method::kWalletPropose, + .parse = Parse::of<&RPCParser::parseWalletPropose>(), + .maxParams = 1, + }, + { + .name = rpc::method::kInternal, + .parse = Parse::of<&RPCParser::parseInternal>(), + .minParams = 1, + }, + + // Event methods, rejected below, so the parameter range is unconstrained + { + .name = rpc::method::kPathFind, + .parse = Parse::of<&RPCParser::parseEvented>(), + }, + { + .name = rpc::method::kSubscribe, + .parse = Parse::of<&RPCParser::parseEvented>(), + }, + { + .name = rpc::method::kUnsubscribe, + .parse = Parse::of<&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 names{}; + std::ranges::transform(kSortedCommands, names.begin(), &Command::name); + return names; + }(); + public: + /** + * Names of every method the command line accepts. + */ + static std::span + 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, or the command line would accept a command it cannot + * answer. RPCCall_test checks the second property, because it can see the + * handler table. This checks the first, and the parameter range. + * + * The parser is not checked: Parse has no default constructor, so an entry + * that omits it does not compile. + * + * This is a function the static_assert below the class calls, rather than + * the assert itself, because the table names members of RPCParser, and that + * 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.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 +1748,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 +1756,48 @@ 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 found->parse(*this, jvParams); } }; +namespace { + +// Out of line because RPCParser is incomplete where Parse is declared. +json::Value +Parse::operator()(RPCParser& parser, json::Value const& jvParams) const +{ + return (parser.*fn_)(jvParams); +} + +} // namespace + +// See the comment on commandsValid() for why this is out here. +static_assert( + RPCParser::commandsValid(), + "xrpl::RPCParser : every command needs a unique name and a valid parameter " + "count range"); + //------------------------------------------------------------------------------ // @@ -1614,6 +1905,12 @@ struct RPCCallImp //------------------------------------------------------------------------------ +std::span +commandLineMethodNames() +{ + return RPCParser::methodNames(); +} + // Used internally by rpcClient. json::Value rpcCmdToJson( diff --git a/src/xrpld/rpc/detail/RPCHandler.cpp b/src/xrpld/rpc/detail/RPCHandler.cpp index 96d6bf72d7..5501cdfde3 100644 --- a/src/xrpld/rpc/detail/RPCHandler.cpp +++ b/src/xrpld/rpc/detail/RPCHandler.cpp @@ -23,6 +23,7 @@ #include #include #include +#include namespace xrpl::rpc { @@ -153,9 +154,8 @@ fillHandler(JsonContext& context, Handler const*& result) return RpcSuccess; } -template 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 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: Handler::Method has no default constructor, so + // every entry in the dispatch table names one. + 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); diff --git a/src/xrpld/rpc/handlers/ledger/Ledger.h b/src/xrpld/rpc/handlers/ledger/Ledger.h index 07d24b497d..17c95e2653 100644 --- a/src/xrpld/rpc/handlers/ledger/Ledger.h +++ b/src/xrpld/rpc/handlers/ledger/Ledger.h @@ -3,6 +3,7 @@ #include #include // IWYU pragma: keep #include +#include #include #include #include @@ -12,12 +13,9 @@ #include #include +#include #include -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; diff --git a/src/xrpld/rpc/handlers/server_info/Version.h b/src/xrpld/rpc/handlers/server_info/Version.h index 40ad4e5e71..98c9fa987c 100644 --- a/src/xrpld/rpc/handlers/server_info/Version.h +++ b/src/xrpld/rpc/handlers/server_info/Version.h @@ -2,6 +2,7 @@ #include // IWYU pragma: keep #include +#include #include #include #include @@ -9,6 +10,8 @@ #include #include +#include + 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;