The voting and activation mechanism for amendments is at the heart of XRPL's decentralized governance. This system allows the network to make collective decisions on protocol changes without a central authority, while ensuring a high level of consensus before any modification.
In this section, we will dive into the technical details of the voting process: how validators express their preferences, how these votes are collected and aggregated, how the threshold (over 80%) is calculated and verified, and finally how the network automatically activates amendments once consensus is reached.
We will continue to follow the example of Subscriptions (XLS-0078) to concretely illustrate each mechanism.
Validator Voting Mechanism
Vote Expression
A validator expresses their vote for an amendment in several ways:
1. Static configuration: Via the rippled.cfg configuration file:
[amendments]# Vote for Subscriptions7B73B9E8D8E6E8E8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8# Vote against (or remove vote) by commenting out or removing the line# 7B73B9E8D8E6E8E8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8
2. Dynamic RPC command: Via the admin interface:
3. Default vote: If no explicit configuration is provided, the default behavior defined in features.macro applies:
Inclusion in Validations
A validator's vote is communicated to the network via the validation messages they publish after validating each ledger.
Structure of a validation message:
Generation of amendments list: This list is generated by the doValidation() function of AmendmentTable:
Network broadcast: The signed validation message is broadcast via the overlay network to all connected peers. Each node that receives this validation extracts and stores the list of voted amendments.
Vote Collection and Aggregation
TrustedVotes Structure
The TrustedVotes class maintains a real-time count of amendment votes from trusted validators.
Internal structure:
Expiration mechanism: Votes are considered "fresh" for 5 minutes. If a validator doesn't publish a new validation within this time, their vote is removed from the count. This ensures counts reflect the current state of active validators.
AmendmentSet: Calculating the Threshold
The AmendmentSet class calculates which amendments have exceeded the required threshold for activation.
Construction:
Threshold calculation (over 80%):
The displayed threshold is calculated as floor(validations * 0.8), but the comparison uses votes > threshold (strictly greater), which ensures over 80% of validators vote for the amendment.
Examples with Subscriptions:
25 trusted validators:
26 validators:
35 validators:
Special case - 1 validator: With a single validator (test networks), the comparison becomes votes >= threshold instead of votes > threshold, allowing the single validator to activate the amendment.
doVoting Function: Vote Orchestration
The doVoting() function is called at each consensus round (before each flag ledger) to determine what actions to take for amendments.
Complete Algorithm
Returned Action Codes
The returned map associates each amendment with an action code:
tfGotMajority (0x00010000): Amendment exceeded the 80% threshold
tfLostMajority (0x00020000): Amendment fell below the threshold
0: Amendment must be activated (stability period elapsed)
These codes are used to generate appropriate pseudo-transactions.
EnableAmendment Pseudo-transactions
Pseudo-transaction Generation
Pseudo-transactions are generated by the consensus engine in RCLConsensus::onAccept() after a ledger has been accepted.
Stability period. At each flag ledger, doVoting() checks:
2025-05-04 14:32:15 UTC (Ledger 86652345)
Exactly 2 weeks after majorityTime. The next flag ledger activates the amendment.
doVoting() returns:
Injected pseudo-transaction:
Change::applyAmendment() adds Subscriptions to sfAmendments.
Final state:
Synchronization and Consistency
doValidatedLedger: Post-Validation Update
After each validated ledger, doValidatedLedger() synchronizes the internal state:
This function ensures that even if a node temporarily misses receiving a ledger, it will resynchronize correctly when it receives the validated ledger.
Protection Against Unsupported Amendments
If an amendment is enabled but the node does not support it, the system enters "amendment blocked" mode to protect network integrity.
Detection:
Consequences:
Node ceases to participate in consensus
Node ceases to validate new ledgers
API remains accessible but signals "amendment blocked" state
Node must be updated to resume operations
Notification: A warning is displayed in logs and via the server_info API:
// Automatically vote for
XRPL_FEATURE(Tickets, Supported::yes, VoteBehavior::DefaultYes)
// Don't vote by default (requires explicit activation)
XRPL_FEATURE(Subscriptions, Supported::yes, VoteBehavior::DefaultNo)
// Never vote (obsolete)
XRPL_FEATURE(OldFeature, Supported::yes, VoteBehavior::Obsolete)
class STValidation {
// Identification of validated ledger
uint256 ledgerHash_;
uint32 ledgerSeq_;
NetClock::time_point signTime_;
// List of supported/desired amendments
std::optional<std::vector<uint256>> amendments_;
// Validator's public key
PublicKey publicKey_;
// Cryptographic signature
Buffer signature_;
};
std::vector<uint256>
AmendmentTableImpl::doValidation(std::set<uint256> const& enabled) const
{
// Get the list of amendments we support and do not
// veto, but that are not already enabled
std::vector<uint256> amendments;
{
std::lock_guard lock(mutex_);
amendments.reserve(amendmentMap_.size());
for (auto const& e : amendmentMap_)
{
// Include only if:
// 1. Amendment is supported by this node
// 2. Node votes "up" for the amendment
// 3. Amendment is not already enabled
if (e.second.supported && e.second.vote == AmendmentVote::up &&
(enabled.count(e.first) == 0))
{
amendments.push_back(e.first);
JLOG(j_.info()) << "Voting for amendment " << e.second.name;
}
}
}
if (!amendments.empty())
// Sort to ensure deterministic order
std::sort(amendments.begin(), amendments.end());
return amendments;
}
class TrustedVotes {
private:
// Structure for an individual vote
struct Vote {
NetClock::time_point expiration; // When this vote expires
std::vector<uint256> amendments; // Voted amendments
};
// Map: Validator's public key -> Current vote
hash_map<PublicKey, Vote> votes_;
// Timeout parameters
NetClock::duration const validationFreshness_{5min};
public:
// Add or update a vote
void addVote(
PublicKey const& key,
std::vector<uint256> const& amendments,
NetClock::time_point now)
{
votes_[key] = Vote{
now + validationFreshness_,
amendments
};
}
// Expire old votes
void expire(NetClock::time_point now) {
for (auto it = votes_.begin(); it != votes_.end();) {
if (it->second.expiration < now)
it = votes_.erase(it);
else
++it;
}
}
// Aggregate votes
std::pair<int, hash_map<uint256, int>>
getVotes(Rules const& rules, std::lock_guard<std::mutex> const&)
{
hash_map<uint256, int> amendmentVotes;
int trustedValidations = 0;
for (auto const& [key, vote] : votes_) {
trustedValidations++;
for (auto const& amendment : vote.amendments) {
amendmentVotes[amendment]++;
}
}
return {trustedValidations, amendmentVotes};
}
};
class AmendmentSet
{
private:
// How many yes votes each amendment received
hash_map<uint256, int> votes_;
// number of trusted validations
int trustedValidations_ = 0;
// number of votes needed
int threshold_ = 0;
public:
AmendmentSet(
Rules const& rules,
TrustedVotes const& trustedVotes,
std::lock_guard<std::mutex> const& lock)
{
// process validations for ledger before flag ledger.
auto [trustedCount, newVotes] = trustedVotes.getVotes(rules, lock);
trustedValidations_ = trustedCount;
votes_.swap(newVotes);
threshold_ = std::max(
1L,
static_cast<long>(
(trustedValidations_ * amendmentMajorityCalcThreshold.num) /
amendmentMajorityCalcThreshold.den));
}
bool
passes(uint256 const& amendment) const
{
auto const& it = votes_.find(amendment);
if (it == votes_.end())
return false;
// One validator is an exception, otherwise it is not possible
// to gain majority.
if (trustedValidations_ == 1)
return it->second >= threshold_;
return it->second > threshold_;
}
int
votes(uint256 const& amendment) const
{
auto const& it = votes_.find(amendment);
if (it == votes_.end())
return 0;
return it->second;
}
int
trustedValidations() const
{
return trustedValidations_;
}
int
threshold() const
{
return threshold_;
}
};
threshold = floor(25 * 0.8) = floor(20.0) = 20
To pass: votes > 20, so minimum 21 votes required
Actual percentage: 21/25 = 84% > 80% ✓
threshold = floor(26 * 0.8) = floor(20.8) = 20
To pass: votes > 20, so minimum 21 votes required
Actual percentage: 21/26 = 80.77% > 80% ✓
threshold = floor(35 * 0.8) = floor(28.0) = 28
To pass: votes > 28, so minimum 29 votes required
Actual percentage: 29/35 = 82.86% > 80% ✓
std::map<uint256, std::uint32_t>
AmendmentTableImpl::doVoting(
Rules const& rules,
NetClock::time_point closeTime,
std::set<uint256> const& enabledAmendments,
majorityAmendments_t const& majorityAmendments,
std::vector<std::shared_ptr<STValidation>> const& valSet)
{
JLOG(j_.trace()) << "voting at " << closeTime.time_since_epoch().count()
<< ": " << enabledAmendments.size() << ", "
<< majorityAmendments.size() << ", " << valSet.size();
std::lock_guard lock(mutex_);
// Keep a record of the votes we received.
previousTrustedVotes_.recordVotes(rules, valSet, closeTime, j_, lock);
// Tally the most recent votes.
auto vote =
std::make_unique<AmendmentSet>(rules, previousTrustedVotes_, lock);
JLOG(j_.debug()) << "Counted votes from " << vote->trustedValidations()
<< " valid trusted validations, threshold is: "
<< vote->threshold();
// Map of amendments to the action to be taken for each one. The action is
// the value of the flags in the pseudo-transaction
std::map<uint256, std::uint32_t> actions;
// process all amendments we know of
for (auto const& entry : amendmentMap_)
{
if (enabledAmendments.contains(entry.first))
{
JLOG(j_.trace()) << entry.first << ": amendment already enabled";
continue;
}
bool const hasValMajority = vote->passes(entry.first);
auto const majorityTime = [&]() -> std::optional<NetClock::time_point> {
auto const it = majorityAmendments.find(entry.first);
if (it != majorityAmendments.end())
return it->second;
return std::nullopt;
}();
bool const hasLedgerMajority = majorityTime.has_value();
auto const logStr = [&entry, &vote]() {
std::stringstream ss;
ss << entry.first << " (" << entry.second.name << ") has "
<< vote->votes(entry.first) << " votes";
return ss.str();
}();
if (hasValMajority && !hasLedgerMajority &&
entry.second.vote == AmendmentVote::up)
{
// Ledger says no majority, validators say yes, and voting yes
// locally
JLOG(j_.debug()) << logStr << ": amendment got majority";
actions[entry.first] = tfGotMajority;
}
else if (!hasValMajority && hasLedgerMajority)
{
// Ledger says majority, validators say no
JLOG(j_.debug()) << logStr << ": amendment lost majority";
actions[entry.first] = tfLostMajority;
}
else if (
hasLedgerMajority &&
((*majorityTime + majorityTime_) <= closeTime) &&
entry.second.vote == AmendmentVote::up)
{
// Ledger says majority held
JLOG(j_.debug()) << logStr << ": amendment majority held";
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
lastVote_ = std::move(vote);
return actions;
}
RCLConsensus::Adaptor::onAccept(
Result const& result,
RCLCxLedger const& prevLedger,
NetClock::duration const& closeResolution,
ConsensusCloseTimes const& rawCloseTimes,
ConsensusMode const& mode,
Json::Value&& consensusJson,
bool const validating)
{
app_.getJobQueue().addJob(
jtACCEPT,
"acceptLedger",
[=, this, cj = std::move(consensusJson)]() mutable {
// Note that no lock is held or acquired during this job.
// This is because generic Consensus guarantees that once a ledger
// is accepted, the consensus results and capture by reference state
// will not change until startRound is called (which happens via
// endConsensus).
RclConsensusLogger clog("onAccept", validating, j_);
this->doAccept(
result,
prevLedger,
closeResolution,
rawCloseTimes,
mode,
std::move(cj));
this->app_.getOPs().endConsensus(clog.ss());
});
}
TER
Change::applyAmendment()
{
// Extract the amendment hash
uint256 amendment(ctx_.tx.getFieldH256(sfAmendment));
// Get the amendments object from the ledger
auto const k = keylet::amendments();
SLE::pointer amendmentObject = view().peek(k);
if (!amendmentObject)
{
amendmentObject = std::make_shared<SLE>(k);
view().insert(amendmentObject);
}
STVector256 amendments = amendmentObject->getFieldV256(sfAmendments);
if (std::find(amendments.begin(), amendments.end(), amendment) !=
amendments.end())
return tefALREADY;
auto flags = ctx_.tx.getFlags();
bool const gotMajority = (flags & tfGotMajority) != 0;
bool const lostMajority = (flags & tfLostMajority) != 0;
if (gotMajority && lostMajority)
return temINVALID_FLAG;
STArray newMajorities(sfMajorities);
bool found = false;
// Check if already enabled
if (amendmentObject->isFieldPresent(sfMajorities))
{
STArray const& oldMajorities =
amendmentObject->getFieldArray(sfMajorities);
for (auto const& majority : oldMajorities)
{
if (majority.getFieldH256(sfAmendment) == amendment)
{
if (gotMajority)
return tefALREADY;
found = true;
}
else
{
// pass through
newMajorities.push_back(majority);
}
}
}
if (!found && lostMajority)
return tefALREADY;
if (gotMajority)
{
// This amendment now has a majority
newMajorities.push_back(STObject::makeInnerObject(sfMajority));
auto& entry = newMajorities.back();
entry[sfAmendment] = amendment;
entry[sfCloseTime] =
view().parentCloseTime().time_since_epoch().count();
if (!ctx_.app.getAmendmentTable().isSupported(amendment))
{
JLOG(j_.warn()) << "Unsupported amendment " << amendment
<< " received a majority.";
}
}
else if (!lostMajority)
{
// No flags, enable amendment
amendments.push_back(amendment);
amendmentObject->setFieldV256(sfAmendments, amendments);
ctx_.app.getAmendmentTable().enable(amendment);
if (!ctx_.app.getAmendmentTable().isSupported(amendment))
{
JLOG(j_.error()) << "Unsupported amendment " << amendment
<< " activated: server blocked.";
ctx_.app.getOPs().setAmendmentBlocked();
}
}
if (newMajorities.empty())
amendmentObject->makeFieldAbsent(sfMajorities);
else
amendmentObject->setFieldArray(sfMajorities, newMajorities);
// Update the object in the ledger
view().update(amendmentObject);
return tesSUCCESS;
}
void
AmendmentTableImpl::doValidatedLedger(
LedgerIndex ledgerSeq,
std::set<uint256> const& enabled,
majorityAmendments_t const& majority)
{
for (auto& e : enabled)
enable(e);
std::lock_guard lock(mutex_);
// Remember the ledger sequence of this update.
lastUpdateSeq_ = ledgerSeq;
// Since we have the whole list in `majority`, reset the time flag, even
// if it's currently set. If it's not set when the loop is done, then any
// prior unknown amendments have lost majority.
firstUnsupportedExpected_.reset();
for (auto const& [hash, time] : majority)
{
AmendmentState& s = add(hash, lock);
if (s.enabled)
continue;
if (!s.supported)
{
JLOG(j_.info()) << "Unsupported amendment " << hash
<< " reached majority at " << to_string(time);
if (!firstUnsupportedExpected_ || firstUnsupportedExpected_ > time)
firstUnsupportedExpected_ = time;
}
}
if (firstUnsupportedExpected_)
firstUnsupportedExpected_ = *firstUnsupportedExpected_ + majorityTime_;
}
if (app_.getAmendmentTable().hasUnsupportedEnabled())
{
JLOG(m_journal.error()) << "One or more unsupported amendments "
"activated: server blocked.";
app_.getOPs().setAmendmentBlocked();
}