# 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"
  }
}
```


---

# 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/voting-and-activation.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.
