How protocol upgrades work on XRPL — the amendment system, feature flags and the AmendmentTable.
What you'll learn
≈60 min · Intermediate · builds on AMM transactions & pathfinding
Watch this short video by XRPL Commons first, then dive into the details below.
A live, decentralized protocol can't just ship a breaking change, it has to vote one in. In this module you'll learn how amendments gate protocol changes behind feature flags, how the AmendmentTable tracks them, and how amendments are classified as supported, obsolete or retired. It's how XRPL evolves without forking.
In brief: the path from proposal, through voting, to activation, at a glance.
An amendment moves through six stages:
The 80% supermajority balances progress against stability: high enough that a large minority cannot be steamrolled, low enough that a few offline validators cannot freeze evolution, and the two-week stability window gives every operator time to upgrade.
Note. This is only the overview. The amendment lifecycle module walks every stage in detail (states,
tfGotMajority/tfLostMajority, the pseudo-transactions, and amendment-blocked mode), so this module stays focused on the architecture.
In brief: four principles, one central table.
The design rests on four principles: distributed consensus (nothing activates without the sustained 80% supermajority), a stability period (two weeks between majority and activation, so operators can upgrade), automatic activation (an EnableAmendment pseudo-transaction injected by the network itself, no human trigger), and network protection (a node that does not support an activated amendment blocks itself rather than apply rules it does not understand).
Key idea. An amendment is a feature flag with a vote attached. The protocol change ships in the code disabled, and only activates once the network agrees to switch it on.
The public interface lives in include/xrpl/ledger/AmendmentTable.h:
| Group | Methods | Purpose |
|---|---|---|
| Management | find, enable, veto, unVeto |
look up and steer individual amendments |
| Status | isEnabled, isSupported, hasUnsupportedEnabled, firstUnsupportedExpected |
what is active, what do we support, when does trouble arrive |
| Voting | doVoting, doValidation, getDesired |
the flag ledger machinery (below) |
| Sync and reporting | doValidatedLedger, getJson, trustChanged |
follow the ledger, feed the RPC |
The implementation (AmendmentTableImpl, in src/xrpld/app/misc/detail/AmendmentTable.cpp) keeps all of its state behind one mutex, because network, consensus, and RPC threads all query it:
hash_map<uint256, AmendmentState> amendmentMap_; // every known amendment
TrustedVotes previousTrustedVotes_; // validator votes, cached
std::unique_ptr<AmendmentSet> lastVote_; // last tally
bool unsupportedEnabled_; // danger flag
std::optional<NetClock::time_point> firstUnsupportedExpected_;
DatabaseCon& db_; // vote persistence
std::mutex mutable mutex_;
Each amendment is one small record:
struct AmendmentState {
AmendmentVote vote; // up, down, or obsolete
bool enabled; // active on the network?
bool supported; // does this node have the code?
std::string name;
};
The enabled / supported distinction is the load-bearing one: enabled is a network fact (the hash is in the ledger's sfAmendments), supported is a local fact (this binary has the code). Enabled-but-not-supported is exactly the combination that blocks a node.
Every known amendment is declared at compile time in include/xrpl/protocol/detail/features.macro:
XRPL_FEATURE(Subscriptions, Supported::yes, VoteBehavior::DefaultNo) // new feature
XRPL_FIX(fixAmendmentMajorityCalc, Supported::yes, VoteBehavior::DefaultYes) // bug fix
XRPL_FEATURE(Tickets, Supported::yes, VoteBehavior::Obsolete) // do not vote
XRPL_RETIRE_FEATURE(MultiSign) // permanent
| Category | Meaning | Marker |
|---|---|---|
| Supported | current features, can be voted and activated | Supported::yes |
| Obsolete | no longer relevant, kept in case they were ever enabled | VoteBehavior::Obsolete |
| Retired | active 2+ years, pre-amendment code deleted, always considered enabled | XRPL_RETIRE_FEATURE |
Registering a new amendment is one line in features.macro (the kNumFeatures count in Feature.h derives automatically), plus the actual feature logic gated behind rules.enabled(featureName), plus tests for both the enabled and disabled paths.
An amendment's identity is the SHA-512Half of its name: that hash appears in validations, in pseudo-transactions, and in the ledger's sfAmendments field.
Vote collection. Each validation carries the validator's amendment wish-list (built by doValidation(): supported, upvoted, not yet enabled). TrustedVotes caches these per validator with a 24-hour expiry (expiresAfter = 24h in AmendmentTable.cpp): a validator that goes silent for a day stops counting, which prevents vote flapping during short outages while ensuring stale votes never linger. The practical consequence: the default is "no", and validators must keep actively signalling "yes".
Tallying. Votes are only counted every flag ledger (every 256 ledgers, about 15 minutes), which keeps the cost low and the schedule predictable. AmendmentSet aggregates the cached votes and computes the threshold:
// AmendmentTable.cpp (kAmendmentMajorityCalcThreshold = ratio<80,100>)
threshold = max(1, (trustedValidations * 80) / 100); // floor of 80%
// passes(): STRICTLY more votes than the threshold...
return votes > threshold;
// ...except with a single trusted validator, where votes >= threshold
Concretely: 25 trusted validators give a threshold of 20, and majority needs 21 votes; 35 give 28, majority needs 29. "More than 80%" is meant literally.
Decisions. For each amendment that is not already enabled, doVoting() compares the vote tally with what the ledger currently records in sfMajorities:
| Situation | Action |
|---|---|
| passes now, no recorded majority | tfGotMajority pseudo-transaction |
| recorded majority, no longer passes | tfLostMajority pseudo-transaction |
| majority held for 2+ weeks | enable (flag-less EnableAmendment) |
| anything else | log, wait for the next flag ledger |
Two functions close the loop. Change::applyAmendment() (src/libxrpl/tx/transactors/system/Change.cpp) applies the pseudo-transactions: tfGotMajority adds the amendment and its close time to sfMajorities, tfLostMajority removes it, and the flag-less form moves the hash into sfAmendments, calls any special activation handler (some amendments migrate data, like fixTrustLinesToSelf), and notifies the AmendmentTable. Duplicate activation returns tefALREADY.
doValidatedLedger() runs after every validated ledger and syncs the table the other way: it enables what the ledger says is enabled, tracks current majorities and their expected activation times, and, if an unsupported amendment is heading for activation, sets firstUnsupportedExpected_, your early warning.
If an unsupported amendment does activate, the node calls setAmendmentBlocked(): it logs a critical error and stops processing ledgers. It can still answer queries, but it no longer validates or proposes; the fix is to upgrade to a release that supports the amendment.
Admin vote preferences survive restarts in the FeatureVotes SQL table (persistVote() writes on every change; readAmendments() replays the most recent vote per amendment at startup, via a window function).
The feature RPC command is the operator's window:
{ "command": "feature", "feature": "AmendmentNameOrHash" }
returns per amendment: name, supported, enabled, vetoed, plus the live count / threshold / validations during voting. With admin rights, the same command sets or clears a veto ("vetoed": true|false), which is persisted and takes effect at the next flag ledger.
There is no explicit "no" vote: withholding support is the veto. Just over 20% of trusted validators not voting for an amendment blocks it, indefinitely, and the veto works even after a majority is reached, right up until the two-week clock completes. The system is deliberately conservative: proponents must build a broad coalition, and controversial changes stall rather than squeak through.
| Scenario | Behaviour |
|---|---|
| very few validators | threshold formula keeps a minimum of 1 vote; small networks stay explicit |
| network partition | neither side sustains 80% for 2 weeks; healing converges on the majority chain |
| emergency bug fix | same mechanism, just faster social coordination; there is no bypass |
| standalone mode | no consensus, so nothing activates by voting; enable manually via [amendments] in the config for testing |
[amendments]
# standalone testing only: force-enable by hash
B2A4DB846F0891BF2C76AB2F2ACC8F5B4EC64437135C6E56F3F859DE5FFD5856
Amendment voting shares its infrastructure with fee voting and Negative UNL voting: all three run on flag ledgers, all three act through pseudo-transactions, and none of them interferes with the others.
include/xrpl/ledger/AmendmentTable.h - interfacesrc/xrpld/app/misc/detail/AmendmentTable.cpp - implementation, TrustedVotes, AmendmentSet, 24h expiryinclude/xrpl/protocol/detail/features.macro - the registryinclude/xrpl/protocol/Feature.h / src/libxrpl/protocol/Feature.cpp - feature definitionssrc/libxrpl/tx/transactors/system/Change.cpp - applyAmendmentsrc/libxrpl/server/Wallet.cpp - voteAmendment / readAmendments persistencesrc/xrpld/rpc/handlers/server_info/Feature.cpp - the feature RPCThis module explained how XRPL upgrades itself without forking. An amendment is a protocol change that ships in the code disabled behind a feature flag and only activates once the network votes it in; the AmendmentTable tracks every known amendment and its state, and amendments are classified as supported, obsolete, or retired. Each is identified by a hash of its feature name.
To remember:
features.macro via XRPL_FEATURE(name, Supported::..., VoteBehavior::...)AmendmentTable tracks every known amendment's state; sfMajorities records current majorities on-ledgerctx.rules.enabled(featureX)feature RPC lists everything with counts and thresholdinclude/xrpl/protocol/Feature.h + src/xrpld/app/misc (AmendmentTable)Next up. You know the machinery; now follow one amendment through it. Next: the amendment lifecycle, from proposal to activation, step by step.
Resources
Assignments
0 of 2 complete