intermediate 90 min

Voting & network coordination

How validators vote on amendments and how the network coordinates activation.

Prerequisites

Complete these before starting this module:

What you'll learn

  • Configure amendment voting (`VoteBehavior`).
  • Explain how votes are tallied across the network.
  • Understand coordination and backward-compatibility concerns.
Complete this module by self-assessment and a quiz. Jump to assessment

Introduction

≈90 min · Intermediate · builds on The amendment lifecycle

Amendments live or die by how validators vote and how the network coordinates the switch. In this module you'll learn to configure voting with VoteBehavior, how votes are tallied across the network, and the real-world concerns (backward compatibility, non-updated nodes, contingency plans) of flipping a protocol change on safely. It's the operational side of governance.


Communication via the Overlay Network

In brief: votes ride along with validations over the overlay.

Validation Propagation

The primary coordination mechanism for amendments is the propagation of validations across the overlay network.

Network structure:

How validations travel: validators A and B exchange messages through a relay node, and every node, whether an API server, another validator, or a full history node, relays the votes onward.

Propagation flow:

  1. A validator publishes a validation containing its list of supported amendments
  2. The validation is broadcast to all connected peers
  3. Each peer propagates the validation to its own peers (flood protocol)
  4. Within seconds, all nodes in the network have received the validation
  5. Each node extracts the amendment list and updates its counters

Validation Broadcast Protocol

Validations are broadcast via XRPL's Peer Protocol:

TMValidation Message:

message TMValidation {
    required bytes validation = 1;    // Serialized and signed validation
    optional bytes cookie = 2;        // Tracking cookie
}

Processing by receiving node:

Duplicate Suppression and Optimization

To avoid message flooding, the overlay network suppresses duplicates (and, separately, squelches redundant validator-message senders via TMSquelch):

Principle: A node only propagates a message if it hasn't seen it yet

Entry expiration: suppression entries expire after a few minutes to free memory.


Deployment Strategies

In brief: how a protocol change is rolled out safely across the network.

Phase 1: Pre-Release Communication

6-8 weeks before release: Public announcement of the proposed amendment

  • Publication of an XLS (XRPL Standard) detailing the amendment
  • Public code review on GitHub
  • Discussions on community forums
  • Technical webinars and presentations

Communication channels:

  • XRPL.org blog
  • GitHub (XLS repository)
  • XRPL Dev Forum
  • Discord XRPL Developers
  • Official Twitter/X

Phase 2: Code Release

Objective: Distribute code before voting begins

Typical timeline:

  • T+0: Release of rippled with the new amendment (e.g., 1.12.0)
  • T+1 week: First non-validator nodes update
  • T+2 weeks: ~30% of nodes updated
  • T+4 weeks: ~60% of nodes updated

Progressive deployment strategy:

The rollout waves: week one covers Testnet and internal nodes, weeks two to three the full history and public API nodes plus exchange infrastructure, and weeks four to six the validators, who vote under intensive monitoring.

Phase 3: Vote Activation

Communication to validators:

Validators receive notifications to activate their vote:

Note. The "Subscriptions" amendment used in the examples below is a hypothetical running example (not a real XRPL amendment); the notification and command output are illustrative of the process, not real records.

Informal coordination: Validators communicate via:

  • Private validator mailing list
  • Private Slack/Discord channels
  • Regular coordination meetings

Phase 4: Majority Period

2 weeks of final preparation:

Once the 80% threshold is exceeded, node operators receive alerts:

[WARNING] Amendment 7B73B9E8... (Subscriptions) has reached majority.
It will activate in approximately 14 days (on 2025-05-04 14:32:15 UTC).

If your node does not support this amendment, please upgrade to rippled 1.12.0 or later immediately.

Current version: 1.11.0
Amendment support: NO
Upgrade urgency: CRITICAL

Required actions:

  • All non-updated nodes must upgrade
  • Exchanges and wallets must finalize their integrations
  • Final tests on staging environments

Phase 5: Activation

Moment of truth: The activation day

  • 24/7 monitoring by technical teams
  • Emergency communication channels open
  • Ready to coordinate response if issue detected

Managing Non-Updated Nodes

In brief: what happens to nodes that have not upgraded in time.

Early Detection

Nodes can detect that they will soon be blocked:

Emergency Migration Strategies

For validators:

If a validator doesn't have time to update before activation:

// Option 1: Withdraw vote to delay activation
xrpld feature 7B73B9E8... reject

// Option 2: Temporarily withdraw from UNL
// (requires coordination with other validators)

For API nodes/Exchanges:

  • Plan A: Quick upgrade (few hours of maintenance)
  • Plan B: Route traffic to already-updated backup nodes
  • Plan C: Degraded mode (read-only via third-party nodes)

Crisis Communication

In case of major problem detected during majority period:

Channel #xrpl-validators (private):

[URGENT] Issue detected with Subscriptions amendment.
Potential impact: [description]
Recommendation: All validators please REJECT the amendment immediately.

Command:
xrpld feature 7B73B9E8... reject

Quick coordination: Validators can withdraw their votes to drop support below 80% and avoid automatic activation.

Watch out. When an amendment activates, a node that does not understand it becomes amendment-blocked and stops validating. Operators must upgrade before activation, not after.


Contingency Plans

Scenario 1: Critical Bug Discovered After Activation

Cannot disable an amendment: There is no rollback mechanism.

Response strategies:

  1. Emergency patch: Develop and deploy a fix quickly
// In doApply(), add temporary guard
TER RecurringPaymentClaim::doApply() {
    // EMERGENCY FIX: Disable temporarily until proper fix
    if (view().info().seq < EMERGENCY_DISABLE_LEDGER) {
        return temDISABLED;
    }

    // Normal logic...
}
  1. Corrective amendment: Develop an amendment that fixes the bug
XRPL_FIX(fixSubscriptionsBug, Supported::yes, VoteBehavior::DefaultYes)
  1. Out-of-band coordination: If the bug is catastrophic, manual coordination for an emergency hard fork (extreme scenario never used on XRPL)

Scenario 2: Network Partition During Activation

Symptom: Two parts of the network activate the amendment at different times or with different results.

Detection:

Resolution:

  • Normal consensus will resolve the partition
  • The minority chain will abandon its progress and adopt the majority chain
  • Unconfirmed transactions will need to be resubmitted

Scenario 3: Validators Divided on Vote

Symptom: Support oscillates around 80%, amendment gains and loses majority multiple times.

Impact:

  • Multiple tfGotMajority and tfLostMajority pseudo-transactions
  • Confusion for node operators
  • Delay in final activation

Prevention:

  • Clear validator communication before activating votes
  • Informal consensus reached before formal vote
  • Avoid "surprise" votes

Monitoring and Observability

Key Metrics

Operators monitor several metrics during the activation process:

1. Vote Count: Number of validators voting for the amendment

# Regular query
watch -n 60 'xrpld feature 3B95AC15... | jq ".result.count"'

2. Network Consensus: Consensus percentage achieved

xrpld server_info | jq ".info.server_state"
# States: proposing, validating, full, connected

3. Peer Count: Number of connected peers

xrpld peers | jq ".result.peers | length"

4. Ledger Progress: Verify ledgers are progressing normally

watch -n 10 'xrpld server_info | jq ".info.validated_ledger.seq"'

Monitoring Dashboards

XRPL Metrics Dashboard (conceptual example):

A conceptual amendment monitoring dashboard: the Subscriptions amendment shows 29 of 35 votes at 82.9 percent against a threshold of 28, majority held since April 20 with activation due in 14 days, and network health checks all green.

Alerting

Automatic alert configuration:


Ecosystem Coordination

Exchanges and Services

Exchanges receive notifications and must coordinate:

Typical timeline for an exchange:

The integrator's countdown: at T minus six weeks the amendment is announced and its API impact reviewed, development starts at T minus four, final staging tests run once majority is reached at T minus two, and activation day brings round-the-clock monitoring with a rollback plan ready.

Application Developers

Client libraries must be updated before activation:

xrpl.js:

// Version 2.12.0 (with Subscriptions)
export const TRANSACTION_TYPES = {
  // ... existing types
  SubscriptionSet: 'SubscriptionSet',
  SubscriptionClaim: 'SubscriptionClaim',
  SubscriptionCancel: 'SubscriptionCancel',
};

xrpl-py:

# Version 2.8.0
class TransactionType(str, Enum):
    # ... existing types
    SUBSCRIPTION_SET = "SubscriptionSet"
    SUBSCRIPTION_CLAIM = "SubscriptionClaim"
    SUBSCRIPTION_CANCEL = "SubscriptionCancel"

Documentation and Communication

Documentation updates:

  • XRPL.org: New pages for RecurringPayment transactions
  • GitHub: Updated README and guides
  • API docs: New endpoints and fields documented

Public announcements:


Lessons Learned: Historical Cases

fixAmendmentMajorityCalc (2021)

Problem: Asymmetry in threshold calculation (rounding up instead of down).

Impact: In certain cases, the effective threshold was slightly higher than 80%.

Resolution: New amendment to correct the calculation.

Lesson: Even simple mathematical calculations must be rigorously tested.

MultiSign (2016)

Success: First major amendment adding significant functionality.

Timeline:

  • Development: 6 months
  • Testing on Testnet: 3 months
  • Deployment: 2 months
  • Activation: Without incident

Lesson: Extensive preparation and rigorous testing are essential for complex amendments.


Voting and Activation


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

In brief: how a validator configures and casts its vote.

Vote Expression

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

1. Static configuration: Via the xrpld.cfg configuration file:

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

# Enable voting
xrpld feature 7B73B9E8D8E6E8E8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8 accept

# Disable voting
xrpld feature 7B73B9E8D8E6E8E8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8A8B8C8D8E8F8 reject

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

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

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

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:

class TrustedVotes {
    // per-validator cached amendment votes, refreshed by each validation
    // and expired after 24h of silence (expiresAfter = 24h)
    hash_map<PublicKey, UpvotesAndTimeout> recordedVotes_;
};

The cache-and-expire rationale (no vote flapping, offline validators stop counting) is covered in the Amendments, overview & architecture module; this module focuses on how those votes travel the wire.

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:

class AmendmentSet {
    hash_map<uint256, int> votes_;   // tally per amendment
    int trustedValidations_;         // how many validators we heard from
    int threshold_;                  // max(1, floor(80% of trusted))
    // passes(): votes > threshold_ (>= when only 1 trusted validator)
};

Construction, rounding, and the exact passes() semantics are detailed in the Amendments, overview & architecture module.

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

std::map<uint256, std::uint32_t>
AmendmentTableImpl::doVoting(Rules const& rules, /* validations, majorities */)
{
    // 1. update TrustedVotes from the latest validations (24h expiry)
    // 2. build an AmendmentSet and compute the 80% threshold
    // 3. for each amendment not yet enabled, compare tally vs sfMajorities:
    //      newly passing  -> action = tfGotMajority
    //      newly failing  -> action = tfLostMajority
    //      2 weeks held   -> action = 0 (enable)
    return actions;   // consumed by the flag ledger's pseudo-transactions
}

(The step-by-step version of this algorithm is in the Amendments, overview & architecture module.)

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:

Pseudo-transaction properties:

  • Type: ttAMENDMENT
  • 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:

TER Change::applyAmendment()
{
    // 1. tfGotMajority  -> record the amendment + close time in sfMajorities
    // 2. tfLostMajority -> remove it from sfMajorities
    // 3. no flag        -> two weeks have passed: move the hash into
    //                      sfAmendments, run any activation handler, and
    //                      notify the AmendmentTable (unsupported -> blocked)
    // Duplicate activation returns tefALREADY.
}

The full walkthrough of this function lives in the Amendments, overview & architecture module; here what matters is the operational effect: the flag ledger's pseudo-transaction is what actually flips the switch.

Modified Ledger Fields

Ledger Amendments Object:

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:

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

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

Verification: votes > threshold29 > 28 (82.9% > 80%)

doVoting() returns:

actions[Subscriptions] = tfGotMajority

Injected pseudo-transaction:

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

State after:

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

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:

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

Injected pseudo-transaction:

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

Change::applyAmendment() adds Subscriptions to sfAmendments.

Final state:

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

Synchronization and Consistency

doValidatedLedger: Post-Validation Update

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

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:

    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:

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

Summary

This module covered the operational side of governance: how validators cast their vote (configured with VoteBehavior), how votes are tallied per ledger from validations, and how a protocol change is rolled out safely across the network. The recurring risk is the non-updated node, which becomes amendment-blocked when a change it does not understand activates, so operators must upgrade before activation, not after.

To remember:

  • A validator's stance: feature <hash> accept|reject (admin) or the config, on top of the code's VoteBehavior default
  • Votes are tallied per ledger from trusted validations (TrustedVotes)
  • Threshold math: floor(validations * 0.8), compared strictly greater, so support must EXCEED 80%
  • Amendment accounting happens at flag ledgers: every 256 ledgers
  • doVoting orchestrates the pseudo-transaction injection
  • Rollout order: announce, release, operators upgrade, validators vote, majority window, activation
  • Operator rule: upgrade BEFORE activation or drop off the network
  • Watch out: an amendment-blocked node stops advancing silently; monitor amendment_blocked in server_info

Next up. The rollout succeeded; now keep it observable. Next: amendment impact and monitoring, the operator's view of an activation.

Assignments

0 of 2 complete

Unlocks

Finishing this module opens up:

XRPL Academy © 2026