Files
xahaud/src/test/consensus/ConsensusRng_test.cpp
2026-07-16 18:02:15 +07:00

1609 lines
55 KiB
C++

//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012-2016 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <test/csf.h>
#include <test/unit_test/SuiteJournal.h>
#include <xrpld/consensus/Consensus.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/EntropyTier.h>
#include <algorithm>
namespace ripple {
namespace test {
class ConsensusRng_test : public beast::unit_test::suite
{
SuiteJournal journal_;
public:
ConsensusRng_test() : journal_("ConsensusRng_test", *this)
{
}
void
testRngCommitRevealConverges()
{
using namespace csf;
using namespace std::chrono;
testcase("RNG commit/reveal converges");
ConsensusParms const parms{};
Sim sim;
PeerGroup peers = sim.createGroup(5);
peers.trustAndConnect(
peers, round<milliseconds>(0.2 * parms.ledgerGRANULARITY));
// Per-peer CE enablement keeps CSF on a single Peer type, which
// minimizes maintenance and upstream sync churn in Sim/PeerGroup.
for (Peer* peer : peers)
peer->ce().enableRngConsensus_ = true;
// Warmup: let peer proposals and close times settle before checking
// the RNG pipeline.
sim.run(1);
BEAST_EXPECT(sim.synchronized());
sim.run(3);
if (BEAST_EXPECT(sim.synchronized()))
{
for (Peer const* peer : peers)
{
BEAST_EXPECT(!peer->ce().lastEntropyWasFallback_);
BEAST_EXPECT(peer->ce().lastEntropyCount_ > 0);
BEAST_EXPECT(
peer->ce().lastEntropyDenominator_ == peers.size());
BEAST_EXPECT(peer->ce().lastEntropyDigest_ != uint256{});
}
}
}
void
testRngCommitRevealConvergesWithTransactions()
{
using namespace csf;
using namespace std::chrono;
testcase("RNG commit/reveal converges with non-empty tx set");
ConsensusParms const parms{};
Sim sim;
PeerGroup peers = sim.createGroup(5);
peers.trustAndConnect(
peers, round<milliseconds>(0.2 * parms.ledgerGRANULARITY));
for (Peer* peer : peers)
peer->ce().enableRngConsensus_ = true;
// Warmup: let peer proposals and close times settle before checking
// the RNG pipeline.
sim.run(1);
BEAST_EXPECT(sim.synchronized());
// Submit transactions for the real test round.
for (Peer* peer : peers)
peer->submit(Tx(static_cast<std::uint32_t>(peer->id)));
sim.run(1);
if (BEAST_EXPECT(sim.synchronized()))
{
for (Peer const* peer : peers)
{
auto const& lcl = peer->lastClosedLedger;
BEAST_EXPECT(!peer->ce().lastEntropyWasFallback_);
BEAST_EXPECT(peer->ce().lastEntropyCount_ > 0);
BEAST_EXPECT(peer->ce().lastEntropyDigest_ != uint256{});
BEAST_EXPECT(lcl.txs().size() > 0);
}
}
}
void
testRngQuorumImpossibleFallsToTier2()
{
using namespace csf;
using namespace std::chrono;
testcase("RNG quorum-impossible cohort falls to participant_aligned");
ConsensusParms const parms{};
Sim sim;
PeerGroup majority = sim.createGroup(2);
PeerGroup isolated = sim.createGroup(1);
PeerGroup network = majority + isolated;
for (Peer* peer : network)
peer->ce().enableRngConsensus_ = true;
// First run fully connected so expected proposers include all three.
network.trust(network);
network.connect(
network, round<milliseconds>(0.2 * parms.ledgerGRANULARITY));
sim.run(1);
// Then isolate one node so 80% quorum becomes impossible for majority.
majority.disconnect(isolated);
isolated.disconnect(majority);
majority.connect(
majority, round<milliseconds>(0.2 * parms.ledgerGRANULARITY));
sim.run(1);
if (BEAST_EXPECT(sim.synchronized(majority)))
{
for (Peer const* peer : majority)
{
// 2 of 3 is below the 80% quorum but at the tier-2 floor (n=3:
// tier2 == 2), so the surviving cohort mints
// participant_aligned entropy instead of falling back — and
// still converges (no hang, no fork): same non-zero digest and
// count across all.
BEAST_EXPECT(!peer->ce().lastEntropyWasFallback_);
BEAST_EXPECT(
peer->ce().lastEntropyTier_ ==
entropyTierParticipantAligned);
BEAST_EXPECT(peer->ce().lastEntropyCount_ == 2);
BEAST_EXPECT(peer->ce().lastEntropyDigest_ != uint256{});
BEAST_EXPECT(
peer->ce().lastEntropyDigest_ ==
majority[0]->ce().lastEntropyDigest_);
}
}
}
void
testRngPersistentLossDoesNotShrinkQuorum()
{
using namespace csf;
using namespace std::chrono;
testcase("RNG persistent loss does not shrink quorum");
ConsensusParms const parms{};
Sim sim;
PeerGroup majority = sim.createGroup(2);
PeerGroup isolated = sim.createGroup(1);
PeerGroup network = majority + isolated;
for (Peer* peer : network)
peer->ce().enableRngConsensus_ = true;
network.trust(network);
network.connect(
network, round<milliseconds>(0.2 * parms.ledgerGRANULARITY));
// Seed recent-proposer hints from a fully connected round.
sim.run(1);
// Then isolate one validator and run multiple degraded rounds. Commit
// quorum must remain fixed to the active trusted set (3 -> threshold 3)
// rather than silently shrinking to the 2 surviving peers.
majority.disconnect(isolated);
isolated.disconnect(majority);
majority.connect(
majority, round<milliseconds>(0.2 * parms.ledgerGRANULARITY));
sim.run(2);
if (BEAST_EXPECT(sim.synchronized(majority)))
{
for (Peer const* peer : majority)
{
// The 2 survivors of a 3-validator UNL mint the labeled-weaker
// participant_aligned (tier 2), NOT validator_quorum (tier 3):
// the tier-3 quorum did not silently shrink to 2 (a min_tier=3
// hook still rejects this). Deterministic + identical across
// the group — no fork.
BEAST_EXPECT(!peer->ce().lastEntropyWasFallback_);
BEAST_EXPECT(
peer->ce().lastEntropyTier_ ==
entropyTierParticipantAligned);
BEAST_EXPECT(peer->ce().lastEntropyCount_ == 2);
BEAST_EXPECT(peer->ce().lastEntropyDigest_ != uint256{});
BEAST_EXPECT(
peer->ce().lastEntropyDigest_ ==
majority[0]->ce().lastEntropyDigest_);
}
}
}
void
testRngTier2MintByBandCohort()
{
using namespace csf;
using namespace std::chrono;
testcase("RNG tier 2 minted by in-band aligned cohort");
// 6 validators — the smallest NON-degenerate tier-2 size: f=1 (one
// tolerated fault) and a one-wide band {4} (tier2=4, quorum=5). Isolate
// 2 so the surviving 4-validator cohort is below the 80% quorum but at
// the tier-2 floor. It mints participant_aligned entropy, and all four
// agree on the same non-zero digest — no hang, no fork.
ConsensusParms const parms{};
Sim sim;
PeerGroup cohort = sim.createGroup(4);
PeerGroup isolated = sim.createGroup(2);
PeerGroup network = cohort + isolated;
for (Peer* peer : network)
peer->ce().enableRngConsensus_ = true;
network.trust(network);
network.connect(
network, round<milliseconds>(0.2 * parms.ledgerGRANULARITY));
sim.run(1);
cohort.disconnect(isolated);
isolated.disconnect(cohort);
cohort.connect(
cohort, round<milliseconds>(0.2 * parms.ledgerGRANULARITY));
sim.run(2);
if (BEAST_EXPECT(sim.synchronized(cohort)))
{
BEAST_EXPECT(sim.branches(cohort) == 1);
for (Peer const* peer : cohort)
{
BEAST_EXPECT(!peer->ce().lastEntropyWasFallback_);
BEAST_EXPECT(
peer->ce().lastEntropyTier_ ==
entropyTierParticipantAligned);
BEAST_EXPECT(peer->ce().lastEntropyCount_ == 4);
BEAST_EXPECT(peer->ce().lastEntropyDigest_ != uint256{});
BEAST_EXPECT(
peer->ce().lastEntropyDigest_ ==
cohort[0]->ce().lastEntropyDigest_);
}
}
}
void
testRngTimeoutWithPartialQuorum()
{
using namespace csf;
using namespace std::chrono;
testcase("RNG timeout with partial quorum keeps entropy");
ConsensusParms const parms{};
Sim sim;
PeerGroup majority = sim.createGroup(4);
PeerGroup isolated = sim.createGroup(1);
PeerGroup network = majority + isolated;
for (Peer* peer : network)
peer->ce().enableRngConsensus_ = true;
network.trust(network);
network.connect(
network, round<milliseconds>(0.2 * parms.ledgerGRANULARITY));
// Seed expected proposers from a fully connected round.
sim.run(1);
// Isolate one expected proposer. Majority should still progress after
// timeout using available commit quorum instead of consensus_fallback
// fallback.
majority.disconnect(isolated);
isolated.disconnect(majority);
majority.connect(
majority, round<milliseconds>(0.2 * parms.ledgerGRANULARITY));
sim.run(1);
if (BEAST_EXPECT(sim.synchronized(majority)))
{
for (Peer const* peer : majority)
{
BEAST_EXPECT(!peer->ce().lastEntropyWasFallback_);
BEAST_EXPECT(peer->ce().lastEntropyDigest_ != uint256{});
BEAST_EXPECT(peer->ce().lastEntropyCount_ > 0);
}
}
}
void
testRngCommitSetMinorityConflictCannotForceFallback()
{
using namespace csf;
using namespace std::chrono;
testcase("RNG commitSet minority conflict cannot force fallback");
ConsensusParms const parms{};
Sim sim;
PeerGroup peers = sim.createGroup(5);
for (Peer* peer : peers)
peer->ce().enableRngConsensus_ = true;
peers.trustAndConnect(
peers, round<milliseconds>(0.2 * parms.ledgerGRANULARITY));
// Keep tx-set convergence and commit/reveal material intact, but force
// one peer to advertise a different commitSetHash. After bounded grace,
// honest peers must reveal and let the entropy-root qV resolve.
peers[0]->ce().forcedCommitSetHash_ =
sha512Half(std::string("forced-csf"));
sim.run(1);
if (BEAST_EXPECT(sim.synchronized(peers)))
{
for (Peer const* peer : peers)
{
BEAST_EXPECT(!peer->ce().lastEntropyWasFallback_);
BEAST_EXPECT(peer->ce().lastEntropyDigest_ != uint256{});
BEAST_EXPECT(peer->ce().lastEntropyCount_ >= 4);
BEAST_EXPECT(
peer->ce().lastEntropyDigest_ ==
peers[0]->ce().lastEntropyDigest_);
}
}
}
void
testRngObserverDoesNotExpectSelfCommit()
{
using namespace csf;
testcase("RNG observer does not expect self commit");
Sim sim;
PeerGroup peers = sim.createGroup(2);
Peer* validator = peers[0];
Peer* observer = peers[1];
PeerGroup validatorGroup{validator};
PeerGroup observerGroup{observer};
for (Peer* peer : peers)
peer->ce().enableRngConsensus_ = true;
observer->runAsValidator = false;
validatorGroup.trust(validatorGroup);
observerGroup.trust(validatorGroup);
observer->ce().cacheUNLReport();
BEAST_EXPECT(observer->ce().unlNodes_.count(observer->id) == 0);
BEAST_EXPECT(observer->ce().unlNodes_.count(validator->id) == 1);
hash_set<PeerID> proposers;
proposers.insert(observer->id);
proposers.insert(validator->id);
observer->ce().setExpectedProposers(std::move(proposers));
BEAST_EXPECT(
observer->ce().likelyParticipants_.count(observer->id) == 0);
BEAST_EXPECT(
observer->ce().likelyParticipants_.count(validator->id) == 1);
observer->ce().pendingCommits_[validator->id] = sha512Half(42u);
BEAST_EXPECT(observer->ce().hasQuorumOfCommits());
}
void
testRngIgnoresNonUNLData()
{
using namespace csf;
testcase("RNG ignores non-UNL data");
Sim sim;
PeerGroup peers = sim.createGroup(1);
Peer* peer = peers[0];
peer->ce().enableRngConsensus_ = true;
peer->ce().cacheUNLReport();
ProposalPosition pos;
pos.myCommitment = sha512Half(1u);
pos.myReveal = sha512Half(2u);
// NodeID 999 is not in this peer's UNL report (which contains only
// self).
peer->ce().harvestRngData(
PeerID{999},
PeerKey{PeerID{999}, 0},
pos,
0,
peer->now(),
peer->lastClosedLedger.id(),
0);
BEAST_EXPECT(peer->ce().pendingCommits_.empty());
BEAST_EXPECT(peer->ce().pendingReveals_.empty());
}
void
testRngRejectsRevealWithoutCommit()
{
using namespace csf;
testcase("RNG rejects reveal without commit");
Sim sim;
PeerGroup peers = sim.createGroup(1);
Peer* peer = peers[0];
peer->ce().enableRngConsensus_ = true;
peer->ce().cacheUNLReport();
ProposalPosition pos;
pos.myReveal = sha512Half(3u);
peer->ce().harvestRngData(
peer->id,
peer->key,
pos,
0,
peer->now(),
peer->lastClosedLedger.id(),
0);
BEAST_EXPECT(peer->ce().pendingCommits_.empty());
BEAST_EXPECT(peer->ce().pendingReveals_.empty());
}
void
testRngRejectsInvalidReveal()
{
using namespace csf;
testcase("RNG rejects invalid reveal");
Sim sim;
PeerGroup peers = sim.createGroup(1);
Peer* peer = peers[0];
peer->ce().enableRngConsensus_ = true;
peer->ce().cacheUNLReport();
auto const seq =
static_cast<std::uint32_t>(peer->lastClosedLedger.seq()) + 1;
auto const committedReveal = sha512Half(10u);
auto const invalidReveal = sha512Half(11u);
auto const commitment = sha512Half(
committedReveal,
static_cast<std::uint32_t>(peer->id),
peer->key.second,
seq);
ProposalPosition commitPos;
commitPos.myCommitment = commitment;
peer->ce().harvestRngData(
peer->id,
peer->key,
commitPos,
0,
peer->now(),
peer->lastClosedLedger.id(),
0);
ProposalPosition revealPos;
revealPos.myReveal = invalidReveal;
peer->ce().harvestRngData(
peer->id,
peer->key,
revealPos,
0,
peer->now(),
peer->lastClosedLedger.id(),
0);
BEAST_EXPECT(peer->ce().pendingCommits_.size() == 1);
BEAST_EXPECT(peer->ce().pendingReveals_.empty());
}
void
testRngCommitChangeClearsStaleReveal()
{
using namespace csf;
testcase("RNG commit change clears stale reveal");
Sim sim;
PeerGroup peers = sim.createGroup(1);
Peer* peer = peers[0];
peer->ce().enableRngConsensus_ = true;
peer->ce().cacheUNLReport();
auto const seq =
static_cast<std::uint32_t>(peer->lastClosedLedger.seq()) + 1;
auto const revealA = sha512Half(20u);
auto const revealB = sha512Half(21u);
auto const commitA = sha512Half(
revealA,
static_cast<std::uint32_t>(peer->id),
peer->key.second,
seq);
auto const commitB = sha512Half(
revealB,
static_cast<std::uint32_t>(peer->id),
peer->key.second,
seq);
ProposalPosition commitPosA;
commitPosA.myCommitment = commitA;
peer->ce().harvestRngData(
peer->id,
peer->key,
commitPosA,
0,
peer->now(),
peer->lastClosedLedger.id(),
0);
ProposalPosition revealPosA;
revealPosA.myReveal = revealA;
peer->ce().harvestRngData(
peer->id,
peer->key,
revealPosA,
0,
peer->now(),
peer->lastClosedLedger.id(),
0);
BEAST_EXPECT(peer->ce().pendingReveals_.size() == 1);
// Commitment changes after reveal was accepted. The old reveal is now
// cryptographically stale and must no longer count toward reveal
// quorum.
ProposalPosition commitPosB;
commitPosB.myCommitment = commitB;
peer->ce().harvestRngData(
peer->id,
peer->key,
commitPosB,
0,
peer->now(),
peer->lastClosedLedger.id(),
0);
BEAST_EXPECT(peer->ce().pendingCommits_.size() == 1);
BEAST_EXPECT(peer->ce().pendingReveals_.empty());
}
void
testRngRevealTimeoutAsymmetricDelays()
{
using namespace csf;
using namespace std::chrono;
testcase("RNG reveal timeout under asymmetric delays");
ConsensusParms const parms{};
Sim sim;
PeerGroup groupA = sim.createGroup(3);
PeerGroup groupB = sim.createGroup(3);
PeerGroup network = groupA + groupB;
for (Peer* peer : network)
peer->ce().enableRngConsensus_ = true;
network.trust(network);
auto const fast = round<milliseconds>(0.2 * parms.ledgerGRANULARITY);
groupA.connect(groupA, fast);
groupB.connect(groupB, fast);
// Cross-group links are intentionally slower than rngREVEAL_TIMEOUT.
auto const slow = round<milliseconds>(2.0 * parms.ledgerGRANULARITY);
groupA.connect(groupB, slow);
groupB.connect(groupA, slow);
sim.run(1);
// If this ever forks/splits, reveal-timeout handling is allowing
// non-deterministic entropy subsets to close.
BEAST_EXPECT(sim.branches(network) == 1);
BEAST_EXPECT(sim.synchronized(network));
}
void
testRngEntropyConvergesWithPartialReveals()
{
using namespace csf;
using namespace std::chrono;
testcase("RNG entropy converges with partial reveal subsets");
// 6 peers in two groups. Group A drops reveals from peer 5,
// group B drops reveals from peer 0. Both groups have > 80%
// quorum of reveals but DIFFERENT subsets.
//
// Without the entropySetHash convergence gate, these groups
// compute different entropy -> different pseudo-tx -> fork.
//
// With the gate, they must either accept the same observed reveal set
// root or both use the labeled consensus_fallback digest. Either way:
// no two validator-entropy roots are accepted.
ConsensusParms const parms{};
Sim sim;
PeerGroup groupA = sim.createGroup(3);
PeerGroup groupB = sim.createGroup(3);
PeerGroup network = groupA + groupB;
for (Peer* peer : network)
peer->ce().enableRngConsensus_ = true;
network.trust(network);
auto const fast = round<milliseconds>(0.2 * parms.ledgerGRANULARITY);
network.connect(network, fast);
// Warmup: let peer proposals and close times settle before checking
// the RNG pipeline.
sim.run(1);
BEAST_EXPECT(sim.synchronized(network));
// Group A never sees peer 5's reveal
for (Peer* peer : groupA)
peer->ce().dropRevealFrom_.insert(network[5]->id);
// Group B never sees peer 0's reveal
for (Peer* peer : groupB)
peer->ce().dropRevealFrom_.insert(network[0]->id);
sim.run(3);
// Must not fork. A lagging peer can transiently fail
// sim.synchronized() without violating the real invariant we care
// about here: peers that accepted the same ledger must agree on
// entropy for that ledger.
BEAST_EXPECT(sim.branches(network) == 1);
auto sameBranch = [](auto const& lhs, auto const& rhs) {
if (lhs.id() == rhs.id())
return true;
if (lhs.seq() < rhs.seq())
return rhs.isAncestor(lhs);
return lhs.isAncestor(rhs);
};
for (Peer const* lhs : network)
{
for (Peer const* rhs : network)
{
BEAST_EXPECT(
sameBranch(lhs->lastClosedLedger, rhs->lastClosedLedger));
if (lhs->lastClosedLedger.id() != rhs->lastClosedLedger.id())
continue;
BEAST_EXPECT(
lhs->ce().lastEntropyDigest_ ==
rhs->ce().lastEntropyDigest_);
BEAST_EXPECT(
lhs->ce().lastEntropyCount_ == rhs->ce().lastEntropyCount_);
BEAST_EXPECT(
lhs->ce().lastEntropyWasFallback_ ==
rhs->ce().lastEntropyWasFallback_);
}
}
}
void
testRngEntropyFallbackOnMajorRevealLoss()
{
using namespace csf;
using namespace std::chrono;
testcase("RNG entropy uses consensus_fallback on major reveal loss");
// 5 peers. Peer 0 drops reveals from peers 2, 3, 4
// (only sees 2/5 reveals = 40%, below 80% quorum).
// All other peers see all reveals.
//
// Peer 0 must fall back to consensus_fallback entropy.
// The network must still agree (either all use full entropy
// from the converged set, or all fall back).
ConsensusParms const parms{};
Sim sim;
PeerGroup peers = sim.createGroup(5);
for (Peer* peer : peers)
peer->ce().enableRngConsensus_ = true;
peers.trustAndConnect(
peers, round<milliseconds>(0.2 * parms.ledgerGRANULARITY));
// Warmup: populate prevProposers.
sim.run(1);
BEAST_EXPECT(sim.synchronized(peers));
// Peer 0 drops most reveals
peers[0]->ce().dropRevealFrom_.insert(peers[2]->id);
peers[0]->ce().dropRevealFrom_.insert(peers[3]->id);
peers[0]->ce().dropRevealFrom_.insert(peers[4]->id);
sim.run(3);
// Peer 0 may desync from the group because it missed most
// reveals and fell behind on a previous round. The important
// invariant is: peers that stayed in sync must agree on
// entropy, and that entropy should be consensus_fallback since
// the reveal asymmetry means not all honest reveal sets
// can converge within the bounded window.
//
// Verify: no multi-branch fork, and the synchronized group
// agrees on the fallback entropy.
BEAST_EXPECT(sim.branches(peers) <= 2);
// Find the majority group and verify they agree
std::vector<Peer const*> majority;
for (Peer* p : peers)
{
if (p->prevLedgerID() == peers[1]->prevLedgerID())
majority.push_back(p);
}
BEAST_EXPECT(majority.size() >= 4);
for (Peer const* peer : majority)
{
// All in the majority group should agree on entropy
BEAST_EXPECT(
peer->ce().lastEntropyDigest_ ==
majority[0]->ce().lastEntropyDigest_);
}
}
void
testRngSingleByzantineCannotDenyEntropy()
{
using namespace csf;
using namespace std::chrono;
testcase("RNG single Byzantine validator cannot deny entropy");
// 5 peers, all see all reveals. Peer 0 forces a different
// entropy set hash (simulating a Byzantine node publishing
// a garbage entropySetHash).
//
// The remaining 4/5 (80%) should still produce valid entropy.
// The Byzantine node's hash should be outvoted by supermajority.
ConsensusParms const parms{};
Sim sim;
PeerGroup peers = sim.createGroup(5);
for (Peer* peer : peers)
peer->ce().enableRngConsensus_ = true;
peers.trustAndConnect(
peers, round<milliseconds>(0.2 * parms.ledgerGRANULARITY));
// Warmup: populate prevProposers.
sim.run(1);
BEAST_EXPECT(sim.synchronized(peers));
peers[0]->ce().forcedEntropySetHash_ =
sha512Half(std::string("byzantine-entropy"));
sim.run(3);
PeerGroup honest{
std::vector<Peer*>{peers[1], peers[2], peers[3], peers[4]}};
BEAST_EXPECT(sim.branches(honest) == 1);
BEAST_EXPECT(sim.synchronized(honest));
// One bad hash is below the validator_quorum threshold. The honest 4/5
// quorum should agree on validator entropy instead of letting a single
// validator deny the round's entropy.
for (Peer const* peer : honest)
{
BEAST_EXPECT(!peer->ce().lastEntropyWasFallback_);
BEAST_EXPECT(peer->ce().lastEntropyDigest_ != uint256{});
BEAST_EXPECT(peer->ce().lastEntropyCount_ > 0);
}
}
void
testRngSingleSilentValidatorCannotDenyEntropy()
{
using namespace csf;
using namespace std::chrono;
testcase("RNG single silent validator cannot deny entropy");
// Peer 0 remains active in tx consensus and builds its reveal sidecar,
// but does not advertise entropySetHash. The fixed active-view
// denominator still includes it; the remaining 4/5 quorum must be
// enough to accept the clean, non-conflicting entropy set.
ConsensusParms const parms{};
Sim sim;
PeerGroup peers = sim.createGroup(5);
for (Peer* peer : peers)
peer->ce().enableRngConsensus_ = true;
peers.trustAndConnect(
peers, round<milliseconds>(0.2 * parms.ledgerGRANULARITY));
// Warmup: populate prevProposers.
sim.run(1);
BEAST_EXPECT(sim.synchronized(peers));
peers[0]->ce().suppressOwnEntropySetHash_ = true;
sim.run(3);
PeerGroup honest{
std::vector<Peer*>{peers[1], peers[2], peers[3], peers[4]}};
BEAST_EXPECT(sim.branches(honest) == 1);
BEAST_EXPECT(sim.synchronized(honest));
for (Peer const* peer : honest)
{
BEAST_EXPECT(!peer->ce().lastEntropyWasFallback_);
BEAST_EXPECT(peer->ce().lastEntropyDigest_ != uint256{});
BEAST_EXPECT(peer->ce().lastEntropyCount_ >= 4);
BEAST_EXPECT(
peer->ce().lastEntropyDigest_ ==
honest[0]->ce().lastEntropyDigest_);
}
}
void
testRngEntropyHashConflictWithoutQuorumFallsBackToZero()
{
using namespace csf;
using namespace std::chrono;
testcase("RNG entropy hash conflict without threshold falls back");
ConsensusParms const parms{};
Sim sim;
PeerGroup peers = sim.createGroup(5);
for (Peer* peer : peers)
peer->ce().enableRngConsensus_ = true;
peers.trustAndConnect(
peers, round<milliseconds>(0.2 * parms.ledgerGRANULARITY));
// Warmup: populate prevProposers.
sim.run(1);
BEAST_EXPECT(sim.synchronized(peers));
// Two peers advertise entropy-set hashes that nobody can materialize
// from local proposal-carried material.
// The remaining 3/5 do not form an entropy quorum, so the safe
// outcome is consensus_fallback instead of mixed validator/fallback
// results.
peers[0]->ce().forcedEntropySetHash_ =
sha512Half(std::string("forced-entropy-conflict-a"));
peers[1]->ce().forcedEntropySetHash_ =
sha512Half(std::string("forced-entropy-conflict-b"));
sim.run(3);
BEAST_EXPECT(sim.branches(peers) == 1);
for (Peer const* peer : peers)
{
BEAST_EXPECT(peer->ce().lastEntropyWasFallback_);
BEAST_EXPECT(peer->ce().lastEntropyDigest_ != uint256{});
BEAST_EXPECT(
peer->ce().lastEntropyDigest_ ==
peers[0]->ce().lastEntropyDigest_);
BEAST_EXPECT(peer->ce().lastEntropyCount_ == 0);
BEAST_EXPECT(peer->ce().lastEntropyDenominator_ == 0);
}
}
void
testRngEntropyRejectsEquivocatedSplitMajorities()
{
using namespace csf;
using namespace std::chrono;
testcase("RNG entropy rejects equivocated split majorities");
// Five active validators. Peer 2 equivocates: left peers see it
// advertise H-left, right peers see it advertise H-right. Each side
// locally observes 2 honest peers + the equivocator = 3/5. A strict
// majority or naive ceil(0.6*n) threshold would admit both sides; the
// real participant threshold is 4/5, so both sides must fall back.
ConsensusParms const parms{};
Sim sim;
PeerGroup peers = sim.createGroup(5);
PeerGroup left{std::vector<Peer*>{peers[0], peers[1]}};
Peer* equivocator = peers[2];
PeerGroup right{std::vector<Peer*>{peers[3], peers[4]}};
PeerGroup honest = left + right;
for (Peer* peer : peers)
peer->ce().enableRngConsensus_ = true;
auto const fast = round<milliseconds>(0.2 * parms.ledgerGRANULARITY);
peers.trustAndConnect(peers, fast);
sim.run(1);
BEAST_EXPECT(sim.synchronized(peers));
left.disconnect(right);
auto const leftHash = sha512Half(std::string("entropy-equiv-left"));
auto const rightHash = sha512Half(std::string("entropy-equiv-right"));
for (Peer* peer : left)
peer->ce().forcedEntropySetHash_ = leftHash;
for (Peer* peer : right)
peer->ce().forcedEntropySetHash_ = rightHash;
for (Peer* peer : left)
equivocator->ce().equivocateSidecarsTo_[peer->id].entropySetHash =
leftHash;
for (Peer* peer : right)
equivocator->ce().equivocateSidecarsTo_[peer->id].entropySetHash =
rightHash;
sim.sidecarStore.publish(
leftHash,
SidecarStore::Type::reveal,
SidecarStore::EntrySet{
{peers[0]->id, sha512Half(std::string("left-0"))},
{peers[1]->id, sha512Half(std::string("left-1"))},
{equivocator->id, sha512Half(std::string("left-equiv"))}});
sim.sidecarStore.publish(
rightHash,
SidecarStore::Type::reveal,
SidecarStore::EntrySet{
{equivocator->id, sha512Half(std::string("right-equiv"))},
{peers[3]->id, sha512Half(std::string("right-3"))},
{peers[4]->id, sha512Half(std::string("right-4"))}});
sim.run(3);
for (Peer const* peer : honest)
{
BEAST_EXPECT(peer->ce().lastEntropyWasFallback_);
BEAST_EXPECT(
peer->ce().lastEntropyTier_ == entropyTierConsensusFallback);
BEAST_EXPECT(peer->ce().lastEntropyCount_ == 0);
BEAST_EXPECT(peer->ce().lastEntropyDenominator_ == 0);
BEAST_EXPECT(peer->ce().lastEntropyDigest_ != uint256{});
}
BEAST_EXPECT(
peers[0]->ce().lastEntropyDigest_ ==
peers[1]->ce().lastEntropyDigest_);
BEAST_EXPECT(
peers[3]->ce().lastEntropyDigest_ ==
peers[4]->ce().lastEntropyDigest_);
}
void
testRngMissingProposalMaterialDoesNotBlockQuorumCohort()
{
using namespace csf;
using namespace std::chrono;
testcase("RNG missing proposal material does not block quorum cohort");
// Same-round sidecar reconciliation is intentionally gone. If peer 0's
// proposal-carried reveal is missed by the rest of the active view, the
// quorum cohort should still be able to build the 4/5 entropy set while
// peer 0 cannot materialize that accepted root locally and falls back.
//
// CSF ledgers model the base tx-set only; production CE pseudo bytes
// would make peer 0's synthetic ledger differ until validations pull it
// back to the quorum-built ledger. Keep the CE side-band assertions
// explicit so this test is not mistaken for a pseudo-ledger hash proof.
ConsensusParms const parms{};
Sim sim;
PeerGroup peers = sim.createGroup(5);
for (Peer* peer : peers)
peer->ce().enableRngConsensus_ = true;
peers.trustAndConnect(
peers, round<milliseconds>(0.2 * parms.ledgerGRANULARITY));
sim.run(1);
BEAST_EXPECT(sim.synchronized(peers));
for (std::size_t i = 1; i < peers.size(); ++i)
peers[i]->ce().dropRevealFrom_.insert(peers[0]->id);
sim.run(1);
BEAST_EXPECT(sim.branches(peers) == 1);
auto const& cohortDigest = peers[1]->ce().lastEntropyDigest_;
BEAST_EXPECT(cohortDigest != uint256{});
for (std::size_t i = 1; i < peers.size(); ++i)
{
BEAST_EXPECT(peers[i]->ce().lastEntropyDigest_ == cohortDigest);
BEAST_EXPECT(!peers[i]->ce().lastEntropyWasFallback_);
BEAST_EXPECT(
peers[i]->ce().lastEntropyTier_ == entropyTierValidatorQuorum);
BEAST_EXPECT(peers[i]->ce().lastEntropyCount_ == 4);
BEAST_EXPECT(peers[i]->ce().lastEntropyDenominator_ == 5);
}
BEAST_EXPECT(peers[0]->ce().lastEntropyWasFallback_);
BEAST_EXPECT(
peers[0]->ce().lastEntropyTier_ == entropyTierConsensusFallback);
BEAST_EXPECT(peers[0]->ce().lastEntropyCount_ == 0);
BEAST_EXPECT(peers[0]->ce().lastEntropyDenominator_ == 0);
BEAST_EXPECT(peers[0]->ce().lastEntropyDigest_ != cohortDigest);
}
void
testRngNoEntropyWithoutPeerAlignment()
{
using namespace csf;
using namespace std::chrono;
testcase("RNG no validator entropy without peer alignment");
// 5 peers. All peers see all reveals (healthy network).
// But peer 0 drops ALL incoming proposals after publishing
// its entropy set — simulating a node that publishes but
// never sees any peer's entropySetHash response.
//
// Without the alignment check, peer 0 would accept non-zero
// entropy based purely on its own local view (no peer
// confirmation).
//
// With the alignment check, peer 0 must see at least some
// peers agreeing on the same hash before accepting non-zero
// entropy. If it can't see any alignment within the bounded
// window, it must fall back to zero.
//
// The key invariant: no node should accept validator entropy
// unless it has observed positive peer agreement on the same
// entropySetHash.
ConsensusParms const parms{};
Sim sim;
PeerGroup peers = sim.createGroup(5);
for (Peer* peer : peers)
peer->ce().enableRngConsensus_ = true;
peers.trustAndConnect(
peers, round<milliseconds>(0.2 * parms.ledgerGRANULARITY));
// Warmup
sim.run(1);
BEAST_EXPECT(sim.synchronized(peers));
sim.run(3);
// All peers should agree — either all have the same entropy
// (since all reveals are available), or some fall back to zero.
// The key check: no peer should have validator entropy that
// differs from the majority.
BEAST_EXPECT(sim.synchronized(peers));
auto const& refDigest = peers[0]->ce().lastEntropyDigest_;
for (Peer const* peer : peers)
BEAST_EXPECT(peer->ce().lastEntropyDigest_ == refDigest);
// At least some peers should have validator entropy
// (healthy network, all reveals available)
BEAST_EXPECT(refDigest != uint256{});
}
void
testRngAlignmentRequiredForNonZeroEntropy()
{
using namespace csf;
using namespace std::chrono;
testcase("RNG alignment required — isolated node falls back");
// 5 peers. Peer 0 is isolated after the warmup round:
// it can still propose but receives no proposals back.
// This means peer 0 publishes its entropySetHash but never
// sees any peer's entropySetHash — aligned=0, peersSeen=0.
//
// The alignment gate should prevent peer 0 from accepting
// validator entropy without peer confirmation. Instead it
// should fall back to zero or desync.
ConsensusParms const parms{};
Sim sim;
PeerGroup peers = sim.createGroup(5);
for (Peer* peer : peers)
peer->ce().enableRngConsensus_ = true;
peers.trustAndConnect(
peers, round<milliseconds>(0.2 * parms.ledgerGRANULARITY));
// Warmup
sim.run(1);
BEAST_EXPECT(sim.synchronized(peers));
// Isolate peer 0: drop all reveals from it so its
// entropy set will differ, AND it won't see peer alignment
// because its entropy hash won't match anyone else's.
for (std::size_t i = 1; i < peers.size(); ++i)
peers[0]->ce().dropRevealFrom_.insert(peers[i]->id);
// Run just 1 round — enough to exercise the gate.
// Run just one round so the scenario stays focused on the reveal
// alignment gate.
sim.run(1);
// The majority (peers 1-4) should agree on validator entropy from
// proposal-carried material they can locally materialize.
std::vector<Peer const*> majority;
for (std::size_t i = 1; i < peers.size(); ++i)
majority.push_back(peers[i]);
auto const& majorityDigest = majority[0]->ce().lastEntropyDigest_;
BEAST_EXPECT(majorityDigest != uint256{});
for (Peer const* peer : majority)
BEAST_EXPECT(peer->ce().lastEntropyDigest_ == majorityDigest);
// Peer 0 cannot reconstruct the majority root without the missing
// proposal-carried reveals. With same-round reconciliation removed, it
// must fall back locally rather than treating the peer-advertised root
// as materialized.
auto const& p0Digest = peers[0]->ce().lastEntropyDigest_;
BEAST_EXPECT(p0Digest != majorityDigest);
BEAST_EXPECT(peers[0]->ce().lastEntropyWasFallback_);
}
void
run() override
{
// Set XAHAU_RNG_TEST=<name> to run a single test method.
// e.g. XAHAU_RNG_TEST=SingleByzantine
auto const* filter = std::getenv("XAHAU_RNG_TEST");
std::string f = filter ? filter : "";
#define RUN(method) \
do \
{ \
if (f.empty() || std::string(#method).find(f) != std::string::npos) \
method(); \
} while (false)
RUN(testRngCommitRevealConverges);
RUN(testRngCommitRevealConvergesWithTransactions);
RUN(testRngQuorumImpossibleFallsToTier2);
RUN(testRngPersistentLossDoesNotShrinkQuorum);
RUN(testRngTier2MintByBandCohort);
RUN(testRngTimeoutWithPartialQuorum);
RUN(testRngCommitSetMinorityConflictCannotForceFallback);
RUN(testRngObserverDoesNotExpectSelfCommit);
RUN(testRngIgnoresNonUNLData);
RUN(testRngRejectsRevealWithoutCommit);
RUN(testRngRejectsInvalidReveal);
RUN(testRngCommitChangeClearsStaleReveal);
RUN(testRngRevealTimeoutAsymmetricDelays);
RUN(testRngEntropyConvergesWithPartialReveals);
RUN(testRngEntropyFallbackOnMajorRevealLoss);
RUN(testRngSingleByzantineCannotDenyEntropy);
RUN(testRngSingleSilentValidatorCannotDenyEntropy);
RUN(testRngEntropyHashConflictWithoutQuorumFallsBackToZero);
RUN(testRngEntropyRejectsEquivocatedSplitMajorities);
RUN(testRngMissingProposalMaterialDoesNotBlockQuorumCohort);
RUN(testRngNoEntropyWithoutPeerAlignment);
RUN(testRngAlignmentRequiredForNonZeroEntropy);
#undef RUN
}
};
BEAST_DEFINE_TESTSUITE(ConsensusRng, consensus, ripple);
class ConsensusExport_test : public beast::unit_test::suite
{
SuiteJournal journal_;
static void
releaseValidatedExports(csf::PeerGroup const& peers)
{
for (csf::Peer* peer : peers)
if (auto const share = peer->ce().releaseExportForValidated(
peer->fullyValidatedLedger))
peer->share(*share);
}
public:
ConsensusExport_test() : journal_("ConsensusExport_test", *this)
{
}
void
testExportOnlySteadyStateSucceeds()
{
using namespace csf;
using namespace std::chrono;
testcase("Export-only sig set converges");
ConsensusParms const parms{};
Sim sim;
PeerGroup peers = sim.createGroup(5);
for (Peer* peer : peers)
peer->ce().enableExportConsensus_ = true;
peers.trustAndConnect(
peers, round<milliseconds>(0.2 * parms.ledgerGRANULARITY));
sim.run(1);
BEAST_EXPECT(sim.synchronized(peers));
releaseValidatedExports(peers);
sim.run(1);
BEAST_EXPECT(sim.synchronized(peers));
for (Peer const* peer : peers)
{
BEAST_EXPECT(peer->ce().lastExportSucceeded_);
BEAST_EXPECT(!peer->ce().lastExportDeferred_);
}
}
void
testExportOnlyQuorumIgnoresMinorityConflict()
{
using namespace csf;
using namespace std::chrono;
testcase("Export-only sig set quorum ignores minority conflict");
ConsensusParms const parms{};
Sim sim;
PeerGroup peers = sim.createGroup(5);
for (Peer* peer : peers)
peer->ce().enableExportConsensus_ = true;
peers.trustAndConnect(
peers, round<milliseconds>(0.2 * parms.ledgerGRANULARITY));
sim.run(1);
BEAST_EXPECT(sim.synchronized(peers));
releaseValidatedExports(peers);
peers[0]->ce().forcedExportSigSetHash_ =
sha512Half(std::string("forced-export-only"));
sim.run(2);
PeerGroup honest{
std::vector<Peer*>{peers[1], peers[2], peers[3], peers[4]}};
BEAST_EXPECT(sim.branches(honest) == 1);
BEAST_EXPECT(sim.synchronized(honest));
for (Peer const* peer : honest)
{
BEAST_EXPECT(peer->ce().lastExportSucceeded_);
BEAST_EXPECT(!peer->ce().lastExportDeferred_);
}
BEAST_EXPECT(!peers[0]->ce().lastExportSucceeded_);
}
void
testExportMissingBothSharePathsRecoversPreferredLedger()
{
using namespace csf;
using namespace std::chrono;
testcase("Export missing both share paths recovers preferred ledger");
ConsensusParms const parms{};
Sim sim;
PeerGroup peers = sim.createGroup(5);
for (Peer* peer : peers)
{
peer->ce().enableExportConsensus_ = true;
peer->ce().modelExportWitnessLedgerEffect_ = true;
}
CollectByNode<JumpCollector> jumps;
sim.collectors.add(jumps);
peers[0]->ce().suppressOwnExportSig_ = true;
for (std::size_t i = 1; i < peers.size(); ++i)
{
peers[0]->ce().dropExportSigFrom_.insert(peers[i]->id);
peers[0]->ce().dropDirectExportSigFrom_.insert(peers[i]->id);
}
peers.trustAndConnect(
peers, round<milliseconds>(0.2 * parms.ledgerGRANULARITY));
// First establish one common fully validated Export origin. Direct
// release is then scheduled before the next simulated round, so it is
// processed after round-state clearing and before initial proposals.
sim.run(1);
BEAST_EXPECT(sim.synchronized(peers));
releaseValidatedExports(peers);
sim.run(1);
PeerGroup honest{
std::vector<Peer*>{peers[1], peers[2], peers[3], peers[4]}};
auto const witnessLedger = peers[1]->lastClosedLedger;
auto const witnessEffect = witnessLedger.consensusExtensionEffect();
BEAST_EXPECT(witnessEffect);
for (Peer const* peer : honest)
{
BEAST_EXPECT(peer->lastClosedLedger.id() == witnessLedger.id());
BEAST_EXPECT(
peer->lastClosedLedger.consensusExtensionEffect() ==
witnessEffect);
BEAST_EXPECT(peer->ce().lastExportSucceeded_);
BEAST_EXPECT(!peer->ce().lastExportDeferred_);
}
auto const witnesslessLedger = peers[0]->lastClosedLedger;
BEAST_EXPECT(!witnesslessLedger.consensusExtensionEffect());
BEAST_EXPECT(witnesslessLedger.id() != witnessLedger.id());
BEAST_EXPECT(witnesslessLedger.parentID() == witnessLedger.parentID());
BEAST_EXPECT(witnesslessLedger.seq() == witnessLedger.seq());
BEAST_EXPECT(witnesslessLedger.txs() == witnessLedger.txs());
BEAST_EXPECT(
witnesslessLedger.closeTimeResolution() ==
witnessLedger.closeTimeResolution());
BEAST_EXPECT(
witnesslessLedger.closeTime() == witnessLedger.closeTime());
BEAST_EXPECT(!peers[0]->ce().lastExportSucceeded_);
BEAST_EXPECT(peers[0]->ce().lastExportDeferred_);
hash_set<PeerID> alignedRootProposers;
auto const positions =
peers[0]->peerPositions.find(witnesslessLedger.parentID());
if (BEAST_EXPECT(positions != peers[0]->peerPositions.end()) &&
witnessEffect)
{
for (auto const& proposal : positions->second)
{
if (proposal.position().exportSigSetHash == witnessEffect)
alignedRootProposers.insert(proposal.nodeID());
}
}
BEAST_EXPECT(
alignedRootProposers.size() >=
peers[0]->ce().exportRootAlignmentThreshold());
for (std::size_t i = 1; i < peers.size(); ++i)
{
BEAST_EXPECT(
peers[0]->ce().droppedDirectExportSigs_.contains(peers[i]->id));
BEAST_EXPECT(peers[0]->ce().droppedProposalExportSigs_.contains(
peers[i]->id));
}
// Four witness-ledger validations make that branch preferred. Peer 0
// must never fully validate its local witness-less build and must
// promote the acquired witness ledger before subsequent recovery.
BEAST_EXPECT(peers[0]->fullyValidatedLedger.id() == witnessLedger.id());
BEAST_EXPECT(
peers[0]->fullyValidatedLedger.id() != witnesslessLedger.id());
for (Peer* peer : peers)
peer->ce().enableExportConsensus_ = false;
sim.run(3);
BEAST_EXPECT(sim.branches(peers) == 1);
auto const recoveredValidated = peers[0]->fullyValidatedLedger.id();
for (Peer const* peer : peers)
{
BEAST_EXPECT(peer->fullyValidatedLedger.id() == recoveredValidated);
BEAST_EXPECT(peer->lastClosedLedger.isAncestor(witnessLedger));
BEAST_EXPECT(peer->fullyValidatedLedger.isAncestor(witnessLedger));
}
auto const& peer0Jumps = jumps[peers[0]->id].closeJumps;
BEAST_EXPECT(std::any_of(
peer0Jumps.begin(), peer0Jumps.end(), [&](auto const& jump) {
return jump.from.id() == witnesslessLedger.id() &&
(jump.to.id() == witnessLedger.id() ||
jump.to.isAncestor(witnessLedger));
}));
}
void
testExportSigSetQuorumAlignmentIgnoresMinorityConflict()
{
using namespace csf;
using namespace std::chrono;
testcase("Export sig set quorum ignores minority conflict");
ConsensusParms const parms{};
Sim sim;
PeerGroup peers = sim.createGroup(5);
for (Peer* peer : peers)
{
peer->ce().enableRngConsensus_ = true;
peer->ce().enableExportConsensus_ = true;
}
peers.trustAndConnect(
peers, round<milliseconds>(0.2 * parms.ledgerGRANULARITY));
// Warmup: let peer proposals and close times settle before checking
// the extension tick scenario.
sim.run(1);
BEAST_EXPECT(sim.synchronized(peers));
releaseValidatedExports(peers);
peers[0]->ce().forcedExportSigSetHash_ =
sha512Half(std::string("forced-export-minority"));
sim.run(3);
PeerGroup honest{
std::vector<Peer*>{peers[1], peers[2], peers[3], peers[4]}};
BEAST_EXPECT(sim.branches(honest) == 1);
BEAST_EXPECT(sim.synchronized(honest));
for (Peer const* peer : honest)
{
BEAST_EXPECT(peer->ce().lastExportSucceeded_);
BEAST_EXPECT(!peer->ce().lastExportDeferred_);
}
BEAST_EXPECT(!peers[0]->ce().lastExportSucceeded_);
}
void
testExportSigSetConflictWithoutQuorumRetries()
{
using namespace csf;
using namespace std::chrono;
testcase("Export sig set conflict without quorum retries");
ConsensusParms const parms{};
Sim sim;
PeerGroup peers = sim.createGroup(5);
for (Peer* peer : peers)
{
peer->ce().enableRngConsensus_ = true;
peer->ce().enableExportConsensus_ = true;
}
peers.trustAndConnect(
peers, round<milliseconds>(0.2 * parms.ledgerGRANULARITY));
sim.run(1);
BEAST_EXPECT(sim.synchronized(peers));
releaseValidatedExports(peers);
peers[0]->ce().forcedExportSigSetHash_ =
sha512Half(std::string("forced-export-conflict-a"));
peers[1]->ce().forcedExportSigSetHash_ =
sha512Half(std::string("forced-export-conflict-b"));
sim.run(3);
BEAST_EXPECT(sim.branches(peers) == 1);
for (Peer const* peer : peers)
{
BEAST_EXPECT(!peer->ce().lastExportSucceeded_);
BEAST_EXPECT(peer->ce().lastExportDeferred_);
}
}
void
testExportSigSetRejectsEquivocatedSplitMajorities()
{
using namespace csf;
using namespace std::chrono;
testcase("Export sig set rejects equivocated split majorities");
// Same shape as the entropy equivocation test: 2 honest validators on
// each side, one equivocator advertising a matching sidecar hash to
// each side. Each side sees 3/5 aligned, which must remain below the
// export quorum threshold of 4/5.
ConsensusParms const parms{};
Sim sim;
PeerGroup peers = sim.createGroup(5);
PeerGroup left{std::vector<Peer*>{peers[0], peers[1]}};
Peer* equivocator = peers[2];
PeerGroup right{std::vector<Peer*>{peers[3], peers[4]}};
PeerGroup honest = left + right;
for (Peer* peer : peers)
peer->ce().enableExportConsensus_ = true;
auto const fast = round<milliseconds>(0.2 * parms.ledgerGRANULARITY);
peers.trustAndConnect(peers, fast);
sim.run(1);
BEAST_EXPECT(sim.synchronized(peers));
releaseValidatedExports(peers);
left.disconnect(right);
auto const leftHash = sha512Half(std::string("export-equiv-left"));
auto const rightHash = sha512Half(std::string("export-equiv-right"));
for (Peer* peer : left)
{
peer->ce().forcedExportSigSetHash_ = leftHash;
for (Peer const* blocked : right)
peer->ce().dropExportSigFrom_.insert(blocked->id);
}
for (Peer* peer : right)
{
peer->ce().forcedExportSigSetHash_ = rightHash;
for (Peer const* blocked : left)
peer->ce().dropExportSigFrom_.insert(blocked->id);
}
for (Peer* peer : left)
equivocator->ce().equivocateSidecarsTo_[peer->id].exportSigSetHash =
leftHash;
for (Peer* peer : right)
equivocator->ce().equivocateSidecarsTo_[peer->id].exportSigSetHash =
rightHash;
sim.run(3);
for (Peer const* peer : honest)
{
BEAST_EXPECT(!peer->ce().lastExportSucceeded_);
BEAST_EXPECT(peer->ce().lastExportDeferred_);
}
}
void
run() override
{
auto const* filter = std::getenv("XAHAU_EXPORT_TEST");
std::string f = filter ? filter : "";
#define RUN(method) \
do \
{ \
if (f.empty() || std::string(#method).find(f) != std::string::npos) \
method(); \
} while (false)
RUN(testExportOnlySteadyStateSucceeds);
RUN(testExportOnlyQuorumIgnoresMinorityConflict);
RUN(testExportMissingBothSharePathsRecoversPreferredLedger);
RUN(testExportSigSetQuorumAlignmentIgnoresMinorityConflict);
RUN(testExportSigSetConflictWithoutQuorumRetries);
RUN(testExportSigSetRejectsEquivocatedSplitMajorities);
#undef RUN
}
};
BEAST_DEFINE_TESTSUITE(ConsensusExport, consensus, ripple);
} // namespace test
} // namespace ripple