diff --git a/src/test/rpc/RuntimeConfig_test.cpp b/src/test/rpc/RuntimeConfig_test.cpp index 307e2604d..ebdbc50f9 100644 --- a/src/test/rpc/RuntimeConfig_test.cpp +++ b/src/test/rpc/RuntimeConfig_test.cpp @@ -22,10 +22,37 @@ #include #include +#include +#include +#include + namespace ripple { class RuntimeConfig_test : public beast::unit_test::suite { + class EnvVarGuard + { + public: + EnvVarGuard(char const* name, char const* value) : name_(name) + { + if (auto const* old = std::getenv(name)) + old_ = old; + setenv(name, value, 1); + } + + ~EnvVarGuard() + { + if (old_) + setenv(name_, old_->c_str(), 1); + else + unsetenv(name_); + } + + private: + char const* name_; + std::optional old_; + }; + // Helper to call runtime_config RPC with JSON params Json::Value runtimeConfig(test::jtx::Env& env, Json::Value const& params) @@ -265,6 +292,71 @@ class RuntimeConfig_test : public beast::unit_test::suite BEAST_EXPECT(!cfg->appliesTo(TrafficCount::category::base)); } + void + testMessageTypeAliases() + { + testcase("Message type aliases expand to multiple categories"); + using namespace test::jtx; + Env env{*this}; + + Json::Value params; + params["set"] = Json::objectValue; + params["set"]["*"] = Json::objectValue; + params["set"]["*"]["send_delay_ms"] = 100; + params["set"]["*"]["message_types"] = Json::arrayValue; + params["set"]["*"]["message_types"].append("candidate_set_fetch"); + auto result = runtimeConfig(env, params); + + auto const& global = result["configs"]["*"]; + BEAST_EXPECT(global.isMember("message_types")); + BEAST_EXPECT(global["message_types"].size() == 4); + + auto cfg = env.app().getRuntimeConfig().getConfig("*"); + if (!BEAST_EXPECT(cfg.has_value())) + return; + + BEAST_EXPECT(cfg->appliesTo(TrafficCount::category::gl_tsc_get)); + BEAST_EXPECT(cfg->appliesTo(TrafficCount::category::gl_tsc_share)); + BEAST_EXPECT(cfg->appliesTo(TrafficCount::category::ld_tsc_get)); + BEAST_EXPECT(cfg->appliesTo(TrafficCount::category::ld_tsc_share)); + BEAST_EXPECT(!cfg->appliesTo(TrafficCount::category::proposal)); + } + + void + testEnvMessageTypeFilter() + { + testcase("XAHAU_RUNTIME_CONFIG parses message_types"); + + EnvVarGuard guard{ + "XAHAU_RUNTIME_CONFIG", + R"({"*":{"send_delay_ms":100,"message_types":["candidate_set_fetch"]}})"}; + + RuntimeConfig rc; + auto cfg = rc.getConfig("*"); + if (!BEAST_EXPECT(cfg.has_value())) + return; + + BEAST_EXPECT(rc.active()); + BEAST_EXPECT(cfg->sendDelayMs == 100); + BEAST_EXPECT(cfg->appliesTo(TrafficCount::category::gl_tsc_get)); + BEAST_EXPECT(cfg->appliesTo(TrafficCount::category::ld_tsc_share)); + BEAST_EXPECT(!cfg->appliesTo(TrafficCount::category::proposal)); + } + + void + testInvalidEnvMessageType() + { + testcase("Invalid XAHAU_RUNTIME_CONFIG message_types are ignored"); + + EnvVarGuard guard{ + "XAHAU_RUNTIME_CONFIG", + R"({"*":{"send_delay_ms":100,"message_types":["not_a_category"]}})"}; + + RuntimeConfig rc; + BEAST_EXPECT(!rc.active()); + BEAST_EXPECT(!rc.getConfig("*").has_value()); + } + void testMessageTypeFilterEmpty() { @@ -313,6 +405,25 @@ class RuntimeConfig_test : public beast::unit_test::suite BEAST_EXPECT(!env.app().getRuntimeConfig().active()); } + void + testInvalidMessageTypesShape() + { + testcase("Non-array message_types returns error"); + using namespace test::jtx; + Env env{*this}; + + Json::Value params; + params["set"] = Json::objectValue; + params["set"]["*"] = Json::objectValue; + params["set"]["*"]["send_delay_ms"] = 100; + params["set"]["*"]["message_types"] = "proposal"; + auto result = runtimeConfig(env, params); + + BEAST_EXPECT(result.isMember("error")); + BEAST_EXPECT(result["error"].asString() == "invalidParams"); + BEAST_EXPECT(!env.app().getRuntimeConfig().active()); + } + void testDropPctClamping() { @@ -515,8 +626,12 @@ public: testClearAll(); testPerPeerWithoutGlobal(); testMessageTypeFilter(); + testMessageTypeAliases(); + testEnvMessageTypeFilter(); + testInvalidEnvMessageType(); testMessageTypeFilterEmpty(); testInvalidMessageType(); + testInvalidMessageTypesShape(); testDropPctClamping(); testRngClaimDropPct(); testRngClaimDropPctClamping(); diff --git a/src/xrpld/app/misc/RuntimeConfig.h b/src/xrpld/app/misc/RuntimeConfig.h index 5a65f04b1..9543aa4d5 100644 --- a/src/xrpld/app/misc/RuntimeConfig.h +++ b/src/xrpld/app/misc/RuntimeConfig.h @@ -26,6 +26,7 @@ #include #include #include +#include namespace ripple { @@ -177,6 +178,22 @@ private: std::unordered_map merged_; }; +/** Expand runtime-config message type names into TrafficCount categories. + + Names are intentionally string-based so test tools can target overlay + traffic without depending on enum values. Aliases may expand to several + categories, for example candidate-set fetch covers the TMGetLedger request + and TMLedgerData reply categories used by tx-set and sidecar acquisition. +*/ +std::optional> +runtimeConfigMessageCategoriesFromNames( + std::vector const& names, + std::string& error); + +/** Human-readable name for one runtime-config message category. */ +std::string +runtimeConfigMessageCategoryName(std::size_t category); + } // namespace ripple #endif diff --git a/src/xrpld/app/misc/detail/RuntimeConfig.cpp b/src/xrpld/app/misc/detail/RuntimeConfig.cpp index c5887011e..3da5e1342 100644 --- a/src/xrpld/app/misc/detail/RuntimeConfig.cpp +++ b/src/xrpld/app/misc/detail/RuntimeConfig.cpp @@ -18,6 +18,7 @@ //============================================================================== #include +#include #include #include @@ -25,12 +26,148 @@ #include #include #include +#include #include +#include +#include #include +#include namespace ripple { namespace { +using CategorySet = std::set; + +struct CategoryAlias +{ + char const* name; + std::vector categories; +}; + +std::vector const& +categoryAliases() +{ + using C = TrafficCount::category; + static std::vector const aliases = { + {"base", {C::base}}, + {"cluster", {C::cluster}}, + {"overlay", {C::overlay}}, + {"proposal", {C::proposal}}, + {"validation", {C::validation}}, + {"transaction", {C::transaction}}, + {"manifests", {C::manifests}}, + {"validator_list", {C::validatorlist}}, + {"validatorlist", {C::validatorlist}}, + + {"have_set", {C::get_set, C::share_set}}, + {"set_get", {C::get_set}}, + {"set_share", {C::share_set}}, + + {"candidate_set_fetch", + {C::gl_tsc_get, C::gl_tsc_share, C::ld_tsc_get, C::ld_tsc_share}}, + {"candidate_set_request", {C::gl_tsc_get}}, + {"candidate_set_reply", {C::ld_tsc_share}}, + + {"ledger_data", + {C::ld_tsc_get, + C::ld_tsc_share, + C::ld_txn_get, + C::ld_txn_share, + C::ld_asn_get, + C::ld_asn_share, + C::ld_get, + C::ld_share}}, + {"ledger_data_tsc_get", {C::ld_tsc_get}}, + {"ledger_data_tsc_share", {C::ld_tsc_share}}, + {"ledger_data_txn_get", {C::ld_txn_get}}, + {"ledger_data_txn_share", {C::ld_txn_share}}, + {"ledger_data_asn_get", {C::ld_asn_get}}, + {"ledger_data_asn_share", {C::ld_asn_share}}, + {"ledger_data_get", {C::ld_get}}, + {"ledger_data_share", {C::ld_share}}, + + {"get_ledger", + {C::gl_tsc_get, + C::gl_tsc_share, + C::gl_txn_get, + C::gl_txn_share, + C::gl_asn_get, + C::gl_asn_share, + C::gl_get, + C::gl_share}}, + {"get_ledger_tsc_get", {C::gl_tsc_get}}, + {"get_ledger_tsc_share", {C::gl_tsc_share}}, + {"get_ledger_txn_get", {C::gl_txn_get}}, + {"get_ledger_txn_share", {C::gl_txn_share}}, + {"get_ledger_asn_get", {C::gl_asn_get}}, + {"get_ledger_asn_share", {C::gl_asn_share}}, + {"get_ledger_get", {C::gl_get}}, + {"get_ledger_share", {C::gl_share}}, + + {"get_object", + {C::share_hash_ledger, + C::get_hash_ledger, + C::share_hash_tx, + C::get_hash_tx, + C::share_hash_txnode, + C::get_hash_txnode, + C::share_hash_asnode, + C::get_hash_asnode, + C::share_cas_object, + C::get_cas_object, + C::share_fetch_pack, + C::get_fetch_pack, + C::get_transactions, + C::share_hash, + C::get_hash}}, + {"get_object_fetch_pack", {C::share_fetch_pack, C::get_fetch_pack}}, + {"get_object_fetch_pack_get", {C::get_fetch_pack}}, + {"get_object_fetch_pack_share", {C::share_fetch_pack}}, + {"get_object_get", {C::get_hash}}, + {"get_object_share", {C::share_hash}}, + {"get_object_transactions", {C::get_transactions}}, + + {"proof_path", {C::proof_path_request, C::proof_path_response}}, + {"proof_path_request", {C::proof_path_request}}, + {"proof_path_response", {C::proof_path_response}}, + {"replay_delta", {C::replay_delta_request, C::replay_delta_response}}, + {"replay_delta_request", {C::replay_delta_request}}, + {"replay_delta_response", {C::replay_delta_response}}, + {"have_transactions", {C::have_transactions}}, + {"requested_transactions", {C::requested_transactions}}, + }; + return aliases; +} + +std::optional +categoriesForName(std::string const& name) +{ + for (auto const& alias : categoryAliases()) + { + if (name == alias.name) + return CategorySet{ + alias.categories.begin(), alias.categories.end()}; + } + + if (!name.empty() && + std::all_of(name.begin(), name.end(), [](unsigned char c) { + return std::isdigit(c); + })) + { + try + { + auto const cat = static_cast(std::stoull(name)); + if (cat <= TrafficCount::category::unknown) + return CategorySet{cat}; + } + catch (std::exception const&) + { + } + } + + return std::nullopt; +} + std::optional parseBoolEnv(char const* env) { @@ -51,7 +188,7 @@ parseBoolEnv(char const* env) return std::nullopt; } -ConfigVals +std::optional parseConfigVals(Json::Value const& v) { ConfigVals cfg; @@ -73,10 +210,56 @@ parseConfigVals(Json::Value const& v) cfg.rngPollMs = std::max(50, v["rng_poll_ms"].asInt()); if (v.isMember("no_export_sig")) cfg.noExportSig = v["no_export_sig"].asBool(); + if (v.isMember("message_types")) + { + if (!v["message_types"].isArray()) + return std::nullopt; + + std::vector names; + for (auto const& mt : v["message_types"]) + names.push_back(mt.asString()); + + std::string error; + auto cats = runtimeConfigMessageCategoriesFromNames(names, error); + if (!cats) + return std::nullopt; + cfg.messageCategories = *cats; + } return cfg; } } // namespace +std::optional> +runtimeConfigMessageCategoriesFromNames( + std::vector const& names, + std::string& error) +{ + CategorySet result; + for (auto const& name : names) + { + auto cats = categoriesForName(name); + if (!cats) + { + error = "Unknown message_type: " + name; + return std::nullopt; + } + result.insert(cats->begin(), cats->end()); + } + return result; +} + +std::string +runtimeConfigMessageCategoryName(std::size_t category) +{ + for (auto const& alias : categoryAliases()) + { + if (alias.categories.size() == 1 && + alias.categories.front() == category) + return alias.name; + } + return std::to_string(category); +} + RuntimeConfig::RuntimeConfig() { // XAHAU_RUNTIME_CONFIG takes precedence (full JSON config) @@ -88,7 +271,10 @@ RuntimeConfig::RuntimeConfig() { std::unique_lock lock(mutex_); for (auto const& target : root.getMemberNames()) - configs_[target] = parseConfigVals(root[target]); + { + if (auto cfg = parseConfigVals(root[target])) + configs_[target] = *cfg; + } rebuildMerged(); updateActive(); } diff --git a/src/xrpld/rpc/handlers/RuntimeConfig.cpp b/src/xrpld/rpc/handlers/RuntimeConfig.cpp index c4ed393d4..54123451e 100644 --- a/src/xrpld/rpc/handlers/RuntimeConfig.cpp +++ b/src/xrpld/rpc/handlers/RuntimeConfig.cpp @@ -19,49 +19,12 @@ #include #include -#include #include +#include + namespace ripple { -namespace { -// Map user-friendly names to TrafficCount::category values. -// Only the commonly useful categories are exposed; extend as needed. -std::optional -categoryFromName(std::string const& name) -{ - static std::unordered_map const map = { - {"proposal", TrafficCount::category::proposal}, - {"validation", TrafficCount::category::validation}, - {"transaction", TrafficCount::category::transaction}, - {"manifests", TrafficCount::category::manifests}, - {"ledger_data", TrafficCount::category::ld_share}, - {"get_ledger", TrafficCount::category::gl_get}, - }; - auto it = map.find(name); - if (it != map.end()) - return it->second; - return std::nullopt; -} - -std::string -categoryToName(std::size_t cat) -{ - static std::unordered_map const map = { - {TrafficCount::category::proposal, "proposal"}, - {TrafficCount::category::validation, "validation"}, - {TrafficCount::category::transaction, "transaction"}, - {TrafficCount::category::manifests, "manifests"}, - {TrafficCount::category::ld_share, "ledger_data"}, - {TrafficCount::category::gl_get, "get_ledger"}, - }; - auto it = map.find(cat); - if (it != map.end()) - return it->second; - return std::to_string(cat); -} -} // namespace - Json::Value doRuntimeConfig(RPC::JsonContext& context) { @@ -111,24 +74,29 @@ doRuntimeConfig(RPC::JsonContext& context) if (v.isMember("message_types")) { auto const& mts = v["message_types"]; - cfg.messageCategories.emplace(); // set to empty = "all" - if (mts.isArray()) + if (!mts.isArray()) { - for (auto const& mt : mts) - { - auto const name = mt.asString(); - auto cat = categoryFromName(name); - if (!cat) - { - Json::Value err{Json::objectValue}; - err["error"] = "invalidParams"; - err["error_message"] = - "Unknown message_type: " + name; - return err; - } - cfg.messageCategories->insert(*cat); - } + Json::Value err{Json::objectValue}; + err["error"] = "invalidParams"; + err["error_message"] = "message_types must be an array"; + return err; } + + std::vector names; + for (auto const& mt : mts) + names.push_back(mt.asString()); + + std::string error; + auto cats = + runtimeConfigMessageCategoriesFromNames(names, error); + if (!cats) + { + Json::Value err{Json::objectValue}; + err["error"] = "invalidParams"; + err["error_message"] = error; + return err; + } + cfg.messageCategories = *cats; } rc.setConfig(target, cfg); } @@ -175,7 +143,7 @@ doRuntimeConfig(RPC::JsonContext& context) { Json::Value types{Json::arrayValue}; for (auto cat : *cfg.messageCategories) - types.append(categoryToName(cat)); + types.append(runtimeConfigMessageCategoryName(cat)); entry["message_types"] = types; } configs[target] = entry;