Refactor Feature name management and creation:

* Only require adding the new feature names in one place. (Also need to
  increment a counter, but a check on startup will catch that.)
* Allows rippled to have the code to support a given amendment, but
  not vote for it by default. This allows the amendment to be enabled in
  a future version without necessarily amendment blocking these older
  versions.
* The default vote is carried with the amendment name in the list of
  supported amendments.
* The amendment table is constructed with the amendment and default
  vote.
This commit is contained in:
Edward Hennis
2020-06-13 22:56:36 -04:00
committed by manojsdoshi
parent 1ca8898703
commit 4a9bd7ed6d
13 changed files with 837 additions and 490 deletions

View File

@@ -1250,27 +1250,29 @@ ApplicationImp::setup()
// Configure the amendments the server supports // Configure the amendments the server supports
{ {
auto const& sa = detail::supportedAmendments(); auto const supported = []() {
std::vector<std::string> saHashes; auto const& amendments = detail::supportedAmendments();
saHashes.reserve(sa.size()); std::vector<AmendmentTable::FeatureInfo> supported;
for (auto const& name : sa) supported.reserve(amendments.size());
for (auto const& [a, vote] : amendments)
{ {
auto const f = getRegisteredFeature(name); auto const f = ripple::getRegisteredFeature(a);
BOOST_ASSERT(f); assert(f);
if (f) if (f)
saHashes.push_back(to_string(*f) + " " + name); supported.emplace_back(a, *f, vote);
} }
Section supportedAmendments("Supported Amendments"); return supported;
supportedAmendments.append(saHashes); }();
Section const& downVoted = config_->section(SECTION_VETO_AMENDMENTS);
Section enabledAmendments = config_->section(SECTION_AMENDMENTS); Section const& upVoted = config_->section(SECTION_AMENDMENTS);
m_amendmentTable = make_AmendmentTable( m_amendmentTable = make_AmendmentTable(
*this, *this,
config().AMENDMENT_MAJORITY_TIME, config().AMENDMENT_MAJORITY_TIME,
supportedAmendments, supported,
enabledAmendments, upVoted,
config_->section(SECTION_VETO_AMENDMENTS), downVoted,
logs_->journal("Amendments")); logs_->journal("Amendments"));
} }

View File

@@ -22,6 +22,7 @@
#include <ripple/app/ledger/Ledger.h> #include <ripple/app/ledger/Ledger.h>
#include <ripple/core/ConfigSections.h> #include <ripple/core/ConfigSections.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/Protocol.h> #include <ripple/protocol/Protocol.h>
#include <ripple/protocol/STValidation.h> #include <ripple/protocol/STValidation.h>
@@ -36,6 +37,19 @@ namespace ripple {
class AmendmentTable class AmendmentTable
{ {
public: public:
struct FeatureInfo
{
FeatureInfo() = delete;
FeatureInfo(std::string const& n, uint256 const& f, DefaultVote v)
: name(n), feature(f), vote(v)
{
}
std::string const name;
uint256 const feature;
DefaultVote const vote;
};
virtual ~AmendmentTable() = default; virtual ~AmendmentTable() = default;
virtual uint256 virtual uint256
@@ -168,7 +182,7 @@ std::unique_ptr<AmendmentTable>
make_AmendmentTable( make_AmendmentTable(
Application& app, Application& app,
std::chrono::seconds majorityTime, std::chrono::seconds majorityTime,
Section const& supported, std::vector<AmendmentTable::FeatureInfo> const& supported,
Section const& enabled, Section const& enabled,
Section const& vetoed, Section const& vetoed,
beast::Journal journal); beast::Journal journal);

View File

@@ -73,8 +73,8 @@ parseSection(Section const& section)
*/ */
struct AmendmentState struct AmendmentState
{ {
/** If an amendment is vetoed, a server will not support it */ /** If an amendment is down-voted, a server will not vote to enable it */
bool vetoed = false; AmendmentVote vote = AmendmentVote::down;
/** Indicates that the amendment has been enabled. /** Indicates that the amendment has been enabled.
This is a one-way switch: once an amendment is enabled This is a one-way switch: once an amendment is enabled
@@ -224,15 +224,16 @@ private:
DatabaseCon& db_; DatabaseCon& db_;
// Finds or creates state. Must be called with mutex_ locked. // Finds or creates state. Must be called with mutex_ locked.
AmendmentState* AmendmentState&
add(uint256 const& amendment, std::lock_guard<std::mutex> const& sl); add(uint256 const& amendment, std::lock_guard<std::mutex> const& lock);
// Finds existing state. Must be called with mutex_ locked. // Finds existing state. Must be called with mutex_ locked.
AmendmentState* AmendmentState*
get(uint256 const& amendment, std::lock_guard<std::mutex> const& sl); get(uint256 const& amendment, std::lock_guard<std::mutex> const& lock);
AmendmentState const* AmendmentState const*
get(uint256 const& amendment, std::lock_guard<std::mutex> const& sl) const; get(uint256 const& amendment,
std::lock_guard<std::mutex> const& lock) const;
// Injects amendment json into v. Must be called with mutex_ locked. // Injects amendment json into v. Must be called with mutex_ locked.
void void
@@ -240,19 +241,19 @@ private:
Json::Value& v, Json::Value& v,
uint256 const& amendment, uint256 const& amendment,
AmendmentState const& state, AmendmentState const& state,
std::lock_guard<std::mutex> const& sl) const; std::lock_guard<std::mutex> const& lock) const;
void void
persistVote( persistVote(
uint256 const& amendment, uint256 const& amendment,
std::string const& name, std::string const& name,
bool vote_to_veto) const; AmendmentVote vote) const;
public: public:
AmendmentTableImpl( AmendmentTableImpl(
Application& app, Application& app,
std::chrono::seconds majorityTime, std::chrono::seconds majorityTime,
Section const& supported, std::vector<FeatureInfo> const& supported,
Section const& enabled, Section const& enabled,
Section const& vetoed, Section const& vetoed,
beast::Journal journal); beast::Journal journal);
@@ -313,7 +314,7 @@ public:
AmendmentTableImpl::AmendmentTableImpl( AmendmentTableImpl::AmendmentTableImpl(
Application& app, Application& app,
std::chrono::seconds majorityTime, std::chrono::seconds majorityTime,
Section const& supported, std::vector<FeatureInfo> const& supported,
Section const& enabled, Section const& enabled,
Section const& vetoed, Section const& vetoed,
beast::Journal journal) beast::Journal journal)
@@ -323,7 +324,7 @@ AmendmentTableImpl::AmendmentTableImpl(
, j_(journal) , j_(journal)
, db_(app.getWalletDB()) , db_(app.getWalletDB())
{ {
std::lock_guard sl(mutex_); std::lock_guard lock(mutex_);
// Find out if the FeatureVotes table exists in WalletDB // Find out if the FeatureVotes table exists in WalletDB
bool const featureVotesExist = [this]() { bool const featureVotesExist = [this]() {
@@ -332,22 +333,24 @@ AmendmentTableImpl::AmendmentTableImpl(
}(); }();
// Parse supported amendments // Parse supported amendments
for (auto const& a : parseSection(supported)) for (auto const& [name, amendment, defaultVote] : supported)
{ {
if (auto s = add(a.first, sl)) AmendmentState& s = add(amendment, lock);
{
JLOG(j_.debug()) << "Amendment " << a.first << " is supported.";
if (!a.second.empty()) s.name = name;
s->name = a.second; s.supported = true;
s.vote = defaultVote == DefaultVote::yes ? AmendmentVote::up
: AmendmentVote::down;
s->supported = true; JLOG(j_.debug()) << "Amendment " << amendment << " (" << s.name
} << ") is supported and will be "
<< (s.vote == AmendmentVote::up ? "up" : "down")
<< " voted if not enabled on the ledger.";
} }
hash_set<uint256> detect_conflict; hash_set<uint256> detect_conflict;
// Parse enabled amendments from config // Parse enabled amendments from config
for (auto const& a : parseSection(enabled)) for (std::pair<uint256, std::string> const& a : parseSection(enabled))
{ {
if (featureVotesExist) if (featureVotesExist)
{ // If the table existed, warn about duplicate config info { // If the table existed, warn about duplicate config info
@@ -358,7 +361,7 @@ AmendmentTableImpl::AmendmentTableImpl(
else else
{ // Otherwise transfer config data into the table { // Otherwise transfer config data into the table
detect_conflict.insert(a.first); detect_conflict.insert(a.first);
persistVote(a.first, a.second, false); // un-veto persistVote(a.first, a.second, AmendmentVote::up);
} }
} }
@@ -376,7 +379,7 @@ AmendmentTableImpl::AmendmentTableImpl(
{ // Otherwise transfer config data into the table { // Otherwise transfer config data into the table
if (detect_conflict.count(a.first) == 0) if (detect_conflict.count(a.first) == 0)
{ {
persistVote(a.first, a.second, true); // veto persistVote(a.first, a.second, AmendmentVote::down);
} }
else else
{ {
@@ -394,57 +397,62 @@ AmendmentTableImpl::AmendmentTableImpl(
*db, *db,
[&](boost::optional<std::string> amendment_hash, [&](boost::optional<std::string> amendment_hash,
boost::optional<std::string> amendment_name, boost::optional<std::string> amendment_name,
boost::optional<int> vote_to_veto) { boost::optional<AmendmentVote> vote) {
uint256 amend_hash; uint256 amend_hash;
if (!amendment_hash || !amendment_name || !vote)
{
// These fields should never have nulls, but check
Throw<std::runtime_error>(
"Invalid FeatureVotes row in wallet.db");
}
if (!amend_hash.parseHex(*amendment_hash)) if (!amend_hash.parseHex(*amendment_hash))
{ {
Throw<std::runtime_error>( Throw<std::runtime_error>(
"Invalid amendment ID '" + *amendment_hash + "Invalid amendment ID '" + *amendment_hash +
" in wallet.db"); " in wallet.db");
} }
if (*vote_to_veto) if (*vote == AmendmentVote::down)
{ {
// Unknown amendments are effectively vetoed already // Unknown amendments are effectively vetoed already
if (auto s = get(amend_hash, sl)) if (auto s = get(amend_hash, lock))
{ {
JLOG(j_.info()) << "Amendment {" << *amendment_name << ", " JLOG(j_.info()) << "Amendment {" << *amendment_name << ", "
<< amend_hash << "} is vetoed."; << amend_hash << "} is downvoted.";
if (!amendment_name->empty()) if (!amendment_name->empty())
s->name = *amendment_name; s->name = *amendment_name;
s->vetoed = true; s->vote = *vote;
} }
} }
else // un-veto else // up-vote
{
if (auto s = add(amend_hash, sl))
{ {
auto s = add(amend_hash, lock);
JLOG(j_.debug()) << "Amendment {" << *amendment_name << ", " JLOG(j_.debug()) << "Amendment {" << *amendment_name << ", "
<< amend_hash << "} is un-vetoed."; << amend_hash << "} is upvoted.";
if (!amendment_name->empty()) if (!amendment_name->empty())
s->name = *amendment_name; s.name = *amendment_name;
s->vetoed = false; s.vote = *vote;
}
} }
}); });
} }
AmendmentState* AmendmentState&
AmendmentTableImpl::add( AmendmentTableImpl::add(
uint256 const& amendmentHash, uint256 const& amendmentHash,
std::lock_guard<std::mutex> const&) std::lock_guard<std::mutex> const&)
{ {
// call with the mutex held // call with the mutex held
return &amendmentMap_[amendmentHash]; return amendmentMap_[amendmentHash];
} }
AmendmentState* AmendmentState*
AmendmentTableImpl::get( AmendmentTableImpl::get(
uint256 const& amendmentHash, uint256 const& amendmentHash,
std::lock_guard<std::mutex> const& sl) std::lock_guard<std::mutex> const& lock)
{ {
// Forward to the const version of get. // Forward to the const version of get.
return const_cast<AmendmentState*>( return const_cast<AmendmentState*>(
std::as_const(*this).get(amendmentHash, sl)); std::as_const(*this).get(amendmentHash, lock));
} }
AmendmentState const* AmendmentState const*
@@ -464,7 +472,7 @@ AmendmentTableImpl::get(
uint256 uint256
AmendmentTableImpl::find(std::string const& name) const AmendmentTableImpl::find(std::string const& name) const
{ {
std::lock_guard sl(mutex_); std::lock_guard lock(mutex_);
for (auto const& e : amendmentMap_) for (auto const& e : amendmentMap_)
{ {
@@ -479,50 +487,50 @@ void
AmendmentTableImpl::persistVote( AmendmentTableImpl::persistVote(
uint256 const& amendment, uint256 const& amendment,
std::string const& name, std::string const& name,
bool vote_to_veto) const AmendmentVote vote) const
{ {
auto db = db_.checkoutDb(); auto db = db_.checkoutDb();
voteAmendment(*db, amendment, name, vote_to_veto); voteAmendment(*db, amendment, name, vote);
} }
bool bool
AmendmentTableImpl::veto(uint256 const& amendment) AmendmentTableImpl::veto(uint256 const& amendment)
{ {
std::lock_guard sl(mutex_); std::lock_guard lock(mutex_);
auto s = add(amendment, sl); AmendmentState& s = add(amendment, lock);
if (s->vetoed) if (s.vote == AmendmentVote::down)
return false; return false;
s->vetoed = true; s.vote = AmendmentVote::down;
persistVote(amendment, s->name, s->vetoed); persistVote(amendment, s.name, s.vote);
return true; return true;
} }
bool bool
AmendmentTableImpl::unVeto(uint256 const& amendment) AmendmentTableImpl::unVeto(uint256 const& amendment)
{ {
std::lock_guard sl(mutex_); std::lock_guard lock(mutex_);
auto s = get(amendment, sl); AmendmentState* const s = get(amendment, lock);
if (!s || !s->vetoed) if (!s || s->vote == AmendmentVote::up)
return false; return false;
s->vetoed = false; s->vote = AmendmentVote::up;
persistVote(amendment, s->name, s->vetoed); persistVote(amendment, s->name, s->vote);
return true; return true;
} }
bool bool
AmendmentTableImpl::enable(uint256 const& amendment) AmendmentTableImpl::enable(uint256 const& amendment)
{ {
std::lock_guard sl(mutex_); std::lock_guard lock(mutex_);
auto s = add(amendment, sl); AmendmentState& s = add(amendment, lock);
if (s->enabled) if (s.enabled)
return false; return false;
s->enabled = true; s.enabled = true;
if (!s->supported) if (!s.supported)
{ {
JLOG(j_.error()) << "Unsupported amendment " << amendment JLOG(j_.error()) << "Unsupported amendment " << amendment
<< " activated."; << " activated.";
@@ -535,30 +543,30 @@ AmendmentTableImpl::enable(uint256 const& amendment)
bool bool
AmendmentTableImpl::isEnabled(uint256 const& amendment) const AmendmentTableImpl::isEnabled(uint256 const& amendment) const
{ {
std::lock_guard sl(mutex_); std::lock_guard lock(mutex_);
auto s = get(amendment, sl); AmendmentState const* s = get(amendment, lock);
return s && s->enabled; return s && s->enabled;
} }
bool bool
AmendmentTableImpl::isSupported(uint256 const& amendment) const AmendmentTableImpl::isSupported(uint256 const& amendment) const
{ {
std::lock_guard sl(mutex_); std::lock_guard lock(mutex_);
auto s = get(amendment, sl); AmendmentState const* s = get(amendment, lock);
return s && s->supported; return s && s->supported;
} }
bool bool
AmendmentTableImpl::hasUnsupportedEnabled() const AmendmentTableImpl::hasUnsupportedEnabled() const
{ {
std::lock_guard sl(mutex_); std::lock_guard lock(mutex_);
return unsupportedEnabled_; return unsupportedEnabled_;
} }
std::optional<NetClock::time_point> std::optional<NetClock::time_point>
AmendmentTableImpl::firstUnsupportedExpected() const AmendmentTableImpl::firstUnsupportedExpected() const
{ {
std::lock_guard sl(mutex_); std::lock_guard lock(mutex_);
return firstUnsupportedExpected_; return firstUnsupportedExpected_;
} }
@@ -570,14 +578,15 @@ AmendmentTableImpl::doValidation(std::set<uint256> const& enabled) const
std::vector<uint256> amendments; std::vector<uint256> amendments;
{ {
std::lock_guard sl(mutex_); std::lock_guard lock(mutex_);
amendments.reserve(amendmentMap_.size()); amendments.reserve(amendmentMap_.size());
for (auto const& e : amendmentMap_) for (auto const& e : amendmentMap_)
{ {
if (e.second.supported && !e.second.vetoed && if (e.second.supported && e.second.vote == AmendmentVote::up &&
(enabled.count(e.first) == 0)) (enabled.count(e.first) == 0))
{ {
amendments.push_back(e.first); amendments.push_back(e.first);
JLOG(j_.info()) << "Voting for amendment " << e.second.name;
} }
} }
} }
@@ -617,7 +626,7 @@ AmendmentTableImpl::doVoting(
// the value of the flags in the pseudo-transaction // the value of the flags in the pseudo-transaction
std::map<uint256, std::uint32_t> actions; std::map<uint256, std::uint32_t> actions;
std::lock_guard sl(mutex_); std::lock_guard lock(mutex_);
// process all amendments we know of // process all amendments we know of
for (auto const& entry : amendmentMap_) for (auto const& entry : amendmentMap_)
@@ -638,7 +647,7 @@ AmendmentTableImpl::doVoting(
} }
else if ( else if (
hasValMajority && (majorityTime == NetClock::time_point{}) && hasValMajority && (majorityTime == NetClock::time_point{}) &&
!entry.second.vetoed) entry.second.vote == AmendmentVote::up)
{ {
// Ledger says no majority, validators say yes // Ledger says no majority, validators say yes
JLOG(j_.debug()) << entry.first << ": amendment got majority"; JLOG(j_.debug()) << entry.first << ": amendment got majority";
@@ -653,7 +662,7 @@ AmendmentTableImpl::doVoting(
else if ( else if (
(majorityTime != NetClock::time_point{}) && (majorityTime != NetClock::time_point{}) &&
((majorityTime + majorityTime_) <= closeTime) && ((majorityTime + majorityTime_) <= closeTime) &&
!entry.second.vetoed) entry.second.vote == AmendmentVote::up)
{ {
// Ledger says majority held // Ledger says majority held
JLOG(j_.debug()) << entry.first << ": amendment majority held"; JLOG(j_.debug()) << entry.first << ": amendment majority held";
@@ -669,7 +678,7 @@ AmendmentTableImpl::doVoting(
bool bool
AmendmentTableImpl::needValidatedLedger(LedgerIndex ledgerSeq) const AmendmentTableImpl::needValidatedLedger(LedgerIndex ledgerSeq) const
{ {
std::lock_guard sl(mutex_); std::lock_guard lock(mutex_);
// Is there a ledger in which an amendment could have been enabled // Is there a ledger in which an amendment could have been enabled
// between these two ledger sequences? // between these two ledger sequences?
@@ -686,7 +695,7 @@ AmendmentTableImpl::doValidatedLedger(
for (auto& e : enabled) for (auto& e : enabled)
enable(e); enable(e);
std::lock_guard sl(mutex_); std::lock_guard lock(mutex_);
// Remember the ledger sequence of this update. // Remember the ledger sequence of this update.
lastUpdateSeq_ = ledgerSeq; lastUpdateSeq_ = ledgerSeq;
@@ -697,12 +706,12 @@ AmendmentTableImpl::doValidatedLedger(
firstUnsupportedExpected_.reset(); firstUnsupportedExpected_.reset();
for (auto const& [hash, time] : majority) for (auto const& [hash, time] : majority)
{ {
auto s = add(hash, sl); AmendmentState& s = add(hash, lock);
if (s->enabled) if (s.enabled)
continue; continue;
if (!s->supported) if (!s.supported)
{ {
JLOG(j_.info()) << "Unsupported amendment " << hash JLOG(j_.info()) << "Unsupported amendment " << hash
<< " reached majority at " << to_string(time); << " reached majority at " << to_string(time);
@@ -725,7 +734,7 @@ AmendmentTableImpl::injectJson(
v[jss::name] = fs.name; v[jss::name] = fs.name;
v[jss::supported] = fs.supported; v[jss::supported] = fs.supported;
v[jss::vetoed] = fs.vetoed; v[jss::vetoed] = fs.vote == AmendmentVote::down;
v[jss::enabled] = fs.enabled; v[jss::enabled] = fs.enabled;
if (!fs.enabled && lastVote_) if (!fs.enabled && lastVote_)
@@ -747,14 +756,14 @@ AmendmentTableImpl::getJson() const
{ {
Json::Value ret(Json::objectValue); Json::Value ret(Json::objectValue);
{ {
std::lock_guard sl(mutex_); std::lock_guard lock(mutex_);
for (auto const& e : amendmentMap_) for (auto const& e : amendmentMap_)
{ {
injectJson( injectJson(
ret[to_string(e.first)] = Json::objectValue, ret[to_string(e.first)] = Json::objectValue,
e.first, e.first,
e.second, e.second,
sl); lock);
} }
} }
return ret; return ret;
@@ -767,10 +776,10 @@ AmendmentTableImpl::getJson(uint256 const& amendmentID) const
Json::Value& jAmendment = (ret[to_string(amendmentID)] = Json::objectValue); Json::Value& jAmendment = (ret[to_string(amendmentID)] = Json::objectValue);
{ {
std::lock_guard sl(mutex_); std::lock_guard lock(mutex_);
auto a = get(amendmentID, sl); AmendmentState const* a = get(amendmentID, lock);
if (a) if (a)
injectJson(jAmendment, amendmentID, *a, sl); injectJson(jAmendment, amendmentID, *a, lock);
} }
return ret; return ret;
@@ -780,7 +789,7 @@ std::unique_ptr<AmendmentTable>
make_AmendmentTable( make_AmendmentTable(
Application& app, Application& app,
std::chrono::seconds majorityTime, std::chrono::seconds majorityTime,
Section const& supported, std::vector<AmendmentTable::FeatureInfo> const& supported,
Section const& enabled, Section const& enabled,
Section const& vetoed, Section const& vetoed,
beast::Journal journal) beast::Journal journal)

View File

@@ -132,11 +132,15 @@ deletePeerReservation(soci::session& session, PublicKey const& nodeId);
bool bool
createFeatureVotes(soci::session& session); createFeatureVotes(soci::session& session);
// For historical reasons the up-vote and down-vote integer representations
// are unintuitive.
enum class AmendmentVote : int { up = 0, down = 1 };
/** /**
* @brief readAmendments Read all amendments from FeatureVotes table. * @brief readAmendments Read all amendments from FeatureVotes table.
* @param session Session with walletDB database. * @param session Session with walletDB database.
* @param callback Callback called for each amendment passing its hash, name * @param callback Callback called for each amendment passing its hash, name
* and teh flag if it should be vetoed as callback parameters * and the flag if it should be vetoed as callback parameters
*/ */
void void
readAmendments( readAmendments(
@@ -144,21 +148,21 @@ readAmendments(
std::function<void( std::function<void(
boost::optional<std::string> amendment_hash, boost::optional<std::string> amendment_hash,
boost::optional<std::string> amendment_name, boost::optional<std::string> amendment_name,
boost::optional<int> vote_to_veto)> const& callback); boost::optional<AmendmentVote> vote)> const& callback);
/** /**
* @brief voteAmendment Set veto value for particular amendment. * @brief voteAmendment Set veto value for particular amendment.
* @param session Session with walletDB database. * @param session Session with walletDB database.
* @param amendment Hash of amendment. * @param amendment Hash of amendment.
* @param name Name of amendment. * @param name Name of amendment.
* @param vote_to_veto Trus if this amendment should be vetoed. * @param vote Whether to vote in favor of this amendment.
*/ */
void void
voteAmendment( voteAmendment(
soci::session& session, soci::session& session,
uint256 const& amendment, uint256 const& amendment,
std::string const& name, std::string const& name,
bool vote_to_veto); AmendmentVote vote);
/* State DB */ /* State DB */

View File

@@ -258,8 +258,14 @@ readAmendments(
std::function<void( std::function<void(
boost::optional<std::string> amendment_hash, boost::optional<std::string> amendment_hash,
boost::optional<std::string> amendment_name, boost::optional<std::string> amendment_name,
boost::optional<int> vote_to_veto)> const& callback) boost::optional<AmendmentVote> vote)> const& callback)
{ {
// lambda that converts the internally stored int to an AmendmentVote.
auto intToVote = [](boost::optional<int> const& dbVote)
-> boost::optional<AmendmentVote> {
return safe_cast<AmendmentVote>(dbVote.value_or(1));
};
soci::transaction tr(session); soci::transaction tr(session);
std::string sql = std::string sql =
"SELECT AmendmentHash, AmendmentName, Veto FROM FeatureVotes"; "SELECT AmendmentHash, AmendmentName, Veto FROM FeatureVotes";
@@ -275,7 +281,7 @@ readAmendments(
st.execute(); st.execute();
while (st.fetch()) while (st.fetch())
{ {
callback(amendment_hash, amendment_name, vote_to_veto); callback(amendment_hash, amendment_name, intToVote(vote_to_veto));
} }
} }
@@ -284,7 +290,7 @@ voteAmendment(
soci::session& session, soci::session& session,
uint256 const& amendment, uint256 const& amendment,
std::string const& name, std::string const& name,
bool vote_to_veto) AmendmentVote vote)
{ {
soci::transaction tr(session); soci::transaction tr(session);
std::string sql = std::string sql =
@@ -292,7 +298,7 @@ voteAmendment(
"('"; "('";
sql += to_string(amendment); sql += to_string(amendment);
sql += "', '" + name; sql += "', '" + name;
sql += "', '" + std::to_string(int{vote_to_veto}) + "');"; sql += "', '" + std::to_string(safe_cast<int>(vote)) + "');";
session << sql; session << sql;
tr.commit(); tr.commit();
} }

View File

@@ -32,121 +32,70 @@
* *
* Steps required to add new features to the code: * Steps required to add new features to the code:
* *
* 1) add the new feature name to the featureNames array below * 1) In this file, increment `numFeatures` and add a uint256 declaration
* 2) add a uint256 declaration for the feature to the bottom of this file * for the feature at the bottom
* 3) add a uint256 definition for the feature to the corresponding source * 2) Add a uint256 definition for the feature to the corresponding source
* file (Feature.cpp) * file (Feature.cpp). Use `registerFeature` to create the feature with
* 4) if the feature is going to be supported in the near future, add its * the feature's name, `Supported::no`, and `DefaultVote::no`. This
* sha512half value and name (matching exactly the featureName here) to * should be the only place the feature's name appears in code as a string.
* the supportedAmendments in Feature.cpp. * 3) Use the uint256 as the parameter to `view.rules.enabled()` to
* control flow into new code that this feature limits.
* 4) If the feature development is COMPLETE, and the feature is ready to be
* SUPPORTED, change the `registerFeature` parameter to Supported::yes.
* 5) When the feature is ready to be ENABLED, change the `registerFeature`
* parameter to `DefaultVote::yes`.
* In general, any newly supported amendments (`Supported::yes`) should have
* a `DefaultVote::no` for at least one full release cycle. High priority
* bug fixes can be an exception to this rule of thumb.
*
* When a feature has been enabled for several years, the conditional code
* may be removed, and the feature "retired". To retire a feature:
* 1) Remove the uint256 declaration from this file.
* 2) MOVE the uint256 definition in Feature.cpp to the "retired features"
* section at the end of the file.
* 3) CHANGE the name of the variable to start with "retired".
* 4) CHANGE the parameters of the `registerFeature` call to `Supported::yes`
* and `DefaultVote::no`.
* The feature must remain registered and supported indefinitely because it
* still exists in the ledger, but there is no need to vote for it because
* there's nothing to vote for. If it is removed completely from the code, any
* instances running that code will get amendment blocked. Removing the
* feature from the ledger is beyond the scope of these instructions.
* *
*/ */
namespace ripple { namespace ripple {
enum class DefaultVote : bool { no = false, yes };
namespace detail { namespace detail {
// *NOTE* // This value SHOULD be equal to the number of amendments registered in
// // Feature.cpp. Because it's only used to reserve storage, and determine how
// Features, or Amendments as they are called elsewhere, are enabled on the // large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
// network at some specific time based on Validator voting. Features are // the actual number of amendments. A LogicError on startup will verify this.
// enabled using run-time conditionals based on the state of the amendment. static constexpr std::size_t numFeatures = 46;
// There is value in retaining that conditional code for some time after
// the amendment is enabled to make it simple to replay old transactions.
// However, once an Amendment has been enabled for, say, more than two years
// then retaining that conditional code has less value since it is
// uncommon to replay such old transactions.
//
// Starting in January of 2020 Amendment conditionals from before January
// 2018 are being removed. So replaying any ledger from before January
// 2018 needs to happen on an older version of the server code. There's
// a log message in Application.cpp that warns about replaying old ledgers.
//
// At some point in the future someone may wish to remove Amendment
// conditional code for Amendments that were enabled after January 2018.
// When that happens then the log message in Application.cpp should be
// updated.
class FeatureCollections /** Amendments that this server supports and the default voting behavior.
{ Whether they are enabled depends on the Rules defined in the validated
static constexpr char const* const featureNames[] = { ledger */
"MultiSign", // Unconditionally supported. std::map<std::string, DefaultVote> const&
"TrustSetAuth", // Unconditionally supported.
"FeeEscalation", // Unconditionally supported.
"OwnerPaysFee",
"PayChan",
"Flow", // Unconditionally supported.
"CompareTakerFlowCross",
"FlowCross",
"CryptoConditions",
"TickSize",
"fix1368",
"Escrow",
"CryptoConditionsSuite",
"fix1373",
"EnforceInvariants",
"SortedDirectories",
"fix1201",
"fix1512",
"fix1513",
"fix1523",
"fix1528",
"DepositAuth",
"Checks",
"fix1571",
"fix1543",
"fix1623",
"DepositPreauth",
"fix1515",
"fix1578",
"MultiSignReserve",
"fixTakerDryOfferRemoval",
"fixMasterKeyAsRegularKey",
"fixCheckThreading",
"fixPayChanRecipientOwnerDir",
"DeletableAccounts",
// fixQualityUpperBound should be activated before FlowCross
"fixQualityUpperBound",
"RequireFullyCanonicalSig",
"fix1781", // XRPEndpointSteps should be included in the circular
// payment check
"HardenedValidations",
"fixAmendmentMajorityCalc", // Fix Amendment majority calculation
"NegativeUNL",
"TicketBatch",
"FlowSortStrands",
"fixSTAmountCanonicalize",
"fixRmSmallIncreasedQOffers",
"CheckCashMakesTrustLine",
};
std::vector<uint256> features;
boost::container::flat_map<uint256, std::size_t> featureToIndex;
boost::container::flat_map<std::string, uint256> nameToFeature;
public:
FeatureCollections();
static constexpr std::size_t
numFeatures()
{
return sizeof(featureNames) / sizeof(featureNames[0]);
}
std::optional<uint256>
getRegisteredFeature(std::string const& name) const;
std::size_t
featureToBitsetIndex(uint256 const& f) const;
uint256 const&
bitsetIndexToFeature(size_t i) const;
};
/** Amendments that this server supports, but doesn't enable by default */
std::vector<std::string> const&
supportedAmendments(); supportedAmendments();
/** Amendments that this server won't vote for by default.
This function is only used in unit tests.
*/
std::size_t
numDownVotedAmendments();
/** Amendments that this server will vote for by default.
This function is only used in unit tests.
*/
std::size_t
numUpVotedAmendments();
} // namespace detail } // namespace detail
std::optional<uint256> std::optional<uint256>
@@ -158,10 +107,12 @@ featureToBitsetIndex(uint256 const& f);
uint256 uint256
bitsetIndexToFeature(size_t i); bitsetIndexToFeature(size_t i);
class FeatureBitset std::string
: private std::bitset<detail::FeatureCollections::numFeatures()> featureToName(uint256 const& f);
class FeatureBitset : private std::bitset<detail::numFeatures>
{ {
using base = std::bitset<detail::FeatureCollections::numFeatures()>; using base = std::bitset<detail::numFeatures>;
template <class... Fs> template <class... Fs>
void void
@@ -195,12 +146,14 @@ public:
explicit FeatureBitset(base const& b) : base(b) explicit FeatureBitset(base const& b) : base(b)
{ {
assert(b.count() == count());
} }
template <class... Fs> template <class... Fs>
explicit FeatureBitset(uint256 const& f, Fs&&... fs) explicit FeatureBitset(uint256 const& f, Fs&&... fs)
{ {
initFromFeatures(f, std::forward<Fs>(fs)...); initFromFeatures(f, std::forward<Fs>(fs)...);
assert(count() == (sizeof...(fs) + 1));
} }
template <class Col> template <class Col>
@@ -208,6 +161,7 @@ public:
{ {
for (auto const& f : fs) for (auto const& f : fs)
set(featureToBitsetIndex(f)); set(featureToBitsetIndex(f));
assert(fs.size() == count());
} }
auto auto

View File

@@ -17,128 +17,317 @@
*/ */
//============================================================================== //==============================================================================
#include <ripple/basics/contract.h>
#include <ripple/protocol/Feature.h> #include <ripple/protocol/Feature.h>
#include <ripple/protocol/digest.h>
#include <ripple/basics/Slice.h>
#include <ripple/basics/contract.h>
#include <ripple/protocol/digest.h>
#include <boost/container_hash/hash.hpp>
#include <boost/multi_index/hashed_index.hpp>
#include <boost/multi_index/key_extractors.hpp>
#include <boost/multi_index/random_access_index.hpp>
#include <boost/multi_index_container.hpp>
#include <cstring> #include <cstring>
namespace ripple { namespace ripple {
inline std::size_t
hash_value(ripple::uint256 const& feature)
{
std::size_t seed = 0;
using namespace boost;
for (auto const& n : feature)
hash_combine(seed, n);
return seed;
}
namespace {
enum class Supported : bool { no = false, yes };
// *NOTE*
//
// Features, or Amendments as they are called elsewhere, are enabled on the
// network at some specific time based on Validator voting. Features are
// enabled using run-time conditionals based on the state of the amendment.
// There is value in retaining that conditional code for some time after
// the amendment is enabled to make it simple to replay old transactions.
// However, once an Amendment has been enabled for, say, more than two years
// then retaining that conditional code has less value since it is
// uncommon to replay such old transactions.
//
// Starting in January of 2020 Amendment conditionals from before January
// 2018 are being removed. So replaying any ledger from before January
// 2018 needs to happen on an older version of the server code. There's
// a log message in Application.cpp that warns about replaying old ledgers.
//
// At some point in the future someone may wish to remove Amendment
// conditional code for Amendments that were enabled after January 2018.
// When that happens then the log message in Application.cpp should be
// updated.
class FeatureCollections
{
struct Feature
{
std::string name;
uint256 feature;
Feature() = delete;
explicit Feature(std::string const& name_, uint256 const& feature_)
: name(name_), feature(feature_)
{
}
// These structs are used by the `features` multi_index_container to
// provide access to the features collection by size_t index, string
// name, and uint256 feature identifier
struct byIndex
{
};
struct byName
{
};
struct byFeature
{
};
};
// Intermediate types to help with readability
template <class tag, typename Type, Type Feature::*PtrToMember>
using feature_hashed_unique = boost::multi_index::hashed_unique<
boost::multi_index::tag<tag>,
boost::multi_index::member<Feature, Type, PtrToMember>>;
// Intermediate types to help with readability
using feature_indexing = boost::multi_index::indexed_by<
boost::multi_index::random_access<
boost::multi_index::tag<Feature::byIndex>>,
feature_hashed_unique<Feature::byFeature, uint256, &Feature::feature>,
feature_hashed_unique<Feature::byName, std::string, &Feature::name>>;
// This multi_index_container provides access to the features collection by
// name, index, and uint256 feature identifier
boost::multi_index::multi_index_container<Feature, feature_indexing>
features;
std::map<std::string, DefaultVote> supported;
std::size_t upVotes = 0;
std::size_t downVotes = 0;
mutable std::atomic<bool> readOnly = false;
// These helper functions provide access to the features collection by name,
// index, and uint256 feature identifier, so the details of
// multi_index_container can be hidden
Feature const&
getByIndex(size_t i) const
{
if (i >= features.size())
LogicError("Invalid FeatureBitset index");
const auto& sequence = features.get<Feature::byIndex>();
return sequence[i];
}
size_t
getIndex(Feature const& feature) const
{
const auto& sequence = features.get<Feature::byIndex>();
auto const it_to = sequence.iterator_to(feature);
return it_to - sequence.begin();
}
Feature const*
getByFeature(uint256 const& feature) const
{
const auto& feature_index = features.get<Feature::byFeature>();
auto const feature_it = feature_index.find(feature);
return feature_it == feature_index.end() ? nullptr : &*feature_it;
}
Feature const*
getByName(std::string const& name) const
{
const auto& name_index = features.get<Feature::byName>();
auto const name_it = name_index.find(name);
return name_it == name_index.end() ? nullptr : &*name_it;
}
public:
FeatureCollections();
std::optional<uint256>
getRegisteredFeature(std::string const& name) const;
uint256
registerFeature(
std::string const& name,
Supported support,
DefaultVote vote);
/** Tell FeatureCollections when registration is complete. */
bool
registrationIsDone();
std::size_t
featureToBitsetIndex(uint256 const& f) const;
uint256 const&
bitsetIndexToFeature(size_t i) const;
std::string
featureToName(uint256 const& f) const;
/** Amendments that this server supports.
Whether they are enabled depends on the Rules defined in the validated
ledger */
std::map<std::string, DefaultVote> const&
supportedAmendments() const
{
return supported;
}
/** Amendments that this server WON'T vote for by default. */
std::size_t
numDownVotedAmendments() const
{
return downVotes;
}
/** Amendments that this server WILL vote for by default. */
std::size_t
numUpVotedAmendments() const
{
return upVotes;
}
};
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
constexpr char const* const detail::FeatureCollections::featureNames[]; FeatureCollections::FeatureCollections()
detail::FeatureCollections::FeatureCollections()
{ {
features.reserve(numFeatures()); features.reserve(ripple::detail::numFeatures);
featureToIndex.reserve(numFeatures());
nameToFeature.reserve(numFeatures());
for (std::size_t i = 0; i < numFeatures(); ++i)
{
auto const name = featureNames[i];
sha512_half_hasher h;
h(name, std::strlen(name));
auto const f = static_cast<uint256>(h);
features.push_back(f);
featureToIndex[f] = i;
nameToFeature[name] = f;
}
} }
std::optional<uint256> std::optional<uint256>
detail::FeatureCollections::getRegisteredFeature(std::string const& name) const FeatureCollections::getRegisteredFeature(std::string const& name) const
{ {
auto const i = nameToFeature.find(name); assert(readOnly);
if (i == nameToFeature.end()) Feature const* feature = getByName(name);
if (feature)
return feature->feature;
return std::nullopt; return std::nullopt;
return i->second; }
void
check(bool condition, const char* logicErrorMessage)
{
if (!condition)
LogicError(logicErrorMessage);
}
uint256
FeatureCollections::registerFeature(
std::string const& name,
Supported support,
DefaultVote vote)
{
check(!readOnly, "Attempting to register a feature after startup.");
check(
support == Supported::yes || vote == DefaultVote::no,
"Invalid feature parameters. Must be supported to be up-voted.");
Feature const* i = getByName(name);
if (!i)
{
// If this check fails, and you just added a feature, increase the
// numFeatures value in Feature.h
check(
features.size() < detail::numFeatures,
"More features defined than allocated. Adjust numFeatures in "
"Feature.h.");
auto const f = sha512Half(Slice(name.data(), name.size()));
features.emplace_back(name, f);
if (support == Supported::yes)
{
supported.emplace(name, vote);
if (vote == DefaultVote::yes)
++upVotes;
else
++downVotes;
}
check(
upVotes + downVotes == supported.size(),
"Feature counting logic broke");
check(
supported.size() <= features.size(),
"More supported features than defined features");
return f;
}
else
// Each feature should only be registered once
LogicError("Duplicate feature registration");
}
/** Tell FeatureCollections when registration is complete. */
bool
FeatureCollections::registrationIsDone()
{
readOnly = true;
return true;
} }
size_t size_t
detail::FeatureCollections::featureToBitsetIndex(uint256 const& f) const FeatureCollections::featureToBitsetIndex(uint256 const& f) const
{ {
auto const i = featureToIndex.find(f); assert(readOnly);
if (i == featureToIndex.end())
Feature const* feature = getByFeature(f);
if (!feature)
LogicError("Invalid Feature ID"); LogicError("Invalid Feature ID");
return i->second;
return getIndex(*feature);
} }
uint256 const& uint256 const&
detail::FeatureCollections::bitsetIndexToFeature(size_t i) const FeatureCollections::bitsetIndexToFeature(size_t i) const
{ {
if (i >= features.size()) assert(readOnly);
LogicError("Invalid FeatureBitset index"); Feature const& feature = getByIndex(i);
return features[i]; return feature.feature;
} }
static detail::FeatureCollections const featureCollections; std::string
FeatureCollections::featureToName(uint256 const& f) const
{
assert(readOnly);
Feature const* feature = getByFeature(f);
return feature ? feature->name : to_string(f);
}
/** Amendments that this server supports, but doesn't enable by default */ static FeatureCollections featureCollections;
std::vector<std::string> const&
} // namespace
/** Amendments that this server supports.
Whether they are enabled depends on the Rules defined in the validated
ledger */
std::map<std::string, DefaultVote> const&
detail::supportedAmendments() detail::supportedAmendments()
{ {
// Commented out amendments will be supported in a future release (and return featureCollections.supportedAmendments();
// uncommented at that time). }
//
// There are also unconditionally supported amendments in the list. /** Amendments that this server won't vote for by default. */
// Those are amendments that were enabled some time ago and the std::size_t
// amendment conditional code has been removed. detail::numDownVotedAmendments()
// {
// ** WARNING ** return featureCollections.numDownVotedAmendments();
// Unconditionally supported amendments need to remain in the list. }
// Removing them will cause servers to become amendment blocked.
static std::vector<std::string> const supported{ /** Amendments that this server will vote for by default. */
"MultiSign", // Unconditionally supported. std::size_t
"TrustSetAuth", // Unconditionally supported. detail::numUpVotedAmendments()
"FeeEscalation", // Unconditionally supported. {
// "OwnerPaysFee", return featureCollections.numUpVotedAmendments();
"PayChan",
"Flow",
"CryptoConditions",
"TickSize",
"fix1368",
"Escrow",
"CryptoConditionsSuite",
"fix1373",
"EnforceInvariants",
"FlowCross",
"SortedDirectories",
"fix1201",
"fix1512",
"fix1513",
"fix1523",
"fix1528",
"DepositAuth",
"Checks",
"fix1571",
"fix1543",
"fix1623",
"DepositPreauth",
// Use liquidity from strands that consume max offers, but mark as dry
"fix1515",
"fix1578",
"MultiSignReserve",
"fixTakerDryOfferRemoval",
"fixMasterKeyAsRegularKey",
"fixCheckThreading",
"fixPayChanRecipientOwnerDir",
"DeletableAccounts",
"fixQualityUpperBound",
"RequireFullyCanonicalSig",
"fix1781",
"HardenedValidations",
"fixAmendmentMajorityCalc",
"NegativeUNL",
"TicketBatch",
"FlowSortStrands",
"fixSTAmountCanonicalize",
"fixRmSmallIncreasedQOffers",
"CheckCashMakesTrustLine",
};
return supported;
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -149,6 +338,27 @@ getRegisteredFeature(std::string const& name)
return featureCollections.getRegisteredFeature(name); return featureCollections.getRegisteredFeature(name);
} }
uint256
registerFeature(std::string const& name, Supported support, DefaultVote vote)
{
return featureCollections.registerFeature(name, support, vote);
}
// Retired features are in the ledger and have no code controlled by the
// feature. They need to be supported, but do not need to be voted on.
uint256
retireFeature(std::string const& name)
{
return registerFeature(name, Supported::yes, DefaultVote::no);
}
/** Tell FeatureCollections when registration is complete. */
bool
registrationIsDone()
{
return featureCollections.registrationIsDone();
}
size_t size_t
featureToBitsetIndex(uint256 const& f) featureToBitsetIndex(uint256 const& f)
{ {
@@ -161,57 +371,109 @@ bitsetIndexToFeature(size_t i)
return featureCollections.bitsetIndexToFeature(i); return featureCollections.bitsetIndexToFeature(i);
} }
std::string
featureToName(uint256 const& f)
{
return featureCollections.featureToName(f);
}
#pragma push_macro("REGISTER_FEATURE")
#undef REGISTER_FEATURE
/**
Takes the name of a feature, whether it's supported, and the default vote. Will
register the feature, and create a variable whose name is "feature" plus the
feature name.
*/
#define REGISTER_FEATURE(fName, supported, defaultvote) \
uint256 const feature##fName = \
registerFeature(#fName, supported, defaultvote)
#pragma push_macro("REGISTER_FIX")
#undef REGISTER_FIX
/**
Takes the name of a feature, whether it's supported, and the default vote. Will
register the feature, and create a variable whose name is the unmodified feature
name.
*/
#define REGISTER_FIX(fName, supported, defaultvote) \
uint256 const fName = registerFeature(#fName, supported, defaultvote)
// clang-format off // clang-format off
uint256 const // All known amendments must be registered either here or below with the
featureOwnerPaysFee = *getRegisteredFeature("OwnerPaysFee"), // "retired" amendments
featureFlow = *getRegisteredFeature("Flow"), REGISTER_FEATURE(OwnerPaysFee, Supported::no, DefaultVote::no);
featureFlowCross = *getRegisteredFeature("FlowCross"), REGISTER_FEATURE(Flow, Supported::yes, DefaultVote::yes);
featureCryptoConditionsSuite = *getRegisteredFeature("CryptoConditionsSuite"), REGISTER_FEATURE(FlowCross, Supported::yes, DefaultVote::yes);
fix1513 = *getRegisteredFeature("fix1513"), REGISTER_FEATURE(CryptoConditionsSuite, Supported::yes, DefaultVote::no);
featureDepositAuth = *getRegisteredFeature("DepositAuth"), REGISTER_FIX (fix1513, Supported::yes, DefaultVote::yes);
featureChecks = *getRegisteredFeature("Checks"), REGISTER_FEATURE(DepositAuth, Supported::yes, DefaultVote::yes);
fix1571 = *getRegisteredFeature("fix1571"), REGISTER_FEATURE(Checks, Supported::yes, DefaultVote::yes);
fix1543 = *getRegisteredFeature("fix1543"), REGISTER_FIX (fix1571, Supported::yes, DefaultVote::yes);
fix1623 = *getRegisteredFeature("fix1623"), REGISTER_FIX (fix1543, Supported::yes, DefaultVote::yes);
featureDepositPreauth = *getRegisteredFeature("DepositPreauth"), REGISTER_FIX (fix1623, Supported::yes, DefaultVote::yes);
fix1515 = *getRegisteredFeature("fix1515"), REGISTER_FEATURE(DepositPreauth, Supported::yes, DefaultVote::yes);
fix1578 = *getRegisteredFeature("fix1578"), // Use liquidity from strands that consume max offers, but mark as dry
featureMultiSignReserve = *getRegisteredFeature("MultiSignReserve"), REGISTER_FIX (fix1515, Supported::yes, DefaultVote::yes);
fixTakerDryOfferRemoval = *getRegisteredFeature("fixTakerDryOfferRemoval"), REGISTER_FIX (fix1578, Supported::yes, DefaultVote::yes);
fixMasterKeyAsRegularKey = *getRegisteredFeature("fixMasterKeyAsRegularKey"), REGISTER_FEATURE(MultiSignReserve, Supported::yes, DefaultVote::yes);
fixCheckThreading = *getRegisteredFeature("fixCheckThreading"), REGISTER_FIX (fixTakerDryOfferRemoval, Supported::yes, DefaultVote::yes);
fixPayChanRecipientOwnerDir = *getRegisteredFeature("fixPayChanRecipientOwnerDir"), REGISTER_FIX (fixMasterKeyAsRegularKey, Supported::yes, DefaultVote::yes);
featureDeletableAccounts = *getRegisteredFeature("DeletableAccounts"), REGISTER_FIX (fixCheckThreading, Supported::yes, DefaultVote::yes);
fixQualityUpperBound = *getRegisteredFeature("fixQualityUpperBound"), REGISTER_FIX (fixPayChanRecipientOwnerDir, Supported::yes, DefaultVote::yes);
featureRequireFullyCanonicalSig = *getRegisteredFeature("RequireFullyCanonicalSig"), REGISTER_FEATURE(DeletableAccounts, Supported::yes, DefaultVote::yes);
fix1781 = *getRegisteredFeature("fix1781"), // fixQualityUpperBound should be activated before FlowCross
featureHardenedValidations = *getRegisteredFeature("HardenedValidations"), REGISTER_FIX (fixQualityUpperBound, Supported::yes, DefaultVote::yes);
fixAmendmentMajorityCalc = *getRegisteredFeature("fixAmendmentMajorityCalc"), REGISTER_FEATURE(RequireFullyCanonicalSig, Supported::yes, DefaultVote::yes);
featureNegativeUNL = *getRegisteredFeature("NegativeUNL"), // fix1781: XRPEndpointSteps should be included in the circular payment check
featureTicketBatch = *getRegisteredFeature("TicketBatch"), REGISTER_FIX (fix1781, Supported::yes, DefaultVote::yes);
featureFlowSortStrands = *getRegisteredFeature("FlowSortStrands"), REGISTER_FEATURE(HardenedValidations, Supported::yes, DefaultVote::yes);
fixSTAmountCanonicalize = *getRegisteredFeature("fixSTAmountCanonicalize"), REGISTER_FIX (fixAmendmentMajorityCalc, Supported::yes, DefaultVote::yes);
fixRmSmallIncreasedQOffers = *getRegisteredFeature("fixRmSmallIncreasedQOffers"), REGISTER_FEATURE(NegativeUNL, Supported::yes, DefaultVote::no);
featureCheckCashMakesTrustLine = *getRegisteredFeature("CheckCashMakesTrustLine"); REGISTER_FEATURE(TicketBatch, Supported::yes, DefaultVote::yes);
REGISTER_FEATURE(FlowSortStrands, Supported::yes, DefaultVote::yes);
REGISTER_FIX (fixSTAmountCanonicalize, Supported::yes, DefaultVote::yes);
REGISTER_FIX (fixRmSmallIncreasedQOffers, Supported::yes, DefaultVote::yes);
REGISTER_FEATURE(CheckCashMakesTrustLine, Supported::yes, DefaultVote::no);
// The following amendments have been active for at least two years. Their // The following amendments have been active for at least two years. Their
// pre-amendment code has been removed and the identifiers are deprecated. // pre-amendment code has been removed and the identifiers are deprecated.
// All known amendments and amendments that may appear in a validated
// ledger must be registered either here or above with the "active" amendments
[[deprecated("The referenced amendment has been retired"), maybe_unused]] [[deprecated("The referenced amendment has been retired"), maybe_unused]]
uint256 const uint256 const
retiredPayChan = *getRegisteredFeature("PayChan"), retiredMultiSign = retireFeature("MultiSign"),
retiredCryptoConditions = *getRegisteredFeature("CryptoConditions"), retiredTrustSetAuth = retireFeature("TrustSetAuth"),
retiredTickSize = *getRegisteredFeature("TickSize"), retiredFeeEscalation = retireFeature("FeeEscalation"),
retiredFix1368 = *getRegisteredFeature("fix1368"), retiredPayChan = retireFeature("PayChan"),
retiredEscrow = *getRegisteredFeature("Escrow"), retiredCryptoConditions = retireFeature("CryptoConditions"),
retiredFix1373 = *getRegisteredFeature("fix1373"), retiredTickSize = retireFeature("TickSize"),
retiredEnforceInvariants = *getRegisteredFeature("EnforceInvariants"), retiredFix1368 = retireFeature("fix1368"),
retiredSortedDirectories = *getRegisteredFeature("SortedDirectories"), retiredEscrow = retireFeature("Escrow"),
retiredFix1201 = *getRegisteredFeature("fix1201"), retiredFix1373 = retireFeature("fix1373"),
retiredFix1512 = *getRegisteredFeature("fix1512"), retiredEnforceInvariants = retireFeature("EnforceInvariants"),
retiredFix1523 = *getRegisteredFeature("fix1523"), retiredSortedDirectories = retireFeature("SortedDirectories"),
retiredFix1528 = *getRegisteredFeature("fix1528"); retiredFix1201 = retireFeature("fix1201"),
retiredFix1512 = retireFeature("fix1512"),
retiredFix1523 = retireFeature("fix1523"),
retiredFix1528 = retireFeature("fix1528");
// clang-format on // clang-format on
#undef REGISTER_FIX
#pragma pop_macro("REGISTER_FIX")
#undef REGISTER_FEATURE
#pragma pop_macro("REGISTER_FEATURE")
// All of the features should now be registered, since variables in a cpp file
// are initialized from top to bottom.
//
// Use initialization of one final static variable to set
// featureCollections::readOnly.
[[maybe_unused]] static const bool readOnlySet =
featureCollections.registrationIsDone();
} // namespace ripple } // namespace ripple

View File

@@ -62,6 +62,8 @@ doFeature(RPC::JsonContext& context)
auto feature = table.find(context.params[jss::feature].asString()); auto feature = table.find(context.params[jss::feature].asString());
// If the feature is not found by name, try to parse the `feature` param as
// a feature ID. If that fails, return an error.
if (!feature && !feature.parseHex(context.params[jss::feature].asString())) if (!feature && !feature.parseHex(context.params[jss::feature].asString()))
return rpcError(rpcBAD_FEATURE); return rpcError(rpcBAD_FEATURE);

View File

@@ -86,11 +86,31 @@ private:
return cfg; return cfg;
} }
static std::vector<AmendmentTable::FeatureInfo>
makeDefaultYes(std::vector<std::string> const& amendments)
{
std::vector<AmendmentTable::FeatureInfo> result;
result.reserve(amendments.size());
for (auto const& a : amendments)
{
result.emplace_back(a, amendmentId(a), DefaultVote::yes);
}
return result;
}
static std::vector<AmendmentTable::FeatureInfo>
makeDefaultYes(uint256 const amendment)
{
std::vector<AmendmentTable::FeatureInfo> result{
{to_string(amendment), amendment, DefaultVote::yes}};
return result;
}
// All useful amendments are supported amendments. // All useful amendments are supported amendments.
// Enabled amendments are typically a subset of supported amendments. // Enabled amendments are typically a subset of supported amendments.
// Vetoed amendments should be supported but not enabled. // Vetoed amendments should be supported but not enabled.
// Unsupported amendments may be added to the AmendmentTable. // Unsupported amendments may be added to the AmendmentTable.
std::vector<std::string> const supported_{ std::vector<std::string> const supportedYes_{
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k",
"l", "m", "n", "o", "p", "q", "r", "s", "t", "u"}; "l", "m", "n", "o", "p", "q", "r", "s", "t", "u"};
std::vector<std::string> const std::vector<std::string> const
@@ -100,6 +120,7 @@ private:
std::vector<std::string> const unsupportedMajority_{"y", "z"}; std::vector<std::string> const unsupportedMajority_{"y", "z"};
Section const emptySection; Section const emptySection;
std::vector<AmendmentTable::FeatureInfo> const emptyYes;
test::SuiteJournal journal; test::SuiteJournal journal;
@@ -112,7 +133,7 @@ public:
makeTable( makeTable(
Application& app, Application& app,
std::chrono::seconds majorityTime, std::chrono::seconds majorityTime,
Section const& supported, std::vector<AmendmentTable::FeatureInfo> const& supported,
Section const& enabled, Section const& enabled,
Section const& vetoed) Section const& vetoed)
{ {
@@ -124,7 +145,7 @@ public:
makeTable( makeTable(
test::jtx::Env& env, test::jtx::Env& env,
std::chrono::seconds majorityTime, std::chrono::seconds majorityTime,
Section const& supported, std::vector<AmendmentTable::FeatureInfo> const& supported,
Section const& enabled, Section const& enabled,
Section const& vetoed) Section const& vetoed)
{ {
@@ -137,7 +158,7 @@ public:
return makeTable( return makeTable(
env.app(), env.app(),
majorityTime, majorityTime,
makeSection(supported_), makeDefaultYes(supportedYes_),
makeSection(enabled_), makeSection(enabled_),
makeSection(vetoed_)); makeSection(vetoed_));
} }
@@ -149,7 +170,7 @@ public:
test::jtx::Env env{*this, makeConfig()}; test::jtx::Env env{*this, makeConfig()};
auto table = makeTable(env, weeks(1)); auto table = makeTable(env, weeks(1));
for (auto const& a : supported_) for (auto const& a : supportedYes_)
{ {
BEAST_EXPECT(table->isSupported(amendmentId(a))); BEAST_EXPECT(table->isSupported(amendmentId(a)));
} }
@@ -174,7 +195,7 @@ public:
test::jtx::Env env{*this, makeConfig()}; test::jtx::Env env{*this, makeConfig()};
auto table = makeTable(env, weeks(1)); auto table = makeTable(env, weeks(1));
for (auto const& a : supported_) for (auto const& a : supportedYes_)
BEAST_EXPECT(table->find(a) == amendmentId(a)); BEAST_EXPECT(table->find(a) == amendmentId(a));
for (auto const& a : enabled_) for (auto const& a : enabled_)
BEAST_EXPECT(table->find(a) == amendmentId(a)); BEAST_EXPECT(table->find(a) == amendmentId(a));
@@ -207,7 +228,8 @@ public:
void void
testBadConfig() testBadConfig()
{ {
auto const section = makeSection(supported_); auto const yesVotes = makeDefaultYes(supportedYes_);
auto const section = makeSection(vetoed_);
auto const id = to_string(amendmentId(enabled_[0])); auto const id = to_string(amendmentId(enabled_[0]));
testcase("Bad Config"); testcase("Bad Config");
@@ -219,12 +241,13 @@ public:
try try
{ {
test::jtx::Env env{*this, makeConfig()}; test::jtx::Env env{*this, makeConfig()};
if (makeTable(env, weeks(2), test, emptySection, emptySection)) if (makeTable(env, weeks(2), yesVotes, test, emptySection))
fail("Accepted only amendment ID"); fail("Accepted only amendment ID");
} }
catch (...) catch (std::exception const& e)
{ {
pass(); BEAST_EXPECT(
e.what() == "Invalid entry '" + id + "' in [Test]");
} }
} }
@@ -235,12 +258,14 @@ public:
try try
{ {
test::jtx::Env env{*this, makeConfig()}; test::jtx::Env env{*this, makeConfig()};
if (makeTable(env, weeks(2), test, emptySection, emptySection)) if (makeTable(env, weeks(2), yesVotes, test, emptySection))
fail("Accepted extra arguments"); fail("Accepted extra arguments");
} }
catch (...) catch (std::exception const& e)
{ {
pass(); BEAST_EXPECT(
e.what() ==
"Invalid entry '" + id + " Test Name' in [Test]");
} }
} }
@@ -254,12 +279,13 @@ public:
try try
{ {
test::jtx::Env env{*this, makeConfig()}; test::jtx::Env env{*this, makeConfig()};
if (makeTable(env, weeks(2), test, emptySection, emptySection)) if (makeTable(env, weeks(2), yesVotes, test, emptySection))
fail("Accepted short amendment ID"); fail("Accepted short amendment ID");
} }
catch (...) catch (std::exception const& e)
{ {
pass(); BEAST_EXPECT(
e.what() == "Invalid entry '" + sid + " Name' in [Test]");
} }
} }
@@ -273,12 +299,13 @@ public:
try try
{ {
test::jtx::Env env{*this, makeConfig()}; test::jtx::Env env{*this, makeConfig()};
if (makeTable(env, weeks(2), test, emptySection, emptySection)) if (makeTable(env, weeks(2), yesVotes, test, emptySection))
fail("Accepted long amendment ID"); fail("Accepted long amendment ID");
} }
catch (...) catch (std::exception const& e)
{ {
pass(); BEAST_EXPECT(
e.what() == "Invalid entry '" + sid + " Name' in [Test]");
} }
} }
@@ -293,12 +320,13 @@ public:
try try
{ {
test::jtx::Env env{*this, makeConfig()}; test::jtx::Env env{*this, makeConfig()};
if (makeTable(env, weeks(2), test, emptySection, emptySection)) if (makeTable(env, weeks(2), yesVotes, test, emptySection))
fail("Accepted non-hex amendment ID"); fail("Accepted non-hex amendment ID");
} }
catch (...) catch (std::exception const& e)
{ {
pass(); BEAST_EXPECT(
e.what() == "Invalid entry '" + sid + " Name' in [Test]");
} }
} }
} }
@@ -311,27 +339,27 @@ public:
test::jtx::Env env{*this, makeConfig()}; test::jtx::Env env{*this, makeConfig()};
std::unique_ptr<AmendmentTable> table = makeTable(env, weeks(2)); std::unique_ptr<AmendmentTable> table = makeTable(env, weeks(2));
// Note which entries are enabled. // Note which entries are enabled
std::set<uint256> allEnabled; std::set<uint256> allEnabled;
for (auto const& a : enabled_)
// Subset of amendments to enable allEnabled.insert(amendmentId(a));
allEnabled.insert(amendmentId(supported_[0]));
allEnabled.insert(amendmentId(enabled_[0]));
allEnabled.insert(amendmentId(vetoed_[0]));
for (uint256 const& a : allEnabled) for (uint256 const& a : allEnabled)
table->enable(a); BEAST_EXPECT(table->enable(a));
// So far all enabled amendments are supported. // So far all enabled amendments are supported.
BEAST_EXPECT(!table->hasUnsupportedEnabled()); BEAST_EXPECT(!table->hasUnsupportedEnabled());
// Verify all enables are enabled and nothing else. // Verify all enables are enabled and nothing else.
for (std::string const& a : supported_) for (std::string const& a : supportedYes_)
{ {
uint256 const supportedID = amendmentId(a); uint256 const supportedID = amendmentId(a);
BEAST_EXPECT( bool const enabled = table->isEnabled(supportedID);
table->isEnabled(supportedID) == bool const found = allEnabled.find(supportedID) != allEnabled.end();
(allEnabled.find(supportedID) != allEnabled.end())); BEAST_EXPECTS(
enabled == found,
a + (enabled ? " enabled " : " disabled ") +
(found ? " found" : " not found"));
} }
// All supported and unVetoed amendments should be returned as desired. // All supported and unVetoed amendments should be returned as desired.
@@ -347,14 +375,14 @@ public:
// Unveto an amendment that is already not vetoed. Shouldn't // Unveto an amendment that is already not vetoed. Shouldn't
// hurt anything, but the values returned by getDesired() // hurt anything, but the values returned by getDesired()
// shouldn't change. // shouldn't change.
table->unVeto(amendmentId(supported_[1])); BEAST_EXPECT(!table->unVeto(amendmentId(supportedYes_[1])));
BEAST_EXPECT(desired == table->getDesired()); BEAST_EXPECT(desired == table->getDesired());
} }
// UnVeto one of the vetoed amendments. It should now be desired. // UnVeto one of the vetoed amendments. It should now be desired.
{ {
uint256 const unvetoedID = amendmentId(vetoed_[0]); uint256 const unvetoedID = amendmentId(vetoed_[0]);
table->unVeto(unvetoedID); BEAST_EXPECT(table->unVeto(unvetoedID));
std::vector<uint256> const desired = table->getDesired(); std::vector<uint256> const desired = table->getDesired();
BEAST_EXPECT( BEAST_EXPECT(
@@ -363,7 +391,7 @@ public:
} }
// Veto all supported amendments. Now desired should be empty. // Veto all supported amendments. Now desired should be empty.
for (std::string const& a : supported_) for (std::string const& a : supportedYes_)
{ {
table->veto(amendmentId(a)); table->veto(amendmentId(a));
} }
@@ -505,7 +533,7 @@ public:
test::jtx::Env env{*this}; test::jtx::Env env{*this};
auto table = auto table =
makeTable(env, weeks(2), emptySection, emptySection, emptySection); makeTable(env, weeks(2), emptyYes, emptySection, emptySection);
std::vector<std::pair<uint256, int>> votes; std::vector<std::pair<uint256, int>> votes;
std::vector<uint256> ourVotes; std::vector<uint256> ourVotes;
@@ -566,11 +594,7 @@ public:
test::jtx::Env env{*this}; test::jtx::Env env{*this};
auto table = makeTable( auto table = makeTable(
env, env, weeks(2), emptyYes, emptySection, makeSection(testAmendment));
weeks(2),
emptySection,
emptySection,
makeSection(testAmendment));
auto const validators = makeValidators(10); auto const validators = makeValidators(10);
@@ -629,7 +653,11 @@ public:
test::jtx::Env env{*this}; test::jtx::Env env{*this};
auto table = makeTable( auto table = makeTable(
env, weeks(2), makeSection(supported_), emptySection, emptySection); env,
weeks(2),
makeDefaultYes(supportedYes_),
emptySection,
emptySection);
auto const validators = makeValidators(10); auto const validators = makeValidators(10);
std::vector<std::pair<uint256, int>> votes; std::vector<std::pair<uint256, int>> votes;
@@ -647,13 +675,13 @@ public:
ourVotes, ourVotes,
enabled, enabled,
majority); majority);
BEAST_EXPECT(ourVotes.size() == supported_.size()); BEAST_EXPECT(ourVotes.size() == supportedYes_.size());
BEAST_EXPECT(enabled.empty()); BEAST_EXPECT(enabled.empty());
for (auto const& i : supported_) for (auto const& i : supportedYes_)
BEAST_EXPECT(majority.find(amendmentId(i)) == majority.end()); BEAST_EXPECT(majority.find(amendmentId(i)) == majority.end());
// Now, everyone votes for this feature // Now, everyone votes for this feature
for (auto const& i : supported_) for (auto const& i : supportedYes_)
votes.emplace_back(amendmentId(i), validators.size()); votes.emplace_back(amendmentId(i), validators.size());
// Week 2: We should recognize a majority // Week 2: We should recognize a majority
@@ -666,10 +694,10 @@ public:
ourVotes, ourVotes,
enabled, enabled,
majority); majority);
BEAST_EXPECT(ourVotes.size() == supported_.size()); BEAST_EXPECT(ourVotes.size() == supportedYes_.size());
BEAST_EXPECT(enabled.empty()); BEAST_EXPECT(enabled.empty());
for (auto const& i : supported_) for (auto const& i : supportedYes_)
BEAST_EXPECT(majority[amendmentId(i)] == weekTime(weeks{2})); BEAST_EXPECT(majority[amendmentId(i)] == weekTime(weeks{2}));
// Week 5: We should enable the amendment // Week 5: We should enable the amendment
@@ -682,7 +710,7 @@ public:
ourVotes, ourVotes,
enabled, enabled,
majority); majority);
BEAST_EXPECT(enabled.size() == supported_.size()); BEAST_EXPECT(enabled.size() == supportedYes_.size());
// Week 6: We should remove it from our votes and from having a majority // Week 6: We should remove it from our votes and from having a majority
doRound( doRound(
@@ -694,9 +722,9 @@ public:
ourVotes, ourVotes,
enabled, enabled,
majority); majority);
BEAST_EXPECT(enabled.size() == supported_.size()); BEAST_EXPECT(enabled.size() == supportedYes_.size());
BEAST_EXPECT(ourVotes.empty()); BEAST_EXPECT(ourVotes.empty());
for (auto const& i : supported_) for (auto const& i : supportedYes_)
BEAST_EXPECT(majority.find(amendmentId(i)) == majority.end()); BEAST_EXPECT(majority.find(amendmentId(i)) == majority.end());
} }
@@ -711,7 +739,7 @@ public:
auto table = makeTable( auto table = makeTable(
env, env,
weeks(2), weeks(2),
makeSection(testAmendment), makeDefaultYes(testAmendment),
emptySection, emptySection,
emptySection); emptySection);
@@ -782,7 +810,7 @@ public:
auto table = makeTable( auto table = makeTable(
env, env,
weeks(8), weeks(8),
makeSection(testAmendment), makeDefaultYes(testAmendment),
emptySection, emptySection,
emptySection); emptySection);

View File

@@ -136,11 +136,10 @@ class TxQ1_test : public beast::unit_test::suite
for (auto i = env.current()->seq(); i <= 257; ++i) for (auto i = env.current()->seq(); i <= 257; ++i)
env.close(); env.close();
// The ledger after the flag ledger creates all the // The ledger after the flag ledger creates all the
// fee (1) and amendment (supportedAmendments().size()) // fee (1) and amendment (numUpVotedAmendments())
// pseudotransactions. The queue treats the fees on these // pseudotransactions. The queue treats the fees on these
// transactions as though they are ordinary transactions. // transactions as though they are ordinary transactions.
auto const flagPerLedger = auto const flagPerLedger = 1 + ripple::detail::numUpVotedAmendments();
1 + ripple::detail::supportedAmendments().size();
auto const flagMaxQueue = ledgersInQueue * flagPerLedger; auto const flagMaxQueue = ledgersInQueue * flagPerLedger;
checkMetrics(env, 0, flagMaxQueue, 0, flagPerLedger, 256); checkMetrics(env, 0, flagMaxQueue, 0, flagPerLedger, 256);
@@ -4157,8 +4156,7 @@ public:
if (!getMajorityAmendments(*env.closed()).empty()) if (!getMajorityAmendments(*env.closed()).empty())
break; break;
} }
auto expectedPerLedger = auto expectedPerLedger = ripple::detail::numUpVotedAmendments() + 1;
ripple::detail::supportedAmendments().size() + 1;
checkMetrics(env, 0, 5 * expectedPerLedger, 0, expectedPerLedger, 256); checkMetrics(env, 0, 5 * expectedPerLedger, 0, expectedPerLedger, 256);
// Now wait 2 weeks modulo 256 ledgers for the amendments to be // Now wait 2 weeks modulo 256 ledgers for the amendments to be
@@ -4213,51 +4211,19 @@ public:
// These particular amendments don't impact any of the queued // These particular amendments don't impact any of the queued
// transactions, so we won't see any change in the transaction // transactions, so we won't see any change in the transaction
// outcomes. But code coverage is affected. // outcomes. But code coverage is affected.
env.close(closeDuration); do
expectedInQueue -= expectedPerLedger + 2;
++expectedPerLedger;
checkMetrics(
env,
expectedInQueue,
5 * expectedPerLedger,
expectedPerLedger + 1,
expectedPerLedger,
256);
{ {
auto const expectedPerAccount = expectedInQueue / 6;
auto const expectedRemainder = expectedInQueue % 6;
BEAST_EXPECT(env.seq(alice) == seqAlice - expectedPerAccount);
BEAST_EXPECT(
env.seq(bob) ==
seqBob - expectedPerAccount - (expectedRemainder > 4 ? 1 : 0));
BEAST_EXPECT(
env.seq(carol) ==
seqCarol - expectedPerAccount -
(expectedRemainder > 3 ? 1 : 0));
BEAST_EXPECT(
env.seq(daria) ==
seqDaria - expectedPerAccount -
(expectedRemainder > 2 ? 1 : 0));
BEAST_EXPECT(
env.seq(ellie) ==
seqEllie - expectedPerAccount -
(expectedRemainder > 1 ? 1 : 0));
BEAST_EXPECT(
env.seq(fiona) ==
seqFiona - expectedPerAccount -
(expectedRemainder > 0 ? 1 : 0));
}
env.close(closeDuration); env.close(closeDuration);
auto expectedInLedger = expectedInQueue; auto expectedInLedger = expectedInQueue;
expectedInQueue = expectedInQueue =
(expectedInQueue > expectedPerLedger + 2 (expectedInQueue > expectedPerLedger + 2
? expectedInQueue - (expectedPerLedger + 2) ? expectedInQueue - (expectedPerLedger + 2)
: 0); : 0);
expectedInLedger -= expectedInQueue;
++expectedPerLedger; ++expectedPerLedger;
checkMetrics( checkMetrics(
env, env,
0, expectedInQueue,
5 * expectedPerLedger, 5 * expectedPerLedger,
expectedInLedger, expectedInLedger,
expectedPerLedger, expectedPerLedger,
@@ -4268,7 +4234,8 @@ public:
BEAST_EXPECT(env.seq(alice) == seqAlice - expectedPerAccount); BEAST_EXPECT(env.seq(alice) == seqAlice - expectedPerAccount);
BEAST_EXPECT( BEAST_EXPECT(
env.seq(bob) == env.seq(bob) ==
seqBob - expectedPerAccount - (expectedRemainder > 4 ? 1 : 0)); seqBob - expectedPerAccount -
(expectedRemainder > 4 ? 1 : 0));
BEAST_EXPECT( BEAST_EXPECT(
env.seq(carol) == env.seq(carol) ==
seqCarol - expectedPerAccount - seqCarol - expectedPerAccount -
@@ -4286,6 +4253,7 @@ public:
seqFiona - expectedPerAccount - seqFiona - expectedPerAccount -
(expectedRemainder > 0 ? 1 : 0)); (expectedRemainder > 0 ? 1 : 0));
} }
} while (expectedInQueue > 0);
} }
void void

View File

@@ -73,8 +73,9 @@ supported_amendments()
auto const& sa = ripple::detail::supportedAmendments(); auto const& sa = ripple::detail::supportedAmendments();
std::vector<uint256> feats; std::vector<uint256> feats;
feats.reserve(sa.size()); feats.reserve(sa.size());
for (auto const& s : sa) for (auto const& [s, vote] : sa)
{ {
(void)vote;
if (auto const f = getRegisteredFeature(s)) if (auto const f = getRegisteredFeature(s))
feats.push_back(*f); feats.push_back(*f);
else else

View File

@@ -812,6 +812,7 @@ public:
auto const missingSomeFeatures = auto const missingSomeFeatures =
supported_amendments() - featureMultiSignReserve - featureFlow; supported_amendments() - featureMultiSignReserve - featureFlow;
BEAST_EXPECT(missingSomeFeatures.count() == (supported.count() - 2));
{ {
// a Env supported_features_except is missing *only* those features // a Env supported_features_except is missing *only* those features
Env env{*this, missingSomeFeatures}; Env env{*this, missingSomeFeatures};

View File

@@ -26,6 +26,77 @@ namespace ripple {
class Feature_test : public beast::unit_test::suite class Feature_test : public beast::unit_test::suite
{ {
void
testInternals()
{
testcase("internals");
std::map<std::string, DefaultVote> const& supported =
ripple::detail::supportedAmendments();
BEAST_EXPECT(
supported.size() ==
ripple::detail::numDownVotedAmendments() +
ripple::detail::numUpVotedAmendments());
std::size_t up = 0, down = 0;
for (std::pair<std::string const, DefaultVote> const& amendment :
supported)
{
if (amendment.second == DefaultVote::no)
++down;
else
{
if (BEAST_EXPECT(amendment.second == DefaultVote::yes))
++up;
}
}
BEAST_EXPECT(down == ripple::detail::numDownVotedAmendments());
BEAST_EXPECT(up == ripple::detail::numUpVotedAmendments());
}
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(featureOwnerPaysFee) == "OwnerPaysFee");
BEAST_EXPECT(featureToName(featureFlow) == "Flow");
BEAST_EXPECT(featureToName(featureNegativeUNL) == "NegativeUNL");
BEAST_EXPECT(featureToName(fix1578) == "fix1578");
BEAST_EXPECT(
featureToName(fixTakerDryOfferRemoval) ==
"fixTakerDryOfferRemoval");
}
void void
testNoParams() testNoParams()
{ {
@@ -34,6 +105,9 @@ class Feature_test : public beast::unit_test::suite
using namespace test::jtx; using namespace test::jtx;
Env env{*this}; Env env{*this};
std::map<std::string, DefaultVote> const& votes =
ripple::detail::supportedAmendments();
auto jrr = env.rpc("feature")[jss::result]; auto jrr = env.rpc("feature")[jss::result];
if (!BEAST_EXPECT(jrr.isMember(jss::features))) if (!BEAST_EXPECT(jrr.isMember(jss::features)))
return; return;
@@ -41,13 +115,15 @@ class Feature_test : public beast::unit_test::suite
{ {
if (!BEAST_EXPECT(feature.isMember(jss::name))) if (!BEAST_EXPECT(feature.isMember(jss::name)))
return; return;
// default config - so all should be disabled, not vetoed, and // default config - so all should be disabled, and
// supported // supported. Some may be vetoed.
bool expectVeto =
!(votes.at(feature[jss::name].asString()) == DefaultVote::yes);
BEAST_EXPECTS( BEAST_EXPECTS(
!feature[jss::enabled].asBool(), !feature[jss::enabled].asBool(),
feature[jss::name].asString() + " enabled"); feature[jss::name].asString() + " enabled");
BEAST_EXPECTS( BEAST_EXPECTS(
!feature[jss::vetoed].asBool(), feature[jss::vetoed].asBool() == expectVeto,
feature[jss::name].asString() + " vetoed"); feature[jss::name].asString() + " vetoed");
BEAST_EXPECTS( BEAST_EXPECTS(
feature[jss::supported].asBool(), feature[jss::supported].asBool(),
@@ -67,6 +143,9 @@ class Feature_test : public beast::unit_test::suite
BEAST_EXPECTS(jrr[jss::status] == jss::success, "status"); BEAST_EXPECTS(jrr[jss::status] == jss::success, "status");
jrr.removeMember(jss::status); jrr.removeMember(jss::status);
BEAST_EXPECT(jrr.size() == 1); BEAST_EXPECT(jrr.size() == 1);
BEAST_EXPECT(
jrr.isMember("586480873651E106F1D6339B0C4A8945BA705A777F3F4524626FF"
"1FC07EFE41D"));
auto feature = *(jrr.begin()); auto feature = *(jrr.begin());
BEAST_EXPECTS(feature[jss::name] == "MultiSignReserve", "name"); BEAST_EXPECTS(feature[jss::name] == "MultiSignReserve", "name");
@@ -121,6 +200,9 @@ class Feature_test : public beast::unit_test::suite
Env env{ Env env{
*this, FeatureBitset(featureDepositAuth, featureDepositPreauth)}; *this, FeatureBitset(featureDepositAuth, featureDepositPreauth)};
std::map<std::string, DefaultVote> const& votes =
ripple::detail::supportedAmendments();
auto jrr = env.rpc("feature")[jss::result]; auto jrr = env.rpc("feature")[jss::result];
if (!BEAST_EXPECT(jrr.isMember(jss::features))) if (!BEAST_EXPECT(jrr.isMember(jss::features)))
return; return;
@@ -135,11 +217,13 @@ class Feature_test : public beast::unit_test::suite
bool expectEnabled = env.app().getAmendmentTable().isEnabled(id); bool expectEnabled = env.app().getAmendmentTable().isEnabled(id);
bool expectSupported = bool expectSupported =
env.app().getAmendmentTable().isSupported(id); env.app().getAmendmentTable().isSupported(id);
bool expectVeto =
!(votes.at((*it)[jss::name].asString()) == DefaultVote::yes);
BEAST_EXPECTS( BEAST_EXPECTS(
(*it)[jss::enabled].asBool() == expectEnabled, (*it)[jss::enabled].asBool() == expectEnabled,
(*it)[jss::name].asString() + " enabled"); (*it)[jss::name].asString() + " enabled");
BEAST_EXPECTS( BEAST_EXPECTS(
!(*it)[jss::vetoed].asBool(), (*it)[jss::vetoed].asBool() == expectVeto,
(*it)[jss::name].asString() + " vetoed"); (*it)[jss::name].asString() + " vetoed");
BEAST_EXPECTS( BEAST_EXPECTS(
(*it)[jss::supported].asBool() == expectSupported, (*it)[jss::supported].asBool() == expectSupported,
@@ -198,6 +282,8 @@ class Feature_test : public beast::unit_test::suite
// There should be at least 5 amendments. Don't do exact comparison // There should be at least 5 amendments. Don't do exact comparison
// to avoid maintenance as more amendments are added in the future. // to avoid maintenance as more amendments are added in the future.
BEAST_EXPECT(majorities.size() >= 5); BEAST_EXPECT(majorities.size() >= 5);
std::map<std::string, DefaultVote> const& votes =
ripple::detail::supportedAmendments();
jrr = env.rpc("feature")[jss::result]; jrr = env.rpc("feature")[jss::result];
if (!BEAST_EXPECT(jrr.isMember(jss::features))) if (!BEAST_EXPECT(jrr.isMember(jss::features)))
@@ -206,9 +292,15 @@ class Feature_test : public beast::unit_test::suite
{ {
if (!BEAST_EXPECT(feature.isMember(jss::name))) if (!BEAST_EXPECT(feature.isMember(jss::name)))
return; return;
bool expectVeto =
!(votes.at(feature[jss::name].asString()) == DefaultVote::yes);
BEAST_EXPECTS( BEAST_EXPECTS(
feature.isMember(jss::majority), expectVeto ^ feature.isMember(jss::majority),
feature[jss::name].asString() + " majority"); feature[jss::name].asString() + " majority");
BEAST_EXPECTS(
feature.isMember(jss::vetoed) &&
feature[jss::vetoed].asBool() == expectVeto,
feature[jss::name].asString() + " vetoed");
BEAST_EXPECTS( BEAST_EXPECTS(
feature.isMember(jss::count), feature.isMember(jss::count),
feature[jss::name].asString() + " count"); feature[jss::name].asString() + " count");
@@ -218,10 +310,12 @@ class Feature_test : public beast::unit_test::suite
BEAST_EXPECTS( BEAST_EXPECTS(
feature.isMember(jss::validations), feature.isMember(jss::validations),
feature[jss::name].asString() + " validations"); feature[jss::name].asString() + " validations");
BEAST_EXPECT(feature[jss::count] == 1); BEAST_EXPECT(feature[jss::count] == (expectVeto ? 0 : 1));
BEAST_EXPECT(feature[jss::threshold] == 1); BEAST_EXPECT(feature[jss::threshold] == 1);
BEAST_EXPECT(feature[jss::validations] == 1); BEAST_EXPECT(feature[jss::validations] == 1);
BEAST_EXPECT(feature[jss::majority] == 2540); BEAST_EXPECTS(
expectVeto || feature[jss::majority] == 2540,
"Majority: " + feature[jss::majority].asString());
} }
} }
@@ -273,6 +367,8 @@ public:
void void
run() override run() override
{ {
testInternals();
testFeatureLookups();
testNoParams(); testNoParams();
testSingleFeature(); testSingleFeature();
testInvalidFeature(); testInvalidFeature();