The path of an amendment from introduction through majority to activation.
What you'll learn
≈45 min · Intermediate · builds on Amendments — overview & architecture
Let's follow one amendment from idea to activation. In this module you'll trace its states through the 80%-for-two-weeks rule, the tfGotMajority / tfLostMajority flags and the EnableAmendment pseudo-transaction, and learn what 'amendment-blocked' means for a node that falls behind. Governance as a state machine.
In brief: the phases an amendment moves through from idea to enabled.
The lifecycle of an amendment can be represented by the following phases:
Each phase has specific criteria that must be satisfied before moving to the next. Let's now explore each phase in detail following the Subscriptions example.
Note. "Subscriptions" is used across these amendment modules purely as a hypothetical running example, to make the process concrete. It is not a real, shipped XRPL amendment; its transactors and fields are illustrative, not code you will find in rippled.
The first phase begins with the development of code that implements the new functionality. For Subscriptions (XLS-0078), this includes:
New data structures:
ltSUBSCRIPTION (0x0055) to store recurring payment authorizationssfAccount: Subscription owner accountsfDestination: Subscription receiver accountsfDestinationTag (optional): Tag to categorize paymentssfAmount: Maximum amount that can be withdrawn per periodsfFrequency: Interval in seconds between periodssfNextPaymentTime: Timestamp of next possible claimsfExpiration (optional): Timestamp of last possible periodNew transactions:
ttSUBSCRIPTION_SET: Create or modify a subscription (creation requires Destination, Amount, Frequency)ttSUBSCRIPTION_CLAIM: Claim a payment within available balance limitsttSUBSCRIPTION_CANCEL: Cancel an existing subscription (owner or destination can initiate)Validation logic:
preflight, preclaim, and doApplyReference: XLS-0078 Subscriptions
Once the code is ready, the amendment is registered in the system via features.macro:
XRPL_FEATURE(Subscriptions, Supported::yes, VoteBehavior::DefaultNo)
Parameters:
Subscriptions: Amendment nameSupported::yes: The code is included and supportedVoteBehavior::DefaultNo: By default, nodes do not vote for (requires explicit activation)The amendment hash is calculated automatically:
Name: "Subscriptions"
Method: SHA-512Half(name)
Hash: 7B73B9E8D8E6E8E8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8
This hash is the unique identifier that will be used in all network communications and data structures.
The code containing the new amendment is distributed via:
At this stage, the amendment exists in the code but is not yet enabled. Nodes that update to this version recognize the amendment but do not apply it yet.
In brief: validators include their support in their validations each ledger.
Validators must explicitly activate their vote for the amendment. This is typically done via the RPC command feature:
# Vote for the amendment
xrpld feature 7B73B9E8D8E6E8E8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8 accept
Or by modifying the configuration file:
[amendments]
7B73B9E8D8E6E8E8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8
Once a validator has activated their vote, this vote is included in each validation they publish. The validation contains:
// Simplified structure of a validation
struct STValidation {
uint256 ledgerHash; // Hash of validated ledger
uint32 ledgerSeq; // Sequence number
NetClock::time_point signTime; // Signing moment
PublicKey publicKey; // Validator's public key
// List of supported amendments
std::vector<uint256> amendments;
// Validation signature
Buffer signature;
};
The amendments field contains the list of all amendments the validator wishes to see enabled, including Subscriptions.
Each node on the network collects validations received from trusted validators and extracts their amendment votes. The system maintains:
TrustedVotes: Structure that aggregates votes over a time period
class TrustedVotes {
// Map: Amendment hash -> Number of votes
hash_map<uint256, int> votes_;
// Total number of trusted validators
int trustedValidations_;
// Expire votes that are too old
void expire(NetClock::time_point now);
// Retrieve aggregated votes
std::pair<int, hash_map<uint256, int>>
getVotes(Rules const& rules);
};
Freshness period: Votes have a limited lifetime (typically 5 minutes). A vote expires if it is not reaffirmed by a new validation within this time. This ensures that vote counts reflect the current state of the network.
During the voting phase, the status of Subscriptions can be queried via the RPC command feature:
{
"7B73B9E8D8E6E8E8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8": {
"count": 15, // 15 validators vote for
"enabled": false, // Not yet enabled
"name": "Subscriptions",
"supported": true, // This node supports the amendment
"threshold": 20, // threshold = floor(25 * 0.8) = 20
"validations": 25, // 25 trusted validators in total
}
}
Interpretation:
Note on threshold: The threshold field displays floor(validations * 0.8) = 20, but the comparison in the code uses votes > threshold, so 21 votes minimum are needed to reach majority.
In brief: crossing 80% support starts a two-week stability countdown.
When enough validators activate their vote, the amendment exceeds the required threshold. Let's assume 6 additional validators activate their vote:
{
"7B73B9E8D8E6E8E8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8": {
"count": 21, // 21 validators vote for (84%)
"enabled": false, // Still not enabled
"majority": 805385181, // ← NEW: XRPL timestamp when majority reached
"name": "Subscriptions",
"supported": true,
"threshold": 20, // floor(25 * 0.8) = 20
"validations": 25,
}
}
The majority field: This is the critical moment. This field contains the XRPL Time (seconds since 2000-01-01 00:00:00 UTC) when the 80% threshold was first exceeded (count > threshold). In our example: 805385181 corresponds approximately to a date in 2025.
Support calculation: 21/25 = 84% > 80%
When the threshold is reached, an EnableAmendment pseudo-transaction with the tfGotMajority flag is injected into the ledger:
{
"TransactionType": "EnableAmendment",
"Account": "rrrrrrrrrrrrrrrrrrrrrhoLvTp", // Special network account
"Amendment": "7B73B9E8D8E6E8E8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8",
"Flags": 65536, // tfGotMajority = 0x00010000
"LedgerSequence": 85432190,
"TransactionIndex": 0
}
Impact on ledger: This pseudo-transaction adds an entry to the sfMajorities field of the global ledger object:
{
"Majorities": [
{
"Amendment": "7B73B9E8D8E6E8E8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8",
"CloseTime": 805385181
}
]
}
From this moment, the amendment enters a mandatory two-week stability period (more precisely, two flag ledgers). Flag ledgers are special ledgers that occur approximately every 15 minutes (256 ledgers).
Duration calculation:
Reason for the period: This window allows:
If during the stability period support falls below 80%, the amendment loses its majority. An EnableAmendment pseudo-transaction with the tfLostMajority flag is injected:
{
"TransactionType": "EnableAmendment",
"Account": "rrrrrrrrrrrrrrrrrrrrrhoLvTp",
"Amendment": "7B73B9E8D8E6E8E8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8",
"Flags": 131072, // tfLostMajority = 0x00020000
"LedgerSequence": 85435000
}
The majority field disappears from the amendment status, and the countdown restarts from zero if the threshold is reached again.
Key idea. The 80%-for-two-weeks rule is deliberate friction: it gives operators time to upgrade before a change goes live, so nobody is caught out.
In brief: an EnableAmendment pseudo-transaction turns the amendment on.
After two weeks of uninterrupted majority, the amendment is ready to be activated. The network automatically calculates when this moment arrives based on the CloseTime recorded in sfMajorities.
At the next flag ledger after the period expires, an EnableAmendment pseudo-transaction without flags is injected:
{
"TransactionType": "EnableAmendment",
"Account": "rrrrrrrrrrrrrrrrrrrrrhoLvTp",
"Amendment": "7B73B9E8D8E6E8E8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8",
"Flags": 0, // No flag = activation
"LedgerSequence": 85987654,
"TransactionIndex": 0,
"hash": "491378DA1BAAE870A6C247B3E42193E80E507A50858A4E51217685E2765E6CE8"
}
Processing by Change::applyAmendment: This pseudo-transaction is processed by the Change::applyAmendment() function in src/libxrpl/tx/transactors/system/Change.cpp:
TER Change::applyAmendment() {
// Extract the amendment hash from the transaction
uint256 amendment(ctx_.tx.getFieldH256(sfAmendment));
// Retrieve the key of the ledger's global amendments object
auto const k = keylet::amendments();
// Read or create the amendments object
SLE::pointer amendmentObject = view().peek(k);
if (!amendmentObject)
{
// If the object doesn't exist yet, create it (rare)
amendmentObject = std::make_shared<SLE>(k);
view().insert(amendmentObject);
}
// Read the list of already enabled amendments
STVector256 amendments = amendmentObject->getFieldV256(sfAmendments);
// Check if this amendment is already enabled
if (std::find(amendments.begin(), amendments.end(), amendment) !=
amendments.end())
return tefALREADY; // Already enabled, do nothing
// Extract flags from the transaction
auto flags = ctx_.tx.getFlags();
bool const gotMajority = (flags & tfGotMajority) != 0;
bool const lostMajority = (flags & tfLostMajority) != 0;
// Check flag consistency (can't have both)
if (gotMajority && lostMajority)
return temINVALID_FLAG;
// Prepare a new majorities array
STArray newMajorities(sfMajorities);
// Iterate through existing majorities to update
bool found = false;
if (amendmentObject->isFieldPresent(sfMajorities))
{
STArray const& oldMajorities =
amendmentObject->getFieldArray(sfMajorities);
for (auto const& majority : oldMajorities)
{
if (majority.getFieldH256(sfAmendment) == amendment)
{
// This amendment is already in majorities
if (gotMajority)
return tefALREADY; // Already recorded as majority
found = true; // Found, don't copy it (lost majority)
}
else
{
// Copy other majorities
newMajorities.push_back(majority);
}
}
}
// If lostMajority but not in list, error
if (!found && lostMajority)
return tefALREADY;
if (gotMajority)
{
// Amendment just reached majority
// Add a new entry to sfMajorities
newMajorities.push_back(STObject::makeInnerObject(sfMajority));
auto& entry = newMajorities.back();
entry[sfAmendment] = amendment;
entry[sfCloseTime] =
view().parentCloseTime().time_since_epoch().count();
// Warn if amendment is not supported by this node
if (!ctx_.app.getAmendmentTable().isSupported(amendment))
{
JLOG(j_.warn()) << "Unsupported amendment " << amendment
<< " received a majority.";
}
}
else if (!lostMajority)
{
// No flags = ACTIVATION of the amendment
// Add to list of enabled amendments
amendments.push_back(amendment);
amendmentObject->setFieldV256(sfAmendments, amendments);
// Enable locally in AmendmentTable
ctx_.app.getAmendmentTable().enable(amendment);
// Check if supported - block node if not supported
if (!ctx_.app.getAmendmentTable().isSupported(amendment))
{
JLOG(j_.error()) << "Unsupported amendment " << amendment
<< " activated: server blocked.";
ctx_.app.getOPs().setAmendmentBlocked();
}
}
// Update the sfMajorities field
if (newMajorities.empty())
amendmentObject->makeFieldAbsent(sfMajorities); // Remove if empty
else
amendmentObject->setFieldArray(sfMajorities, newMajorities);
// Save changes to ledger
view().update(amendmentObject);
return tesSUCCESS;
}
The amendment is added to the sfAmendments field of the global ledger object:
{
"Amendments": [
"...", // Other already enabled amendments
"7B73B9E8D8E6E8E8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8"
]
}
The corresponding entry is removed from the sfMajorities field since the amendment is no longer pending.
As soon as the amendment is added to the sfAmendments field, all new transactions and all new ledgers must apply the Subscriptions rules.
For Subscriptions (XLS-0078), this means:
ttSUBSCRIPTION_SET transactions are now valid and can be submitted to create/modify subscriptionsttSUBSCRIPTION_CLAIM transactions can claim payments within available balance limitsttSUBSCRIPTION_CANCEL transactions can cancel existing subscriptionsltSUBSCRIPTION objects (type 0x0055)The state of amendments is encapsulated in the Rules object which is constructed for each ledger:
class Rules {
private:
std::set<uint256> enabledAmendments_;
public:
bool enabled(uint256 const& amendment) const {
return enabledAmendments_.count(amendment) > 0;
}
};
Transactors and ledger logic consult this object to know which rules to apply:
// In SubscriptionSet::preflight()
NotTEC SubscriptionSet::preflight(PreflightContext const& ctx) {
if (!ctx.rules.enabled(featureSubscriptions))
return temDISABLED;
// ... rest of validation
}
The amendment status after activation simplifies:
{
"7B73B9E8D8E6E8E8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8": {
"enabled": true, // ← Amendment active
"name": "Subscriptions",
"supported": true
}
}
The fields count, threshold, validations, majority disappear because they are no longer relevant once the amendment is enabled.
Here is a realistic timeline for Subscriptions on mainnet:
2025-03-01: Release of rippled 2.3.0 including Subscriptions (XLS-0078)
2025-03-15: First validators activate their vote
2025-04-01: Growing momentum
2025-04-20 14:32:15 UTC: Threshold exceeded
majority field appears with timestamp 806021535tfGotMajority pseudo-transaction injected at ledger 862457892025-04-20 → 2025-05-04: Stability period
2025-05-04 14:32:15 UTC: Activation
EnableAmendment pseudo-transaction (without flag) injected at ledger 86652345sfAmendments field2025-05-04 14:32:18 UTC: Enablement
ttSUBSCRIPTION_SET transactions processedAfter each validated ledger, the AmendmentTableImpl::doValidatedLedger() function synchronizes the local state of amendments with the ledger state:
void AmendmentTableImpl::doValidatedLedger(
std::shared_ptr<ReadView const> const& ledger)
{
std::lock_guard lock(mutex_);
// Read enabled amendments from ledger
auto const amendments = ledger->rules().prevalidate();
// Enable locally all amendments from ledger
for (auto const& amendment : amendments) {
auto it = amendmentMap_.find(amendment);
if (it != amendmentMap_.end()) {
it->second.enabled = true;
}
}
// Update majority status
updateMajorities(ledger);
// Check unsupported amendments
checkUnsupported(ledger);
}
This synchronization ensures that the node's internal state always reflects the validated ledger state.
If an amendment is enabled but the local node does not support it, the node enters "amendment blocked" mode:
if (!isSupported(amendment)) {
JLOG(j_.error())
<< "Unsupported amendment " << amendment << " activated!";
app_.getOPs().setAmendmentBlocked();
// Node ceases to participate in consensus
}
This protects network integrity by preventing an outdated node from applying incorrect rules.
If the list of trusted validators (UNL) changes during the voting or majority phase, the threshold calculation adapts dynamically:
There is no mechanism to disable an amendment once enabled. If a critical problem is discovered after activation:
This is why the two-week stability period is crucial: it offers a last opportunity to detect problems before irreversible activation.
This module followed one amendment from proposal to activation. You traced its states through the rule that 80% of trusted validators must support it continuously for two weeks, the tfGotMajority / tfLostMajority flags, and the EnableAmendment pseudo-transaction that finally turns it on. You also learned what amendment-blocked means for a node that falls behind: it stops processing to avoid diverging from the network.
To remember:
tfGotMajority marks the threshold crossing (timer starts); tfLostMajority marks a drop (timer resets)EnableAmendment finally switches it onfeature <hash> shows count vs threshold and the majority timestampNext up. Activation is not just protocol, it is people. Next: voting and network coordination, how the ecosystem actually rolls a change out.
Resources
Assignments
0 of 2 complete