Files
rippled/src/test/rpc/Feature_test.cpp
Jingchen 508937f3d1 refactor: Retire MultiSignReserve and ExpandedSignerList amendments (#5981)
Amendments activated for more than 2 years can be retired. This change retires the MultiSignReserve and ExpandedSignerList amendments.
2025-11-13 11:42:45 +00:00

598 lines
22 KiB
C++

#include <test/jtx.h>
#include <xrpld/app/misc/AmendmentTable.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/jss.h>
namespace ripple {
class Feature_test : public beast::unit_test::suite
{
void
testInternals()
{
testcase("internals");
auto const& supportedAmendments = ripple::detail::supportedAmendments();
auto const& allAmendments = ripple::allAmendments();
BEAST_EXPECT(
supportedAmendments.size() ==
ripple::detail::numDownVotedAmendments() +
ripple::detail::numUpVotedAmendments());
{
std::size_t up = 0, down = 0, obsolete = 0;
for (auto const& [name, vote] : supportedAmendments)
{
switch (vote)
{
case VoteBehavior::DefaultYes:
++up;
break;
case VoteBehavior::DefaultNo:
++down;
break;
case VoteBehavior::Obsolete:
++obsolete;
break;
default:
fail("Unknown VoteBehavior", __FILE__, __LINE__);
}
if (vote == VoteBehavior::Obsolete)
{
BEAST_EXPECT(
allAmendments.contains(name) &&
allAmendments.at(name) == AmendmentSupport::Retired);
}
else
{
BEAST_EXPECT(
allAmendments.contains(name) &&
allAmendments.at(name) == AmendmentSupport::Supported);
}
}
BEAST_EXPECT(
down + obsolete == ripple::detail::numDownVotedAmendments());
BEAST_EXPECT(up == ripple::detail::numUpVotedAmendments());
}
{
std::size_t supported = 0, unsupported = 0, retired = 0;
for (auto const& [name, support] : allAmendments)
{
switch (support)
{
case AmendmentSupport::Supported:
++supported;
BEAST_EXPECT(supportedAmendments.contains(name));
break;
case AmendmentSupport::Unsupported:
++unsupported;
break;
case AmendmentSupport::Retired:
++retired;
break;
default:
fail("Unknown AmendmentSupport", __FILE__, __LINE__);
}
}
BEAST_EXPECT(supported + retired == supportedAmendments.size());
BEAST_EXPECT(
allAmendments.size() - unsupported ==
supportedAmendments.size());
}
}
void
testFeatureLookups()
{
testcase("featureToName");
// Test all the supported features. In a perfect world, this would test
// FeatureCollections::featureNames, but that's private. Leave it that
// way.
auto const supported = ripple::detail::supportedAmendments();
for (auto const& [feature, vote] : supported)
{
(void)vote;
auto const registered = getRegisteredFeature(feature);
if (BEAST_EXPECT(registered))
{
BEAST_EXPECT(featureToName(*registered) == feature);
BEAST_EXPECT(
bitsetIndexToFeature(featureToBitsetIndex(*registered)) ==
*registered);
}
}
// Test an arbitrary unknown feature
uint256 zero{0};
BEAST_EXPECT(featureToName(zero) == to_string(zero));
BEAST_EXPECT(
featureToName(zero) ==
"0000000000000000000000000000000000000000000000000000000000000000");
// Test looking up an unknown feature
BEAST_EXPECT(!getRegisteredFeature("unknown"));
// Test a random sampling of the variables. If any of these get retired
// or removed, swap out for any other feature.
BEAST_EXPECT(
featureToName(fixRemoveNFTokenAutoTrustLine) ==
"fixRemoveNFTokenAutoTrustLine");
BEAST_EXPECT(featureToName(featureFlow) == "Flow");
BEAST_EXPECT(featureToName(featureNegativeUNL) == "NegativeUNL");
BEAST_EXPECT(
featureToName(fixIncludeKeyletFields) == "fixIncludeKeyletFields");
BEAST_EXPECT(featureToName(featureTokenEscrow) == "TokenEscrow");
}
void
testNoParams()
{
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("feature")[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
testSingleFeature()
{
testcase("Feature Param");
using namespace test::jtx;
Env env{*this};
auto jrr = env.rpc("feature", "RequireFullyCanonicalSig")[jss::result];
BEAST_EXPECTS(jrr[jss::status] == jss::success, "status");
jrr.removeMember(jss::status);
BEAST_EXPECT(jrr.size() == 1);
BEAST_EXPECT(
jrr.isMember("00C1FC4A53E60AB02C864641002B3172F38677E29C26C54066851"
"79B37E1EDAC"));
auto feature = *(jrr.begin());
BEAST_EXPECTS(feature[jss::name] == "RequireFullyCanonicalSig", "name");
BEAST_EXPECTS(!feature[jss::enabled].asBool(), "enabled");
BEAST_EXPECTS(
feature[jss::vetoed].isBool() && !feature[jss::vetoed].asBool(),
"vetoed");
BEAST_EXPECTS(feature[jss::supported].asBool(), "supported");
// feature names are case-sensitive - expect error here
jrr = env.rpc("feature", "requireFullyCanonicalSig")[jss::result];
BEAST_EXPECT(jrr[jss::error] == "badFeature");
BEAST_EXPECT(jrr[jss::error_message] == "Feature unknown or invalid.");
}
void
testInvalidFeature()
{
testcase("Invalid Feature");
using namespace test::jtx;
Env env{*this};
auto testInvalidParam = [&](auto const& param) {
Json::Value params;
params[jss::feature] = param;
auto jrr =
env.rpc("json", "feature", to_string(params))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
BEAST_EXPECT(jrr[jss::error_message] == "Invalid parameters.");
};
testInvalidParam(1);
testInvalidParam(1.1);
testInvalidParam(true);
testInvalidParam(Json::Value(Json::nullValue));
testInvalidParam(Json::Value(Json::objectValue));
testInvalidParam(Json::Value(Json::arrayValue));
{
auto jrr = env.rpc("feature", "AllTheThings")[jss::result];
BEAST_EXPECT(jrr[jss::error] == "badFeature");
BEAST_EXPECT(
jrr[jss::error_message] == "Feature unknown or invalid.");
}
}
void
testNonAdmin()
{
testcase("Feature Without Admin");
using namespace test::jtx;
Env env{*this, envconfig([](std::unique_ptr<Config> cfg) {
(*cfg)["port_rpc"].set("admin", "");
(*cfg)["port_ws"].set("admin", "");
return cfg;
})};
{
auto result = env.rpc("feature")[jss::result];
BEAST_EXPECT(result.isMember(jss::features));
// There should be at least 50 amendments. Don't do exact
// comparison to avoid maintenance as more amendments are added in
// the future.
BEAST_EXPECT(result[jss::features].size() >= 50);
for (auto it = result[jss::features].begin();
it != result[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);
BEAST_EXPECTS(
(*it).isMember(jss::enabled) &&
(*it)[jss::enabled].asBool() == expectEnabled,
(*it)[jss::name].asString() + " enabled");
BEAST_EXPECTS(
(*it).isMember(jss::supported) &&
(*it)[jss::supported].asBool() == expectSupported,
(*it)[jss::name].asString() + " supported");
BEAST_EXPECT(!(*it).isMember(jss::vetoed));
BEAST_EXPECT(!(*it).isMember(jss::majority));
BEAST_EXPECT(!(*it).isMember(jss::count));
BEAST_EXPECT(!(*it).isMember(jss::validations));
BEAST_EXPECT(!(*it).isMember(jss::threshold));
}
}
{
Json::Value params;
// invalid feature
params[jss::feature] =
"1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCD"
"EF";
auto const result =
env.rpc("json", "feature", to_string(params))[jss::result];
BEAST_EXPECTS(
result[jss::error] == "badFeature", result.toStyledString());
BEAST_EXPECT(
result[jss::error_message] == "Feature unknown or invalid.");
}
{
Json::Value params;
params[jss::feature] =
"93E516234E35E08CA689FA33A6D38E103881F8DCB53023F728C307AA89D515"
"A7";
// invalid param
params[jss::vetoed] = true;
auto const result =
env.rpc("json", "feature", to_string(params))[jss::result];
BEAST_EXPECTS(
result[jss::error] == "noPermission",
result[jss::error].asString());
BEAST_EXPECT(
result[jss::error_message] ==
"You don't have permission for this command.");
}
}
void
testSomeEnabled()
{
testcase("No Params, Some Enabled");
using namespace test::jtx;
Env env{*this, FeatureBitset{}};
std::map<std::string, VoteBehavior> const& votes =
ripple::detail::supportedAmendments();
auto jrr = env.rpc("feature")[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()
{
testcase("With Majorities");
using namespace test::jtx;
Env env{*this, envconfig(validator, "")};
auto jrr = env.rpc("feature")[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("feature")[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
testVeto()
{
testcase("Veto");
using namespace test::jtx;
Env env{*this, FeatureBitset{featureRequireFullyCanonicalSig}};
constexpr char const* featureName = "RequireFullyCanonicalSig";
auto jrr = env.rpc("feature", featureName)[jss::result];
if (!BEAST_EXPECTS(jrr[jss::status] == jss::success, "status"))
return;
jrr.removeMember(jss::status);
if (!BEAST_EXPECT(jrr.size() == 1))
return;
auto feature = *(jrr.begin());
BEAST_EXPECTS(feature[jss::name] == featureName, "name");
BEAST_EXPECTS(
feature[jss::vetoed].isBool() && !feature[jss::vetoed].asBool(),
"vetoed");
jrr = env.rpc("feature", featureName, "reject")[jss::result];
if (!BEAST_EXPECTS(jrr[jss::status] == jss::success, "status"))
return;
jrr.removeMember(jss::status);
if (!BEAST_EXPECT(jrr.size() == 1))
return;
feature = *(jrr.begin());
BEAST_EXPECTS(feature[jss::name] == featureName, "name");
BEAST_EXPECTS(
feature[jss::vetoed].isBool() && feature[jss::vetoed].asBool(),
"vetoed");
jrr = env.rpc("feature", featureName, "accept")[jss::result];
if (!BEAST_EXPECTS(jrr[jss::status] == jss::success, "status"))
return;
jrr.removeMember(jss::status);
if (!BEAST_EXPECT(jrr.size() == 1))
return;
feature = *(jrr.begin());
BEAST_EXPECTS(feature[jss::name] == featureName, "name");
BEAST_EXPECTS(
feature[jss::vetoed].isBool() && !feature[jss::vetoed].asBool(),
"vetoed");
// anything other than accept or reject is an error
jrr = env.rpc("feature", featureName, "maybe");
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
BEAST_EXPECT(jrr[jss::error_message] == "Invalid parameters.");
}
void
testObsolete()
{
testcase("Obsolete");
using namespace test::jtx;
Env env{*this};
constexpr char const* featureName = "CryptoConditionsSuite";
auto jrr = env.rpc("feature", featureName)[jss::result];
if (!BEAST_EXPECTS(jrr[jss::status] == jss::success, "status"))
return;
jrr.removeMember(jss::status);
if (!BEAST_EXPECT(jrr.size() == 1))
return;
auto feature = *(jrr.begin());
BEAST_EXPECTS(feature[jss::name] == featureName, "name");
BEAST_EXPECTS(
feature[jss::vetoed].isString() &&
feature[jss::vetoed].asString() == "Obsolete",
"vetoed");
jrr = env.rpc("feature", featureName, "reject")[jss::result];
if (!BEAST_EXPECTS(jrr[jss::status] == jss::success, "status"))
return;
jrr.removeMember(jss::status);
if (!BEAST_EXPECT(jrr.size() == 1))
return;
feature = *(jrr.begin());
BEAST_EXPECTS(feature[jss::name] == featureName, "name");
BEAST_EXPECTS(
feature[jss::vetoed].isString() &&
feature[jss::vetoed].asString() == "Obsolete",
"vetoed");
jrr = env.rpc("feature", featureName, "accept")[jss::result];
if (!BEAST_EXPECTS(jrr[jss::status] == jss::success, "status"))
return;
jrr.removeMember(jss::status);
if (!BEAST_EXPECT(jrr.size() == 1))
return;
feature = *(jrr.begin());
BEAST_EXPECTS(feature[jss::name] == featureName, "name");
BEAST_EXPECTS(
feature[jss::vetoed].isString() &&
feature[jss::vetoed].asString() == "Obsolete",
"vetoed");
// anything other than accept or reject is an error
jrr = env.rpc("feature", featureName, "maybe");
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
BEAST_EXPECT(jrr[jss::error_message] == "Invalid parameters.");
}
public:
void
run() override
{
testInternals();
testFeatureLookups();
testNoParams();
testSingleFeature();
testInvalidFeature();
testNonAdmin();
testSomeEnabled();
testWithMajorities();
testVeto();
testObsolete();
}
};
BEAST_DEFINE_TESTSUITE(Feature, rpc, ripple);
} // namespace ripple