From fb36b0adc0797a4f121d4abbba4857b6ccb2f281 Mon Sep 17 00:00:00 2001 From: Bart <11445373+bthomee@users.noreply.github.com> Date: Tue, 11 Aug 2026 07:07:16 -0400 Subject: [PATCH] refactor: Build the RPC dispatch tables at compile time A new MethodNames.h gives every RPC method a string_view constant, shared by the handler table and the command-line parser table so the two cannot drift apart. That lets a handler carry its name as a string_view, and both tables become sorted constexpr arrays found by binary search, replacing the HandlerTable singleton and its multimap and RPCParser's runtime table. Their invariants -- a unique name, a method, and a valid API version range -- are now a static_assert rather than a logicError on the first request of the process. Handler::Method becomes a plain function pointer: every method is a free function known at compile time, so the std::function never captured anything, and dropping it makes Handler a literal type, which is what a constexpr table needs; it also makes doCommand's null-method branch dead, since an empty std::function is no longer representable. The names being program-lifetime constants removes work throughout. getHandlerNames and commandLineMethodNames return a span over a constexpr array instead of building a fresh std::set per call, one of those on the startup path; PerfLogImp's rpc map keys on string_view, so it needs neither a transparent hasher nor a string copy per key, and reports counters through json::StaticString rather than duplicating each key into the object. Command parameter counts become unsigned with a named kUnlimitedParams; as ints with -1 for unlimited they were compared against an unsigned size, so `count < minParams` promoted -1 and was always true, and only the `>= 0` guards kept that from being reachable. PerfLog no longer reaches into the RPC layer to learn which methods to count: makePerfLog takes the names, so xrpld.perflog stops depending on xrpld.rpc, and PerfLog_test uses five made-up labels, which drops test.basics > xrpld.rpc as well. Both edges are gone from levelization. Handler_test's benchmark now asks for kApiMinimumSupportedVersion instead of a hardcoded 1 and fails if a lookup misses -- once API versions 1 and 2 retire, getHandler(1, ...) would return at its bounds check and the benchmark would have silently reported timings for that check. Co-Authored-By: Claude Opus 5 --- .../scripts/levelization/results/ordering.txt | 3 +- include/xrpl/core/PerfLog.h | 17 +- src/test/basics/PerfLog_test.cpp | 39 +- src/test/core/Workers_test.cpp | 8 +- src/test/jtx/TestHelpers.h | 7 - src/test/rpc/Handler_test.cpp | 29 +- src/test/rpc/RPCCall_test.cpp | 34 ++ src/xrpld/app/main/Application.cpp | 2 + src/xrpld/perflog/detail/PerfLogImp.cpp | 32 +- src/xrpld/perflog/detail/PerfLogImp.h | 33 +- src/xrpld/rpc/MethodNames.h | 93 ++++ src/xrpld/rpc/RPCCall.h | 11 + src/xrpld/rpc/RPCHandler.h | 4 +- src/xrpld/rpc/detail/Handler.cpp | 505 ++++++++--------- src/xrpld/rpc/detail/Handler.h | 29 +- src/xrpld/rpc/detail/RPCCall.cpp | 526 ++++++++++-------- src/xrpld/rpc/detail/RPCHandler.cpp | 35 +- src/xrpld/rpc/handlers/ledger/Ledger.h | 8 +- src/xrpld/rpc/handlers/server_info/Version.h | 5 +- 19 files changed, 818 insertions(+), 602 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/core/PerfLog.h b/include/xrpl/core/PerfLog.h index f09665e291..0bd819230d 100644 --- a/include/xrpl/core/PerfLog.h +++ b/include/xrpl/core/PerfLog.h @@ -10,7 +10,9 @@ #include #include #include +#include #include +#include namespace beast { class Journal; @@ -68,7 +70,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 @@ -77,7 +79,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 @@ -86,7 +88,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 @@ -151,10 +153,17 @@ public: PerfLog::Setup setupPerfLog(Section const& section, boost::filesystem::path const& configDir); +/** + * @param methodNames The RPC methods to count, one counter per name. The names + * must outlive the returned object, which holds views of them. Callers + * pass rpc::getHandlerNames(); it is an argument so that this layer needs + * to know nothing about the RPC dispatch table. + */ std::unique_ptr makePerfLog( PerfLog::Setup const& setup, Application& app, + std::span methodNames, beast::Journal journal, std::function&& signalStop); @@ -162,7 +171,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 24ea971515..37f1543e18 100644 --- a/src/test/basics/PerfLog_test.cpp +++ b/src/test/basics/PerfLog_test.cpp @@ -1,9 +1,6 @@ #include -#include #include -#include - #include #include #include @@ -21,6 +18,7 @@ #include #include +#include #include #include #include @@ -30,7 +28,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -45,6 +45,21 @@ class PerfLog_test : public beast::unit_test::Suite using path = boost::filesystem::path; + // The method names to count. PerfLog treats these as opaque keys, so these + // are made up rather than taken from the RPC dispatch table: this test then + // needs to know nothing about the RPC layer, and does not silently change + // shape when a method is added or removed. + // + // They are string literals because PerfLog stores views of these names and + // reads them back as C strings, so they must be null-terminated and outlive + // the PerfLog. Sorted, because the counters are reported in sorted order. + static constexpr std::array kMethodNames{ + std::string_view{"method_a"}, + std::string_view{"method_b"}, + std::string_view{"method_c"}, + std::string_view{"method_d"}, + std::string_view{"method_e"}}; + // We're only using Env for its Journal. That Journal gives better // coverage in unit tests. test::jtx::Env env_{*this, test::jtx::envconfig(), nullptr, beast::Severity::Disabled}; @@ -117,7 +132,7 @@ class PerfLog_test : public beast::unit_test::Suite { perf::PerfLog::Setup const setup{ .perfLog = withFile == WithFile::No ? "" : logFile(), .logInterval = logInterval()}; - return perf::makePerfLog(setup, app, j, [this]() { + return perf::makePerfLog(setup, app, kMethodNames, j, [this]() { signalStop(); return; }); @@ -310,9 +325,11 @@ public: auto perfLog{fixture.perfLog(withFile)}; perfLog->start(); - // Get the all the labels we can use for RPC interfaces without - // causing an assert. - std::vector labels = test::jtx::makeVector(xrpl::rpc::getHandlerNames()); + // The labels we can use for RPC interfaces without causing an assert: + // exactly those the PerfLog was constructed with. Copied into a vector + // because they are shuffled below and then paired positionally with the + // request ids. + auto labels = std::ranges::to(kMethodNames); std::shuffle(labels.begin(), labels.end(), defaultPrng()); // Get two IDs to associate with each label. Errors tend to happen at @@ -347,7 +364,7 @@ public: for (auto& label : labels) { // Expect every label in labels to have the same contents. - json::Value const& counter{countersJson[label]}; + json::Value const& counter{countersJson[std::string{label}]}; BEAST_EXPECT(counter[jss::duration_us] == "0"); BEAST_EXPECT(counter[jss::errored] == "0"); BEAST_EXPECT(counter[jss::finished] == "0"); @@ -404,7 +421,7 @@ public: // their durations with the appropriate labels. { // The first label is special. It should have "errored" : "0". - json::Value const& first = rpc[labels[0]]; + json::Value const& first = rpc[std::string{labels[0]}]; BEAST_EXPECT(first[jss::duration_us] != "0"); BEAST_EXPECT(first[jss::errored] == "0"); BEAST_EXPECT(first[jss::finished] == "1"); @@ -415,7 +432,7 @@ public: std::uint64_t prevDur = std::numeric_limits::max(); for (int i = 1; i < labels.size(); ++i) { - json::Value const& counter{rpc[labels[i]]}; + json::Value const& counter{rpc[std::string{labels[i]}]}; std::uint64_t const dur{jsonToUInt64(counter[jss::duration_us])}; BEAST_EXPECT(dur != 0 && dur < prevDur); prevDur = dur; @@ -447,7 +464,7 @@ public: BEAST_EXPECT(only.size() == 2); BEAST_EXPECT(only.isObject()); BEAST_EXPECT(only[jss::duration_us] != "0"); - BEAST_EXPECT(only[jss::method] == labels[0]); + BEAST_EXPECT(only[jss::method] == std::string{labels[0]}); }; // Validate the final state of the PerfLog. diff --git a/src/test/core/Workers_test.cpp b/src/test/core/Workers_test.cpp index fe3820b84a..22bb34b159 100644 --- a/src/test/core/Workers_test.cpp +++ b/src/test/core/Workers_test.cpp @@ -8,7 +8,7 @@ #include #include #include -#include +#include namespace xrpl { @@ -21,17 +21,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 dur) override { } diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index 801c3627b8..d7b3e59f7a 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -316,13 +316,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..55a7b6f6b1 100644 --- a/src/test/rpc/Handler_test.cpp +++ b/src/test/rpc/Handler_test.cpp @@ -1,6 +1,4 @@ -#include - #include #include @@ -13,8 +11,9 @@ #include #include #include +#include +#include #include -#include // cspell: words stdev namespace xrpl::test { @@ -88,21 +87,41 @@ class Handler_test : public beast::unit_test::Suite std::random_device dev; std::ranlux48 prng(dev()); - std::vector names = test::jtx::makeVector(xrpl::rpc::getHandlerNames()); + // Contiguous, so the timed loop's pick-a-name-by-index costs nothing + // and the measurement reflects getHandler() alone. + auto const names = xrpl::rpc::getHandlerNames(); std::uniform_int_distribution distr{0, names.size() - 1}; + // The lowest version still served. Asking for one outside the supported + // range would make getHandler() return at its bounds check, without + // searching, and the benchmark would then be timing that check. + constexpr unsigned kVersion = rpc::kApiMinimumSupportedVersion; + std::size_t dummy = 0; + std::size_t misses = 0; auto const [mean, stdev, n] = time( 1'000'000, [&](std::size_t i) { - auto const d = rpc::getHandler(1, false, names[i]); + auto const d = rpc::getHandler(kVersion, false, names[i]); + if (d == nullptr) + { + ++misses; + return; + } dummy = dummy + i + (int)d->role; }, [&]() -> std::size_t { return distr(prng); }); std::cout << "mean=" << mean << " stdev=" << stdev << " N=" << n << '\n'; + // A miss means the timed call did no lookup, so the figure above is not + // a measurement of one. Every name comes from getHandlerNames(), so a + // handler answering at kVersion is the only way this holds. + BEAST_EXPECTS( + misses == 0, + std::to_string(misses) + " of " + std::to_string(n) + " lookups at API version " + + std::to_string(kVersion) + " found no handler, so nothing was measured"); BEAST_EXPECT(dummy != 0); } diff --git a/src/test/rpc/RPCCall_test.cpp b/src/test/rpc/RPCCall_test.cpp index ef3213008c..d3c830c1dd 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,38 @@ public: } } + // Every name in the command-line table must also name a server method, so + // that a command accepted on the command line can actually be dispatched. + // + // The reverse does not hold: a server method needs no command-line form. + // Three command-line names are also exempt because they are wrappers that + // forward a caller-supplied method rather than naming one themselves. + void + testCommandLineNamesAreDispatchable() + { + testcase("Command-line methods are dispatchable"); + + static constexpr std::array kWrappers{ + rpc::method::kInternal, rpc::method::kJson, rpc::method::kJson2}; + + auto const dispatchable = rpc::getHandlerNames(); + BEAST_EXPECT(!dispatchable.empty()); + + for (auto const& name : commandLineMethodNames()) + { + if (std::ranges::find(kWrappers, name) != kWrappers.end()) + continue; + + // Both name lists are sorted, so a binary search suffices. + BEAST_EXPECTS(std::ranges::binary_search(dispatchable, name), std::string{name}); + } + } + void run() override { forAllApiVersions([this](unsigned apiVersion) { testRPCCall(apiVersion); }); + testCommandLineNamesAreDispatchable(); } }; 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 3aa7e38ea2..7a26884423 100644 --- a/src/xrpld/perflog/detail/PerfLogImp.cpp +++ b/src/xrpld/perflog/detail/PerfLogImp.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -27,22 +28,23 @@ #include #include #include -#include +#include #include +#include #include #include #include namespace xrpl::perf { -PerfLogImp::Counters::Counters(std::set const& labels, JobTypes const& jobTypes) +PerfLogImp::Counters::Counters(std::span labels, JobTypes const& jobTypes) { { // populateRpc rpc.reserve(labels.size()); - for (std::string const label : labels) + for (auto const& label : labels) { - auto const inserted = rpc.emplace(label, Rpc()).second; + auto const inserted = rpc.try_emplace(label).second; if (!inserted) { // Ensure that no other function populates this entry. @@ -101,7 +103,9 @@ PerfLogImp::Counters::countersJson() const totalRpc.errored += value.errored; p[jss::duration_us] = std::to_string(value.duration.count()); totalRpc.duration += value.duration; - rpcobj[proc.first] = p; + // The key is a method name constant, which outlives the program, so it + // can be borrowed rather than duplicated into the object. + rpcobj[json::StaticString{proc.first.data()}] = p; } if (totalRpc.started != 0u) @@ -196,7 +200,7 @@ PerfLogImp::Counters::currentJson() const for (auto m : methods) { json::Value methodobj(json::ValueType::Object); - methodobj[jss::method] = m.first; + 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); @@ -300,9 +304,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(); } @@ -313,7 +322,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()) @@ -329,11 +338,11 @@ PerfLogImp::rpcStart(std::string const& method, std::uint64_t const requestId) ++counter->second.value.started; } std::scoped_lock const lock(counters_.methodsMutex); - counters_.methods[requestId] = {counter->first.c_str(), steady_clock::now()}; + counters_.methods[requestId] = {counter->first, steady_clock::now()}; } void -PerfLogImp::rpcEnd(std::string const& method, std::uint64_t const requestId, bool finish) +PerfLogImp::rpcEnd(std::string_view method, std::uint64_t const requestId, bool finish) { auto counter = counters_.rpc.find(method); if (counter == counters_.rpc.end()) @@ -502,10 +511,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..4269770745 100644 --- a/src/xrpld/perflog/detail/PerfLogImp.h +++ b/src/xrpld/perflog/detail/PerfLogImp.h @@ -1,7 +1,5 @@ #pragma once -#include - #include #include #include @@ -15,8 +13,9 @@ #include #include #include -#include +#include #include +#include #include #include #include @@ -59,7 +58,8 @@ class PerfLogImp : public PerfLog struct Counters { public: - using MethodStart = std::pair; + using MethodStart = std::pair; + /** * RPC performance counters. */ @@ -91,14 +91,18 @@ class PerfLogImp : public PerfLog // rpc and jq do not need mutex protection because all // keys and values are created before more threads are started. - std::unordered_map> rpc; + // + // Every key is a view of a method name constant, which outlives the + // program, so the map neither copies a name to store it nor needs one + // materialized to look it up. + std::unordered_map> rpc; std::unordered_map> jq; std::vector> jobs; mutable std::mutex jobsMutex; 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 +113,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 +130,35 @@ class PerfLogImp : public PerfLog void report(); void - rpcEnd(std::string const& method, std::uint64_t const requestId, bool finish); + rpcEnd(std::string_view method, std::uint64_t const requestId, bool finish); public: + /** + * @param methodNames The RPC methods to count, one counter per name. The + * names must outlive this object, which holds views of them. Passed + * in rather than looked up here so that this layer needs to know + * nothing about the RPC dispatch table. + */ PerfLogImp( Setup setup, Application& app, + std::span 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..eb9bce32cd --- /dev/null +++ b/src/xrpld/rpc/MethodNames.h @@ -0,0 +1,93 @@ +#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. + */ + +inline constexpr std::string_view kAccountChannels{"account_channels"}; +inline constexpr std::string_view kAccountCurrencies{"account_currencies"}; +inline constexpr std::string_view kAccountInfo{"account_info"}; +inline constexpr std::string_view kAccountLines{"account_lines"}; +inline constexpr std::string_view kAccountNfts{"account_nfts"}; +inline constexpr std::string_view kAccountObjects{"account_objects"}; +inline constexpr std::string_view kAccountOffers{"account_offers"}; +inline constexpr std::string_view kAccountTx{"account_tx"}; +inline constexpr std::string_view kAmmInfo{"amm_info"}; +inline constexpr std::string_view kBlacklist{"blacklist"}; // no command-line form +inline constexpr std::string_view kBookChanges{"book_changes"}; +inline constexpr std::string_view kBookOffers{"book_offers"}; +inline constexpr std::string_view kCanDelete{"can_delete"}; +inline constexpr std::string_view kChannelAuthorize{"channel_authorize"}; +inline constexpr std::string_view kChannelVerify{"channel_verify"}; +inline constexpr std::string_view kConnect{"connect"}; +inline constexpr std::string_view kConsensusInfo{"consensus_info"}; +inline constexpr std::string_view kDepositAuthorized{"deposit_authorized"}; +inline constexpr std::string_view kFeature{"feature"}; +inline constexpr std::string_view kFee{"fee"}; // no command-line form +inline constexpr std::string_view kFetchInfo{"fetch_info"}; +inline constexpr std::string_view kGatewayBalances{"gateway_balances"}; +inline constexpr std::string_view kGetAggregatePrice{ + "get_aggregate_price"}; // no command-line form +inline constexpr std::string_view kGetCounts{"get_counts"}; +inline constexpr std::string_view kInternal{"internal"}; // command-line wrapper +inline constexpr std::string_view kJson{"json"}; // command-line wrapper +inline constexpr std::string_view kJson2{"json2"}; // command-line wrapper +inline constexpr std::string_view kLedger{"ledger"}; +inline constexpr std::string_view kLedgerAccept{"ledger_accept"}; +inline constexpr std::string_view kLedgerCleaner{"ledger_cleaner"}; // no command-line form +inline constexpr std::string_view kLedgerClosed{"ledger_closed"}; +inline constexpr std::string_view kLedgerCurrent{"ledger_current"}; +inline constexpr std::string_view kLedgerData{"ledger_data"}; // no command-line form +inline constexpr std::string_view kLedgerEntry{"ledger_entry"}; +inline constexpr std::string_view kLedgerHeader{"ledger_header"}; +inline constexpr std::string_view kLedgerRequest{"ledger_request"}; +inline constexpr std::string_view kLogLevel{"log_level"}; +inline constexpr std::string_view kLogrotate{"logrotate"}; +inline constexpr std::string_view kManifest{"manifest"}; +inline constexpr std::string_view kNftBuyOffers{"nft_buy_offers"}; // no command-line form +inline constexpr std::string_view kNftSellOffers{"nft_sell_offers"}; // no command-line form +inline constexpr std::string_view kNorippleCheck{"noripple_check"}; // no command-line form +inline constexpr std::string_view kOwnerInfo{"owner_info"}; +inline constexpr std::string_view kPathFind{"path_find"}; +inline constexpr std::string_view kPeerReservationsAdd{"peer_reservations_add"}; +inline constexpr std::string_view kPeerReservationsDel{"peer_reservations_del"}; +inline constexpr std::string_view kPeerReservationsList{"peer_reservations_list"}; +inline constexpr std::string_view kPeers{"peers"}; +inline constexpr std::string_view kPing{"ping"}; +inline constexpr std::string_view kPrint{"print"}; +inline constexpr std::string_view kRandom{"random"}; +inline constexpr std::string_view kRipplePathFind{"ripple_path_find"}; +inline constexpr std::string_view kServerDefinitions{"server_definitions"}; +inline constexpr std::string_view kServerInfo{"server_info"}; +inline constexpr std::string_view kServerState{"server_state"}; +inline constexpr std::string_view kSign{"sign"}; +inline constexpr std::string_view kSignFor{"sign_for"}; +inline constexpr std::string_view kSimulate{"simulate"}; +inline constexpr std::string_view kStop{"stop"}; +inline constexpr std::string_view kSubmit{"submit"}; +inline constexpr std::string_view kSubmitMultisigned{"submit_multisigned"}; +inline constexpr std::string_view kSubscribe{"subscribe"}; +inline constexpr std::string_view kTransactionEntry{"transaction_entry"}; +inline constexpr std::string_view kTx{"tx"}; +inline constexpr std::string_view kTxHistory{"tx_history"}; +inline constexpr std::string_view kTxReduceRelay{"tx_reduce_relay"}; // no command-line form +inline constexpr std::string_view kUnlList{"unl_list"}; +inline constexpr std::string_view kUnsubscribe{"unsubscribe"}; +inline constexpr std::string_view kValidationCreate{"validation_create"}; +inline constexpr std::string_view kValidatorInfo{"validator_info"}; +inline constexpr std::string_view kValidatorListSites{ + "validator_list_sites"}; // no command-line form +inline constexpr std::string_view kValidators{"validators"}; // no command-line form +inline constexpr std::string_view kVaultInfo{"vault_info"}; +inline constexpr std::string_view kVersion{"version"}; +inline constexpr std::string_view kWalletPropose{"wallet_propose"}; + +} // namespace xrpl::rpc::method 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..5e18d617a2 100644 --- a/src/xrpld/rpc/detail/Handler.cpp +++ b/src/xrpld/rpc/detail/Handler.cpp @@ -1,50 +1,52 @@ #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 { /** * 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 +67,400 @@ 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, + &handle, HandlerImpl::role, HandlerImpl::condition, HandlerImpl::minApiVer, HandlerImpl::maxApiVer}; } -Handler const kHandlerArray[]{ - // Some handlers not specified here are added to the table via addHandler() +// The handlers, in whatever order reads best. getHandler() searches kHandlers +// below, which is this array sorted; the order here carries no meaning. +constexpr Handler kHandlerArray[]{ // Request-response methods - {.name = "account_info", - .valueMethod = byRef(&doAccountInfo), + {.name = method::kAccountInfo, + .valueMethod = &byRef<&doAccountInfo>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "account_currencies", - .valueMethod = byRef(&doAccountCurrencies), + {.name = method::kAccountCurrencies, + .valueMethod = &byRef<&doAccountCurrencies>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "account_lines", - .valueMethod = byRef(&doAccountLines), + {.name = method::kAccountLines, + .valueMethod = &byRef<&doAccountLines>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "account_channels", - .valueMethod = byRef(&doAccountChannels), + {.name = method::kAccountChannels, + .valueMethod = &byRef<&doAccountChannels>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "account_nfts", - .valueMethod = byRef(&doAccountNFTs), + {.name = method::kAccountNfts, + .valueMethod = &byRef<&doAccountNFTs>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "account_objects", - .valueMethod = byRef(&doAccountObjects), + {.name = method::kAccountObjects, + .valueMethod = &byRef<&doAccountObjects>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "account_offers", - .valueMethod = byRef(&doAccountOffers), + {.name = method::kAccountOffers, + .valueMethod = &byRef<&doAccountOffers>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "account_tx", - .valueMethod = byRef(&doAccountTx), + {.name = method::kAccountTx, + .valueMethod = &byRef<&doAccountTx>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "amm_info", - .valueMethod = byRef(&doAMMInfo), + {.name = method::kAmmInfo, + .valueMethod = &byRef<&doAMMInfo>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "blacklist", - .valueMethod = byRef(&doBlackList), + {.name = method::kBlacklist, + .valueMethod = &byRef<&doBlackList>, .role = Role::ADMIN, .condition = Condition::NoCondition}, - {.name = "book_changes", - .valueMethod = byRef(&doBookChanges), + {.name = method::kBookChanges, + .valueMethod = &byRef<&doBookChanges>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "book_offers", - .valueMethod = byRef(&doBookOffers), + {.name = method::kBookOffers, + .valueMethod = &byRef<&doBookOffers>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "can_delete", - .valueMethod = byRef(&doCanDelete), + {.name = method::kCanDelete, + .valueMethod = &byRef<&doCanDelete>, .role = Role::ADMIN, .condition = Condition::NoCondition}, - {.name = "channel_authorize", - .valueMethod = byRef(&doChannelAuthorize), + {.name = method::kChannelAuthorize, + .valueMethod = &byRef<&doChannelAuthorize>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "channel_verify", - .valueMethod = byRef(&doChannelVerify), + {.name = method::kChannelVerify, + .valueMethod = &byRef<&doChannelVerify>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "connect", - .valueMethod = byRef(&doConnect), + {.name = method::kConnect, + .valueMethod = &byRef<&doConnect>, .role = Role::ADMIN, .condition = Condition::NoCondition}, - {.name = "consensus_info", - .valueMethod = byRef(&doConsensusInfo), + {.name = method::kConsensusInfo, + .valueMethod = &byRef<&doConsensusInfo>, .role = Role::ADMIN, .condition = Condition::NoCondition}, - {.name = "deposit_authorized", - .valueMethod = byRef(&doDepositAuthorized), + {.name = method::kDepositAuthorized, + .valueMethod = &byRef<&doDepositAuthorized>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "feature", - .valueMethod = byRef(&doFeature), + {.name = method::kFeature, + .valueMethod = &byRef<&doFeature>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "fee", - .valueMethod = byRef(&doFee), + {.name = method::kFee, + .valueMethod = &byRef<&doFee>, .role = Role::USER, .condition = Condition::NeedsCurrentLedger}, - {.name = "fetch_info", - .valueMethod = byRef(&doFetchInfo), + {.name = method::kFetchInfo, + .valueMethod = &byRef<&doFetchInfo>, .role = Role::ADMIN, .condition = Condition::NoCondition}, - {.name = "gateway_balances", - .valueMethod = byRef(&doGatewayBalances), + {.name = method::kGatewayBalances, + .valueMethod = &byRef<&doGatewayBalances>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "get_counts", - .valueMethod = byRef(&doGetCounts), + {.name = method::kGetCounts, + .valueMethod = &byRef<&doGetCounts>, .role = Role::ADMIN, .condition = Condition::NoCondition}, - {.name = "get_aggregate_price", - .valueMethod = byRef(&doGetAggregatePrice), + {.name = method::kGetAggregatePrice, + .valueMethod = &byRef<&doGetAggregatePrice>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "ledger_accept", - .valueMethod = byRef(&doLedgerAccept), + {.name = method::kLedgerAccept, + .valueMethod = &byRef<&doLedgerAccept>, .role = Role::ADMIN, .condition = Condition::NeedsCurrentLedger}, - {.name = "ledger_cleaner", - .valueMethod = byRef(&doLedgerCleaner), + {.name = method::kLedgerCleaner, + .valueMethod = &byRef<&doLedgerCleaner>, .role = Role::ADMIN, .condition = Condition::NeedsNetworkConnection}, - {.name = "ledger_closed", - .valueMethod = byRef(&doLedgerClosed), + {.name = method::kLedgerClosed, + .valueMethod = &byRef<&doLedgerClosed>, .role = Role::USER, .condition = Condition::NeedsClosedLedger}, - {.name = "ledger_current", - .valueMethod = byRef(&doLedgerCurrent), + {.name = method::kLedgerCurrent, + .valueMethod = &byRef<&doLedgerCurrent>, .role = Role::USER, .condition = Condition::NeedsCurrentLedger}, - {.name = "ledger_data", - .valueMethod = byRef(&doLedgerData), + {.name = method::kLedgerData, + .valueMethod = &byRef<&doLedgerData>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "ledger_entry", - .valueMethod = byRef(&doLedgerEntry), + {.name = method::kLedgerEntry, + .valueMethod = &byRef<&doLedgerEntry>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "ledger_header", - .valueMethod = byRef(&doLedgerHeader), + {.name = method::kLedgerHeader, + .valueMethod = &byRef<&doLedgerHeader>, .role = Role::USER, .condition = Condition::NoCondition, .minApiVer = 1, .maxApiVer = 1}, - {.name = "ledger_request", - .valueMethod = byRef(&doLedgerRequest), + {.name = method::kLedgerRequest, + .valueMethod = &byRef<&doLedgerRequest>, .role = Role::ADMIN, .condition = Condition::NoCondition}, - {.name = "log_level", - .valueMethod = byRef(&doLogLevel), + {.name = method::kLogLevel, + .valueMethod = &byRef<&doLogLevel>, .role = Role::ADMIN, .condition = Condition::NoCondition}, - {.name = "logrotate", - .valueMethod = byRef(&doLogRotate), + {.name = method::kLogrotate, + .valueMethod = &byRef<&doLogRotate>, .role = Role::ADMIN, .condition = Condition::NoCondition}, - {.name = "manifest", - .valueMethod = byRef(&doManifest), + {.name = method::kManifest, + .valueMethod = &byRef<&doManifest>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "nft_buy_offers", - .valueMethod = byRef(&doNFTBuyOffers), + {.name = method::kNftBuyOffers, + .valueMethod = &byRef<&doNFTBuyOffers>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "nft_sell_offers", - .valueMethod = byRef(&doNFTSellOffers), + {.name = method::kNftSellOffers, + .valueMethod = &byRef<&doNFTSellOffers>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "noripple_check", - .valueMethod = byRef(&doNoRippleCheck), + {.name = method::kNorippleCheck, + .valueMethod = &byRef<&doNoRippleCheck>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "owner_info", - .valueMethod = byRef(&doOwnerInfo), + {.name = method::kOwnerInfo, + .valueMethod = &byRef<&doOwnerInfo>, .role = Role::USER, .condition = Condition::NeedsCurrentLedger}, - {.name = "peers", - .valueMethod = byRef(&doPeers), + {.name = method::kPeers, + .valueMethod = &byRef<&doPeers>, .role = Role::ADMIN, .condition = Condition::NoCondition}, - {.name = "path_find", - .valueMethod = byRef(&doPathFind), + {.name = method::kPathFind, + .valueMethod = &byRef<&doPathFind>, .role = Role::USER, .condition = Condition::NeedsCurrentLedger}, - {.name = "ping", - .valueMethod = byRef(&doPing), + {.name = method::kPing, + .valueMethod = &byRef<&doPing>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "print", - .valueMethod = byRef(&doPrint), + {.name = method::kPrint, + .valueMethod = &byRef<&doPrint>, .role = Role::ADMIN, .condition = Condition::NoCondition}, - // { "profile", byRef (&doProfile), Role::USER, - // NEEDS_CURRENT_LEDGER }, - {.name = "random", - .valueMethod = byRef(&doRandom), + {.name = method::kRandom, + .valueMethod = &byRef<&doRandom>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "peer_reservations_add", - .valueMethod = byRef(&doPeerReservationsAdd), + {.name = method::kPeerReservationsAdd, + .valueMethod = &byRef<&doPeerReservationsAdd>, .role = Role::ADMIN, .condition = Condition::NoCondition}, - {.name = "peer_reservations_del", - .valueMethod = byRef(&doPeerReservationsDel), + {.name = method::kPeerReservationsDel, + .valueMethod = &byRef<&doPeerReservationsDel>, .role = Role::ADMIN, .condition = Condition::NoCondition}, - {.name = "peer_reservations_list", - .valueMethod = byRef(&doPeerReservationsList), + {.name = method::kPeerReservationsList, + .valueMethod = &byRef<&doPeerReservationsList>, .role = Role::ADMIN, .condition = Condition::NoCondition}, - {.name = "ripple_path_find", - .valueMethod = byRef(&doRipplePathFind), + {.name = method::kRipplePathFind, + .valueMethod = &byRef<&doRipplePathFind>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "server_definitions", - .valueMethod = byRef(&doServerDefinitions), + {.name = method::kServerDefinitions, + .valueMethod = &byRef<&doServerDefinitions>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "server_info", - .valueMethod = byRef(&doServerInfo), + {.name = method::kServerInfo, + .valueMethod = &byRef<&doServerInfo>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "server_state", - .valueMethod = byRef(&doServerState), + {.name = method::kServerState, + .valueMethod = &byRef<&doServerState>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "sign", - .valueMethod = byRef(&doSign), + {.name = method::kSign, + .valueMethod = &byRef<&doSign>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "sign_for", - .valueMethod = byRef(&doSignFor), + {.name = method::kSignFor, + .valueMethod = &byRef<&doSignFor>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "simulate", - .valueMethod = byRef(&doSimulate), + {.name = method::kSimulate, + .valueMethod = &byRef<&doSimulate>, .role = Role::USER, .condition = Condition::NeedsCurrentLedger}, - {.name = "stop", - .valueMethod = byRef(&doStop), + {.name = method::kStop, + .valueMethod = &byRef<&doStop>, .role = Role::ADMIN, .condition = Condition::NoCondition}, - {.name = "submit", - .valueMethod = byRef(&doSubmit), + {.name = method::kSubmit, + .valueMethod = &byRef<&doSubmit>, .role = Role::USER, .condition = Condition::NeedsCurrentLedger}, - {.name = "submit_multisigned", - .valueMethod = byRef(&doSubmitMultiSigned), + {.name = method::kSubmitMultisigned, + .valueMethod = &byRef<&doSubmitMultiSigned>, .role = Role::USER, .condition = Condition::NeedsCurrentLedger}, - {.name = "transaction_entry", - .valueMethod = byRef(&doTransactionEntry), + {.name = method::kTransactionEntry, + .valueMethod = &byRef<&doTransactionEntry>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "tx", - .valueMethod = byRef(&doTxJson), + {.name = method::kTx, + .valueMethod = &byRef<&doTxJson>, .role = Role::USER, .condition = Condition::NeedsNetworkConnection}, - {.name = "tx_history", - .valueMethod = byRef(&doTxHistory), + {.name = method::kTxHistory, + .valueMethod = &byRef<&doTxHistory>, .role = Role::USER, .condition = Condition::NoCondition, .minApiVer = 1, .maxApiVer = 1}, - {.name = "tx_reduce_relay", - .valueMethod = byRef(&doTxReduceRelay), + {.name = method::kTxReduceRelay, + .valueMethod = &byRef<&doTxReduceRelay>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "unl_list", - .valueMethod = byRef(&doUnlList), + {.name = method::kUnlList, + .valueMethod = &byRef<&doUnlList>, .role = Role::ADMIN, .condition = Condition::NoCondition}, - {.name = "validation_create", - .valueMethod = byRef(&doValidationCreate), + {.name = method::kValidationCreate, + .valueMethod = &byRef<&doValidationCreate>, .role = Role::ADMIN, .condition = Condition::NoCondition}, - {.name = "validators", - .valueMethod = byRef(&doValidators), + {.name = method::kValidators, + .valueMethod = &byRef<&doValidators>, .role = Role::ADMIN, .condition = Condition::NoCondition}, - {.name = "validator_list_sites", - .valueMethod = byRef(&doValidatorListSites), + {.name = method::kValidatorListSites, + .valueMethod = &byRef<&doValidatorListSites>, .role = Role::ADMIN, .condition = Condition::NoCondition}, - {.name = "validator_info", - .valueMethod = byRef(&doValidatorInfo), + {.name = method::kValidatorInfo, + .valueMethod = &byRef<&doValidatorInfo>, .role = Role::ADMIN, .condition = Condition::NoCondition}, - {.name = "vault_info", - .valueMethod = byRef(&doVaultInfo), + {.name = method::kVaultInfo, + .valueMethod = &byRef<&doVaultInfo>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "wallet_propose", - .valueMethod = byRef(&doWalletPropose), + {.name = method::kWalletPropose, + .valueMethod = &byRef<&doWalletPropose>, .role = Role::ADMIN, .condition = Condition::NoCondition}, // Event methods - {.name = "subscribe", - .valueMethod = byRef(&doSubscribe), + {.name = method::kSubscribe, + .valueMethod = &byRef<&doSubscribe>, .role = Role::USER, .condition = Condition::NoCondition}, - {.name = "unsubscribe", - .valueMethod = byRef(&doUnsubscribe), + {.name = method::kUnsubscribe, + .valueMethod = &byRef<&doUnsubscribe>, .role = Role::USER, .condition = Condition::NoCondition}, }; -class HandlerTable -{ -private: - using handler_table_t = std::multimap; +// The whole dispatch table: the array above plus the new-style handlers, which +// carry their name and API range as members rather than as a table entry. +constexpr auto kHandlers = [] { + std::array all{}; + auto out = std::ranges::copy(kHandlerArray, all.begin()).out; + *out++ = handlerFrom(); + *out++ = handlerFrom(); - // 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"); + // Sorted by name, so a handler can be found by binary search. + std::ranges::sort(all, {}, &Handler::name); + return all; +}(); - return std::any_of( - range.first, - range.second, // - [minVer, maxVer](auto const& item) { - return item.second.minApiVer <= maxVer && item.second.maxApiVer >= minVer; - }); - } +// 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"); - template - explicit HandlerTable(Handler const (&entries)[N]) - { - for (auto const& entry : entries) +// A name must select exactly one handler, otherwise a request would have two +// answers. Where a method's behaviour differs by API version, the handler +// branches on context.apiVersion rather than being registered once per range. +// Checked here, at compile time, rather than on the first dispatch. +static_assert( + []() { + for (std::size_t i = 0; i < kHandlers.size(); ++i) { - 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.valueMethod == nullptr || 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, a method, and a valid API " + "version range"); - // This is where the new-style handlers are added. - addHandler(); - addHandler(); - } - -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)) +// The names are handed to json::StaticString, and read as C strings from there, +// so each must be a view of a whole string literal rather than a slice of one. +static_assert( + []() { + for (auto const& handler : kHandlers) { - logicError( - std::string("Handler for ") + HandlerImpl::name + - " overlaps with an existing handler"); + if (handler.name.data()[handler.name.size()] != '\0') + return false; } + return true; + }(), + "xrpl::rpc : every handler name must be null-terminated"); - table_.insert({HandlerImpl::name, handlerFrom()}); - } -}; +// The handler names, which are already distinct and sorted. +constexpr auto kHandlerNames = [] { + std::array names{}; + std::ranges::transform(kHandlers, names.begin(), &Handler::name); + return names; +}(); } // namespace Handler const* -getHandler(unsigned version, bool betaEnabled, std::string const& name) +getHandler(unsigned version, bool betaEnabled, std::string_view name) { - return HandlerTable::instance().getHandler(version, betaEnabled, name); + if (version < rpc::kApiMinimumSupportedVersion || + version > (betaEnabled ? rpc::kApiBetaVersion : rpc::kApiMaximumSupportedVersion)) + return nullptr; + + // Names are unique, so the binary search finds the only candidate; it then + // answers this request only if it serves this version. + auto const i = std::ranges::lower_bound(kHandlers, name, {}, &Handler::name); + if (i == kHandlers.end() || i->name != name) + return nullptr; + + if (i->minApiVer <= version && version <= i->maxApiVer) + return &*i; + + return nullptr; } -std::set +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..6fb64d1b5a 100644 --- a/src/xrpld/rpc/detail/Handler.h +++ b/src/xrpld/rpc/detail/Handler.h @@ -12,13 +12,8 @@ #include #include -#include -#include -#include - -namespace json { -class Object; -} // namespace json +#include +#include namespace xrpl::rpc { @@ -32,11 +27,14 @@ enum class Condition { struct Handler { - template - using Method = std::function; + // A plain function pointer, not a std::function: every method is a free + // function known at compile time, so nothing needs to be captured. This + // also keeps Handler a literal type, letting the dispatch table be built + // and checked at compile time. + using Method = Status (*)(JsonContext&, json::Value&); - char const* name; - Method valueMethod; + std::string_view name; + Method valueMethod; Role role; rpc::Condition condition; @@ -45,7 +43,7 @@ struct Handler }; Handler const* -getHandler(unsigned int version, bool betaEnabled, std::string const&); +getHandler(unsigned int version, bool betaEnabled, std::string_view name); /** * Return a json::ValueType::Object with a single entry. @@ -60,9 +58,12 @@ makeObjectValue(Value const& value, json::StaticString const& field = jss::messa } /** - * Return names of all methods. + * Return the names of all methods, sorted and without duplicates. + * + * The names view refers to storage that outlives the program, so it is safe to + * hold on to. */ -std::set +std::span getHandlerNames(); template diff --git a/src/xrpld/rpc/detail/RPCCall.cpp b/src/xrpld/rpc/detail/RPCCall.cpp index a752858527..fd47926e90 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,14 @@ #include #include #include +#include #include +#include +#include #include #include #include +#include #include #include #include @@ -56,6 +61,27 @@ namespace xrpl { class RPCParser; +namespace { + +// One command the command line accepts: the method it names, the parser that +// turns arguments into a request, and how many arguments that parser needs. +// +// Declared out here, rather than nested in RPCParser, so that the defaults +// below can be used: a default member initializer is not available while the +// enclosing class is still incomplete, which is when the table is built. +struct Command +{ + // For a command that accepts any number of parameters. + static constexpr unsigned kUnlimitedParams = std::numeric_limits::max(); + + std::string_view name; + json::Value (RPCParser::*parse)(json::Value const& jvParams); + unsigned minParams = 0; + unsigned maxParams = kUnlimitedParams; +}; + +} // namespace + // // HTTP protocol // @@ -1239,7 +1265,242 @@ private: return jvRequest; } + // An omitted minParams means the command takes no arguments; an omitted + // maxParams means it takes any number. See Command. + // + // The commands, in whatever order reads best. parseCommand() searches + // kSortedCommands below, so the order here carries no meaning. + static constexpr auto kCommandArray = std::to_array({ + // Request-response methods + // - Returns an error, or the request. + // - To modify the method, provide a new method in the request. + {.name = rpc::method::kAccountCurrencies, + .parse = &RPCParser::parseAccountCurrencies, + .minParams = 1, + .maxParams = 3}, + {.name = rpc::method::kAccountInfo, + .parse = &RPCParser::parseAccountItems, + .minParams = 1, + .maxParams = 3}, + {.name = rpc::method::kAccountLines, + .parse = &RPCParser::parseAccountLines, + .minParams = 1, + .maxParams = 5}, + {.name = rpc::method::kAccountChannels, + .parse = &RPCParser::parseAccountChannels, + .minParams = 1, + .maxParams = 3}, + {.name = rpc::method::kAccountNfts, + .parse = &RPCParser::parseAccountItems, + .minParams = 1, + .maxParams = 5}, + {.name = rpc::method::kAccountObjects, + .parse = &RPCParser::parseAccountItems, + .minParams = 1, + .maxParams = 5}, + {.name = rpc::method::kAccountOffers, + .parse = &RPCParser::parseAccountItems, + .minParams = 1, + .maxParams = 4}, + {.name = rpc::method::kAccountTx, + .parse = &RPCParser::parseAccountTransactions, + .minParams = 1, + .maxParams = 8}, + {.name = rpc::method::kAmmInfo, + .parse = &RPCParser::parseAsIs, + .minParams = 1, + .maxParams = 2}, + {.name = rpc::method::kVaultInfo, + .parse = &RPCParser::parseVault, + .minParams = 1, + .maxParams = 2}, + {.name = rpc::method::kBookChanges, + .parse = &RPCParser::parseLedgerId, + .minParams = 1, + .maxParams = 1}, + {.name = rpc::method::kBookOffers, + .parse = &RPCParser::parseBookOffers, + .minParams = 2, + .maxParams = 7}, + {.name = rpc::method::kCanDelete, .parse = &RPCParser::parseCanDelete, .maxParams = 1}, + {.name = rpc::method::kChannelAuthorize, + .parse = &RPCParser::parseChannelAuthorize, + .minParams = 3, + .maxParams = 4}, + {.name = rpc::method::kChannelVerify, + .parse = &RPCParser::parseChannelVerify, + .minParams = 4, + .maxParams = 4}, + {.name = rpc::method::kConnect, + .parse = &RPCParser::parseConnect, + .minParams = 1, + .maxParams = 2}, + {.name = rpc::method::kConsensusInfo, .parse = &RPCParser::parseAsIs, .maxParams = 0}, + {.name = rpc::method::kDepositAuthorized, + .parse = &RPCParser::parseDepositAuthorized, + .minParams = 2, + .maxParams = 11}, + {.name = rpc::method::kFeature, .parse = &RPCParser::parseFeature, .maxParams = 2}, + {.name = rpc::method::kFetchInfo, .parse = &RPCParser::parseFetchInfo, .maxParams = 1}, + {.name = rpc::method::kGatewayBalances, + .parse = &RPCParser::parseGatewayBalances, + .minParams = 1}, + {.name = rpc::method::kGetCounts, .parse = &RPCParser::parseGetCounts, .maxParams = 1}, + {.name = rpc::method::kJson, + .parse = &RPCParser::parseJson, + .minParams = 2, + .maxParams = 2}, + {.name = rpc::method::kJson2, + .parse = &RPCParser::parseJson2, + .minParams = 1, + .maxParams = 1}, + {.name = rpc::method::kLedger, .parse = &RPCParser::parseLedger, .maxParams = 2}, + {.name = rpc::method::kLedgerAccept, .parse = &RPCParser::parseAsIs, .maxParams = 0}, + {.name = rpc::method::kLedgerClosed, .parse = &RPCParser::parseAsIs, .maxParams = 0}, + {.name = rpc::method::kLedgerCurrent, .parse = &RPCParser::parseAsIs, .maxParams = 0}, + {.name = rpc::method::kLedgerEntry, + .parse = &RPCParser::parseLedgerEntry, + .minParams = 1, + .maxParams = 2}, + {.name = rpc::method::kLedgerHeader, + .parse = &RPCParser::parseLedgerId, + .minParams = 1, + .maxParams = 1}, + {.name = rpc::method::kLedgerRequest, + .parse = &RPCParser::parseLedgerId, + .minParams = 1, + .maxParams = 1}, + {.name = rpc::method::kLogLevel, .parse = &RPCParser::parseLogLevel, .maxParams = 2}, + {.name = rpc::method::kLogrotate, .parse = &RPCParser::parseAsIs, .maxParams = 0}, + {.name = rpc::method::kManifest, + .parse = &RPCParser::parseManifest, + .minParams = 1, + .maxParams = 1}, + {.name = rpc::method::kOwnerInfo, + .parse = &RPCParser::parseAccountItems, + .minParams = 1, + .maxParams = 3}, + {.name = rpc::method::kPeers, .parse = &RPCParser::parseAsIs, .maxParams = 0}, + {.name = rpc::method::kPing, .parse = &RPCParser::parseAsIs, .maxParams = 0}, + {.name = rpc::method::kPrint, .parse = &RPCParser::parseAsIs, .maxParams = 1}, + {.name = rpc::method::kRandom, .parse = &RPCParser::parseAsIs, .maxParams = 0}, + {.name = rpc::method::kPeerReservationsAdd, + .parse = &RPCParser::parsePeerReservationsAdd, + .minParams = 1, + .maxParams = 2}, + {.name = rpc::method::kPeerReservationsDel, + .parse = &RPCParser::parsePeerReservationsDel, + .minParams = 1, + .maxParams = 1}, + {.name = rpc::method::kPeerReservationsList, + .parse = &RPCParser::parseAsIs, + .maxParams = 0}, + {.name = rpc::method::kRipplePathFind, + .parse = &RPCParser::parseRipplePathFind, + .minParams = 1, + .maxParams = 2}, + {.name = rpc::method::kServerDefinitions, + .parse = &RPCParser::parseServerDefinitions, + .maxParams = 1}, + {.name = rpc::method::kServerInfo, .parse = &RPCParser::parseServerInfo, .maxParams = 1}, + {.name = rpc::method::kServerState, .parse = &RPCParser::parseServerInfo, .maxParams = 1}, + {.name = rpc::method::kSign, + .parse = &RPCParser::parseSignSubmit, + .minParams = 2, + .maxParams = 4}, + {.name = rpc::method::kSignFor, + .parse = &RPCParser::parseSignFor, + .minParams = 3, + .maxParams = 4}, + {.name = rpc::method::kStop, .parse = &RPCParser::parseAsIs, .maxParams = 0}, + {.name = rpc::method::kSimulate, + .parse = &RPCParser::parseSimulate, + .minParams = 1, + .maxParams = 2}, + {.name = rpc::method::kSubmit, + .parse = &RPCParser::parseSignSubmit, + .minParams = 1, + .maxParams = 4}, + {.name = rpc::method::kSubmitMultisigned, + .parse = &RPCParser::parseSubmitMultiSigned, + .minParams = 1, + .maxParams = 1}, + {.name = rpc::method::kTransactionEntry, + .parse = &RPCParser::parseTransactionEntry, + .minParams = 2, + .maxParams = 2}, + {.name = rpc::method::kTx, .parse = &RPCParser::parseTx, .minParams = 1, .maxParams = 4}, + {.name = rpc::method::kTxHistory, + .parse = &RPCParser::parseTxHistory, + .minParams = 1, + .maxParams = 1}, + {.name = rpc::method::kUnlList, .parse = &RPCParser::parseAsIs, .maxParams = 0}, + {.name = rpc::method::kValidationCreate, + .parse = &RPCParser::parseValidationCreate, + .maxParams = 1}, + {.name = rpc::method::kValidatorInfo, .parse = &RPCParser::parseAsIs, .maxParams = 0}, + {.name = rpc::method::kVersion, .parse = &RPCParser::parseAsIs, .maxParams = 0}, + {.name = rpc::method::kWalletPropose, + .parse = &RPCParser::parseWalletPropose, + .maxParams = 1}, + {.name = rpc::method::kInternal, .parse = &RPCParser::parseInternal, .minParams = 1}, + + // Event methods -- rejected outright below, so any parameters will do + {.name = rpc::method::kPathFind, .parse = &RPCParser::parseEvented}, + {.name = rpc::method::kSubscribe, .parse = &RPCParser::parseEvented}, + {.name = rpc::method::kUnsubscribe, .parse = &RPCParser::parseEvented}, + }); + + // kCommandArray sorted by name, so a command can be found by binary search. + static constexpr auto kSortedCommands = [] { + auto commands = kCommandArray; + std::ranges::sort(commands, {}, &Command::name); + return commands; + }(); + + // parseCommand() relies on this being sorted to binary search it, and + // kCommandNames below inherits the order. + static_assert( + std::ranges::is_sorted(kSortedCommands, {}, &Command::name), + "xrpl::RPCParser : kSortedCommands must be sorted"); + + // A name must select exactly one command, and must name a method the server + // can dispatch -- otherwise the command line would accept something that + // then goes nowhere. The latter is checked by RPCCall_test, which can see + // the handler table; here we can at least rule out duplicates and gaps. + static_assert( + []() { + for (std::size_t i = 0; i < kSortedCommands.size(); ++i) + { + auto const& command = kSortedCommands[i]; + if (command.name.empty() || command.parse == nullptr || + command.minParams > command.maxParams) + return false; + if (i > 0 && kSortedCommands[i - 1].name == command.name) + return false; + } + return true; + }(), + "xrpl::RPCParser : every command needs a unique name, a parser, and a " + "valid parameter count range"); + + // 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; + } + //-------------------------------------------------------------------------- explicit RPCParser(unsigned apiVersion, beast::Journal j) : apiVersion_(apiVersion), j_(j) @@ -1251,7 +1512,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,251 +1520,28 @@ public: stream << "Params: " << jvParams; } - struct Command + auto const found = std::ranges::lower_bound(kSortedCommands, strMethod, {}, &Command::name); + + if (found == kSortedCommands.end() || found->name != strMethod) { - char const* name; - parseFuncPtr parse; - int minParams; - int maxParams; - }; + // The command could not be found + if (!allowAnyCommand) + return rpcError(RpcUnknownCommand); - static constexpr Command kCommands[] = { - // Request-response methods - // - Returns an error, or the request. - // - To modify the method, provide a new method in the request. - {.name = "account_currencies", - .parse = &RPCParser::parseAccountCurrencies, - .minParams = 1, - .maxParams = 3}, - {.name = "account_info", - .parse = &RPCParser::parseAccountItems, - .minParams = 1, - .maxParams = 3}, - {.name = "account_lines", - .parse = &RPCParser::parseAccountLines, - .minParams = 1, - .maxParams = 5}, - {.name = "account_channels", - .parse = &RPCParser::parseAccountChannels, - .minParams = 1, - .maxParams = 3}, - {.name = "account_nfts", - .parse = &RPCParser::parseAccountItems, - .minParams = 1, - .maxParams = 5}, - {.name = "account_objects", - .parse = &RPCParser::parseAccountItems, - .minParams = 1, - .maxParams = 5}, - {.name = "account_offers", - .parse = &RPCParser::parseAccountItems, - .minParams = 1, - .maxParams = 4}, - {.name = "account_tx", - .parse = &RPCParser::parseAccountTransactions, - .minParams = 1, - .maxParams = 8}, - {.name = "amm_info", .parse = &RPCParser::parseAsIs, .minParams = 1, .maxParams = 2}, - {.name = "vault_info", .parse = &RPCParser::parseVault, .minParams = 1, .maxParams = 2}, - {.name = "book_changes", - .parse = &RPCParser::parseLedgerId, - .minParams = 1, - .maxParams = 1}, - {.name = "book_offers", - .parse = &RPCParser::parseBookOffers, - .minParams = 2, - .maxParams = 7}, - {.name = "can_delete", - .parse = &RPCParser::parseCanDelete, - .minParams = 0, - .maxParams = 1}, - {.name = "channel_authorize", - .parse = &RPCParser::parseChannelAuthorize, - .minParams = 3, - .maxParams = 4}, - {.name = "channel_verify", - .parse = &RPCParser::parseChannelVerify, - .minParams = 4, - .maxParams = 4}, - {.name = "connect", .parse = &RPCParser::parseConnect, .minParams = 1, .maxParams = 2}, - {.name = "consensus_info", - .parse = &RPCParser::parseAsIs, - .minParams = 0, - .maxParams = 0}, - {.name = "deposit_authorized", - .parse = &RPCParser::parseDepositAuthorized, - .minParams = 2, - .maxParams = 11}, - {.name = "feature", .parse = &RPCParser::parseFeature, .minParams = 0, .maxParams = 2}, - {.name = "fetch_info", - .parse = &RPCParser::parseFetchInfo, - .minParams = 0, - .maxParams = 1}, - {.name = "gateway_balances", - .parse = &RPCParser::parseGatewayBalances, - .minParams = 1, - .maxParams = -1}, - {.name = "get_counts", - .parse = &RPCParser::parseGetCounts, - .minParams = 0, - .maxParams = 1}, - {.name = "json", .parse = &RPCParser::parseJson, .minParams = 2, .maxParams = 2}, - {.name = "json2", .parse = &RPCParser::parseJson2, .minParams = 1, .maxParams = 1}, - {.name = "ledger", .parse = &RPCParser::parseLedger, .minParams = 0, .maxParams = 2}, - {.name = "ledger_accept", - .parse = &RPCParser::parseAsIs, - .minParams = 0, - .maxParams = 0}, - {.name = "ledger_closed", - .parse = &RPCParser::parseAsIs, - .minParams = 0, - .maxParams = 0}, - {.name = "ledger_current", - .parse = &RPCParser::parseAsIs, - .minParams = 0, - .maxParams = 0}, - {.name = "ledger_entry", - .parse = &RPCParser::parseLedgerEntry, - .minParams = 1, - .maxParams = 2}, - {.name = "ledger_header", - .parse = &RPCParser::parseLedgerId, - .minParams = 1, - .maxParams = 1}, - {.name = "ledger_request", - .parse = &RPCParser::parseLedgerId, - .minParams = 1, - .maxParams = 1}, - {.name = "log_level", - .parse = &RPCParser::parseLogLevel, - .minParams = 0, - .maxParams = 2}, - {.name = "logrotate", .parse = &RPCParser::parseAsIs, .minParams = 0, .maxParams = 0}, - {.name = "manifest", - .parse = &RPCParser::parseManifest, - .minParams = 1, - .maxParams = 1}, - {.name = "owner_info", - .parse = &RPCParser::parseAccountItems, - .minParams = 1, - .maxParams = 3}, - {.name = "peers", .parse = &RPCParser::parseAsIs, .minParams = 0, .maxParams = 0}, - {.name = "ping", .parse = &RPCParser::parseAsIs, .minParams = 0, .maxParams = 0}, - {.name = "print", .parse = &RPCParser::parseAsIs, .minParams = 0, .maxParams = 1}, - // { "profile", &RPCParser::parseProfile, 1, 9 - // }, - {.name = "random", .parse = &RPCParser::parseAsIs, .minParams = 0, .maxParams = 0}, - {.name = "peer_reservations_add", - .parse = &RPCParser::parsePeerReservationsAdd, - .minParams = 1, - .maxParams = 2}, - {.name = "peer_reservations_del", - .parse = &RPCParser::parsePeerReservationsDel, - .minParams = 1, - .maxParams = 1}, - {.name = "peer_reservations_list", - .parse = &RPCParser::parseAsIs, - .minParams = 0, - .maxParams = 0}, - {.name = "ripple_path_find", - .parse = &RPCParser::parseRipplePathFind, - .minParams = 1, - .maxParams = 2}, - {.name = "server_definitions", - .parse = &RPCParser::parseServerDefinitions, - .minParams = 0, - .maxParams = 1}, - {.name = "server_info", - .parse = &RPCParser::parseServerInfo, - .minParams = 0, - .maxParams = 1}, - {.name = "server_state", - .parse = &RPCParser::parseServerInfo, - .minParams = 0, - .maxParams = 1}, - {.name = "sign", .parse = &RPCParser::parseSignSubmit, .minParams = 2, .maxParams = 4}, - {.name = "sign_for", .parse = &RPCParser::parseSignFor, .minParams = 3, .maxParams = 4}, - {.name = "stop", .parse = &RPCParser::parseAsIs, .minParams = 0, .maxParams = 0}, - {.name = "simulate", - .parse = &RPCParser::parseSimulate, - .minParams = 1, - .maxParams = 2}, - {.name = "submit", - .parse = &RPCParser::parseSignSubmit, - .minParams = 1, - .maxParams = 4}, - {.name = "submit_multisigned", - .parse = &RPCParser::parseSubmitMultiSigned, - .minParams = 1, - .maxParams = 1}, - {.name = "transaction_entry", - .parse = &RPCParser::parseTransactionEntry, - .minParams = 2, - .maxParams = 2}, - {.name = "tx", .parse = &RPCParser::parseTx, .minParams = 1, .maxParams = 4}, - {.name = "tx_history", - .parse = &RPCParser::parseTxHistory, - .minParams = 1, - .maxParams = 1}, - {.name = "unl_list", .parse = &RPCParser::parseAsIs, .minParams = 0, .maxParams = 0}, - {.name = "validation_create", - .parse = &RPCParser::parseValidationCreate, - .minParams = 0, - .maxParams = 1}, - {.name = "validator_info", - .parse = &RPCParser::parseAsIs, - .minParams = 0, - .maxParams = 0}, - {.name = "version", .parse = &RPCParser::parseAsIs, .minParams = 0, .maxParams = 0}, - {.name = "wallet_propose", - .parse = &RPCParser::parseWalletPropose, - .minParams = 0, - .maxParams = 1}, - {.name = "internal", - .parse = &RPCParser::parseInternal, - .minParams = 1, - .maxParams = -1}, - - // Event methods - {.name = "path_find", - .parse = &RPCParser::parseEvented, - .minParams = -1, - .maxParams = -1}, - {.name = "subscribe", - .parse = &RPCParser::parseEvented, - .minParams = -1, - .maxParams = -1}, - {.name = "unsubscribe", - .parse = &RPCParser::parseEvented, - .minParams = -1, - .maxParams = -1}, - }; - - auto const count = jvParams.size(); - - for (auto const& command : kCommands) - { - if (strMethod == command.name) - { - if ((command.minParams >= 0 && count < command.minParams) || - (command.maxParams >= 0 && count > command.maxParams)) - { - JLOG(j_.debug()) << "Wrong number of parameters for " << command.name - << " minimum=" << command.minParams - << " maximum=" << command.maxParams << " actual=" << count; - - return rpcError(RpcBadSyntax); - } - - return (this->*(command.parse))(jvParams); - } + return parseAsIs(jvParams); } - // The command could not be found - if (!allowAnyCommand) - return rpcError(RpcUnknownCommand); + auto const count = jvParams.size(); + if (count < found->minParams || count > found->maxParams) + { + JLOG(j_.debug()) << "Wrong number of parameters for " << found->name + << " minimum=" << found->minParams << " maximum=" << found->maxParams + << " actual=" << count; - return parseAsIs(jvParams); + return rpcError(RpcBadSyntax); + } + + return (this->*(found->parse))(jvParams); } }; @@ -1614,6 +1652,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..e9e6c36a27 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: every entry in the dispatch table carries + // one, which a static_assert there enforces at compile time. + if (!context.headers.user.empty() || !context.headers.forwardedFor.empty()) { - if (!context.headers.user.empty() || !context.headers.forwardedFor.empty()) - { - JLOG(context.j.debug()) - << "start command: " << handler->name << ", user: " << context.headers.user - << ", forwarded for: " << context.headers.forwardedFor; + JLOG(context.j.debug()) << "start command: " << handler->name + << ", user: " << context.headers.user + << ", forwarded for: " << context.headers.forwardedFor; - auto ret = callMethod(context, method, handler->name, result); + auto const ret = callMethod(context, handler->valueMethod, handler->name, result); - JLOG(context.j.debug()) - << "finish command: " << handler->name << ", user: " << context.headers.user - << ", forwarded for: " << context.headers.forwardedFor; + JLOG(context.j.debug()) << "finish command: " << handler->name + << ", user: " << context.headers.user + << ", forwarded for: " << context.headers.forwardedFor; - return ret; - } - - auto ret = callMethod(context, method, handler->name, result); return ret; } - return RpcUnknownCommand; + return callMethod(context, handler->valueMethod, handler->name, result); } Role -roleRequired(unsigned int version, bool betaEnabled, std::string const& method) +roleRequired(unsigned int version, bool betaEnabled, std::string_view method) { auto handler = rpc::getHandler(version, betaEnabled, method); 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;