The activation of an amendment has profound consequences on the XRPL protocol. Unlike simple software updates, an amendment modifies the fundamental rules that govern consensus, transaction validation, and ledger structure. These changes are irreversible and must be applied uniformly by all nodes on the network.
In this section, we will analyze the impact that an amendment like Subscriptions (XLS-0078) has on the core protocol: modifications to consensus rules, changes in transaction validation, compatibility considerations, and management of nodes that do not support an enabled amendment.
Understanding this impact is crucial for assessing the risks associated with an amendment and for planning deployment and migration strategies.
Modification of Consensus Rules
The Rules Object
The XRPL protocol encapsulates the set of active rules in a Rules object that is constructed for each ledger based on enabled amendments.
Conceptual structure:
classRules{private:// Set of all enabled amendmentsstd::set<uint256> enabledAmendments_;// Pre-calculation of commonly used rulesbool fixQualityUpperBound_;bool fixTrustLinesToSelf_;bool flowCross_;bool ownerPaysFee_;// ... etc for each impacting amendmentpublic:// Check if an amendment is enabledboolenabled(uint256const&amendment)const{returnenabledAmendments_.count(amendment)>0;}// Quick accessors for specific rulesboolfixQualityUpperBound()const{return fixQualityUpperBound_;}boolflowCross()const{return flowCross_;}// ... etc};
Construction: The Rules object is constructed from the sfAmendments field of the ledger:
Impact of Subscriptions on Rules
When Subscriptions is enabled, several aspects of the protocol change:
1. New transaction types:
2. New ledger entry types:
3. New validation logic:
Changes in Transaction Validation
Validation Phases
Every XRPL transaction goes through several validation phases. Amendments can affect each of these phases.
1. Preflight: Basic syntactic validation (before applying to ledger)
2. Preclaim: Contextual validation (with ledger access but without modifying it)
3. DoApply: Actual application to ledger
Backward Compatibility
Before activation: ttSUBSCRIPTION_SET transactions submitted before Subscriptions activation are rejected with temDISABLED.
After activation: These transactions become valid and can be processed.
No backward compatibility for ledgers: A ledger built after activation potentially contains ltSUBSCRIPTION objects that would not exist in a pre-activation ledger. A node that does not support Subscriptions cannot correctly validate such a ledger.
Impact on Consensus
LedgerHash Calculation
The hash of a ledger is calculated from all its components, including:
The root hash of the accounts SHAMap
The root hash of the transactions SHAMap
The ledger index, close time, parent hash
The sfAmendments and sfMajorities fields
Importance: If two nodes apply different rules (one with Subscriptions, the other without), they will build different ledgers with different hashes, which will prevent consensus.
Consensus on Rules
XRPL consensus is not only about which transactions to include, but also about the rules to apply. All nodes must:
Agree on the parent ledger
Agree on transactions to include
Agree on enabled amendments (rules to apply)
Protection mechanism: If a node does not support an enabled amendment, it enters "amendment blocked" mode and ceases to participate in consensus, thus avoiding building an invalid ledger.
Management of Unsupported Amendments
Detection of Unsupported Amendment
When an amendment is enabled, the system immediately checks if the node supports it:
Amendment Blocked Mode
When a node enters "amendment blocked" mode:
Consensus stopped: The node ceases to propose and validate ledgers
API still functional: The node can still:
Respond to RPC queries
Synchronize ledgers from peers
Provide historical data
But it cannot:
Propose new ledgers
Validate peer proposals
Submit new transactions to the network
Signaling via server_info:
Prior Warnings
The system warns operators before an unsupported amendment is activated:
firstUnsupportedExpected: If an unsupported amendment has reached majority, the system calculates when it will be activated and emits warnings.
Forward Compatibility: Anticipating Changes
Unsupported vs Obsolete
Unsupported: An amendment that this node does not recognize or for which it does not have implementation code.
Obsolete: A historical amendment that is no longer relevant but must remain in the code in case it was activated on a historical ledger.
Design for Forward Compatibility
rippled developers design code to anticipate future changes:
1. Tolerant parsing: Data structures ignore unknown fields rather than rejecting
2. Structure versioning: Some structures include version fields
3. Feature flags everywhere: Each new code explicitly checks amendments
Backward Compatibility: Supporting Old Ledgers
Historical Rules
Even after an amendment is enabled, nodes must be able to validate historical ledgers that date from before activation.
Reconstruction of historical Rules:
Retired Amendments
After an amendment has been active for at least 2 years, the pre-amendment code can be removed:
Marking in features.macro:
This indicates that:
Pre-amendment code has been removed
The identifier is deprecated
New ledgers always assume the amendment is active
Special Cases and Edge Cases
Simultaneous Activation of Multiple Amendments
It is possible that multiple amendments reach their activation period at the same flag ledger.
Application order: Pseudo-transactions are ordered deterministically by amendment hash:
Dependencies between amendments: Some amendments may depend on others. Code must manage these dependencies:
Conflicting Amendments
It is theoretically possible to create two amendments that modify the same logic in incompatible ways.
Prevention strategy:
Rigorous code review before release
Integration tests with all amendments enabled
Coordination between development teams
Management if conflict detected:
Validators must withdraw their vote for one of the conflicting amendments
Or a third corrective amendment must be developed
Network Partition During Activation
If the network partitions at the moment of amendment activation, two scenarios are possible:
1. Both partitions activate the amendment: No problem, they will converge when the partition is resolved.
2. One partition activates, the other doesn't: Partitions will build incompatible ledgers. When the partition is resolved, the minority partition must abandon its chain and adopt the majority partition's chain (normal consensus).
Migration Strategies
Progressive Deployment
Node operators can update their software before an amendment is activated:
Phase 1: Code release with the new amendment (e.g., rippled 1.12.0 with Subscriptions)
Nodes update but the amendment is not yet voted on
Phase 2: Validators begin voting
Amendment is supported but not yet enabled
Nodes not updated can still function
Phase 3: Amendment exceeds 80% and enters majority
2 weeks notice for the last nodes not updated
Phase 4: Amendment activation
Nodes not updated are blocked
Read-Only Mode Nodes
Nodes that are not validators (simple fullhistory or API nodes) can choose to:
1. Update quickly: To continue following the network in real-time
2. Stay in historical mode: Serve only data up to the ledger preceding activation, and synchronize from other nodes for following ledgers without applying the rules themselves
3. Depend on other nodes: Relay requests to updated nodes
Impact on Third-Party Applications
Wallets and Exchanges
Applications that interact with XRPL must adapt their code when an amendment is activated:
New transactions: Support for ttSUBSCRIPTION_SET, ttSUBSCRIPTION_CLAIM, and ttSUBSCRIPTION_CANCEL in the UI
New fields: Parsing of fields sfAccount, sfDestination, sfDestinationTag, sfAmount, sfFrequency, sfNextPaymentTime, sfExpiration, etc.
New business logic: Management of recurring subscriptions (display, cancellation, claim, monitoring)
APIs and Libraries
Libraries like xrpl.js, xrpl-py must be updated to support new features:
Rules makeRules(std::shared_ptr<ReadView const> const& ledger) {
// Read amendments from ledger
auto const amendments = ledger->read(keylet::amendments());
std::set<uint256> enabled;
if (amendments && amendments->isFieldPresent(sfAmendments)) {
auto const& vec = amendments->getFieldV256(sfAmendments);
enabled.insert(vec.begin(), vec.end());
}
return Rules(enabled);
}
// In Transactor::preflight()
NotTEC checkTransactionType(TxType type, Rules const& rules) {
if (type == ttSUBSCRIPTION_SET ||
type == ttSUBSCRIPTION_CLAIM ||
type == ttSUBSCRIPTION_CANCEL)
{
if (!rules.enabled(featureSubscriptions))
return temDISABLED;
}
return tesSUCCESS;
}
// In View::read()
if (entry->getType() == ltSUBSCRIPTION) {
if (!rules.enabled(featureSubscriptions))
return nullptr; // Ignore if amendment not enabled
}
// In SubscriptionClaim::doApply()
TER SubscriptionClaim::doApply() {
// This function can only be called if
// featureSubscriptions is enabled
assert(ctx_.view().rules().enabled(featureSubscriptions));
// Logic specific to recurring subscriptions
// ...
}
NotTEC SubscriptionSet::preflight(PreflightContext const& ctx) {
// Check that the amendment is enabled
if (!ctx.rules.enabled(featureSubscriptions))
return temDISABLED;
// Syntactic validation
if (!ctx.tx.isFieldPresent(sfDestination))
return temMALFORMED;
if (!ctx.tx.isFieldPresent(sfAmount))
return temMALFORMED;
if (!ctx.tx.isFieldPresent(sfFrequency))
return temMALFORMED;
// Basic preflight validation
auto const ret = preflight1(ctx);
if (!isTesSuccess(ret))
return ret;
return preflight2(ctx);
}
TER SubscriptionSet::preclaim(PreclaimContext const& ctx) {
// Check that source account exists
auto const account = ctx.view.read(keylet::account(ctx.tx[sfAccount]));
if (!account)
return terNO_ACCOUNT;
// Check that destination account exists
auto const dest = ctx.view.read(keylet::account(ctx.tx[sfDestination]));
if (!dest)
return tecNO_DST;
// Check sufficient funds (approximate)
auto const balance = (*account)[sfBalance];
auto const amount = ctx.tx[sfAmount].xrp();
if (balance < amount)
return tecUNFUNDED;
return tesSUCCESS;
}
TER SubscriptionSet::doApply() {
// Create subscription entry
auto const subKey = keylet::subscription(
ctx_.tx[sfAccount],
ctx_.tx[sfSubscriptionID]);
auto sub = std::make_shared<SLE>(subKey);
sub->setAccountID(sfAccount, ctx_.tx[sfAccount]);
sub->setAccountID(sfDestination, ctx_.tx[sfDestination]);
sub->setFieldAmount(sfAmount, ctx_.tx[sfAmount]);
sub->setFieldU32(sfFrequency, ctx_.tx[sfFrequency]);
sub->setFieldU32(sfNextPaymentTime, view().info().closeTime + frequency);
// Add to ledger
view().insert(sub);
return tesSUCCESS;
}
// In Change::applyAmendment()
if (!ctx.app.getAmendmentTable().isSupported(amendment)) {
JLOG(ctx.journal.fatal())
<< "Unsupported amendment " << amendment
<< " (" << amendmentName << ") activated!";
// Block the server
ctx.app.getOPs().setAmendmentBlocked();
// Record expected moment
ctx.app.getAmendmentTable().recordFirstUnsupported(
ctx.view().info().closeTime);
}
void NetworkOPsImp::setAmendmentBlocked() {
amendmentBlocked_ = true;
// Stop participating in consensus
setMode(OperatingMode::DISCONNECTED);
JLOG(j_.fatal())
<< "Server is amendment blocked. "
<< "Please upgrade to a newer version.";
}
// If a new field is added by a future amendment
STObject obj = parseTransaction(data);
// Unknown fields are ignored, no error
// Allows old nodes to read new data
if (obj.isFieldPresent(sfVersion)) {
auto version = obj.getFieldU32(sfVersion);
if (version > SUPPORTED_VERSION) {
// Handle with caution or reject
}
}
if (rules.enabled(featureNewFeature)) {
// New logic
} else {
// Old logic
}
// To validate ledger 86000000 (before Subscriptions)
Rules historicalRules = makeRules(ledger86000000);
// Subscriptions is not enabled in these rules
assert(!historicalRules.enabled(featureSubscriptions));
// Apply transactions with old rules
for (auto const& tx : ledger86000000->txs()) {
applyTransaction(tx, historicalRules);
}
// Before retirement
if (rules.enabled(featureMultiSign)) {
// New multi-signature logic
} else {
// Old logic (can be removed after 2 years)
}
// After retirement
// Always assume MultiSign is enabled
// No more conditional branch
XRPL_RETIRE(MultiSign)
std::map<uint256, std::uint32_t> actions; // Map sorted by key
for (auto const& [hash, action] : actions) {
// Amendments are activated in hash order
applyAmendmentTransaction(hash, action);
}
// featureB depends on featureA
if (rules.enabled(featureB)) {
assert(rules.enabled(featureA)); // Must be true
}
// xrpl.js after Subscriptions
const tx = {
TransactionType: 'SubscriptionSet',
Account: 'rN7n7otQDd6FczFgLdlqtyMVrn3M1tXB7V',
Destination: 'rLHzPsX6oXkzU9rFYvT1EZUcqPFiSv5xdJ',
Amount: '100000000', // 100 XRP max per period
Frequency: 2592000, // 30 days in seconds
StartTime: 711232800 // Optional
};
await client.submit(tx);