Files
rippled/src/test/consensus/Consensus_test.cpp
Brad Chase 00c60d408a Improve Consensus interface and documentation (RIPD-1340):
- Add Consensus::Result, which represents the result of the
establish state and includes the consensus transaction set, final
proposed position and disputes.
- Add Consensus::Mode to track how we are participating in
consensus and ensures the onAccept callback can distinguish when
we entered the round with consensus versus when we recovered from
a wrong ledger during a round.
- Rename Consensus::Phase to Consensus::State and eliminate the
processing phase.  Instead, accept is a terminal phase which
notifies RCLConsensus via onAccept callbacks.  Even if clients
dispatch accepting to another thread, all future calls except to
startRound will not change the state of consensus.
- Move validate_ status from Consensus to RCLConsensus, since
generic implementation does not directly reference whether a node
is validating or not.
- Eliminate gotTxSetInternal and handle externally received
TxSets distinct from locally generated positions.
- Change ConsensusProposal::changePosition to always update the
internal close time and position even if we have bowed out. This
enforces the invariant that our proposal's position always
matches our transaction set.
2017-04-24 13:13:23 -07:00

529 lines
18 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 <BeastConfig.h>
#include <ripple/beast/clock/manual_clock.h>
#include <ripple/beast/unit_test.h>
#include <ripple/consensus/Consensus.h>
#include <ripple/consensus/ConsensusProposal.h>
#include <boost/function_output_iterator.hpp>
#include <test/csf.h>
#include <utility>
namespace ripple {
namespace test {
class Consensus_test : public beast::unit_test::suite
{
public:
void
testStandalone()
{
using namespace csf;
auto tg = TrustGraph::makeComplete(1);
Sim s(tg, topology(tg, fixed{LEDGER_GRANULARITY}));
auto& p = s.peers[0];
p.targetLedgers = 1;
p.start();
p.submit(Tx{1});
s.net.step();
// Inspect that the proper ledger was created
BEAST_EXPECT(p.prevLedgerID().seq == 1);
BEAST_EXPECT(p.prevLedgerID() == p.lastClosedLedger.id());
BEAST_EXPECT(p.lastClosedLedger.id().txs.size() == 1);
BEAST_EXPECT(
p.lastClosedLedger.id().txs.find(Tx{1}) !=
p.lastClosedLedger.id().txs.end());
BEAST_EXPECT(p.prevProposers() == 0);
}
void
testPeersAgree()
{
using namespace csf;
using namespace std::chrono;
auto tg = TrustGraph::makeComplete(5);
Sim sim(
tg,
topology(tg, fixed{round<milliseconds>(0.2 * LEDGER_GRANULARITY)}));
// everyone submits their own ID as a TX and relay it to peers
for (auto& p : sim.peers)
p.submit(Tx(p.id));
// Verify all peers have the same LCL and it has all the Txs
sim.run(1);
for (auto& p : sim.peers)
{
auto const& lgrID = p.prevLedgerID();
BEAST_EXPECT(lgrID.seq == 1);
BEAST_EXPECT(p.prevProposers() == sim.peers.size() - 1);
for (std::uint32_t i = 0; i < sim.peers.size(); ++i)
BEAST_EXPECT(lgrID.txs.find(Tx{i}) != lgrID.txs.end());
// Matches peer 0 ledger
BEAST_EXPECT(lgrID.txs == sim.peers[0].prevLedgerID().txs);
}
}
void
testSlowPeer()
{
using namespace csf;
using namespace std::chrono;
// Run two tests
// 1. The slow peer is participating in consensus
// 2. The slow peer is just observing
for (auto isParticipant : {true, false})
{
auto tg = TrustGraph::makeComplete(5);
Sim sim(tg, topology(tg, [](PeerID i, PeerID j) {
auto delayFactor = (i == 0 || j == 0) ? 1.1 : 0.2;
return round<milliseconds>(
delayFactor * LEDGER_GRANULARITY);
}));
sim.peers[0].proposing_ = sim.peers[0].validating_ = isParticipant;
// All peers submit their own ID as a transaction and relay it to
// peers
for (auto& p : sim.peers)
{
p.submit(Tx{p.id});
}
sim.run(1);
// Verify all peers have same LCL but are missing transaction 0
// which was not received by all peers before the ledger closed
for (auto& p : sim.peers)
{
auto const& lgrID = p.prevLedgerID();
BEAST_EXPECT(lgrID.seq == 1);
// If peer 0 is participating
if (isParticipant)
{
BEAST_EXPECT(p.prevProposers() == sim.peers.size() - 1);
// Peer 0 closes first because it sees a quorum of agreeing
// positions from all other peers in one hop (1->0, 2->0,
// ..) The other peers take an extra timer period before
// they find that Peer 0 agrees with them ( 1->0->1,
// 2->0->2, ...)
if (p.id != 0)
BEAST_EXPECT(
p.prevRoundTime() > sim.peers[0].prevRoundTime());
}
else // peer 0 is not participating
{
auto const proposers = p.prevProposers();
if (p.id == 0)
BEAST_EXPECT(proposers == sim.peers.size() - 1);
else
BEAST_EXPECT(proposers == sim.peers.size() - 2);
// so all peers should have closed together
BEAST_EXPECT(
p.prevRoundTime() == sim.peers[0].prevRoundTime());
}
BEAST_EXPECT(lgrID.txs.find(Tx{0}) == lgrID.txs.end());
for (std::uint32_t i = 1; i < sim.peers.size(); ++i)
BEAST_EXPECT(lgrID.txs.find(Tx{i}) != lgrID.txs.end());
// Matches peer 0 ledger
BEAST_EXPECT(lgrID.txs == sim.peers[0].prevLedgerID().txs);
}
BEAST_EXPECT(
sim.peers[0].openTxs.find(Tx{0}) != sim.peers[0].openTxs.end());
}
}
void
testCloseTimeDisagree()
{
using namespace csf;
using namespace std::chrono;
// This is a very specialized test to get ledgers to disagree on
// the close time. It unfortunately assumes knowledge about current
// timing constants. This is a necessary evil to get coverage up
// pending more extensive refactorings of timing constants.
// In order to agree-to-disagree on the close time, there must be no
// clear majority of nodes agreeing on a close time. This test
// sets a relative offset to the peers internal clocks so that they
// send proposals with differing times.
// However, they have to agree on the effective close time, not the
// exact close time. The minimum closeTimeResolution is given by
// ledgerPossibleTimeResolutions[0], which is currently 10s. This means
// the skews need to be at least 10 seconds.
// Complicating this matter is that nodes will ignore proposals
// with times more than PROPOSE_FRESHNESS =20s in the past. So at
// the minimum granularity, we have at most 3 types of skews
// (0s,10s,20s).
// This test therefore has 6 nodes, with 2 nodes having each type of
// skew. Then no majority (1/3 < 1/2) of nodes will agree on an
// actual close time.
auto tg = TrustGraph::makeComplete(6);
Sim sim(
tg,
topology(tg, fixed{round<milliseconds>(0.2 * LEDGER_GRANULARITY)}));
// Run consensus without skew until we have a short close time
// resolution
while (sim.peers.front().lastClosedLedger.closeTimeResolution() >=
PROPOSE_FRESHNESS)
sim.run(1);
// Introduce a shift on the time of half the peers
sim.peers[0].clockSkew = PROPOSE_FRESHNESS / 2;
sim.peers[1].clockSkew = PROPOSE_FRESHNESS / 2;
sim.peers[2].clockSkew = PROPOSE_FRESHNESS;
sim.peers[3].clockSkew = PROPOSE_FRESHNESS;
// Verify all peers have the same LCL and it has all the Txs
sim.run(1);
for (auto& p : sim.peers)
{
BEAST_EXPECT(!p.lastClosedLedger.closeAgree());
}
}
void
testWrongLCL()
{
using namespace csf;
using namespace std::chrono;
// Specialized test to exercise a temporary fork in which some peers
// are working on an incorrect prior ledger.
// Vary the time it takes to process validations to exercise detecting
// the wrong LCL at different phases of consensus
for (auto validationDelay : {0s, LEDGER_MIN_CLOSE})
{
// Consider 10 peers:
// 0 1 2 3 4 5 6 7 8 9
//
// Nodes 0-1 trust nodes 0-4
// Nodes 2-9 trust nodes 2-9
//
// By submitting tx 0 to nodes 0-4 and tx 1 to nodes 5-9,
// nodes 0-1 will generate the wrong LCL (with tx 0). The remaining
// nodes will instead accept the ledger with tx 1.
// Nodes 0-1 will detect this mismatch during a subsequent round
// since nodes 2-4 will validate a different ledger.
// Nodes 0-1 will acquire the proper ledger from the network and
// resume consensus and eventually generate the dominant network
// ledger
std::vector<UNL> unls;
unls.push_back({2, 3, 4, 5, 6, 7, 8, 9});
unls.push_back({0, 1, 2, 3, 4});
std::vector<int> membership(10, 0);
membership[0] = 1;
membership[1] = 1;
TrustGraph tg{unls, membership};
// This topology can fork, which is why we are using it for this
// test.
BEAST_EXPECT(tg.canFork(minimumConsensusPercentage / 100.));
auto netDelay = round<milliseconds>(0.2 * LEDGER_GRANULARITY);
Sim sim(tg, topology(tg, fixed{netDelay}));
// initial round to set prior state
sim.run(1);
// Nodes in smaller UNL have seen tx 0, nodes in other unl have seen
// tx 1
for (auto& p : sim.peers)
{
p.validationDelay = validationDelay;
p.missingLedgerDelay = netDelay;
if (unls[1].find(p.id) != unls[1].end())
p.openTxs.insert(Tx{0});
else
p.openTxs.insert(Tx{1});
}
// Run for additional rounds
// With no validation delay, only 2 more rounds are needed.
// 1. Round to generate different ledgers
// 2. Round to detect different prior ledgers (but still generate
// wrong ones) and recover within that round since wrong LCL
// is detected before we close
//
// With a validation delay of LEDGER_MIN_CLOSE, we need 3 more
// rounds.
// 1. Round to generate different ledgers
// 2. Round to detect different prior ledgers (but still generate
// wrong ones) but end up declaring consensus on wrong LCL (but
// with the right transaction set!). This is because we detect
// the wrong LCL after we have closed the ledger, so we declare
// consensus based solely on our peer proposals. But we haven't
// had time to acquire the right LCL
// 3. Round to correct
sim.run(3);
bc::flat_map<int, bc::flat_set<Ledger::ID>> ledgers;
for (auto& p : sim.peers)
{
for (auto const& l : p.ledgers)
{
ledgers[l.first.seq].insert(l.first);
}
}
BEAST_EXPECT(ledgers[0].size() == 1);
BEAST_EXPECT(ledgers[1].size() == 1);
if (validationDelay == 0s)
{
BEAST_EXPECT(ledgers[2].size() == 2);
BEAST_EXPECT(ledgers[3].size() == 1);
BEAST_EXPECT(ledgers[4].size() == 1);
}
else
{
BEAST_EXPECT(ledgers[2].size() == 2);
BEAST_EXPECT(ledgers[3].size() == 2);
BEAST_EXPECT(ledgers[4].size() == 1);
}
}
// Additional test engineered to switch LCL during the establish phase.
// This was added to trigger a scenario that previously crashed, in which
// switchLCL switched from establish to open phase, but still processed
// the establish phase logic.
{
using namespace csf;
using namespace std::chrono;
// A mostly disjoint topology
std::vector<UNL> unls;
unls.push_back({0, 1});
unls.push_back({2});
unls.push_back({3});
unls.push_back({0, 1, 2, 3, 4});
std::vector<int> membership = {0, 0, 1, 2, 3};
TrustGraph tg{unls, membership};
Sim sim(tg, topology(tg, fixed{round<milliseconds>(
0.2 * LEDGER_GRANULARITY)}));
// initial ground to set prior state
sim.run(1);
for (auto &p : sim.peers) {
// A long delay to acquire a missing ledger from the network
p.missingLedgerDelay = 2 * LEDGER_MIN_CLOSE;
// Everyone sees only their own LCL
p.openTxs.insert(Tx(p.id));
}
// additional rounds to generate wrongLCL and recover
sim.run(2);
// Check all peers recovered
for (auto &p : sim.peers)
BEAST_EXPECT(p.prevLedgerID() == sim.peers[0].prevLedgerID());
}
}
void
testFork()
{
using namespace csf;
using namespace std::chrono;
int numPeers = 10;
for (int overlap = 0; overlap <= numPeers; ++overlap)
{
auto tg = TrustGraph::makeClique(numPeers, overlap);
Sim sim(
tg,
topology(
tg, fixed{round<milliseconds>(0.2 * LEDGER_GRANULARITY)}));
// Initial round to set prior state
sim.run(1);
for (auto& p : sim.peers)
{
// Nodes have only seen transactions from their neighbors
p.openTxs.insert(Tx{p.id});
for (auto const link : sim.net.links(&p))
p.openTxs.insert(Tx{link.to->id});
}
sim.run(1);
// See if the network forked
bc::flat_set<Ledger::ID> ledgers;
for (auto& p : sim.peers)
{
ledgers.insert(p.prevLedgerID());
}
// Fork should not happen for 40% or greater overlap
// Since the overlapped nodes have a UNL that is the union of the
// two cliques, the maximum sized UNL list is the number of peers
if (overlap > 0.4 * numPeers)
BEAST_EXPECT(ledgers.size() == 1);
else // Even if we do fork, there shouldn't be more than 3 ledgers
// One for cliqueA, one for cliqueB and one for nodes in both
BEAST_EXPECT(ledgers.size() <= 3);
}
}
void
simClockSkew()
{
using namespace csf;
// Attempting to test what happens if peers enter consensus well
// separated in time. Initial round (in which peers are not staggered)
// is used to get the network going, then transactions are submitted
// together and consensus continues.
// For all the times below, the same ledger is built but the close times
// disgree. BUT THE LEDGER DOES NOT SHOW disagreeing close times.
// It is probably because peer proposals are stale, so they get ignored
// but with no peer proposals, we always assume close time consensus is
// true.
// Disabled while continuing to understand testt.
for (auto stagger : {800ms, 1600ms, 3200ms, 30000ms, 45000ms, 300000ms})
{
auto tg = TrustGraph::makeComplete(5);
Sim sim(tg, topology(tg, [](PeerID i, PeerID) {
return 200ms * (i + 1);
}));
// all transactions submitted before starting
// Initial round to set prior state
sim.run(1);
for (auto& p : sim.peers)
{
p.openTxs.insert(Tx{0});
p.targetLedgers = p.completedLedgers + 1;
}
// stagger start of consensus
for (auto& p : sim.peers)
{
p.start();
sim.net.step_for(stagger);
}
// run until all peers have accepted all transactions
sim.net.step_while([&]() {
for (auto& p : sim.peers)
{
if (p.prevLedgerID().txs.size() != 1)
{
return true;
}
}
return false;
});
}
}
void
simScaleFree()
{
using namespace std::chrono;
using namespace csf;
// Generate a quasi-random scale free network and simulate consensus
// for a single transaction
int N = 100; // Peers
int numUNLs = 15; // UNL lists
int minUNLSize = N / 4, maxUNLSize = N / 2;
double transProb = 0.5;
std::mt19937_64 rng;
auto tg = TrustGraph::makeRandomRanked(
N,
numUNLs,
PowerLawDistribution{1, 3},
std::uniform_int_distribution<>{minUNLSize, maxUNLSize},
rng);
Sim sim{
tg,
topology(tg, fixed{round<milliseconds>(0.2 * LEDGER_GRANULARITY)})};
// Initial round to set prior state
sim.run(1);
std::uniform_real_distribution<> u{};
for (auto& p : sim.peers)
{
// 50-50 chance to have seen a transaction
if (u(rng) >= transProb)
p.openTxs.insert(Tx{0});
}
sim.run(1);
// See if the network forked
bc::flat_set<Ledger::ID> ledgers;
for (auto& p : sim.peers)
{
ledgers.insert(p.prevLedgerID());
}
BEAST_EXPECT(ledgers.size() == 1);
}
void
run() override
{
testStandalone();
testPeersAgree();
testSlowPeer();
testCloseTimeDisagree();
testWrongLCL();
testFork();
simClockSkew();
simScaleFree();
}
};
BEAST_DEFINE_TESTSUITE(Consensus, consensus, ripple);
} // test
} // ripple