# Amendment Lifecycle

[← Back to Building an Amendments I: Lifecycle and Impact on Core Protocol](/core-dev-bootcamp/module10.md)

***

### Introduction

The lifecycle of an amendment on XRPL is a carefully orchestrated process that ensures protocol changes are adopted in a coordinated and consensual manner across the decentralized network. This process involves several distinct phases, each with its own rules, timeouts, and transition mechanisms.

In this section, we will follow the complete journey of the **Subscriptions** amendment (XLS-0078) from its initial conception to its final activation on the ledger. This amendment introduces a subscription mechanism (recurring payments) on XRPL, allowing account owners to authorize automatic recurring payments with predefined parameters including amount, frequency, and destination - similar to bank direct debits.

Understanding this lifecycle is essential for anticipating when a protocol change will become effective, coordinating node updates, and managing risks associated with protocol transitions.

***

## Lifecycle Overview

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

```
┌─────────────┐
│  Proposal   │  Code developed, amendment added to registry
└──────┬──────┘
       │
       ▼
┌─────────────┐
│    Vote     │  Validators vote for/against the amendment
└──────┬──────┘
       │
       ▼
┌─────────────┐
│  Majority   │  More than 80% of validators vote for (2 weeks minimum)
└──────┬──────┘
       │
       ▼
┌─────────────┐
│ Activation  │  EnableAmendment pseudo-transaction injected
└──────┬──────┘
       │
       ▼
┌─────────────┐
│ Enablement  │  Amendment active, new rules applied
└─────────────┘
```

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.

***

## 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](https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0078-subscriptions)

### Amendment Registration

Once the code is ready, the amendment is registered in the system via `features.macro`:

```cpp
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

### Vote Activation by Validators

Validators must explicitly activate their vote for the amendment. This is typically done via the RPC command `feature`:

```bash
# Vote for the amendment
rippled feature 7B73B9E8D8E6E8E8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8 accept
```

Or by modifying the configuration file:

```ini
[amendments]
7B73B9E8D8E6E8E8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8
```

### Inclusion in Validations

Once a validator has activated their vote, this vote is included in each validation they publish. The validation contains:

```cpp
// 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.

### Vote Collection

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

```cpp
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.

### Status Monitoring

During the voting phase, the status of Subscriptions can be queried via the RPC command `feature`:

```json
{
  "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.

***

## Phase 3: Majority

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

```json
{
  "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% ✓

### tfGotMajority Pseudo-transaction

When the threshold is reached, an `EnableAmendment` pseudo-transaction with the `tfGotMajority` flag is injected into the ledger:

```json
{
  "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:

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

### Two-Week Stability Period

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

* 1 flag ledger ≈ 15 minutes (256 ledgers × 3.5 seconds/ledger)
* 2 flag ledgers ≈ 30 minutes in terms of ledgers
* But in practice, the minimum period is **2 weeks in real time**

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

```json
{
  "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.

***

## Phase 4: Activation

### Expiration of Stability Period

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

### EnableAmendment Pseudo-transaction

At the next flag ledger after the period expires, an `EnableAmendment` pseudo-transaction **without flags** is injected:

```json
{
  "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/xrpld/app/tx/detail/Change.cpp`:

```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;
}
```

### Ledger Update

The amendment is added to the `sfAmendments` field of the global ledger object:

```json
{
  "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:

```cpp
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:

```cpp
// 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:

```json
{
  "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 86245789

**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:32:15 UTC**: Activation

* `EnableAmendment` pseudo-transaction (without flag) injected at ledger 86652345
* Subscriptions added to `sfAmendments` field

**2025-05-04 14:32:18 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 `AmendmentTableImpl::doValidatedLedger()` function synchronizes the local state of amendments with the ledger state:

```cpp
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.

### Detection of Unsupported Amendments

If an amendment is enabled but the local node does not support it, the node enters "amendment blocked" mode:

```cpp
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.

***

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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.xrpl-commons.org/core-dev-bootcamp/module10/amendment-lifecycle.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
