intermediate 60 min

Amendments — overview & architecture

How protocol upgrades work on XRPL — the amendment system, feature flags and the AmendmentTable.

Prerequisites

Complete these before starting this module:

What you'll learn

  • Explain how amendments gate protocol changes.
  • Map `features.macro`, the AmendmentTable and `sfMajorities`.
  • Understand supported / obsolete / retired amendments.
Complete this module by self-assessment and a quiz. Jump to assessment

Introduction

≈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.

Amendment Lifecycle

In brief: the path from proposal, through voting, to activation, at a glance.

An amendment moves through six stages:

  1. Community proposal: anyone can propose; the change is specified (usually as an XLS) and discussed publicly.
  2. Development and testing: the feature is implemented behind a flag, reviewed, and tested.
  3. Network deployment: a rippled release ships the (disabled) code; operators upgrade at their own pace.
  4. Voting period: validators signal support in their validations, ledger by ledger.
  5. Threshold and activation: once more than 80% of trusted validators support it continuously for two weeks, an EnableAmendment pseudo-transaction switches it on.
  6. Network integration: the new rules apply; nodes that do not understand the amendment become amendment-blocked.

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.

Amendment System Overview

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).

The amendment system: features.macro registers every known amendment into the AmendmentTable, doValidatedLedger syncs the table from each validated ledger, the FeatureVotes database restores admin votes at startup, and the feature RPC exposes status and veto.

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.

AmendmentTable: the Central Manager

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.

The Registry: features.macro

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.

The Voting Machinery

From validator votes to activation: validations carry amendment lists into the TrustedVotes cache with its 24-hour expiry, AmendmentSet tallies them at each flag ledger against the 80 percent threshold, producing tfGotMajority, tfLostMajority, or, after two weeks of held majority, the enable action applied by Change::applyAmendment.

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

Activation

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.

Persistence and the RPC Interface

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.

Veto Power and Its Consequences

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.

Edge Cases

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.

References to Source Code


Summary

This 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:

  • An amendment = a protocol change shipped disabled behind a feature flag; its id = SHA-512Half of the feature name
  • Registered in features.macro via XRPL_FEATURE(name, Supported::..., VoteBehavior::...)
  • The AmendmentTable tracks every known amendment's state; sfMajorities records current majorities on-ledger
  • Categories: supported, obsolete, retired
  • All gated behaviour branches on ctx.rules.enabled(featureX)
  • The feature RPC lists everything with counts and threshold
  • Code: include/xrpl/protocol/Feature.h + src/xrpld/app/misc (AmendmentTable)
  • Watch out: changing transaction processing WITHOUT an amendment gate is a consensus break, the cardinal sin of core development

Next up. You know the machinery; now follow one amendment through it. Next: the amendment lifecycle, from proposal to activation, step by step.

Assignments

0 of 2 complete

Unlocks

Finishing this module opens up:

XRPL Academy © 2026