intermediate 45 min

The amendment lifecycle

The path of an amendment from introduction through majority to activation.

Prerequisites

Complete these before starting this module:

What you'll learn

  • Trace an amendment's states and the 80% / two-week rule.
  • Understand `tfGotMajority` / `tfLostMajority` and the Change pseudo-transaction.
  • Explain amendment-blocked mode.
Complete this module by self-assessment and a quiz. Jump to assessment

Introduction

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


Lifecycle Overview

In brief: the phases an amendment moves through from idea to enabled.

The lifecycle of an amendment can be represented by the following phases:

The five phases of the amendment lifecycle: proposal, validator voting, a sustained 80 percent majority for at least two weeks, activation through an EnableAmendment pseudo-transaction, and enablement of the new rules.

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.


Phase 1: Proposal

Code Development

The first phase begins with the development of code that implements the new functionality. For Subscriptions (XLS-0078), this includes:

New data structures:

  • Ledger entry type ltSUBSCRIPTION (0x0055) to store recurring payment authorizations
  • New fields:
  • sfAccount: Subscription owner account
  • sfDestination: Subscription receiver account
  • sfDestinationTag (optional): Tag to categorize payments
  • sfAmount: Maximum amount that can be withdrawn per period
  • sfFrequency: Interval in seconds between periods
  • sfNextPaymentTime: Timestamp of next possible claim
  • sfExpiration (optional): Timestamp of last possible period

New transactions:

  • ttSUBSCRIPTION_SET: Create or modify a subscription (creation requires Destination, Amount, Frequency)
  • ttSUBSCRIPTION_CLAIM: Claim a payment within available balance limits
  • ttSUBSCRIPTION_CANCEL: Cancel an existing subscription (owner or destination can initiate)

Validation logic:

  • Checks in preflight, preclaim, and doApply
  • Management of amounts per period (≤ Amount)
  • Support for XRP, IOUs (with trustlines and transfer rates), and MPTs (Multi-Purpose Tokens)
  • Arrears management: if a period passes without claim, the opportunity is lost and NextPaymentTime advances by one Frequency interval
  • Single claim per period to avoid spam
  • Respect freeze states (IOU) and lock (MPT)
  • Auto-creation of trustlines and MPToken holdings if authorizations and reserves are sufficient

Reference: XLS-0078 Subscriptions

Amendment Registration

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 name
  • Supported::Yes: The code is included and supported
  • VoteBehavior::DefaultNo: By default, nodes do not vote for (requires explicit activation)

Hash Calculation

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.

Code Distribution

The code containing the new amendment is distributed via:

  • A new version of rippled (e.g., 2.3.0)
  • Publication on GitHub with detailed release notes
  • Announcements on community communication channels
  • Documentation on XRPL.org and link to XLS-0078

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.


Phase 2: Vote

In brief: validators include their support in the validation issued just before each flag ledger.

Vote Activation by Validators

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

Inclusion in Validations

Once a validator has activated their vote, the vote is not attached to every validation. It is attached only to the validation of a voting ledger — the ledger immediately before a flag ledger. In RCLConsensus.cpp, the validation-building code checks ledger.ledger->isVotingLedger() before setting sfAmendments; Ledger::isVotingLedger() returns true when the next sequence is a multiple of kFlagLedgerInterval (256), i.e. for ledgers with seq ≡ 255 mod 256. So amendment votes travel once per 256-ledger cycle (roughly every 15 minutes), not each ledger. The validation contains:

The amendments field contains the list of all amendments the validator wishes to see enabled, including Subscriptions.

Vote Collection

Each node on the network collects validations received from trusted validators and extracts their amendment votes. The system maintains:

TrustedVotes: Structure that records the most recent votes seen from each trusted validator

Note that the record is per validator, not a simple per-amendment counter: getVotes() produces the amendment → count aggregation on demand from the per-validator records.

Vote retention: A recorded vote stays live for 24 hours (kExpiresAfter = 24h in AmendmentTable.cpp). The purpose of retaining the last vote from each trusted validator is to avoid "flapping": if a validator loses synchronization near a flag ledger, its validations (and therefore its votes) may be missing for that round, which would otherwise make an amendment appear to repeatedly gain and lose support. By holding on to the last vote seen, the node assumes an offline validator did not change its position. Only after 24 hours with no validation from a validator does the node clear that validator's recorded upVotes (there is no separate expire() call — the cleanup happens inside recordVotes()).

Status Monitoring

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:

  • 15 out of 25 validators vote for Subscriptions (60%)
  • The 80% threshold is not yet exceeded (need more than 20 votes, so minimum 21)
  • The amendment is supported but not enabled

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.

Note on connection role: The count, validations, and threshold fields (and vetoed) are only returned on admin connections — in AmendmentTableImpl::injectJson they are gated on isAdmin. Querying a public server returns only name, enabled, and supported, plus majority when present (the majority field comes from the validated ledger's Majorities and is not admin-gated).


Phase 3: Majority

In brief: crossing 80% support starts a two-week stability countdown.

Exceeding the 80% Threshold

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 recorded
    "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) recorded as sfCloseTime when the tfGotMajority pseudo-transaction was applied. In Change::applyAmendment, the value written is view().parentCloseTime() — and since the pseudo-transaction lands in the ledger right after the flag ledger, this is the flag ledger's close time. Because votes are only tallied at flag-ledger boundaries, this is the moment the majority was first observed. In our example: 805385181 corresponds approximately to a date in 2025.

Support calculation: 21/25 = 84% > 80%

tfGotMajority Pseudo-transaction

Vote tallying and pseudo-transaction injection happen once per flag-ledger cycle: in RCLConsensus.cpp, when the previous ledger was a flag ledger (prevLedger->isFlagLedger()), AmendmentTable::doVoting runs and injects the amendment pseudo-transactions into the consensus transaction set. They therefore land in the ledger after the flag ledger (seq ≡ 1 mod 256) — the flag ledger itself contains no amendment pseudo-transactions. At the first such voting round where the tally exceeds the threshold, an EnableAmendment pseudo-transaction with the tfGotMajority flag is injected:

{
  "TransactionType": "EnableAmendment",
  "Account": "rrrrrrrrrrrrrrrrrrrrrhoLvTp",  // Special network account
  "Amendment": "7B73B9E8D8E6E8E8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8",
  "Flags": 65536,  // tfGotMajority = 0x00010000
  "LedgerSequence": 85432065,
  "TransactionIndex": 0
}

Impact on ledger: This pseudo-transaction adds an entry to the sfMajorities field of the Amendments singleton ledger entry (ltAMENDMENTS, addressed by keylet::amendments()):

{
  "Majorities": [
    {
      "Amendment": "7B73B9E8D8E6E8E8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8",
      "CloseTime": 805385181
    }
  ]
}

Two-Week Stability Period

From this moment, the amendment enters a mandatory stability period. The hold is purely time-based: kDefaultAmendmentMajorityTime = weeks{2} in SystemParameters.h.

How the duration is enforced:

  • At each flag-ledger voting round, AmendmentTableImpl::doVoting compares the CloseTime recorded in sfMajorities plus the majority time against the ledger close time: the enable action fires when hasLedgerMajority && ((*majorityTime + majorityTime_) <= closeTime)
  • The flag-ledger cadence (kFlagLedgerInterval = 256 ledgers, roughly every 15 minutes) only sets where the check happens, not the duration
  • The duration is configurable per network via [amendment_majority_time] in the config file (minimum 15 minutes); on mainnet it is the default 2 weeks

Reason for the period: This window allows:

  • Node operators to prepare and update their systems
  • The community to detect any potential problems
  • Exchanges and wallets to adapt their integrations to support subscriptions
  • A last opportunity for validators to withdraw their vote if necessary

Loss of Majority (Edge Case)

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": 85434881
}

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.


Phase 4: Activation

In brief: an EnableAmendment pseudo-transaction turns the amendment on.

Expiration of Stability Period

After two weeks of uninterrupted majority, the amendment is ready to be activated. The network detects this at the flag-ledger voting rounds: the amendment is enabled at the first round whose close time satisfies CloseTime (from sfMajorities) + amendmentMajorityTime ≤ close time.

EnableAmendment Pseudo-transaction

At the first flag-ledger voting round after the period expires, an EnableAmendment pseudo-transaction without flags is injected into the ledger following the flag ledger (seq ≡ 1 mod 256):

{
  "TransactionType": "EnableAmendment",
  "Account": "rrrrrrrrrrrrrrrrrrrrrhoLvTp",
  "Amendment": "7B73B9E8D8E6E8E8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8",
  "Flags": 0,  // No flag = activation
  "LedgerSequence": 85987585,
  "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:

Ledger Update

The amendment is added to the sfAmendments field of the Amendments singleton ledger entry:

{
  "Amendments": [
    "...",  // Other already enabled amendments
    "7B73B9E8D8E6E8E8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8"
  ]
}

The corresponding entry is removed from the sfMajorities field since the amendment is no longer pending.


Phase 5: Enablement

Application of New Rules

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 subscriptions
  • ttSUBSCRIPTION_CLAIM transactions can claim payments within available balance limits
  • ttSUBSCRIPTION_CANCEL transactions can cancel existing subscriptions
  • The ledger can contain ltSUBSCRIPTION objects (type 0x0055)
  • Logic for managing periods, balances, and arrears is activated
  • Support for XRP, IOUs with trustlines, and MPTs with their specific rules

Rules Object Construction

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
}

Final Status

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.


Complete Timeline: Concrete Example

Here is a realistic timeline for Subscriptions on mainnet:

2025-03-01: Release of rippled 2.3.0 including Subscriptions (XLS-0078)

  • Code distributed, nodes begin to update
  • Publication of detailed documentation on XRPL.org

2025-03-15: First validators activate their vote

  • 5 out of 35 validators vote for (14%)

2025-04-01: Growing momentum

  • 20 out of 35 validators vote for (57%)
  • Exchanges and wallets begin to prepare their integrations

2025-04-20 14:32:15 UTC: Threshold exceeded

  • 29 out of 35 validators vote for (82.9% > 80%)
  • majority field appears with timestamp 806021535
  • tfGotMajority pseudo-transaction injected at ledger 86245633 (the ledger after the flag ledger)

2025-04-20 → 2025-05-04: Stability period

  • 2 weeks mandatory waiting
  • Support remains stable above 80%
  • Final tests on staging environments

2025-05-04 14:41:02 UTC: Activation

  • The enable fires at the first flag-ledger voting round at or after the two-week mark, so up to one flag-ledger cycle (~15 minutes) later than the exact anniversary
  • EnableAmendment pseudo-transaction (without flag) injected at ledger 86652161
  • Subscriptions added to sfAmendments field

2025-05-04 14:41:05 UTC: Enablement

  • First ledger with Subscriptions active
  • First ttSUBSCRIPTION_SET transactions processed
  • Users can now create subscriptions for recurring payments

State Transition Management

doValidatedLedger: Local Synchronization

After each validated ledger, the amendment table synchronizes its local state with the ledger state. The interface (include/xrpl/ledger/AmendmentTable.h) provides a wrapper that takes the validated ledger, skips the work unless a flag-ledger boundary was crossed (needValidatedLedger()), and extracts the ledger's data with the free functions getEnabledAmendments() and getMajorityAmendments():

/** Called when a new fully-validated ledger is accepted. */
void
doValidatedLedger(std::shared_ptr<ReadView const> const& lastValidatedLedger)
{
    if (needValidatedLedger(lastValidatedLedger->seq()))
    {
        doValidatedLedger(
            lastValidatedLedger->seq(),
            getEnabledAmendments(*lastValidatedLedger),
            getMajorityAmendments(*lastValidatedLedger));
    }
}

The implementation (src/xrpld/app/misc/detail/AmendmentTable.cpp) enables locally everything the ledger says is enabled, then scans the pending majorities to compute firstUnsupportedExpected_ — the earliest time an amendment this node does not support could become enabled, which drives the "amendment warned" state:

This synchronization ensures that the node's internal state always reflects the validated ledger state.

Detection of Unsupported Amendments

If an amendment is enabled but the local node does not support it, the node enters "amendment blocked" mode. This happens in two places. First, when the pseudo-transaction is applied, Change::applyAmendment blocks the node directly (see the listing above):

if (!ctx_.registry.get().getAmendmentTable().isSupported(amendment))
{
    JLOG(j_.error()) << "Unsupported amendment " << amendment
                     << " activated: server blocked.";
    ctx_.registry.get().getOPs().setAmendmentBlocked();
}

Second, LedgerMaster::setValidLedger re-checks after every call to doValidatedLedger — this catches a node that acquired an already-enabled ledger rather than applying the pseudo-transaction itself:

app_.getAmendmentTable().doValidatedLedger(l);
if (!app_.getOPs().isBlocked())
{
    if (app_.getAmendmentTable().hasUnsupportedEnabled())
    {
        JLOG(journal_.error()) << "One or more unsupported amendments "
                                  "activated: server blocked.";
        app_.getOPs().setAmendmentBlocked();
    }
    // ...
}

This protects network integrity by preventing an outdated node from applying incorrect rules.


Special Cases and Edge Cases

Changes in UNL List

If the list of trusted validators (UNL) changes during the voting or majority phase, the threshold calculation adapts dynamically:

  • If validators are added to the UNL, the absolute threshold increases
  • If validators are removed, the threshold decreases
  • An amendment can lose its majority if enough validators supporting the amendment leave the UNL

Rollback and Contingency

There is no mechanism to disable an amendment once enabled. If a critical problem is discovered after activation:

  1. A fix must be developed and deployed via a new amendment
  2. Or a security patch must be distributed urgently
  3. In extreme cases, out-of-band coordination may be necessary

This is why the two-week stability period is crucial: it offers a last opportunity to detect problems before irreversible activation.


Summary

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:

  • Phases: proposal, vote, majority, activation, enablement
  • The rule: MORE than 80% of trusted validators, held continuously for two weeks (kDefaultAmendmentMajorityTime)
  • tfGotMajority marks the threshold crossing (timer starts); tfLostMajority marks a drop (timer resets)
  • EnableAmendment pseudo-transactions (processed by the Change transactor) record it on-ledger; the flagless EnableAmendment finally switches it on
  • After enablement, nodes that do not support the amendment become amendment-blocked and stop processing
  • Votes travel in the validation just before each flag ledger (once per 256-ledger cycle) and recorded votes are retained for 24 hours
  • Track it live: feature <hash> shows count vs threshold and the majority timestamp (count/threshold/validations require an admin connection)
  • Watch out: losing majority resets the two-week clock entirely; support must be continuous, not cumulative

Next up. Activation is not just protocol, it is people. Next: voting and network coordination, how the ecosystem actually rolls a change out.

Assignments

0 of 2 complete

XRPL Academy © 2026