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 <richard.holland@starstone.co.nz>
This commit is contained in:
Denis Angell
2023-11-16 23:38:13 +01:00
committed by GitHub
parent b77b0e70e3
commit 5447c4010d
2 changed files with 330 additions and 12 deletions

View File

@@ -18,6 +18,7 @@
//==============================================================================
#include <ripple/app/main/Application.h>
#include <ripple/app/misc/AmendmentTable.h>
#include <ripple/app/misc/NetworkOPs.h>
#include <ripple/app/reporting/P2pProxy.h>
#include <ripple/json/json_value.h>
@@ -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

View File

@@ -17,7 +17,9 @@
*/
//==============================================================================
#include <ripple/app/misc/AmendmentTable.h>
#include <ripple/beast/unit_test.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/jss.h>
#include <test/jtx.h>
@@ -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<std::string, VoteBehavior> 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<std::string, VoteBehavior> 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<std::string, VoteBehavior> 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);
}
};