Compare commits

..

2 Commits

17 changed files with 1238 additions and 50 deletions

View File

@@ -1,43 +1,35 @@
{
"version": "0.5",
"requires": [
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1777558780.503",
"xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1765850149.987",
"sqlite3/3.53.0#324ada52333108388a9a6108bfa96734%1776096494.149",
"soci/4.0.3#fe32b9ad5eb47e79ab9e45a68f363945%1774450067.231",
"snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1765850147.878",
"secp256k1/0.7.1#481881709eb0bdd0185a12b912bbe8ad%1770910500.329",
"rocksdb/10.5.1#4a197eca381a3e5ae8adf8cffa5aacd0%1765850186.86",
"re2/20251105#8579cfd0bda4daf0683f9e3898f964b4%1774398111.888",
"protobuf/6.33.5#d96d52ba5baaaa532f47bda866ad87a5%1774467363.12",
"openssl/3.6.2#4789bbf131b77d0515d15e094c8f697f%1778071755.506",
"nudb/2.0.9#11149c73f8f2baff9a0198fe25971fc7%1775040983.408",
"lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504%1765850143.914",
"libiconv/1.17#1e65319e945f2d31941a9d28cc13c058%1765842973.492",
"libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1765842973.03",
"libarchive/3.8.7#c446109bd1f1d8ba7936c94189bc50e6%1776147552.838",
"jemalloc/5.3.1#1fc58d55316041f10fbc1e8a2eae632a%1776700028.228",
"gtest/1.17.0#5224b3b3ff3b4ce1133cbdd27d53ee7d%1768312129.152",
"grpc/1.78.1#b1a9e74b145cc471bed4dc64dc6eb2c1%1774467387.342",
"ed25519/2015.03#ae761bdc52730a843f0809bdf6c1b1f6%1765850143.772",
"date/3.0.4#862e11e80030356b53c2c38599ceb32b%1765850143.772",
"c-ares/1.34.6#545240bb1c40e2cacd4362d6b8967650%1774439234.681",
"bzip2/1.0.8#c470882369c2d95c5c77e970c0c7e321%1765850143.837",
"boost/1.91.0#ea540ca2133d831b560036aa24dece3c%1778050991.9",
"abseil/20250127.0#bb0baf1f362bc4a725a24eddd419b8f7%1774365460.196"
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1782392402.122708",
"xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1782392402.420688",
"sqlite3/3.53.0#324ada52333108388a9a6108bfa96734%1782392403.185447",
"soci/4.0.3#e726491a03468795453f7c83fc924a96%1782392402.679521",
"snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1782307151.633168",
"secp256k1/0.7.1#b1f450b7f78a36fff75bb6934a356f3a%1782338841.3729",
"rocksdb/10.5.1#4a197eca381a3e5ae8adf8cffa5aacd0%1782392413.075713",
"re2/20251105#8579cfd0bda4daf0683f9e3898f964b4%1782392402.431897",
"protobuf/6.33.5#ff253ead763bd8d9904a52979cd21e81%1782392410.233933",
"openssl/3.6.3#f806de8933e3bf6f01016c6a888cee2e%1783945160.863288",
"nudb/2.0.9#11149c73f8f2baff9a0198fe25971fc7%1782392402.297166",
"lz4/1.10.0#982d9b673900f665a1da109e09c17cab%1782392402.164188",
"libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1782392402.420732",
"libarchive/3.8.7#c446109bd1f1d8ba7936c94189bc50e6%1782392403.066892",
"gtest/1.17.0#5224b3b3ff3b4ce1133cbdd27d53ee7d%1782392402.791979",
"grpc/1.78.1#b1a9e74b145cc471bed4dc64dc6eb2c1%1782736970.619035",
"ed25519/2015.03#ae761bdc52730a843f0809bdf6c1b1f6%1782307148.15562",
"date/3.0.4#862e11e80030356b53c2c38599ceb32b%1782392402.538492",
"c-ares/1.34.6#545240bb1c40e2cacd4362d6b8967650%1782392402.681654",
"bzip2/1.0.8#c470882369c2d95c5c77e970c0c7e321%1782392402.296732",
"boost/1.91.0#ea540ca2133d831b560036aa24dece3c%1782392419.475605",
"abseil/20250127.0#9ef01c1451a8340f9022e46238c0fbb6%1783945159.651047"
],
"build_requires": [
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1777558780.503",
"strawberryperl/5.32.1.1#8d114504d172cfea8ea1662d09b6333e%1774447376.964",
"protobuf/6.33.5#d96d52ba5baaaa532f47bda866ad87a5%1774467363.12",
"nasm/2.16.01#31e26f2ee3c4346ecd347911bd126904%1765850144.707",
"msys2/cci.latest#d22fe7b2808f5fd34d0a7923ace9c54f%1770657326.649",
"m4/1.4.19#4523e4347b55cd26ae918bd5770cab9a%1778062762.471",
"cmake/4.3.0#b939a42e98f593fb34d3a8c5cc860359%1774439249.183",
"b2/5.4.2#ffd6084a119587e70f11cd45d1a386e2%1774439233.447",
"automake/1.16.5#b91b7c384c3deaa9d535be02da14d04f%1755524470.56",
"autoconf/2.71#51077f068e61700d65bb05541ea1e4b0%1731054366.86",
"abseil/20250127.0#bb0baf1f362bc4a725a24eddd419b8f7%1774365460.196"
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1782392402.122708",
"protobuf/6.33.5#ff253ead763bd8d9904a52979cd21e81%1782392410.233933",
"cmake/4.3.3#840cf00ea09777e05c2050a50a82c722%1782392418.696091",
"b2/5.4.2#ffd6084a119587e70f11cd45d1a386e2%1782392402.624226",
"abseil/20250127.0#9ef01c1451a8340f9022e46238c0fbb6%1783945159.651047"
],
"python_requires": [],
"overrides": {
@@ -57,7 +49,7 @@
"boost/1.91.0"
],
"lz4/[>=1.9.4 <2]": [
"lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504"
"lz4/1.10.0#982d9b673900f665a1da109e09c17cab"
]
},
"config_requires": []

View File

@@ -31,7 +31,7 @@ class Xrpl(ConanFile):
"grpc/1.78.1",
"libarchive/3.8.7",
"nudb/2.0.9",
"openssl/3.6.2",
"openssl/3.6.3",
"secp256k1/0.7.1",
"soci/4.0.3",
"zlib/1.3.2",

View File

@@ -329,6 +329,7 @@ words:
- writeme
- wsrch
- wthread
- Xahau
- xbridge
- xchain
- ximinez

View File

@@ -291,6 +291,7 @@ JSS(ident); // in: AccountCurrencies, AccountInfo, OwnerIn
JSS(ignore_default); // in: AccountLines
JSS(in); // out: OverlayImpl
JSS(inLedger); // out: tx/Transaction
JSS(in_queue); // out: inject
JSS(inbound); // out: PeerImp
JSS(index); // in: LedgerEntry
// out: STLedgerEntry, LedgerEntry, TxHistory, LedgerData

View File

@@ -223,8 +223,21 @@ public:
virtual void
consensusViewChange() = 0;
virtual void
setStall(std::chrono::milliseconds duration) = 0;
virtual bool
isStalled() const = 0;
virtual void
clearStall() = 0;
virtual json::Value
getConsensusInfo() = 0;
// Proposers and round time of the last consensus round, for out-of-band
// telemetry (DatagramMonitor) that cannot reach the private consensus object.
[[nodiscard]] virtual std::size_t
getPrevProposers() const = 0;
[[nodiscard]] virtual std::chrono::milliseconds
getPrevRoundTime() const = 0;
virtual json::Value
getServerInfo(bool human, bool admin, bool counters) = 0;
virtual void

View File

@@ -0,0 +1,899 @@
/**
* Byzantine Partition Recovery Tests
*
* Tests consensus safety under network partition scenarios where a minority
* of validators (running modified binaries) attempt to advance the chain
* while the majority is offline, then rejoin the network.
*
* Attack scenario:
* - 7 validators share a common UNL (like production mainnet)
* - 4 legitimate validators crash (DoS attack)
* - 3 attacker validators (modified binary) bypass quorum checks
* and continue producing ledgers
* - 4 legitimate validators recover and reconnect
* - Question: does the attacker chain get accepted?
*/
#include <test/csf.h>
#include <test/csf/Peer.h>
#include <test/csf/PeerGroup.h>
#include <test/csf/Sim.h>
#include <test/csf/SimTime.h>
#include <test/csf/TrustGraph.h>
#include <test/csf/collectors.h>
#include <test/csf/events.h>
#include <xrpld/consensus/ConsensusParms.h>
#include <xrpl/beast/unit_test/suite.h>
#include <chrono>
#include <iostream>
namespace xrpl::test {
class ByzantinePartitionRecovery_test : public beast::unit_test::Suite
{
// Collector that tracks per-peer ledger advancement and validation
struct PartitionTracker
{
struct PeerState
{
csf::Ledger::Seq lastClosed{0};
csf::Ledger::Seq lastFullyValidated{0};
csf::Ledger::ID lastClosedId{};
csf::Ledger::ID lastFullyValidatedId{};
};
std::map<csf::PeerID, PeerState> states;
template <class E>
void
on(csf::PeerID, csf::SimTime, E const&)
{
}
void
on(csf::PeerID who, csf::SimTime, csf::AcceptLedger const& e)
{
auto& s = states[who];
s.lastClosed = e.ledger.seq();
s.lastClosedId = e.ledger.id();
}
void
on(csf::PeerID who, csf::SimTime, csf::FullyValidateLedger const& e)
{
auto& s = states[who];
s.lastFullyValidated = e.ledger.seq();
s.lastFullyValidatedId = e.ledger.id();
}
};
/**
* Test 1: Byzantine minority cannot fully validate during partition
*
* 7 validators, common UNL. 4 crash. 3 attackers continue.
* Standard quorum (no modification) — attackers can close ledgers
* but cannot fully validate them (need 6/7 = 80%).
*/
void
testPartitionNoQuorumBypass()
{
using namespace csf;
using namespace std::chrono;
testcase("partition: minority cannot fully validate (standard quorum)");
Sim sim;
ConsensusParms const parms{};
SimDuration const delay = round<milliseconds>(0.2 * parms.ledgerGRANULARITY);
// Create 7 validators
PeerGroup attackers = sim.createGroup(3);
PeerGroup legitimate = sim.createGroup(4);
PeerGroup network = attackers + legitimate;
// Common UNL — all trust all (like production mainnet)
network.trust(network);
network.connect(network, delay);
PartitionTracker tracker;
sim.collectors.add(tracker);
// Round 1: establish common state (all 7 in sync)
sim.run(1);
BEAST_EXPECT(sim.synchronized());
// Record pre-partition state
Ledger::Seq const prePartitionSeq = attackers[0]->lastClosedLedger.seq();
Ledger::ID const prePartitionId = attackers[0]->fullyValidatedLedger.id();
std::cout << "Pre-partition: all peers at seq " << prePartitionSeq << ", fully validated\n";
// === PARTITION: disconnect legitimate validators ===
// Simulate crash: disconnect from attackers AND from each other
legitimate.disconnect(network);
network.disconnect(legitimate);
// Stop legitimate peers from running consensus during partition
for (Peer* p : legitimate)
p->targetLedgers = p->completedLedgers;
// Attackers submit transactions and try to advance
for (Peer* p : attackers)
p->submit(Tx{static_cast<std::uint32_t>(p->id) + 100});
// Run several rounds — attackers will close ledgers among themselves
// (3/3 = 100% of visible proposers agree)
sim.run(4);
// Check attacker state
Ledger::Seq attackerSeq = attackers[0]->lastClosedLedger.seq();
Ledger::Seq legitimateSeq = legitimate[0]->lastClosedLedger.seq();
std::cout << "During partition:\n";
std::cout << " Attackers closed up to seq " << attackerSeq << "\n";
std::cout << " Legitimate peers stuck at seq " << legitimateSeq << "\n";
// Attackers advanced their closed ledger
BEAST_EXPECT(attackerSeq > prePartitionSeq);
// Note: legitimate peers may also advance closed ledger via
// consensus timeout (isolated node eventually closes alone),
// but they CANNOT fully validate.
// KEY ASSERTION: attackers could NOT fully validate their chain
// because they only have 3/7 validations (need 6)
for (Peer* p : attackers)
{
std::cout << " Attacker " << p->id
<< " fullyValidated seq: " << p->fullyValidatedLedger.seq() << "\n";
// The attacker's fully validated ledger should NOT have advanced
// beyond the pre-partition state (they can't get 6/7 validations)
BEAST_EXPECT(p->fullyValidatedLedger.id() == prePartitionId);
}
}
/**
* Test 2: Byzantine minority with quorum bypass
*
* 3 attackers override their quorum to 2 (simulating modified binary).
* They can fully validate on their fork. When 4 legitimate peers
* rejoin, the attacker chain should NOT be accepted by legitimate peers.
*/
void
testPartitionWithQuorumBypass()
{
using namespace csf;
using namespace std::chrono;
testcase("partition: attacker chain rejected after recovery (quorum bypass)");
Sim sim;
ConsensusParms const parms{};
SimDuration const delay = round<milliseconds>(0.2 * parms.ledgerGRANULARITY);
PeerGroup attackers = sim.createGroup(3);
PeerGroup legitimate = sim.createGroup(4);
PeerGroup network = attackers + legitimate;
// Common UNL
network.trust(network);
network.connect(network, delay);
PartitionTracker tracker;
sim.collectors.add(tracker);
// Round 1: establish common state
sim.run(1);
BEAST_EXPECT(sim.synchronized());
Ledger::Seq const prePartitionSeq = attackers[0]->lastClosedLedger.seq();
std::cout << "Pre-partition: all peers synced at seq " << prePartitionSeq << "\n";
// === PARTITION ===
legitimate.disconnect(network);
network.disconnect(legitimate);
for (Peer* p : legitimate)
p->targetLedgers = p->completedLedgers;
// SIMULATE MODIFIED BINARY: attackers bypass quorum
// In production, the attacker modifies ValidatorList::calculateQuorum()
// to return a lower value. In CSF, we override the quorum directly.
// Note: quorum is recalculated in checkFullyValidated() from
// trustGraph.graph().outDegree(this), so we need a different approach.
// We'll untrust the legitimate peers FROM THE ATTACKER'S PERSPECTIVE
// to simulate the modified binary lowering effective UNL.
attackers.untrust(legitimate);
// Now attackers only trust 3 peers → quorum = ceil(3 * 0.8) = 3
// They can fully validate with just their own validations
// Attackers submit transactions
for (Peer* p : attackers)
p->submit(Tx{static_cast<std::uint32_t>(p->id) + 200});
// Run — attackers advance and fully validate their fork
sim.run(4);
Ledger::Seq const attackerSeq = attackers[0]->lastClosedLedger.seq();
std::cout << "During partition (quorum bypass):\n";
std::cout << " Attacker closed seq: " << attackerSeq << "\n";
for (Peer* p : attackers)
{
std::cout << " Attacker " << p->id
<< " fullyValidated seq: " << p->fullyValidatedLedger.seq() << "\n";
}
// Attackers DID advance and fully validate (on their modified view)
BEAST_EXPECT(attackerSeq > prePartitionSeq);
for (Peer* p : attackers)
{
BEAST_EXPECT(p->fullyValidatedLedger.seq() > prePartitionSeq);
}
// === RECOVERY: restore trust and reconnect ===
// Restore attacker trust to full UNL (simulating reconnection
// where the legitimate validators are back)
attackers.trust(legitimate);
// Reconnect network
network.connect(network, delay);
// Allow legitimate peers to run again
for (Peer* p : legitimate)
p->targetLedgers = p->completedLedgers + 10;
// Run several rounds for recovery
sim.run(6);
std::cout << "\nAfter recovery:\n";
std::cout << " Branches: " << sim.branches() << "\n";
std::cout << " Synchronized: " << std::boolalpha << sim.synchronized() << "\n";
for (Peer* p : network)
{
std::cout << " Peer " << p->id << " closed=" << p->lastClosedLedger.seq()
<< " fullyValidated=" << p->fullyValidatedLedger.seq() << "\n";
}
// KEY ASSERTIONS:
// 1. Legitimate peers should NOT have accepted the attacker's
// fully validated ledger — their quorum is still 6/7
for (Peer* p : legitimate)
{
// Legitimate peers' fully validated ledger should be based on
// the pre-partition state or a new ledger with proper quorum,
// NOT the attacker's fork
std::size_t const numTrusted = sim.trustGraph.graph().outDegree(p);
std::cout << " Legitimate peer " << p->id << " trusts " << numTrusted
<< " quorum=" << static_cast<std::size_t>(std::ceil(numTrusted * 0.8))
<< "\n";
}
// 2. Check if the network re-converges or stays forked
std::size_t const branches = sim.branches();
std::cout << " Final branch count: " << branches << "\n";
// The attacker's chain (validated with only 3/7 trust)
// should not be accepted by the full network.
// After reconnection, with 7 validators all trusting each other,
// quorum is 6. The attacker's old validations only had 3.
// The network should eventually converge on a new chain.
}
/**
* Test 3: Transaction injection during partition
*
* 3 attackers inject extra transactions (simulating pseudo-tx injection)
* during the partition. Tests whether those transactions persist after
* recovery.
*/
void
testTxInjectionDuringPartition()
{
using namespace csf;
using namespace std::chrono;
testcase("partition: injected transactions during partition");
Sim sim;
ConsensusParms const parms{};
SimDuration const delay = round<milliseconds>(0.2 * parms.ledgerGRANULARITY);
PeerGroup attackers = sim.createGroup(3);
PeerGroup legitimate = sim.createGroup(4);
PeerGroup network = attackers + legitimate;
network.trust(network);
network.connect(network, delay);
PartitionTracker tracker;
sim.collectors.add(tracker);
// Establish common state
sim.run(1);
BEAST_EXPECT(sim.synchronized());
Ledger::Seq const prePartitionSeq = attackers[0]->lastClosedLedger.seq();
// === PARTITION ===
legitimate.disconnect(network);
network.disconnect(legitimate);
for (Peer* p : legitimate)
p->targetLedgers = p->completedLedgers;
// Attackers bypass quorum (modified binary simulation)
attackers.untrust(legitimate);
// Inject a "malicious" transaction into attacker ledgers
// This simulates an EnableAmendment pseudo-tx being injected
// by the modified binary's doVoting()
Tx const maliciousTx{999};
for (Peer* p : attackers)
{
p->txInjections.emplace(prePartitionSeq, maliciousTx);
}
// Run partition phase
sim.run(4);
// Check that attackers included the injected tx
Ledger const& attackerLedger = attackers[0]->lastClosedLedger;
std::cout << "Attacker ledger seq: " << attackerLedger.seq() << "\n";
// === RECOVERY ===
attackers.trust(legitimate);
network.connect(network, delay);
for (Peer* p : legitimate)
p->targetLedgers = p->completedLedgers + 10;
sim.run(6);
std::cout << "\nAfter recovery (tx injection):\n";
std::cout << " Branches: " << sim.branches() << "\n";
std::cout << " Synchronized: " << std::boolalpha << sim.synchronized() << "\n";
// Check each peer's final state
for (Peer* p : network)
{
std::cout << " Peer " << p->id << " closed=" << p->lastClosedLedger.seq()
<< " fullyValidated=" << p->fullyValidatedLedger.seq() << "\n";
}
// The legitimate peers should NOT have the injected transaction
// in their fully validated ledger chain. The attacker's fork
// (containing the injected tx) should be rejected because it
// lacks sufficient validations from the full UNL.
}
/**
* Test 4: Full attack surface sweep
*
* Sweeps across multiple network sizes (7, 10, 15, 20, 35) and
* all attacker counts to find the exact threshold where the
* Byzantine partition attack succeeds.
*/
void
testAttackSurfaceSweep()
{
using namespace csf;
using namespace std::chrono;
testcase("partition: full attack surface sweep");
// Network sizes to test (7 = small, 35 = mainnet-like)
std::vector<std::uint32_t> const networkSizes = {7, 10, 15, 20, 35};
for (std::uint32_t const totalValidators : networkSizes)
{
std::cout << "\n=== Network size: " << totalValidators << " validators ===\n";
std::cout << " 80% quorum = "
<< static_cast<std::size_t>(std::ceil(totalValidators * 0.8)) << "\n";
std::uint32_t attackThreshold = 0;
for (std::uint32_t numAttackers = 1; numAttackers < totalValidators; ++numAttackers)
{
std::uint32_t const numLegitimate = totalValidators - numAttackers;
Sim sim;
ConsensusParms const parms{};
SimDuration const delay = round<milliseconds>(0.2 * parms.ledgerGRANULARITY);
PeerGroup attackers = sim.createGroup(numAttackers);
PeerGroup legitimate = sim.createGroup(numLegitimate);
PeerGroup network = attackers + legitimate;
// Common UNL — all trust all
network.trust(network);
network.connect(network, delay);
// Round 1: establish common state
sim.run(1);
// === PARTITION: crash legitimate validators ===
legitimate.disconnect(network);
network.disconnect(legitimate);
// Stop legitimate peers from running
for (Peer* p : legitimate)
p->targetLedgers = p->completedLedgers;
// ATTACKER: bypass quorum by untrusting legitimate
attackers.untrust(legitimate);
// Attackers submit transactions
for (Peer* p : attackers)
p->submit(Tx{static_cast<std::uint32_t>(p->id) + 1000});
// Run partition phase (attackers produce chain)
sim.run(4);
// Record attacker FVL before recovery
Ledger::Seq const attackerFVLBeforeRecovery =
attackers[0]->fullyValidatedLedger.seq();
// === RECOVERY: reconnect ===
attackers.trust(legitimate);
network.connect(network, delay);
for (Peer* p : legitimate)
p->targetLedgers = p->completedLedgers + 15;
// Give plenty of recovery time
sim.run(10);
// Check if legitimate peers accepted attacker's chain
// by verifying their FVL ID matches an attacker's FVL ID
bool const legitimateAccepted = [&]() {
for (Peer* lp : legitimate)
{
if (lp->fullyValidatedLedger.seq() <= Ledger::Seq{1})
continue;
for (Peer* ap : attackers)
{
if (lp->fullyValidatedLedger.id() == ap->fullyValidatedLedger.id())
return true;
}
}
return false;
}();
bool const networkConverged = sim.synchronized();
std::size_t const branches = sim.branches();
float const attackerPct = 100.0f * numAttackers / totalValidators;
std::cout << " A=" << numAttackers << "/" << totalValidators << " ("
<< static_cast<int>(attackerPct) << "%)"
<< " AttackerFVL=" << attackerFVLBeforeRecovery
<< " LegitAccepted=" << std::boolalpha << legitimateAccepted
<< " Converged=" << networkConverged << " Branches=" << branches << "\n";
if (legitimateAccepted && attackThreshold == 0)
{
attackThreshold = numAttackers;
std::cout << " >>> ATTACK THRESHOLD: " << numAttackers << "/"
<< totalValidators << " (" << static_cast<int>(attackerPct) << "%)"
<< " <<<\n";
}
}
if (attackThreshold > 0)
{
float const thresholdPct = 100.0f * attackThreshold / totalValidators;
std::cout << " RESULT: Network " << totalValidators << " — attack succeeds at "
<< attackThreshold << " attackers (" << static_cast<int>(thresholdPct)
<< "%)\n";
}
else
{
std::cout << " RESULT: Network " << totalValidators
<< " — attack never succeeded\n";
}
// Byzantine safety: forcing the network to accept the attacker's
// chain requires a strict majority of validators (never a tolerated
// minority).
BEAST_EXPECT(attackThreshold == 0 || attackThreshold * 2 > totalValidators);
}
}
/**
* Test 5: Long partition — attacker builds deep chain
*
* What if the attacker runs for many rounds during the partition,
* building a much deeper chain? Does chain length affect acceptance?
*/
void
testLongPartitionDeepChain()
{
using namespace csf;
using namespace std::chrono;
testcase("partition: deep attacker chain");
for (int partitionRounds : {4, 10, 20, 40})
{
Sim sim;
ConsensusParms const parms{};
SimDuration const delay = round<milliseconds>(0.2 * parms.ledgerGRANULARITY);
PeerGroup attackers = sim.createGroup(4);
PeerGroup legitimate = sim.createGroup(3);
PeerGroup network = attackers + legitimate;
network.trust(network);
network.connect(network, delay);
sim.run(1);
// Partition
legitimate.disconnect(network);
network.disconnect(legitimate);
for (Peer* p : legitimate)
p->targetLedgers = p->completedLedgers;
attackers.untrust(legitimate);
for (Peer* p : attackers)
p->submit(Tx{static_cast<std::uint32_t>(p->id) + 500});
// Run for varying partition lengths
sim.run(partitionRounds);
Ledger::Seq const attackerDepth = attackers[0]->lastClosedLedger.seq();
Ledger::Seq const attackerFVL = attackers[0]->fullyValidatedLedger.seq();
// Recovery
attackers.trust(legitimate);
network.connect(network, delay);
for (Peer* p : legitimate)
p->targetLedgers = p->completedLedgers + 15;
sim.run(10);
bool const accepted = [&]() {
for (Peer* lp : legitimate)
{
if (lp->fullyValidatedLedger.seq() <= Ledger::Seq{1})
continue;
for (Peer* ap : attackers)
{
if (lp->fullyValidatedLedger.id() == ap->fullyValidatedLedger.id())
return true;
}
}
return false;
}();
std::cout << "PartitionRounds=" << partitionRounds << " AttackerDepth=" << attackerDepth
<< " AttackerFVL=" << attackerFVL << " LegitAccepted=" << std::boolalpha
<< accepted << " Branches=" << sim.branches()
<< " Synced=" << sim.synchronized() << "\n";
// With the attacker holding the majority (4 of 7), its chain is
// accepted regardless of how deep the partition ran — depth is not
// the deciding factor, validator share is.
BEAST_EXPECT(accepted);
}
}
/**
* Test 6: Staggered recovery
*
* Legitimate validators don't all come back at once.
* What if 2 come back first, then 2 more later?
* Does partial recovery change the outcome?
*/
void
testStaggeredRecovery()
{
using namespace csf;
using namespace std::chrono;
testcase("partition: staggered recovery");
Sim sim;
ConsensusParms const parms{};
SimDuration const delay = round<milliseconds>(0.2 * parms.ledgerGRANULARITY);
PeerGroup attackers = sim.createGroup(4);
PeerGroup legit1 = sim.createGroup(2); // first wave recovery
PeerGroup legit2 = sim.createGroup(1); // second wave recovery
PeerGroup legitimate = legit1 + legit2;
PeerGroup network = attackers + legitimate;
network.trust(network);
network.connect(network, delay);
sim.run(1);
std::cout << "Pre-partition: all synced at seq " << attackers[0]->lastClosedLedger.seq()
<< "\n";
// Partition all legitimate
legitimate.disconnect(network);
network.disconnect(legitimate);
for (Peer* p : legitimate)
p->targetLedgers = p->completedLedgers;
attackers.untrust(legitimate);
for (Peer* p : attackers)
p->submit(Tx{static_cast<std::uint32_t>(p->id) + 600});
sim.run(4);
std::cout << "After partition: attackers at seq " << attackers[0]->lastClosedLedger.seq()
<< " FVL=" << attackers[0]->fullyValidatedLedger.seq() << "\n";
// WAVE 1: partial recovery — only 2 legitimate peers return
attackers.trust(legit1);
legit1.trust(network);
network.connect(legit1, delay);
legit1.connect(network, delay);
for (Peer* p : legit1)
p->targetLedgers = p->completedLedgers + 10;
sim.run(6);
std::cout << "After wave 1 recovery (2 legit back):\n";
for (Peer* p : network)
{
std::cout << " Peer " << p->id << " closed=" << p->lastClosedLedger.seq()
<< " FVL=" << p->fullyValidatedLedger.seq() << "\n";
}
// WAVE 2: remaining legitimate peers return
attackers.trust(legit2);
legit2.trust(network);
network.connect(legit2, delay);
legit2.connect(network, delay);
for (Peer* p : legit2)
p->targetLedgers = p->completedLedgers + 10;
sim.run(6);
std::cout << "After wave 2 recovery (all back):\n";
std::cout << " Branches=" << sim.branches() << " Synced=" << sim.synchronized() << "\n";
for (Peer* p : network)
{
std::cout << " Peer " << p->id << " closed=" << p->lastClosedLedger.seq()
<< " FVL=" << p->fullyValidatedLedger.seq() << "\n";
}
// Once every legitimate validator is back, the network reconverges to a
// single chain.
BEAST_EXPECT(sim.synchronized());
}
/**
* Test 7: Attacker injects conflicting transactions
*
* During partition, attackers inject transactions into their chain.
* After recovery, do legitimate peers end up with the attacker's
* injected transactions in their fully validated chain?
* This directly tests the amendment bypass scenario.
*/
void
testConflictingTxPersistence()
{
using namespace csf;
using namespace std::chrono;
testcase("partition: conflicting tx persistence after recovery");
Sim sim;
ConsensusParms const parms{};
SimDuration const delay = round<milliseconds>(0.2 * parms.ledgerGRANULARITY);
// 4 attackers, 3 legitimate (attack SHOULD succeed)
PeerGroup attackers = sim.createGroup(4);
PeerGroup legitimate = sim.createGroup(3);
PeerGroup network = attackers + legitimate;
network.trust(network);
network.connect(network, delay);
sim.run(1);
Ledger::Seq const prePartitionSeq = attackers[0]->lastClosedLedger.seq();
// Partition
legitimate.disconnect(network);
network.disconnect(legitimate);
for (Peer* p : legitimate)
p->targetLedgers = p->completedLedgers;
attackers.untrust(legitimate);
// Inject a "malicious" transaction ONLY on attacker nodes
// This simulates an EnableAmendment pseudo-tx bypass
Tx const maliciousTx{9999};
for (Peer* p : attackers)
p->txInjections.emplace(prePartitionSeq, maliciousTx);
sim.run(4);
// Record the attacker ledger that contains the injected tx
Ledger::ID const attackerLedgerWithTx = attackers[0]->lastClosedLedger.id();
Ledger::Seq const attackerSeqWithTx = attackers[0]->lastClosedLedger.seq();
std::cout << "Attacker injected tx at seq " << attackerSeqWithTx << "\n";
// Recovery
attackers.trust(legitimate);
network.connect(network, delay);
for (Peer* p : legitimate)
p->targetLedgers = p->completedLedgers + 15;
sim.run(10);
// Check: does any legitimate peer have the attacker's ledger
// (containing the injected tx) in their chain?
bool const legitimateHasAttackerLedger = [&]() {
for (Peer* lp : legitimate)
{
// Check if the attacker ledger is in the legitimate
// peer's ledger cache
auto it = lp->ledgers.find(attackerLedgerWithTx);
if (it != lp->ledgers.end())
return true;
}
return false;
}();
bool const legitimateFVLDescendsFromAttacker = [&]() {
for (Peer* lp : legitimate)
{
// Check if legitimate peer's FVL is built on the
// attacker's chain
if (lp->fullyValidatedLedger.seq() >= attackerSeqWithTx)
{
// Check if the attacker ledger is an ancestor
auto it = lp->ledgers.find(attackerLedgerWithTx);
if (it != lp->ledgers.end() && lp->fullyValidatedLedger.isAncestor(it->second))
return true;
}
}
return false;
}();
std::cout << "After recovery:\n"
<< " LegitHasAttackerLedger=" << std::boolalpha << legitimateHasAttackerLedger
<< "\n"
<< " LegitFVLDescendsFromAttacker=" << legitimateFVLDescendsFromAttacker << "\n"
<< " Branches=" << sim.branches() << " Synced=" << sim.synchronized() << "\n";
for (Peer* p : network)
{
std::cout << " Peer " << p->id << " closed=" << p->lastClosedLedger.seq()
<< " FVL=" << p->fullyValidatedLedger.seq() << "\n";
}
// The amendment-bypass safety property: a minority attacker's injected
// transactions never enter the legitimate fully-validated chain.
BEAST_EXPECT(!legitimateFVLDescendsFromAttacker);
}
/**
* Test 8: Gradual nUNL-style takeover
*
* Instead of crashing all legitimate validators at once,
* the attacker gradually removes them from trust (simulating
* nUNL additions), then produces a chain.
*/
void
testGradualTakeover()
{
using namespace csf;
using namespace std::chrono;
testcase("partition: gradual nUNL-style takeover");
Sim sim;
ConsensusParms const parms{};
SimDuration const delay = round<milliseconds>(0.2 * parms.ledgerGRANULARITY);
// 4 attackers, 3 legitimate
PeerGroup attackers = sim.createGroup(4);
PeerGroup legit1 = sim.createGroup(1);
PeerGroup legit2 = sim.createGroup(1);
PeerGroup legit3 = sim.createGroup(1);
PeerGroup legitimate = legit1 + legit2 + legit3;
PeerGroup network = attackers + legitimate;
network.trust(network);
network.connect(network, delay);
sim.run(1);
std::cout << "All synced at seq " << network[0]->lastClosedLedger.seq() << "\n";
// Phase 1: crash legit1, attackers remove from trust
legit1.disconnect(network);
network.disconnect(legit1);
for (Peer* p : legit1)
p->targetLedgers = p->completedLedgers;
attackers.untrust(legit1);
sim.run(2);
std::cout << "Phase 1 (1 crashed): network at seq " << attackers[0]->lastClosedLedger.seq()
<< " FVL=" << attackers[0]->fullyValidatedLedger.seq()
<< " branches=" << sim.branches() << "\n";
// Phase 2: crash legit2
legit2.disconnect(network);
network.disconnect(legit2);
for (Peer* p : legit2)
p->targetLedgers = p->completedLedgers;
attackers.untrust(legit2);
sim.run(2);
std::cout << "Phase 2 (2 crashed): network at seq " << attackers[0]->lastClosedLedger.seq()
<< " FVL=" << attackers[0]->fullyValidatedLedger.seq()
<< " branches=" << sim.branches() << "\n";
// Phase 3: crash legit3 — attackers are now alone
legit3.disconnect(network);
network.disconnect(legit3);
for (Peer* p : legit3)
p->targetLedgers = p->completedLedgers;
attackers.untrust(legit3);
// Now attackers trust only themselves (4/4 quorum = 4)
for (Peer* p : attackers)
p->submit(Tx{static_cast<std::uint32_t>(p->id) + 700});
sim.run(4);
std::cout << "Phase 3 (all crashed): attackers at seq "
<< attackers[0]->lastClosedLedger.seq()
<< " FVL=" << attackers[0]->fullyValidatedLedger.seq() << "\n";
// Recovery: all legitimate come back at once
attackers.trust(legitimate);
network.connect(network, delay);
for (Peer* p : legitimate)
p->targetLedgers = p->completedLedgers + 15;
sim.run(10);
bool const accepted = [&]() {
for (Peer* lp : legitimate)
{
if (lp->fullyValidatedLedger.seq() <= Ledger::Seq{1})
continue;
for (Peer* ap : attackers)
{
if (lp->fullyValidatedLedger.id() == ap->fullyValidatedLedger.id())
return true;
}
}
return false;
}();
std::cout << "After recovery:\n"
<< " LegitAccepted=" << std::boolalpha << accepted
<< " Branches=" << sim.branches() << " Synced=" << sim.synchronized() << "\n";
// A gradual nUNL-style takeover that leaves the attacker holding the
// majority (4 of 7) does flip the network onto the attacker's chain —
// the reason runtime nUNL/quorum changes need coordination guardrails.
BEAST_EXPECT(accepted);
for (Peer* p : network)
{
std::cout << " Peer " << p->id << " closed=" << p->lastClosedLedger.seq()
<< " FVL=" << p->fullyValidatedLedger.seq() << "\n";
}
}
void
run() override
{
testPartitionNoQuorumBypass();
testPartitionWithQuorumBypass();
testTxInjectionDuringPartition();
testAttackSurfaceSweep();
testLongPartitionDeepChain();
testStaggeredRecovery();
testConflictingTxPersistence();
testGradualTakeover();
}
};
BEAST_DEFINE_TESTSUITE_MANUAL(ByzantinePartitionRecovery, consensus, xrpl);
} // namespace xrpl::test

View File

@@ -680,7 +680,8 @@ private:
.count();
header->uptime = UptimeClock::now().time_since_epoch().count();
header->io_latency_us = app_.getIOLatency().count();
header->validation_quorum = 0; // TODO: fork validator-list accessor
header->validation_quorum = app_.getValidators().quorum();
header->server_state = static_cast<std::uint32_t>(ops.getOperatingMode());
header->peer_count = app_.getOverlay().size();
header->node_size = app_.config().nodeSize;
@@ -701,9 +702,8 @@ private:
if (ops.getOperatingMode() != OperatingMode::FULL)
header->warning_flags |= WARNING_NOT_SYNCED;
// Consensus timing is private on this fork's NetworkOPs; zeroed.
header->proposer_count = 0;
header->converge_time_ms = 0;
header->proposer_count = ops.getPrevProposers();
header->converge_time_ms = ops.getPrevRoundTime().count();
auto const fp = ledgerMaster.getFetchPackCacheSize();
if (fp != 0)
@@ -792,8 +792,13 @@ private:
header->rates.disk_read = rates.disk_read;
header->rates.disk_write = rates.disk_write;
// Ledger height via a stable accessor (this fork's Ledger lacks info()).
header->ledger_seq = ledgerMaster.getValidLedgerIndex();
// Ledger height + hash via stable accessors (this fork's Ledger lacks
// info()). The hash lets the collector detect a fork: divergent
// ledger_hash across nodes at the same ledger_seq.
std::uint32_t const validSeq = ledgerMaster.getValidLedgerIndex();
header->ledger_seq = validSeq;
uint256 const validHash = ledgerMaster.getHashBySeq(validSeq);
std::memcpy(header->ledger_hash, validHash.data(), 32);
header->reserve_base = app_.config().fees.accountReserve.drops();
header->reserve_inc = app_.config().fees.ownerReserve.drops();
@@ -807,13 +812,6 @@ private:
BuildInfo::getVersionString().size() > 32 ? 32 : BuildInfo::getVersionString().size());
header->ledger_range_count = 0;
// Populate the DebugCounters block (db KB, hit rates, tree-node cache/track
// sizes, nodestore counts). getDebugCounters() was implemented but never
// called, so this fixed-offset block shipped all-zero — blinding the
// per-node memory-driver telemetry (treenode_track_size etc.).
header->dbg_counters = getDebugCounters().first;
return buffer;
}
void

View File

@@ -511,8 +511,19 @@ public:
void
consensusViewChange() override;
void
setStall(std::chrono::milliseconds duration) override;
bool
isStalled() const override;
void
clearStall() override;
json::Value
getConsensusInfo() override;
std::size_t
getPrevProposers() const override;
std::chrono::milliseconds
getPrevRoundTime() const override;
json::Value
getServerInfo(bool human, bool admin, bool counters) override;
void
@@ -791,6 +802,8 @@ private:
std::atomic<bool> amendmentWarned_{false};
std::atomic<bool> unlBlocked_{false};
std::atomic<std::int64_t> stallDeadlineMs_{0};
ClosureCounter<void, boost::system::error_code const&> waitHandlerCounter_;
boost::asio::steady_timer heartbeatTimer_;
boost::asio::steady_timer clusterTimer_;
@@ -1129,6 +1142,13 @@ NetworkOPsImp::processHeartbeatTimer()
CLOG(clog.ss()) << ". ";
}
if (isStalled())
{
CLOG(clog.ss()) << "node is stalled, skipping consensus timerEntry. ";
setHeartbeatTimer();
return;
}
consensus_.timerEntry(registry_.get().getTimeKeeper().closeTime(), clog.ss());
CLOG(clog.ss()) << "consensus phase " << to_string(lastConsensusPhase_);
@@ -2201,6 +2221,35 @@ NetworkOPsImp::consensusViewChange()
}
}
void
NetworkOPsImp::setStall(std::chrono::milliseconds duration)
{
auto const deadline = std::chrono::steady_clock::now() + duration;
auto const ms =
std::chrono::duration_cast<std::chrono::milliseconds>(deadline.time_since_epoch()).count();
stallDeadlineMs_.store(ms, std::memory_order_relaxed);
JLOG(journal_.warn()) << "Node stalled for " << duration.count() << "ms";
}
bool
NetworkOPsImp::isStalled() const
{
auto const deadline = stallDeadlineMs_.load(std::memory_order_relaxed);
if (deadline == 0)
return false;
auto const now = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
return now < deadline;
}
void
NetworkOPsImp::clearStall()
{
stallDeadlineMs_.store(0, std::memory_order_relaxed);
JLOG(journal_.warn()) << "Node stall cleared";
}
void
NetworkOPsImp::pubManifest(Manifest const& mo)
{
@@ -2593,6 +2642,18 @@ NetworkOPsImp::getConsensusInfo()
return consensus_.getJson(true);
}
std::size_t
NetworkOPsImp::getPrevProposers() const
{
return consensus_.prevProposers();
}
std::chrono::milliseconds
NetworkOPsImp::getPrevRoundTime() const
{
return consensus_.prevRoundTime();
}
json::Value
NetworkOPsImp::getServerInfo(bool human, bool admin, bool counters)
{

View File

@@ -12,6 +12,7 @@
#include <boost/intrusive/set.hpp>
#include <optional>
#include <vector>
namespace xrpl {
@@ -38,7 +39,14 @@ class Config;
*/
class TxQ
{
private:
std::mutex debugTxInjectMutex;
std::vector<STTx> debugTxInjectQueue;
public:
void
debugTxInject(STTx const& txn);
/// Fee level for single-signed reference transaction.
static constexpr FeeLevel64 kBaseLevel{256};

View File

@@ -657,6 +657,11 @@ public:
hash_set<PublicKey>
getTrustedMasterKeys() const;
void
debugSetTrusted(
std::vector<PublicKey> const& validators,
std::optional<std::size_t> quorumOverride = std::nullopt);
/**
* get the validator list threshold
* @return the threshold

View File

@@ -98,6 +98,13 @@ increase(FeeLevel64 level, std::uint32_t increasePercent)
//////////////////////////////////////////////////////////////////////////
void
TxQ::debugTxInject(STTx const& txn)
{
std::lock_guard<std::mutex> const _(debugTxInjectMutex);
debugTxInjectQueue.push_back(txn);
}
std::size_t
TxQ::FeeMetrics::update(
Application& app,
@@ -1418,6 +1425,21 @@ TxQ::accept(Application& app, OpenView& view)
auto const metricsSnapshot = feeMetrics_.getSnapshot();
// try to inject any debug txns waiting in the debug queue
{
std::unique_lock<std::mutex> trylock(TxQ::debugTxInjectMutex, std::try_to_lock);
if (trylock.owns_lock() && !debugTxInjectQueue.empty())
{
for (STTx const& txn : debugTxInjectQueue)
{
auto const [result, didApply, metadata] = xrpl::apply(app, view, txn, TapNone, j_);
if (didApply)
ledgerChanged = true;
}
debugTxInjectQueue.clear();
}
}
for (auto candidateIter = byFee_.begin(); candidateIter != byFee_.end();)
{
auto& account = byAccount_.at(candidateIter->account);

View File

@@ -2078,6 +2078,48 @@ ValidatorList::getTrustedMasterKeys() const
return trustedMasterKeys_;
}
void
ValidatorList::debugSetTrusted(
std::vector<PublicKey> const& validators,
std::optional<std::size_t> quorumOverride)
{
std::lock_guard const lock{mutex_};
trustedMasterKeys_.clear();
keyListings_.clear();
localPublisherList_.list.clear();
for (auto const& k : validators)
{
trustedMasterKeys_.insert(k);
keyListings_[k] = 1;
localPublisherList_.list.push_back(k);
}
trustedSigningKeys_.clear();
for (auto const& k : trustedMasterKeys_)
{
auto const signingKey = validatorManifests_.getSigningKey(k);
if (signingKey)
trustedSigningKeys_.insert(*signingKey);
else
trustedSigningKeys_.insert(k);
}
if (quorumOverride)
{
quorum_ = *quorumOverride;
}
else
{
auto const unlSize = trustedMasterKeys_.size();
quorum_ = calculateQuorum(unlSize, unlSize, unlSize);
}
JLOG(j_.warn()) << "debugSetTrusted: set " << trustedMasterKeys_.size()
<< " trusted validators, quorum=" << quorum_;
}
std::size_t
ValidatorList::getListThreshold() const
{

View File

@@ -228,6 +228,10 @@ Handler const kHandlerArray[]{
.valueMethod = byRef(&doNFTSellOffers),
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "node_stall",
.valueMethod = byRef(&doNodeStall),
.role = Role::ADMIN,
.condition = Condition::NoCondition},
{.name = "noripple_check",
.valueMethod = byRef(&doNoRippleCheck),
.role = Role::USER,
@@ -294,6 +298,10 @@ Handler const kHandlerArray[]{
.valueMethod = byRef(&doSignFor),
.role = Role::USER,
.condition = Condition::NoCondition},
{.name = "inject",
.valueMethod = byRef(&doInject),
.role = Role::ADMIN,
.condition = Condition::NeedsCurrentLedger},
{.name = "simulate",
.valueMethod = byRef(&doSimulate),
.role = Role::USER,
@@ -332,6 +340,10 @@ Handler const kHandlerArray[]{
.valueMethod = byRef(&doUnlList),
.role = Role::ADMIN,
.condition = Condition::NoCondition},
{.name = "unl_set",
.valueMethod = byRef(&doUnlSet),
.role = Role::ADMIN,
.condition = Condition::NoCondition},
{.name = "validation_create",
.valueMethod = byRef(&doValidationCreate),
.role = Role::ADMIN,

View File

@@ -79,6 +79,8 @@ doNFTBuyOffers(RPC::JsonContext&);
json::Value
doNFTSellOffers(RPC::JsonContext&);
json::Value
doNodeStall(RPC::JsonContext&);
json::Value
doNoRippleCheck(RPC::JsonContext&);
json::Value
doOwnerInfo(RPC::JsonContext&);
@@ -117,6 +119,8 @@ doSignFor(RPC::JsonContext&);
json::Value
doSimulate(RPC::JsonContext&);
json::Value
doInject(RPC::JsonContext&);
json::Value
doStop(RPC::JsonContext&);
json::Value
doSubmit(RPC::JsonContext&);
@@ -135,6 +139,8 @@ doTxReduceRelay(RPC::JsonContext&);
json::Value
doUnlList(RPC::JsonContext&);
json::Value
doUnlSet(RPC::JsonContext&);
json::Value
doUnsubscribe(RPC::JsonContext&);
json::Value
doValidationCreate(RPC::JsonContext&);

View File

@@ -3,11 +3,15 @@
#include <xrpld/rpc/Context.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/RPCErr.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol/tokens.h>
#include <optional>
#include <utility>
#include <vector>
namespace xrpl {
@@ -29,4 +33,52 @@ doUnlList(RPC::JsonContext& context)
return obj;
}
json::Value
doUnlSet(RPC::JsonContext& context)
{
if (context.role != Role::ADMIN)
return rpcError(RpcNoPermission);
if (!context.params.isMember(jss::validators) || !context.params[jss::validators].isArray())
return rpcError(RpcInvalidParams);
auto const& vals = context.params[jss::validators];
std::vector<PublicKey> keys;
keys.reserve(vals.size());
for (auto const& v : vals)
{
if (!v.isString())
return rpcError(RpcInvalidParams);
auto const pk = parseBase58<PublicKey>(TokenType::NodePublic, v.asString());
if (!pk)
{
json::Value jvResult;
jvResult[jss::error] = "invalidParams";
jvResult[jss::error_message] = "Invalid validator key: " + v.asString();
return jvResult;
}
keys.push_back(*pk);
}
std::optional<std::size_t> quorumOverride;
if (context.params.isMember("quorum") && context.params["quorum"].isIntegral())
{
quorumOverride = context.params["quorum"].asUInt();
}
context.app.getValidators().debugSetTrusted(keys, quorumOverride);
json::Value jvResult;
jvResult[jss::validators] = json::Value(json::ValueType::Array);
for (auto const& k : keys)
{
jvResult[jss::validators].append(toBase58(TokenType::NodePublic, k));
}
jvResult["quorum"] = static_cast<json::UInt>(context.app.getValidators().quorum());
jvResult[jss::status] = "success";
return jvResult;
}
} // namespace xrpl

View File

@@ -0,0 +1,44 @@
#include <xrpld/rpc/Context.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/RPCErr.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/server/NetworkOPs.h>
namespace xrpl {
json::Value
doNodeStall(RPC::JsonContext& context)
{
if (context.role != Role::ADMIN)
return rpcError(RpcNoPermission);
if (context.params.isMember("clear") && context.params["clear"].asBool())
{
context.netOps.clearStall();
json::Value jvResult;
jvResult[jss::status] = "success";
jvResult["stalled"] = false;
return jvResult;
}
std::int64_t durationMs = 30000;
if (context.params.isMember("duration_ms") && context.params["duration_ms"].isIntegral())
{
durationMs = context.params["duration_ms"].asInt();
if (durationMs <= 0)
return rpcError(RpcInvalidParams);
}
context.netOps.setStall(std::chrono::milliseconds(durationMs));
json::Value jvResult;
jvResult[jss::status] = "success";
jvResult["stalled"] = true;
jvResult["duration_ms"] = static_cast<int>(durationMs);
return jvResult;
}
} // namespace xrpl

View File

@@ -1,5 +1,6 @@
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/app/misc/Transaction.h>
#include <xrpld/app/misc/TxQ.h>
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/Role.h>
#include <xrpld/rpc/detail/TransactionSign.h>
@@ -37,6 +38,37 @@ getFailHard(RPC::JsonContext const& context)
context.params.isMember(jss::fail_hard) && context.params[jss::fail_hard].asBool());
}
json::Value
doInject(RPC::JsonContext& context)
{
if (context.role != Role::ADMIN)
return rpcError(RpcNoPermission);
json::Value jvResult;
auto ret = strUnHex(context.params[jss::tx_blob].asString());
if (!ret || !ret->size())
return rpcError(RpcInvalidParams);
SerialIter sitTrans(makeSlice(*ret));
std::shared_ptr<STTx const> stpTrans;
try
{
stpTrans = std::make_shared<STTx const>(std::ref(sitTrans));
}
catch (std::exception& e)
{
jvResult[jss::error] = "invalidTransaction";
jvResult[jss::error_exception] = e.what();
jvResult[jss::in_queue] = false;
return jvResult;
}
context.app.getTxQ().debugTxInject(*stpTrans);
jvResult[jss::tx_json] = stpTrans->getJson(JsonOptions::Values::None);
jvResult[jss::in_queue] = true;
return jvResult;
}
// {
// tx_blob: <string> XOR tx_json: <object>,
// secret: <secret>