From 5447c4010db6f9ad736325595602ebd495c685da Mon Sep 17 00:00:00 2001 From: Denis Angell Date: Thu, 16 Nov 2023 23:38:13 +0100 Subject: [PATCH] Add features to `server_definitions` (#190) * add features to `server_definitions` * clang-format * Update RPCCall.cpp * only return features without params * clang-format * include features in hashed value * clang-format * rework features addition to server_defintions to be cached at flag ledgers * fix clang, duplicate hash key --------- Co-authored-by: Richard Holland --- src/ripple/rpc/handlers/ServerDefinitions.cpp | 67 ++++- src/test/rpc/ServerDefinitions_test.cpp | 275 +++++++++++++++++- 2 files changed, 330 insertions(+), 12 deletions(-) diff --git a/src/ripple/rpc/handlers/ServerDefinitions.cpp b/src/ripple/rpc/handlers/ServerDefinitions.cpp index e975f55b3..f9a443c47 100644 --- a/src/ripple/rpc/handlers/ServerDefinitions.cpp +++ b/src/ripple/rpc/handlers/ServerDefinitions.cpp @@ -18,6 +18,7 @@ //============================================================================== #include +#include #include #include #include @@ -368,14 +369,13 @@ private: } ret[jss::native_currency_code] = systemCurrencyCode(); + // generate hash { const std::string out = Json::FastWriter().write(ret); defsHash = ripple::sha512Half(ripple::Slice{out.data(), out.size()}); - ret[jss::hash] = to_string(*defsHash); } - return ret; } @@ -385,10 +385,18 @@ private: public: Definitions() : defs(generate()){}; - bool - hashMatches(uint256 hash) const + uint256 const& + getHash() const { - return defsHash && *defsHash == hash; + if (!defsHash) + { + // should be unreachable + // if this does happen we don't want 0 xor 0 so use a random value + // here + return uint256( + "DF4220E93ADC6F5569063A01B4DC79F8DB9553B6A3222ADE23DEA0"); + } + return *defsHash; } Json::Value const& @@ -403,23 +411,62 @@ doServerDefinitions(RPC::JsonContext& context) { auto& params = context.params; - uint256 hash; + uint256 reqHash; if (params.isMember(jss::hash)) { if (!params[jss::hash].isString() || - !hash.parseHex(params[jss::hash].asString())) + !reqHash.parseHex(params[jss::hash].asString())) return RPC::invalid_field_error(jss::hash); } + uint32_t curLgrSeq = context.ledgerMaster.getValidatedLedger()->info().seq; + + // static values used for cache + static uint32_t lastGenerated = 0; // last ledger seq it was generated + static Json::Value lastFeatures{ + Json::objectValue}; // the actual features JSON last generated + static uint256 lastFeatureHash; // the hash of the features JSON last time + // it was generated + + // if a flag ledger has passed since it was last generated, regenerate it, + // update the cache above + if (curLgrSeq > ((lastGenerated >> 8) + 1) << 8 || lastGenerated == 0) + { + majorityAmendments_t majorities; + if (auto const valLedger = context.ledgerMaster.getValidatedLedger()) + majorities = getMajorityAmendments(*valLedger); + auto& table = context.app.getAmendmentTable(); + auto features = table.getJson(); + for (auto const& [h, t] : majorities) + features[to_string(h)][jss::majority] = + t.time_since_epoch().count(); + + lastFeatures = features; + { + const std::string out = Json::FastWriter().write(features); + lastFeatureHash = + ripple::sha512Half(ripple::Slice{out.data(), out.size()}); + } + } + static const Definitions defs{}; - if (defs.hashMatches(hash)) + + // the hash is the xor of the two parts + uint256 retHash = lastFeatureHash ^ defs.getHash(); + + if (reqHash == retHash) { Json::Value jv = Json::objectValue; - jv[jss::hash] = to_string(hash); + jv[jss::hash] = to_string(retHash); return jv; } - return defs(); + // definitions + Json::Value ret = defs(); + ret[jss::hash] = to_string(retHash); + ret[jss::features] = lastFeatures; + + return ret; } } // namespace ripple diff --git a/src/test/rpc/ServerDefinitions_test.cpp b/src/test/rpc/ServerDefinitions_test.cpp index 166681c90..0a9dcbc3d 100644 --- a/src/test/rpc/ServerDefinitions_test.cpp +++ b/src/test/rpc/ServerDefinitions_test.cpp @@ -17,7 +17,9 @@ */ //============================================================================== +#include #include +#include #include #include @@ -46,8 +48,10 @@ public: } void - testServerDefinitions() + testDefinitions(FeatureBitset features) { + testcase("Definitions"); + using namespace test::jtx; std::string jsonLE = R"json({ @@ -358,10 +362,277 @@ public: } } + void + testDefitionsHash(FeatureBitset features) + { + testcase("Definitions Hash"); + + using namespace test::jtx; + // test providing the same hash + { + Env env(*this, features); + auto const firstResult = env.rpc("server_definitions"); + auto const hash = firstResult[jss::result][jss::hash].asString(); + Json::Value params; + params[jss::hash] = hash; + auto const result = + env.rpc("json", "server_definitions", to_string(params)); + BEAST_EXPECT(!result[jss::result].isMember(jss::error)); + BEAST_EXPECT(result[jss::result][jss::status] == "success"); + BEAST_EXPECT(!result[jss::result].isMember(jss::FIELDS)); + BEAST_EXPECT( + !result[jss::result].isMember(jss::LEDGER_ENTRY_TYPES)); + BEAST_EXPECT( + !result[jss::result].isMember(jss::TRANSACTION_RESULTS)); + BEAST_EXPECT(!result[jss::result].isMember(jss::TRANSACTION_TYPES)); + BEAST_EXPECT(!result[jss::result].isMember(jss::TYPES)); + BEAST_EXPECT(result[jss::result].isMember(jss::hash)); + } + + // test providing a different hash + { + Env env(*this, features); + std::string const hash = + "54296160385A27154BFA70A239DD8E8FD4CC2DB7BA32D970BA3A5B132CF749" + "D1"; + Json::Value params; + params[jss::hash] = hash; + auto const result = + env.rpc("json", "server_definitions", to_string(params)); + BEAST_EXPECT(!result[jss::result].isMember(jss::error)); + BEAST_EXPECT(result[jss::result][jss::status] == "success"); + BEAST_EXPECT(result[jss::result].isMember(jss::FIELDS)); + BEAST_EXPECT(result[jss::result].isMember(jss::LEDGER_ENTRY_TYPES)); + BEAST_EXPECT( + result[jss::result].isMember(jss::TRANSACTION_RESULTS)); + BEAST_EXPECT(result[jss::result].isMember(jss::TRANSACTION_TYPES)); + BEAST_EXPECT(result[jss::result].isMember(jss::TYPES)); + BEAST_EXPECT(result[jss::result].isMember(jss::hash)); + } + } + + void + testNoParams(FeatureBitset features) + { + testcase("No Params, None Enabled"); + + using namespace test::jtx; + Env env{*this}; + + std::map const& votes = + ripple::detail::supportedAmendments(); + + auto jrr = env.rpc("server_definitions")[jss::result]; + if (!BEAST_EXPECT(jrr.isMember(jss::features))) + return; + for (auto const& feature : jrr[jss::features]) + { + if (!BEAST_EXPECT(feature.isMember(jss::name))) + return; + // default config - so all should be disabled, and + // supported. Some may be vetoed. + bool expectVeto = + (votes.at(feature[jss::name].asString()) == + VoteBehavior::DefaultNo); + bool expectObsolete = + (votes.at(feature[jss::name].asString()) == + VoteBehavior::Obsolete); + BEAST_EXPECTS( + feature.isMember(jss::enabled) && + !feature[jss::enabled].asBool(), + feature[jss::name].asString() + " enabled"); + BEAST_EXPECTS( + feature.isMember(jss::vetoed) && + feature[jss::vetoed].isBool() == !expectObsolete && + (!feature[jss::vetoed].isBool() || + feature[jss::vetoed].asBool() == expectVeto) && + (feature[jss::vetoed].isBool() || + feature[jss::vetoed].asString() == "Obsolete"), + feature[jss::name].asString() + " vetoed"); + BEAST_EXPECTS( + feature.isMember(jss::supported) && + feature[jss::supported].asBool(), + feature[jss::name].asString() + " supported"); + } + } + + void + testSomeEnabled(FeatureBitset features) + { + testcase("No Params, Some Enabled"); + + using namespace test::jtx; + Env env{ + *this, FeatureBitset(featureDepositAuth, featureDepositPreauth)}; + + std::map const& votes = + ripple::detail::supportedAmendments(); + + auto jrr = env.rpc("server_definitions")[jss::result]; + if (!BEAST_EXPECT(jrr.isMember(jss::features))) + return; + for (auto it = jrr[jss::features].begin(); + it != jrr[jss::features].end(); + ++it) + { + uint256 id; + (void)id.parseHex(it.key().asString().c_str()); + if (!BEAST_EXPECT((*it).isMember(jss::name))) + return; + bool expectEnabled = env.app().getAmendmentTable().isEnabled(id); + bool expectSupported = + env.app().getAmendmentTable().isSupported(id); + bool expectVeto = + (votes.at((*it)[jss::name].asString()) == + VoteBehavior::DefaultNo); + bool expectObsolete = + (votes.at((*it)[jss::name].asString()) == + VoteBehavior::Obsolete); + BEAST_EXPECTS( + (*it).isMember(jss::enabled) && + (*it)[jss::enabled].asBool() == expectEnabled, + (*it)[jss::name].asString() + " enabled"); + if (expectEnabled) + BEAST_EXPECTS( + !(*it).isMember(jss::vetoed), + (*it)[jss::name].asString() + " vetoed"); + else + BEAST_EXPECTS( + (*it).isMember(jss::vetoed) && + (*it)[jss::vetoed].isBool() == !expectObsolete && + (!(*it)[jss::vetoed].isBool() || + (*it)[jss::vetoed].asBool() == expectVeto) && + ((*it)[jss::vetoed].isBool() || + (*it)[jss::vetoed].asString() == "Obsolete"), + (*it)[jss::name].asString() + " vetoed"); + BEAST_EXPECTS( + (*it).isMember(jss::supported) && + (*it)[jss::supported].asBool() == expectSupported, + (*it)[jss::name].asString() + " supported"); + } + } + + void + testWithMajorities(FeatureBitset features) + { + testcase("With Majorities"); + + using namespace test::jtx; + Env env{*this, envconfig(validator, "")}; + + auto jrr = env.rpc("server_definitions")[jss::result]; + if (!BEAST_EXPECT(jrr.isMember(jss::features))) + return; + + // at this point, there are no majorities so no fields related to + // amendment voting + for (auto const& feature : jrr[jss::features]) + { + if (!BEAST_EXPECT(feature.isMember(jss::name))) + return; + BEAST_EXPECTS( + !feature.isMember(jss::majority), + feature[jss::name].asString() + " majority"); + BEAST_EXPECTS( + !feature.isMember(jss::count), + feature[jss::name].asString() + " count"); + BEAST_EXPECTS( + !feature.isMember(jss::threshold), + feature[jss::name].asString() + " threshold"); + BEAST_EXPECTS( + !feature.isMember(jss::validations), + feature[jss::name].asString() + " validations"); + BEAST_EXPECTS( + !feature.isMember(jss::vote), + feature[jss::name].asString() + " vote"); + } + + auto majorities = getMajorityAmendments(*env.closed()); + if (!BEAST_EXPECT(majorities.empty())) + return; + + // close ledgers until the amendments show up. + for (auto i = 0; i <= 256; ++i) + { + env.close(); + majorities = getMajorityAmendments(*env.closed()); + if (!majorities.empty()) + break; + } + + // There should be at least 5 amendments. Don't do exact comparison + // to avoid maintenance as more amendments are added in the future. + BEAST_EXPECT(majorities.size() >= 5); + std::map const& votes = + ripple::detail::supportedAmendments(); + + jrr = env.rpc("server_definitions")[jss::result]; + if (!BEAST_EXPECT(jrr.isMember(jss::features))) + return; + for (auto const& feature : jrr[jss::features]) + { + if (!BEAST_EXPECT(feature.isMember(jss::name))) + return; + bool expectVeto = + (votes.at(feature[jss::name].asString()) == + VoteBehavior::DefaultNo); + bool expectObsolete = + (votes.at(feature[jss::name].asString()) == + VoteBehavior::Obsolete); + BEAST_EXPECTS( + (expectVeto || expectObsolete) ^ + feature.isMember(jss::majority), + feature[jss::name].asString() + " majority"); + BEAST_EXPECTS( + feature.isMember(jss::vetoed) && + feature[jss::vetoed].isBool() == !expectObsolete && + (!feature[jss::vetoed].isBool() || + feature[jss::vetoed].asBool() == expectVeto) && + (feature[jss::vetoed].isBool() || + feature[jss::vetoed].asString() == "Obsolete"), + feature[jss::name].asString() + " vetoed"); + BEAST_EXPECTS( + feature.isMember(jss::count), + feature[jss::name].asString() + " count"); + BEAST_EXPECTS( + feature.isMember(jss::threshold), + feature[jss::name].asString() + " threshold"); + BEAST_EXPECTS( + feature.isMember(jss::validations), + feature[jss::name].asString() + " validations"); + BEAST_EXPECT( + feature[jss::count] == + ((expectVeto || expectObsolete) ? 0 : 1)); + BEAST_EXPECT(feature[jss::threshold] == 1); + BEAST_EXPECT(feature[jss::validations] == 1); + BEAST_EXPECTS( + expectVeto || expectObsolete || feature[jss::majority] == 2540, + "Majority: " + feature[jss::majority].asString()); + } + } + + void + testServerFeatures(FeatureBitset features) + { + testNoParams(features); + testSomeEnabled(features); + testWithMajorities(features); + } + + void + testServerDefinitions(FeatureBitset features) + { + testDefinitions(features); + testDefitionsHash(features); + } + void run() override { - testServerDefinitions(); + using namespace test::jtx; + auto const sa = supported_amendments(); + testServerDefinitions(sa); + testServerFeatures(sa); } };