> For the complete documentation index, see [llms.txt](https://docs.xrpl-commons.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.xrpl-commons.org/core-dev-bootcamp/module10/voting-and-activation.md).

# Voting and Activation

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

***

### Introduction

The voting and activation mechanism for amendments is at the heart of XRPL's decentralized governance. This system allows the network to make collective decisions on protocol changes without a central authority, while ensuring a high level of consensus before any modification.

In this section, we will dive into the technical details of the voting process: how validators express their preferences, how these votes are collected and aggregated, how the threshold (over 80%) is calculated and verified, and finally how the network automatically activates amendments once consensus is reached.

We will continue to follow the example of **Subscriptions** (XLS-0078) to concretely illustrate each mechanism.

***

## Validator Voting Mechanism

### Vote Expression

A validator expresses their vote for an amendment in several ways:

**1. Static configuration**: Via the `rippled.cfg` configuration file:

```ini
[amendments]
# Vote for Subscriptions
7B73B9E8D8E6E8E8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8

# Vote against (or remove vote) by commenting out or removing the line
# 7B73B9E8D8E6E8E8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8
```

**2. Dynamic RPC command**: Via the admin interface:

```bash
# Enable voting
rippled feature 7B73B9E8D8E6E8E8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8 accept

# Disable voting
rippled feature 7B73B9E8D8E6E8E8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8 reject
```

**3. Default vote**: If no explicit configuration is provided, the default behavior defined in `features.macro` applies:

```cpp
// Automatically vote for
XRPL_FEATURE(Tickets, Supported::yes, VoteBehavior::DefaultYes)

// Don't vote by default (requires explicit activation)
XRPL_FEATURE(Subscriptions, Supported::yes, VoteBehavior::DefaultNo)

// Never vote (obsolete)
XRPL_FEATURE(OldFeature, Supported::yes, VoteBehavior::Obsolete)
```

### Inclusion in Validations

A validator's vote is communicated to the network via the **validation messages** they publish after validating each ledger.

**Structure of a validation message**:

```cpp
class STValidation {
    // Identification of validated ledger
    uint256 ledgerHash_;
    uint32 ledgerSeq_;
    NetClock::time_point signTime_;

    // List of supported/desired amendments
    std::optional<std::vector<uint256>> amendments_;

    // Validator's public key
    PublicKey publicKey_;

    // Cryptographic signature
    Buffer signature_;
};
```

**Generation of amendments list**: This list is generated by the `doValidation()` function of AmendmentTable:

```cpp
std::vector<uint256>
AmendmentTableImpl::doValidation(std::set<uint256> const& enabled) const
{
    // Get the list of amendments we support and do not
    // veto, but that are not already enabled
    std::vector<uint256> amendments;

    {
        std::lock_guard lock(mutex_);
        amendments.reserve(amendmentMap_.size());
        for (auto const& e : amendmentMap_)
        {
            // Include only if:
            // 1. Amendment is supported by this node
            // 2. Node votes "up" for the amendment
            // 3. Amendment is not already enabled
            if (e.second.supported && e.second.vote == AmendmentVote::up &&
                (enabled.count(e.first) == 0))
            {
                amendments.push_back(e.first);
                JLOG(j_.info()) << "Voting for amendment " << e.second.name;
            }
        }
    }

    if (!amendments.empty())
        // Sort to ensure deterministic order
        std::sort(amendments.begin(), amendments.end());

    return amendments;
}
```

**Network broadcast**: The signed validation message is broadcast via the overlay network to all connected peers. Each node that receives this validation extracts and stores the list of voted amendments.

***

## Vote Collection and Aggregation

### TrustedVotes Structure

The `TrustedVotes` class maintains a real-time count of amendment votes from trusted validators.

**Internal structure**:

```cpp
class TrustedVotes {
private:
    // Structure for an individual vote
    struct Vote {
        NetClock::time_point expiration;  // When this vote expires
        std::vector<uint256> amendments;  // Voted amendments
    };

    // Map: Validator's public key -> Current vote
    hash_map<PublicKey, Vote> votes_;

    // Timeout parameters
    NetClock::duration const validationFreshness_{5min};

public:
    // Add or update a vote
    void addVote(
        PublicKey const& key,
        std::vector<uint256> const& amendments,
        NetClock::time_point now)
    {
        votes_[key] = Vote{
            now + validationFreshness_,
            amendments
        };
    }

    // Expire old votes
    void expire(NetClock::time_point now) {
        for (auto it = votes_.begin(); it != votes_.end();) {
            if (it->second.expiration < now)
                it = votes_.erase(it);
            else
                ++it;
        }
    }

    // Aggregate votes
    std::pair<int, hash_map<uint256, int>>
    getVotes(Rules const& rules, std::lock_guard<std::mutex> const&)
    {
        hash_map<uint256, int> amendmentVotes;
        int trustedValidations = 0;

        for (auto const& [key, vote] : votes_) {
            trustedValidations++;
            for (auto const& amendment : vote.amendments) {
                amendmentVotes[amendment]++;
            }
        }

        return {trustedValidations, amendmentVotes};
    }
};
```

**Expiration mechanism**: Votes are considered "fresh" for 5 minutes. If a validator doesn't publish a new validation within this time, their vote is removed from the count. This ensures counts reflect the current state of active validators.

### AmendmentSet: Calculating the Threshold

The `AmendmentSet` class calculates which amendments have exceeded the required threshold for activation.

**Construction**:

```cpp
class AmendmentSet
{
private:
    // How many yes votes each amendment received
    hash_map<uint256, int> votes_;
    // number of trusted validations
    int trustedValidations_ = 0;
    // number of votes needed
    int threshold_ = 0;

public:
    AmendmentSet(
        Rules const& rules,
        TrustedVotes const& trustedVotes,
        std::lock_guard<std::mutex> const& lock)
    {
        // process validations for ledger before flag ledger.
        auto [trustedCount, newVotes] = trustedVotes.getVotes(rules, lock);

        trustedValidations_ = trustedCount;
        votes_.swap(newVotes);

        threshold_ = std::max(
            1L,
            static_cast<long>(
                (trustedValidations_ * amendmentMajorityCalcThreshold.num) /
                amendmentMajorityCalcThreshold.den));
    }

    bool
    passes(uint256 const& amendment) const
    {
        auto const& it = votes_.find(amendment);

        if (it == votes_.end())
            return false;

        // One validator is an exception, otherwise it is not possible
        // to gain majority.
        if (trustedValidations_ == 1)
            return it->second >= threshold_;

        return it->second > threshold_;
    }

    int
    votes(uint256 const& amendment) const
    {
        auto const& it = votes_.find(amendment);

        if (it == votes_.end())
            return 0;

        return it->second;
    }

    int
    trustedValidations() const
    {
        return trustedValidations_;
    }

    int
    threshold() const
    {
        return threshold_;
    }
};
```

**Threshold calculation (over 80%)**:

The displayed threshold is calculated as `floor(validations * 0.8)`, but the comparison uses `votes > threshold` (strictly greater), which ensures over 80% of validators vote for the amendment.

Examples with Subscriptions:

**25 trusted validators**:

```
threshold = floor(25 * 0.8) = floor(20.0) = 20
To pass: votes > 20, so minimum 21 votes required
Actual percentage: 21/25 = 84% > 80% ✓
```

**26 validators**:

```
threshold = floor(26 * 0.8) = floor(20.8) = 20
To pass: votes > 20, so minimum 21 votes required
Actual percentage: 21/26 = 80.77% > 80% ✓
```

**35 validators**:

```
threshold = floor(35 * 0.8) = floor(28.0) = 28
To pass: votes > 28, so minimum 29 votes required
Actual percentage: 29/35 = 82.86% > 80% ✓
```

**Special case - 1 validator**: With a single validator (test networks), the comparison becomes `votes >= threshold` instead of `votes > threshold`, allowing the single validator to activate the amendment.

***

## doVoting Function: Vote Orchestration

The `doVoting()` function is called at each consensus round (before each flag ledger) to determine what actions to take for amendments.

### Complete Algorithm

```cpp
std::map<uint256, std::uint32_t>
AmendmentTableImpl::doVoting(
    Rules const& rules,
    NetClock::time_point closeTime,
    std::set<uint256> const& enabledAmendments,
    majorityAmendments_t const& majorityAmendments,
    std::vector<std::shared_ptr<STValidation>> const& valSet)
{
    JLOG(j_.trace()) << "voting at " << closeTime.time_since_epoch().count()
                     << ": " << enabledAmendments.size() << ", "
                     << majorityAmendments.size() << ", " << valSet.size();

    std::lock_guard lock(mutex_);

    // Keep a record of the votes we received.
    previousTrustedVotes_.recordVotes(rules, valSet, closeTime, j_, lock);

    // Tally the most recent votes.
    auto vote =
        std::make_unique<AmendmentSet>(rules, previousTrustedVotes_, lock);
    JLOG(j_.debug()) << "Counted votes from " << vote->trustedValidations()
                     << " valid trusted validations, threshold is: "
                     << vote->threshold();

    // Map of amendments to the action to be taken for each one. The action is
    // the value of the flags in the pseudo-transaction
    std::map<uint256, std::uint32_t> actions;

    // process all amendments we know of
    for (auto const& entry : amendmentMap_)
    {
        if (enabledAmendments.contains(entry.first))
        {
            JLOG(j_.trace()) << entry.first << ": amendment already enabled";

            continue;
        }

        bool const hasValMajority = vote->passes(entry.first);

        auto const majorityTime = [&]() -> std::optional<NetClock::time_point> {
            auto const it = majorityAmendments.find(entry.first);
            if (it != majorityAmendments.end())
                return it->second;
            return std::nullopt;
        }();

        bool const hasLedgerMajority = majorityTime.has_value();

        auto const logStr = [&entry, &vote]() {
            std::stringstream ss;
            ss << entry.first << " (" << entry.second.name << ") has "
               << vote->votes(entry.first) << " votes";
            return ss.str();
        }();

        if (hasValMajority && !hasLedgerMajority &&
            entry.second.vote == AmendmentVote::up)
        {
            // Ledger says no majority, validators say yes, and voting yes
            // locally
            JLOG(j_.debug()) << logStr << ": amendment got majority";
            actions[entry.first] = tfGotMajority;
        }
        else if (!hasValMajority && hasLedgerMajority)
        {
            // Ledger says majority, validators say no
            JLOG(j_.debug()) << logStr << ": amendment lost majority";
            actions[entry.first] = tfLostMajority;
        }
        else if (
            hasLedgerMajority &&
            ((*majorityTime + majorityTime_) <= closeTime) &&
            entry.second.vote == AmendmentVote::up)
        {
            // Ledger says majority held
            JLOG(j_.debug()) << logStr << ": amendment majority held";
            actions[entry.first] = 0;
        }
        // Logging only below this point
        else if (hasValMajority && hasLedgerMajority)
        {
            JLOG(j_.debug())
                << logStr
                << ": amendment holding majority, waiting to be enabled";
        }
        else if (!hasValMajority)
        {
            JLOG(j_.debug()) << logStr << ": amendment does not have majority";
        }
    }

    // Stash for reporting
    lastVote_ = std::move(vote);
    return actions;
}
```

### Returned Action Codes

The returned map associates each amendment with an action code:

* **`tfGotMajority` (0x00010000)**: Amendment exceeded the 80% threshold
* **`tfLostMajority` (0x00020000)**: Amendment fell below the threshold
* **`0`**: Amendment must be activated (stability period elapsed)

These codes are used to generate appropriate pseudo-transactions.

***

## EnableAmendment Pseudo-transactions

### Pseudo-transaction Generation

Pseudo-transactions are generated by the consensus engine in `RCLConsensus::onAccept()` after a ledger has been accepted.

**Process**:

```cpp
RCLConsensus::Adaptor::onAccept(
    Result const& result,
    RCLCxLedger const& prevLedger,
    NetClock::duration const& closeResolution,
    ConsensusCloseTimes const& rawCloseTimes,
    ConsensusMode const& mode,
    Json::Value&& consensusJson,
    bool const validating)
{
    app_.getJobQueue().addJob(
        jtACCEPT,
        "acceptLedger",
        [=, this, cj = std::move(consensusJson)]() mutable {
            // Note that no lock is held or acquired during this job.
            // This is because generic Consensus guarantees that once a ledger
            // is accepted, the consensus results and capture by reference state
            // will not change until startRound is called (which happens via
            // endConsensus).
            RclConsensusLogger clog("onAccept", validating, j_);
            this->doAccept(
                result,
                prevLedger,
                closeResolution,
                rawCloseTimes,
                mode,
                std::move(cj));
            this->app_.getOPs().endConsensus(clog.ss());
        });
}
```

**Pseudo-transaction properties**:

* **Type**: `ttENABLE_AMENDMENT`
* **Account**: `rrrrrrrrrrrrrrrrrrrrrhoLvTp` (special account = AccountID(0))
* **No signature**: Pseudo-transactions are not signed
* **No fees**: No fee is deducted
* **TransactionIndex**: Generally 0 (first transaction in ledger)
* **Automatically injected**: Generated by the network, not submitted by a user

### Processing by Change::applyAmendment

`EnableAmendment` pseudo-transactions are processed by a specialized function in `Change.cpp`:

```cpp


TER
Change::applyAmendment()
{
    // Extract the amendment hash
    uint256 amendment(ctx_.tx.getFieldH256(sfAmendment));

    // Get the amendments object from the ledger
    auto const k = keylet::amendments();

    SLE::pointer amendmentObject = view().peek(k);

    if (!amendmentObject)
    {
        amendmentObject = std::make_shared<SLE>(k);
        view().insert(amendmentObject);
    }

    STVector256 amendments = amendmentObject->getFieldV256(sfAmendments);

    if (std::find(amendments.begin(), amendments.end(), amendment) !=
        amendments.end())
        return tefALREADY;

    auto flags = ctx_.tx.getFlags();

    bool const gotMajority = (flags & tfGotMajority) != 0;
    bool const lostMajority = (flags & tfLostMajority) != 0;

    if (gotMajority && lostMajority)
        return temINVALID_FLAG;

    STArray newMajorities(sfMajorities);

    bool found = false;
    // Check if already enabled
    if (amendmentObject->isFieldPresent(sfMajorities))
    {
        STArray const& oldMajorities =
            amendmentObject->getFieldArray(sfMajorities);
        for (auto const& majority : oldMajorities)
        {
            if (majority.getFieldH256(sfAmendment) == amendment)
            {
                if (gotMajority)
                    return tefALREADY;
                found = true;
            }
            else
            {
                // pass through
                newMajorities.push_back(majority);
            }
        }
    }

    if (!found && lostMajority)
        return tefALREADY;

    if (gotMajority)
    {
        // This amendment now has a majority
        newMajorities.push_back(STObject::makeInnerObject(sfMajority));
        auto& entry = newMajorities.back();
        entry[sfAmendment] = amendment;
        entry[sfCloseTime] =
            view().parentCloseTime().time_since_epoch().count();

        if (!ctx_.app.getAmendmentTable().isSupported(amendment))
        {
            JLOG(j_.warn()) << "Unsupported amendment " << amendment
                            << " received a majority.";
        }
    }
    else if (!lostMajority)
    {
        // No flags, enable amendment
        amendments.push_back(amendment);
        amendmentObject->setFieldV256(sfAmendments, amendments);

        ctx_.app.getAmendmentTable().enable(amendment);

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

    if (newMajorities.empty())
        amendmentObject->makeFieldAbsent(sfMajorities);
    else
        amendmentObject->setFieldArray(sfMajorities, newMajorities);

    // Update the object in the ledger
    view().update(amendmentObject);

    return tesSUCCESS;
}
```

### Modified Ledger Fields

**Ledger Amendments Object**:

```json
{
  "LedgerEntryType": "Amendments",
  "Amendments": [
    "42426C4D4F1009EE67080A9B7965B44656D7714D104A72F9B4369F97ABF044EE",
    // ... other enabled amendments ...
    "7B73B9E8D8E6E8E8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8"  // Subscriptions
  ],
  "Majorities": [
    {
      "Majority": {
        "Amendment": "AAAA...",  // Another amendment pending
        "CloseTime": 805500000
      }
    }
  ],
  "Flags": 0,
  "index": "7DB0788C020F02780A673DC74757F23823FA3014C1866E72CC4CD8B226CD6EF4"
}
```

**Modification during GotMajority**: Amendment is added to `sfMajorities` with its `CloseTime`.

**Modification during Activation**: Amendment is moved from `sfMajorities` to `sfAmendments`.

***

## Detailed Timeline: Subscriptions

Let's review the complete timeline with voting details:

### 2025-04-20 14:30:00 UTC (Ledger 86245750)

**State before**:

```json
{
  "count": 27,              // 27/35 validators = 77%
  "threshold": 28,          // threshold = floor(35 * 0.8) = 28
  "enabled": false,
  "majority": null          // Not yet in majority
}
```

### 2025-04-20 14:32:15 UTC (Ledger 86245789)

A 29th validator activates their vote. The next flag ledger detects the threshold crossing.

**Votes collected by TrustedVotes**:

```cpp
trustedValidations = 35
votes[Subscriptions] = 29  // Exceeds threshold of 28
threshold = max(1, (35 * 4) / 5) = 28
```

**Verification**: `votes > threshold` → `29 > 28` ✓ (82.9% > 80%)

**doVoting() returns**:

```cpp
actions[Subscriptions] = tfGotMajority
```

**Injected pseudo-transaction**:

```json
{
  "TransactionType": "EnableAmendment",
  "Account": "rrrrrrrrrrrrrrrrrrrrrhoLvTp",
  "Amendment": "7B73B9E8D8E6E8E8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8",
  "Flags": 65536,
  "LedgerSequence": 86245789,
  "hash": "ABC123..."
}
```

**State after**:

```json
{
  "count": 29,              // 29/35 = 82.9% > 80% ✓
  "threshold": 28,          // floor(35 * 0.8) = 28
  "enabled": false,
  "majority": 806021535,    // ← CloseTime of ledger 86245789
  "name": "Subscriptions"
}
```

### 2025-04-20 → 2025-05-04

Stability period. At each flag ledger, `doVoting()` checks:

```cpp
if (closeTime >= (majorityTime + 2weeks)) {
    // Ready for activation
} else {
    // Wait longer
}
```

### 2025-05-04 14:32:15 UTC (Ledger 86652345)

Exactly 2 weeks after majorityTime. The next flag ledger activates the amendment.

**doVoting() returns**:

```cpp
actions[Subscriptions] = 0  // Code 0 = activate
```

**Injected pseudo-transaction**:

```json
{
  "TransactionType": "EnableAmendment",
  "Account": "rrrrrrrrrrrrrrrrrrrrrhoLvTp",
  "Amendment": "7B73B9E8D8E6E8E8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8",
  "Flags": 0,               // ← No flag = activation
  "LedgerSequence": 86652345,
  "hash": "DEF456..."
}
```

**Change::applyAmendment()** adds Subscriptions to `sfAmendments`.

**Final state**:

```json
{
  "enabled": true,
  "name": "Subscriptions",
  "supported": true
}
```

***

## Synchronization and Consistency

### doValidatedLedger: Post-Validation Update

After each validated ledger, `doValidatedLedger()` synchronizes the internal state:

```cpp
void
AmendmentTableImpl::doValidatedLedger(
    LedgerIndex ledgerSeq,
    std::set<uint256> const& enabled,
    majorityAmendments_t const& majority)
{
    for (auto& e : enabled)
        enable(e);

    std::lock_guard lock(mutex_);

    // Remember the ledger sequence of this update.
    lastUpdateSeq_ = ledgerSeq;

    // Since we have the whole list in `majority`, reset the time flag, even
    // if it's currently set. If it's not set when the loop is done, then any
    // prior unknown amendments have lost majority.
    firstUnsupportedExpected_.reset();
    for (auto const& [hash, time] : majority)
    {
        AmendmentState& s = add(hash, lock);

        if (s.enabled)
            continue;

        if (!s.supported)
        {
            JLOG(j_.info()) << "Unsupported amendment " << hash
                            << " reached majority at " << to_string(time);
            if (!firstUnsupportedExpected_ || firstUnsupportedExpected_ > time)
                firstUnsupportedExpected_ = time;
        }
    }
    if (firstUnsupportedExpected_)
        firstUnsupportedExpected_ = *firstUnsupportedExpected_ + majorityTime_;
}
```

This function ensures that even if a node temporarily misses receiving a ledger, it will resynchronize correctly when it receives the validated ledger.

***

## Protection Against Unsupported Amendments

If an amendment is enabled but the node does not support it, the system enters "amendment blocked" mode to protect network integrity.

**Detection**:

```cpp
    if (app_.getAmendmentTable().hasUnsupportedEnabled())
    {
        JLOG(m_journal.error()) << "One or more unsupported amendments "
                                    "activated: server blocked.";
        app_.getOPs().setAmendmentBlocked();
    }
```

**Consequences**:

* Node ceases to participate in consensus
* Node ceases to validate new ledgers
* API remains accessible but signals "amendment blocked" state
* Node must be updated to resume operations

**Notification**: A warning is displayed in logs and via the `server_info` API:

```json
{
  "info": {
    "amendment_blocked": true,
    "build_version": "1.11.0"
  }
}
```
