Add logging and improve counting of amendment votes from UNL (#5173)

* Add logging for amendment voting decision process
* When counting "received validations" to determine quorum, count the number of validators actually voting, not the total number of possible votes.
This commit is contained in:
Ed Hennis
2025-02-20 13:35:04 -05:00
committed by GitHub
parent 159dfb5acb
commit dce3e1efa6
3 changed files with 119 additions and 32 deletions

View File

@@ -360,7 +360,7 @@ RCLConsensus::Adaptor::onClose(
{ {
feeVote_->doVoting(prevLedger, validations, initialSet); feeVote_->doVoting(prevLedger, validations, initialSet);
app_.getAmendmentTable().doVoting( app_.getAmendmentTable().doVoting(
prevLedger, validations, initialSet); prevLedger, validations, initialSet, j_);
} }
} }
else if ( else if (

View File

@@ -147,7 +147,8 @@ public:
doVoting( doVoting(
std::shared_ptr<ReadView const> const& lastClosedLedger, std::shared_ptr<ReadView const> const& lastClosedLedger,
std::vector<std::shared_ptr<STValidation>> const& parentValidations, std::vector<std::shared_ptr<STValidation>> const& parentValidations,
std::shared_ptr<SHAMap> const& initialPosition) std::shared_ptr<SHAMap> const& initialPosition,
beast::Journal j)
{ {
// Ask implementation what to do // Ask implementation what to do
auto actions = doVoting( auto actions = doVoting(
@@ -174,6 +175,10 @@ public:
Serializer s; Serializer s;
amendTx.add(s); amendTx.add(s);
JLOG(j.debug()) << "Amendments: Adding pseudo-transaction: "
<< amendTx.getTransactionID() << ": "
<< strHex(s.slice()) << ": " << amendTx;
initialPosition->addGiveItem( initialPosition->addGiveItem(
SHAMapNodeType::tnTRANSACTION_NM, SHAMapNodeType::tnTRANSACTION_NM,
make_shamapitem(amendTx.getTransactionID(), s.slice())); make_shamapitem(amendTx.getTransactionID(), s.slice()));

View File

@@ -25,7 +25,10 @@
#include <xrpl/protocol/STValidation.h> #include <xrpl/protocol/STValidation.h>
#include <xrpl/protocol/TxFlags.h> #include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h> #include <xrpl/protocol/jss.h>
#include <boost/algorithm/string.hpp>
#include <boost/format.hpp> #include <boost/format.hpp>
#include <boost/range/adaptor/transformed.hpp>
#include <boost/regex.hpp> #include <boost/regex.hpp>
#include <algorithm> #include <algorithm>
#include <mutex> #include <mutex>
@@ -88,15 +91,17 @@ parseSection(Section const& section)
class TrustedVotes class TrustedVotes
{ {
private: private:
static constexpr NetClock::time_point maxTimeout =
NetClock::time_point::max();
// Associates each trusted validator with the last votes we saw from them // Associates each trusted validator with the last votes we saw from them
// and an expiration for that record. // and an expiration for that record.
struct UpvotesAndTimeout struct UpvotesAndTimeout
{ {
std::vector<uint256> upVotes; std::vector<uint256> upVotes;
NetClock::time_point timeout = maxTimeout; /** An unseated timeout indicates that either
1. No validations have ever been received
2. The validator has not been heard from in long enough that the
timeout passed, and votes expired.
*/
std::optional<NetClock::time_point> timeout;
}; };
hash_map<PublicKey, UpvotesAndTimeout> recordedVotes_; hash_map<PublicKey, UpvotesAndTimeout> recordedVotes_;
@@ -130,7 +135,7 @@ public:
else else
{ {
// New validators have a starting position of no on everything. // New validators have a starting position of no on everything.
// Add the entry with an empty vector and maxTimeout. // Add the entry with an empty vector and unseated timeout.
newRecordedVotes[trusted]; newRecordedVotes[trusted];
} }
} }
@@ -147,6 +152,7 @@ public:
Rules const& rules, Rules const& rules,
std::vector<std::shared_ptr<STValidation>> const& valSet, std::vector<std::shared_ptr<STValidation>> const& valSet,
NetClock::time_point const closeTime, NetClock::time_point const closeTime,
beast::Journal j,
std::lock_guard<std::mutex> const& lock) std::lock_guard<std::mutex> const& lock)
{ {
// When we get an STValidation we save the upVotes it contains, but // When we get an STValidation we save the upVotes it contains, but
@@ -163,38 +169,86 @@ public:
using namespace std::chrono_literals; using namespace std::chrono_literals;
static constexpr NetClock::duration expiresAfter = 24h; static constexpr NetClock::duration expiresAfter = 24h;
auto const newTimeout = closeTime + expiresAfter;
// Walk all validations and replace previous votes from trusted // Walk all validations and replace previous votes from trusted
// validators with these newest votes. // validators with these newest votes.
for (auto const& val : valSet) for (auto const& val : valSet)
{ {
auto const pkHuman =
toBase58(TokenType::NodePublic, val->getSignerPublic());
// If this validation comes from one of our trusted validators... // If this validation comes from one of our trusted validators...
if (auto const iter = recordedVotes_.find(val->getSignerPublic()); if (auto const iter = recordedVotes_.find(val->getSignerPublic());
iter != recordedVotes_.end()) iter != recordedVotes_.end())
{ {
iter->second.timeout = closeTime + expiresAfter; iter->second.timeout = newTimeout;
if (val->isFieldPresent(sfAmendments)) if (val->isFieldPresent(sfAmendments))
{ {
auto const& choices = val->getFieldV256(sfAmendments); auto const& choices = val->getFieldV256(sfAmendments);
iter->second.upVotes.assign(choices.begin(), choices.end()); iter->second.upVotes.assign(choices.begin(), choices.end());
JLOG(j.debug())
<< "recordVotes: Validation from trusted " << pkHuman
<< " has " << choices.size() << " amendment votes: "
<< boost::algorithm::join(
iter->second.upVotes |
boost::adaptors::transformed(
to_string<256, void>),
", ");
// TODO: Maybe transform using to_short_string once #5126 is
// merged
//
// iter->second.upVotes |
// boost::adaptors::transformed(to_short_string<256, void>)
} }
else else
{ {
// This validator does not upVote any amendments right now. // This validator does not upVote any amendments right now.
iter->second.upVotes.clear(); iter->second.upVotes.clear();
JLOG(j.debug()) << "recordVotes: Validation from trusted "
<< pkHuman << " has no amendment votes.";
} }
} }
else
{
JLOG(j.debug())
<< "recordVotes: Ignoring validation from untrusted "
<< pkHuman;
}
} }
// Now remove any expired records from recordedVotes_. // Now remove any expired records from recordedVotes_.
std::for_each( std::for_each(
recordedVotes_.begin(), recordedVotes_.begin(),
recordedVotes_.end(), recordedVotes_.end(),
[&closeTime](decltype(recordedVotes_)::value_type& votes) { [&closeTime, newTimeout, &j](
if (closeTime > votes.second.timeout) decltype(recordedVotes_)::value_type& votes) {
auto const pkHuman =
toBase58(TokenType::NodePublic, votes.first);
if (!votes.second.timeout)
{ {
votes.second.timeout = maxTimeout; assert(votes.second.upVotes.empty());
JLOG(j.debug())
<< "recordVotes: Have not received any "
"amendment votes from "
<< pkHuman << " since last timeout or startup";
}
else if (closeTime > votes.second.timeout)
{
JLOG(j.debug())
<< "recordVotes: Timeout: Clearing votes from "
<< pkHuman;
votes.second.timeout.reset();
votes.second.upVotes.clear(); votes.second.upVotes.clear();
} }
else if (votes.second.timeout != newTimeout)
{
assert(votes.second.timeout < newTimeout);
using namespace std::chrono;
auto const age = duration_cast<minutes>(
newTimeout - *votes.second.timeout);
JLOG(j.debug()) << "recordVotes: Using " << age.count()
<< "min old cached votes from " << pkHuman;
}
}); });
} }
@@ -205,14 +259,20 @@ public:
getVotes(Rules const& rules, std::lock_guard<std::mutex> const& lock) const getVotes(Rules const& rules, std::lock_guard<std::mutex> const& lock) const
{ {
hash_map<uint256, int> ret; hash_map<uint256, int> ret;
int available = 0;
for (auto& validatorVotes : recordedVotes_) for (auto& validatorVotes : recordedVotes_)
{ {
assert(
validatorVotes.second.timeout ||
validatorVotes.second.upVotes.empty());
if (validatorVotes.second.timeout)
++available;
for (uint256 const& amendment : validatorVotes.second.upVotes) for (uint256 const& amendment : validatorVotes.second.upVotes)
{ {
ret[amendment] += 1; ret[amendment] += 1;
} }
} }
return {recordedVotes_.size(), ret}; return {available, ret};
} }
}; };
@@ -789,13 +849,13 @@ AmendmentTableImpl::doVoting(
std::lock_guard lock(mutex_); std::lock_guard lock(mutex_);
// Keep a record of the votes we received. // Keep a record of the votes we received.
previousTrustedVotes_.recordVotes(rules, valSet, closeTime, lock); previousTrustedVotes_.recordVotes(rules, valSet, closeTime, j_, lock);
// Tally the most recent votes. // Tally the most recent votes.
auto vote = auto vote =
std::make_unique<AmendmentSet>(rules, previousTrustedVotes_, lock); std::make_unique<AmendmentSet>(rules, previousTrustedVotes_, lock);
JLOG(j_.debug()) << "Received " << vote->trustedValidations() JLOG(j_.debug()) << "Counted votes from " << vote->trustedValidations()
<< " trusted validations, threshold is: " << " valid trusted validations, threshold is: "
<< vote->threshold(); << vote->threshold();
// Map of amendments to the action to be taken for each one. The action is // Map of amendments to the action to be taken for each one. The action is
@@ -805,43 +865,65 @@ AmendmentTableImpl::doVoting(
// process all amendments we know of // process all amendments we know of
for (auto const& entry : amendmentMap_) for (auto const& entry : amendmentMap_)
{ {
NetClock::time_point majorityTime = {}; if (enabledAmendments.contains(entry.first))
{
JLOG(j_.trace()) << entry.first << ": amendment already enabled";
continue;
}
bool const hasValMajority = vote->passes(entry.first); bool const hasValMajority = vote->passes(entry.first);
{ auto const majorityTime = [&]() -> std::optional<NetClock::time_point> {
auto const it = majorityAmendments.find(entry.first); auto const it = majorityAmendments.find(entry.first);
if (it != majorityAmendments.end()) if (it != majorityAmendments.end())
majorityTime = it->second; return it->second;
} return std::nullopt;
}();
if (enabledAmendments.count(entry.first) != 0) bool const hasLedgerMajority = majorityTime.has_value();
{
JLOG(j_.debug()) << entry.first << ": amendment already enabled"; auto const logStr = [&entry, &vote]() {
} std::stringstream ss;
else if ( ss << entry.first << " (" << entry.second.name << ") has "
hasValMajority && (majorityTime == NetClock::time_point{}) && << vote->votes(entry.first) << " votes";
return ss.str();
}();
if (hasValMajority && !hasLedgerMajority &&
entry.second.vote == AmendmentVote::up) entry.second.vote == AmendmentVote::up)
{ {
// Ledger says no majority, validators say yes // Ledger says no majority, validators say yes, and voting yes
JLOG(j_.debug()) << entry.first << ": amendment got majority"; // locally
JLOG(j_.debug()) << logStr << ": amendment got majority";
actions[entry.first] = tfGotMajority; actions[entry.first] = tfGotMajority;
} }
else if (!hasValMajority && (majorityTime != NetClock::time_point{})) else if (!hasValMajority && hasLedgerMajority)
{ {
// Ledger says majority, validators say no // Ledger says majority, validators say no
JLOG(j_.debug()) << entry.first << ": amendment lost majority"; JLOG(j_.debug()) << logStr << ": amendment lost majority";
actions[entry.first] = tfLostMajority; actions[entry.first] = tfLostMajority;
} }
else if ( else if (
(majorityTime != NetClock::time_point{}) && hasLedgerMajority &&
((majorityTime + majorityTime_) <= closeTime) && ((*majorityTime + majorityTime_) <= closeTime) &&
entry.second.vote == AmendmentVote::up) entry.second.vote == AmendmentVote::up)
{ {
// Ledger says majority held // Ledger says majority held
JLOG(j_.debug()) << entry.first << ": amendment majority held"; JLOG(j_.debug()) << logStr << ": amendment majority held";
actions[entry.first] = 0; actions[entry.first] = 0;
} }
// Logging only below this point
else if (hasValMajority && hasLedgerMajority)
{
JLOG(j_.debug())
<< logStr
<< ": amendment holding majority, waiting to be enabled";
}
else if (!hasValMajority)
{
JLOG(j_.debug()) << logStr << ": amendment does not have majority";
}
} }
// Stash for reporting // Stash for reporting