Refactor consensus for simulation (RIPD-1011):

This is a substantial refactor of the consensus code and also introduces
a basic consensus simulation and testing framework.  The new generic/templated
version is in src/ripple/consensus and documents the current type requirements.
The version adapted for the RCL is in src/ripple/app/consensus.  The testing
framework is in src/test/csf.

Minor behavioral changes/fixes include:
* Adjust close time offset even when not validating.
* Remove spurious proposing_ = false call at end of handleLCL.
* Remove unused functionality provided by checkLastValidation.
* Separate open and converge time
* Don't send a bow out if we're not proposing
* Prevent consensus stopping if NetworkOPs switches to disconnect mode while
  consensus accepts a ledger
* Prevent a corner case in which Consensus::gotTxSet or Consensus::peerProposal
  has the potential to update internal state while an dispatched accept job is
  running.
* Distinguish external and internal calls to startNewRound.  Only external
  calls can reset the proposing_ state of consensus
This commit is contained in:
Brad Chase
2016-11-02 15:16:02 -07:00
parent fc0d64f5ee
commit bc5a74057d
56 changed files with 6492 additions and 3785 deletions

917
src/test/csf/BasicNetwork.h Normal file
View File

@@ -0,0 +1,917 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012, 2013 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.
*/
//==============================================================================
#ifndef RIPPLE_TEST_CSF_BASICNETWORK_H_INCLUDED
#define RIPPLE_TEST_CSF_BASICNETWORK_H_INCLUDED
#include <ripple/basics/qalloc.h>
#include <ripple/beast/clock/manual_clock.h>
#include <ripple/beast/hash/hash_append.h>
#include <ripple/beast/hash/uhash.h>
#include <boost/container/flat_map.hpp>
#include <boost/intrusive/list.hpp>
#include <boost/intrusive/set.hpp>
#include <boost/range/adaptor/transformed.hpp>
#include <boost/range/iterator_range.hpp>
#include <boost/tuple/tuple.hpp>
#include <deque>
#include <memory>
#include <cassert>
#include <cstdint>
#include <iomanip>
#include <type_traits>
#include <unordered_map>
#include <unordered_set>
namespace ripple {
namespace test {
namespace csf {
/** Peer to peer network simulator.
The network is formed from a set of Peer objects representing
vertices and configurable connections representing edges.
The caller is responsible for creating the Peer objects ahead
of time.
Peer objects cannot be destroyed once the BasicNetwork is
constructed. To handle peers going online and offline,
callers can simply disconnect all links and reconnect them
later. Connections are directed, one end is the inbound
Peer and the other is the outbound Peer.
Peers may send messages along their connections. To simulate
the effects of latency, these messages can be delayed by a
configurable duration set when the link is established.
Messages always arrive in the order they were sent on a
particular connection.
A message is modeled using a lambda function. The caller
provides the code to execute upon delivery of the message.
If a Peer is disconnected, all messages pending delivery
at either end of the associated connection are discarded.
A timer may be set for a Peer. When the timer expires,
a caller provided lambda is invoked. Timers may be canceled
using a token returned when the timer is created.
After creating the Peer set, constructing the network,
and establishing connections, the caller uses one or more
of the step, step_one, step_for, step_until and step_while
functions to iterate the network,
Peer Requirements:
Peer should be a lightweight type, cheap to copy
and/or move. A good candidate is a simple pointer to
the underlying user defined type in the simulation.
Expression Type Requirements
---------- ---- ------------
P Peer
u, v Values of type P
P u(v) CopyConstructible
u.~P() Destructible
u == v bool EqualityComparable
u < v bool LessThanComparable
std::hash<P> class std::hash is defined for P
! u bool true if u is not-a-peer
*/
template <class Peer>
class BasicNetwork
{
public:
using peer_type = Peer;
using clock_type =
beast::manual_clock<
std::chrono::steady_clock>;
using duration =
typename clock_type::duration;
using time_point =
typename clock_type::time_point;
private:
struct by_to_tag {};
struct by_from_tag {};
struct by_when_tag {};
using by_to_hook =
boost::intrusive::list_base_hook<
boost::intrusive::link_mode<
boost::intrusive::normal_link>,
boost::intrusive::tag<by_to_tag>>;
using by_from_hook =
boost::intrusive::list_base_hook<
boost::intrusive::link_mode<
boost::intrusive::normal_link>,
boost::intrusive::tag<by_from_tag>>;
using by_when_hook =
boost::intrusive::set_base_hook<
boost::intrusive::link_mode<
boost::intrusive::normal_link>>;
struct msg
: by_to_hook, by_from_hook, by_when_hook
{
Peer to;
Peer from;
time_point when;
msg (msg const&) = delete;
msg& operator= (msg const&) = delete;
virtual ~msg() = default;
virtual void operator()() const = 0;
msg (Peer const& from_, Peer const& to_,
time_point when_)
: to(to_), from(from_), when(when_)
{
}
bool
operator< (msg const& other) const
{
return when < other.when;
}
};
template <class Handler>
class msg_impl : public msg
{
private:
Handler const h_;
public:
msg_impl (msg_impl const&) = delete;
msg_impl& operator= (msg_impl const&) = delete;
msg_impl (Peer const& from_, Peer const& to_,
time_point when_, Handler&& h)
: msg (from_, to_, when_)
, h_ (std::move(h))
{
}
msg_impl (Peer const& from_, Peer const& to_,
time_point when_, Handler const& h)
: msg (from_, to_, when_)
, h_ (h)
{
}
void operator()() const override
{
h_();
}
};
class queue_type
{
private:
using by_to_list = typename
boost::intrusive::make_list<msg,
boost::intrusive::base_hook<by_to_hook>,
boost::intrusive::constant_time_size<false>>::type;
using by_from_list = typename
boost::intrusive::make_list<msg,
boost::intrusive::base_hook<by_from_hook>,
boost::intrusive::constant_time_size<false>>::type;
using by_when_set = typename
boost::intrusive::make_multiset<msg,
boost::intrusive::constant_time_size<false>>::type;
qalloc alloc_;
by_when_set by_when_;
std::unordered_map<Peer, by_to_list> by_to_;
std::unordered_map<Peer, by_from_list> by_from_;
public:
using iterator =
typename by_when_set::iterator;
queue_type (queue_type const&) = delete;
queue_type& operator= (queue_type const&) = delete;
explicit
queue_type (qalloc const& alloc);
~queue_type();
bool
empty() const;
iterator
begin();
iterator
end();
template <class Handler>
typename by_when_set::iterator
emplace (Peer const& from, Peer const& to,
time_point when, Handler&& h);
void
erase (iterator iter);
void
remove (Peer const& from, Peer const& to);
};
struct link_type
{
bool inbound;
duration delay;
link_type (bool inbound_, duration delay_)
: inbound (inbound_)
, delay (delay_)
{
}
};
using links_type =
boost::container::flat_map<Peer, link_type>;
class link_transform;
qalloc alloc_;
queue_type queue_;
// VFALCO This is an ugly wart, aged containers
// want a non-const reference to a clock.
clock_type mutable clock_;
std::unordered_map<Peer, links_type> links_;
public:
BasicNetwork (BasicNetwork const&) = delete;
BasicNetwork& operator= (BasicNetwork const&) = delete;
BasicNetwork();
/** Return the allocator. */
qalloc const&
alloc() const;
/** Return the clock. */
clock_type&
clock() const;
/** Return the current network time.
@note The epoch is unspecified
*/
time_point
now() const;
/** Connect two peers.
The link is directed, with `from` establishing
the outbound connection and `to` receiving the
incoming connection.
Preconditions:
from != to (self connect disallowed).
A link between from and to does not
already exist (duplicates disallowed).
Effects:
Creates a link between from and to.
@param `from` The source of the outgoing connection
@param `to` The recipient of the incoming connection
@param `delay` The time delay of all delivered messages
@return `true` if a new connection was established
*/
bool
connect (Peer const& from, Peer const& to,
duration const& delay = std::chrono::seconds{0});
/** Break a link.
Effects:
If a connection is present, both ends are
disconnected.
Any pending messages on the connection
are discarded.
@return `true` if a connection was broken.
*/
bool
disconnect (Peer const& peer1, Peer const& peer2);
/** Return the range of active links.
@return A random access range.
*/
boost::transformed_range<
link_transform, links_type>
links (Peer const& from);
/** Send a message to a peer.
Preconditions:
A link exists between from and to.
Effects:
If the link is not broken when the
link's `delay` time has elapsed,
the function will be invoked with
no arguments.
@note Its the caller's responsibility to
ensure that the body of the function performs
activity consistent with `from`'s receipt of
a message from `to`.
*/
template <class Function>
void
send (Peer const& from, Peer const& to,
Function&& f);
// Used to cancel timers
struct cancel_token;
/** Deliver a timer notification.
Effects:
When the network time is reached,
the function will be called with
no arguments.
*/
template <class Function>
cancel_token
timer (time_point const& when,
Function&& f);
/** Deliver a timer notification.
Effects:
When the specified time has elapsed,
the function will be called with
no arguments.
*/
template <class Function>
cancel_token
timer (duration const& delay,
Function&& f);
/** Cancel a timer.
Preconditions:
`token` was the return value of a call
timer() which has not yet been invoked.
*/
void
cancel (cancel_token const& token);
/** Perform breadth-first search.
Function will be called with this signature:
void(std::size_t, Peer&);
The second argument is the distance of the
peer from the start peer, in hops.
*/
template <class Function>
void
bfs (Peer const& start, Function&& f);
/** Run the network for up to one message.
Effects:
The clock is advanced to the time
of the last delivered message.
@return `true` if a message was processed.
*/
bool
step_one();
/** Run the network until no messages remain.
Effects:
The clock is advanced to the time
of the last delivered message.
@return `true` if any message was processed.
*/
bool
step();
/** Run the network while a condition is true.
Function takes no arguments and will be called
repeatedly after each message is processed to
decide whether to continue.
Effects:
The clock is advanced to the time
of the last delivered message.
@return `true` if any message was processed.
*/
template <class Function>
bool
step_while(Function && func);
/** Run the network until the specified time.
Effects:
The clock is advanced to the
specified time.
@return `true` if any messages remain.
*/
bool
step_until (time_point const& until);
/** Run the network until time has elapsed.
Effects:
The clock is advanced by the
specified duration.
@return `true` if any messages remain.
*/
template <class Period, class Rep>
bool
step_for (std::chrono::duration<
Period, Rep> const& amount);
};
//------------------------------------------------------------------------------
template <class Peer>
BasicNetwork<Peer>::queue_type::queue_type(
qalloc const& alloc)
: alloc_ (alloc)
{
}
template <class Peer>
BasicNetwork<Peer>::queue_type::~queue_type()
{
for(auto iter = by_when_.begin();
iter != by_when_.end();)
{
auto m = &*iter;
++iter;
m->~msg();
alloc_.dealloc(m, 1);
}
}
template <class Peer>
inline
bool
BasicNetwork<Peer>::queue_type::empty() const
{
return by_when_.empty();
}
template <class Peer>
inline
auto
BasicNetwork<Peer>::queue_type::begin() ->
iterator
{
return by_when_.begin();
}
template <class Peer>
inline
auto
BasicNetwork<Peer>::queue_type::end() ->
iterator
{
return by_when_.end();
}
template <class Peer>
template <class Handler>
auto
BasicNetwork<Peer>::queue_type::emplace(
Peer const& from, Peer const& to, time_point when,
Handler&& h) ->
typename by_when_set::iterator
{
using msg_type = msg_impl<
std::decay_t<Handler>>;
auto const p = alloc_.alloc<msg_type>(1);
auto& m = *new(p) msg_type(from, to,
when, std::forward<Handler>(h));
if (to)
by_to_[to].push_back(m);
if (from)
by_from_[from].push_back(m);
return by_when_.insert(m);
}
template <class Peer>
void
BasicNetwork<Peer>::queue_type::erase(
iterator iter)
{
auto& m = *iter;
if (iter->to)
{
auto& list = by_to_[iter->to];
list.erase(list.iterator_to(m));
}
if (iter->from)
{
auto& list = by_from_[iter->from];
list.erase(list.iterator_to(m));
}
by_when_.erase(iter);
m.~msg();
alloc_.dealloc(&m, 1);
}
template <class Peer>
void
BasicNetwork<Peer>::queue_type::remove(
Peer const& from, Peer const& to)
{
{
auto& list = by_to_[to];
for(auto iter = list.begin();
iter != list.end();)
{
auto& m = *iter++;
if (m.from == from)
erase(by_when_.iterator_to(m));
}
}
{
auto& list = by_to_[from];
for(auto iter = list.begin();
iter != list.end();)
{
auto& m = *iter++;
if (m.from == to)
erase(by_when_.iterator_to(m));
}
}
}
//------------------------------------------------------------------------------
template <class Peer>
class BasicNetwork<Peer>::link_transform
{
private:
BasicNetwork& net_;
Peer from_;
public:
using argument_type =
typename links_type::value_type;
class result_type
{
public:
Peer to;
bool inbound;
result_type (result_type const&) = default;
result_type (BasicNetwork& net,
Peer const& from, Peer const& to_,
bool inbound_)
: to(to_)
, inbound(inbound_)
, net_(net)
, from_(from)
{
}
/** Disconnect this link.
Effects:
The connection is removed at both ends.
*/
bool
disconnect() const
{
return net_.disconnect(from_, to);
}
private:
BasicNetwork& net_;
Peer from_;
};
link_transform (BasicNetwork& net,
Peer const& from)
: net_(net)
, from_(from)
{
}
result_type const
operator()(argument_type const& v) const
{
return result_type(net_, from_,
v.first, v.second.inbound);
}
};
//------------------------------------------------------------------------------
template <class Peer>
struct BasicNetwork<Peer>::cancel_token
{
private:
typename queue_type::iterator iter_;
public:
cancel_token() = delete;
cancel_token (cancel_token const&) = default;
cancel_token& operator= (cancel_token const&) = default;
private:
friend class BasicNetwork;
cancel_token(typename
queue_type::iterator iter)
: iter_ (iter)
{
}
};
//------------------------------------------------------------------------------
template <class Peer>
BasicNetwork<Peer>::BasicNetwork()
: queue_ (alloc_)
{
}
template <class Peer>
inline
qalloc const&
BasicNetwork<Peer>::alloc() const
{
return alloc_;
}
template <class Peer>
inline
auto
BasicNetwork<Peer>::clock() const ->
clock_type&
{
return clock_;
}
template <class Peer>
inline
auto
BasicNetwork<Peer>::now() const ->
time_point
{
return clock_.now();
}
template <class Peer>
bool
BasicNetwork<Peer>::connect(
Peer const& from, Peer const& to,
duration const& delay)
{
if (to == from)
return false;
using namespace std;
if (! links_[from].emplace(to,
link_type{ false, delay }).second)
return false;
auto const result = links_[to].emplace(
from, link_type{ true, delay });
(void)result;
assert(result.second);
return true;
}
template <class Peer>
bool
BasicNetwork<Peer>::disconnect(
Peer const& peer1, Peer const& peer2)
{
if (links_[peer1].erase(peer2) == 0)
return false;
auto const n =
links_[peer2].erase(peer1);
(void)n;
assert(n);
queue_.remove(peer1, peer2);
return true;
}
template <class Peer>
inline
auto
BasicNetwork<Peer>::links(Peer const& from) ->
boost::transformed_range<
link_transform, links_type>
{
return boost::adaptors::transform(
links_[from],
link_transform{ *this, from });
}
template <class Peer>
template <class Function>
inline
void
BasicNetwork<Peer>::send(
Peer const& from, Peer const& to,
Function&& f)
{
using namespace std;
auto const iter =
links_[from].find(to);
queue_.emplace(from, to,
clock_.now() + iter->second.delay,
forward<Function>(f));
}
template <class Peer>
template <class Function>
inline
auto
BasicNetwork<Peer>::timer(
time_point const& when, Function&& f) ->
cancel_token
{
using namespace std;
return queue_.emplace(
nullptr, nullptr, when,
forward<Function>(f));
}
template <class Peer>
template <class Function>
inline
auto
BasicNetwork<Peer>::timer(
duration const& delay, Function&& f) ->
cancel_token
{
return timer(clock_.now() + delay,
std::forward<Function>(f));
}
template <class Peer>
inline
void
BasicNetwork<Peer>::cancel(
cancel_token const& token)
{
queue_.erase(token.iter_);
}
template <class Peer>
bool
BasicNetwork<Peer>::step_one()
{
if (queue_.empty())
return false;
auto const iter = queue_.begin();
clock_.set(iter->when);
(*iter)();
queue_.erase(iter);
return true;
}
template <class Peer>
bool
BasicNetwork<Peer>::step()
{
if (! step_one())
return false;
for(;;)
if (! step_one())
break;
return true;
}
template <class Peer>
template <class Function>
bool
BasicNetwork<Peer>::step_while(Function && f)
{
bool ran = false;
while (f() && step_one())
ran = true;
return ran;
}
template <class Peer>
bool
BasicNetwork<Peer>::step_until(
time_point const& until)
{
// VFALCO This routine needs optimizing
if(queue_.empty())
{
clock_.set(until);
return false;
}
auto iter = queue_.begin();
if(iter->when > until)
{
clock_.set(until);
return true;
}
do
{
step_one();
iter = queue_.begin();
}
while(iter != queue_.end() &&
iter->when <= until);
clock_.set(until);
return iter != queue_.end();
}
template <class Peer>
template <class Period, class Rep>
inline
bool
BasicNetwork<Peer>::step_for(
std::chrono::duration<Period, Rep> const& amount)
{
return step_until(now() + amount);
}
template <class Peer>
template <class Function>
void
BasicNetwork<Peer>::bfs(
Peer const& start, Function&& f)
{
std::deque<std::pair<Peer, std::size_t>> q;
std::unordered_set<Peer> seen;
q.emplace_back(start, 0);
seen.insert(start);
while(! q.empty())
{
auto v = q.front();
q.pop_front();
f(v.second, v.first);
for(auto const& link : links_[v.first])
{
auto const& w = link.first;
if (seen.count(w) == 0)
{
q.emplace_back(w, v.second + 1);
seen.insert(w);
}
}
}
}
} // csf
} // test
} // ripple
#endif

View File

@@ -0,0 +1,133 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012-2015 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 <test/csf/BasicNetwork.h>
#include <ripple/beast/unit_test.h>
#include <set>
#include <vector>
namespace ripple {
namespace test {
class BasicNetwork_test : public beast::unit_test::suite
{
public:
struct Peer
{
int id;
std::set<int> set;
Peer (Peer const&) = default;
Peer (Peer&&) = default;
explicit Peer(int id_)
: id(id_)
{
}
template <class Net>
void start(Net& net)
{
using namespace std::chrono_literals;
auto t = net.timer(1s,
[&]{ set.insert(0); });
if (id == 0)
{
for(auto const& link : net.links(this))
net.send(this, link.to,
[&, to = link.to]
{
to->receive(net, this, 1);
});
}
else
{
net.cancel(t);
}
}
template <class Net>
void receive(Net& net, Peer* from, int m)
{
set.insert(m);
++m;
if (m < 5)
{
for(auto const& link : net.links(this))
net.send(this, link.to,
[&, mm = m, to = link.to]
{
to->receive(net, this, mm);
});
}
}
};
void run() override
{
using namespace std::chrono_literals;
std::vector<Peer> pv;
pv.emplace_back(0);
pv.emplace_back(1);
pv.emplace_back(2);
csf::BasicNetwork<Peer*> net;
BEAST_EXPECT(! net.connect(&pv[0], &pv[0]));
BEAST_EXPECT(net.connect(&pv[0], &pv[1], 1s));
BEAST_EXPECT(net.connect(&pv[1], &pv[2], 1s));
BEAST_EXPECT(! net.connect(&pv[0], &pv[1]));
std::size_t diameter = 0;
net.bfs(&pv[0],
[&](auto d, Peer*)
{ diameter = std::max(d, diameter); });
BEAST_EXPECT(diameter == 2);
for(auto& peer : pv)
peer.start(net);
BEAST_EXPECT(net.step_for(0s));
BEAST_EXPECT(net.step_for(1s));
BEAST_EXPECT(net.step());
BEAST_EXPECT(! net.step());
BEAST_EXPECT(! net.step_for(1s));
net.send(&pv[0], &pv[1], []{});
net.send(&pv[1], &pv[0], []{});
BEAST_EXPECT(net.disconnect(&pv[0], &pv[1]));
BEAST_EXPECT(! net.disconnect(&pv[0], &pv[1]));
for(;;)
{
auto const links = net.links(&pv[1]);
if(links.empty())
break;
BEAST_EXPECT(links[0].disconnect());
}
BEAST_EXPECT(pv[0].set ==
std::set<int>({0, 2, 4}));
BEAST_EXPECT(pv[1].set ==
std::set<int>({1, 3}));
BEAST_EXPECT(pv[2].set ==
std::set<int>({2, 4}));
net.timer(0s, []{});
}
};
BEAST_DEFINE_TESTSUITE(BasicNetwork, test, ripple);
} // test
} // ripple

197
src/test/csf/Ledger.h Normal file
View File

@@ -0,0 +1,197 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012-2017 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.
*/
//==============================================================================
#ifndef RIPPLE_TEST_CSF_LEDGER_H_INCLUDED
#define RIPPLE_TEST_CSF_LEDGER_H_INCLUDED
#include <ripple/basics/chrono.h>
#include <test/csf/Tx.h>
namespace ripple {
namespace test {
namespace csf {
/** A ledger is a set of observed transactions and a sequence number
identifying the ledger.
Peers in the consensus process are trying to agree on a set of transactions
to include in a ledger. For unit testing, each transaction is a
single integer and the ledger is a set of observed integers. This means
future ledgers have prior ledgers as subsets, e.g.
Ledger 0 : {}
Ledger 1 : {1,4,5}
Ledger 2 : {1,2,4,5,10}
....
Tx - Integer
TxSet - Set of Tx
Ledger - Set of Tx and sequence number
*/
class Ledger
{
public:
struct ID
{
std::uint32_t seq = 0;
TxSetType txs = TxSetType{};
bool operator==(ID const & o) const
{
return seq == o.seq && txs == o.txs;
}
bool operator!=(ID const & o) const
{
return !(*this == o);
}
bool operator<(ID const & o) const
{
return std::tie(seq, txs) < std::tie(o.seq, o.txs);
}
};
auto const &
id() const
{
return id_;
}
auto
seq() const
{
return id_.seq;
}
auto
closeTimeResolution() const
{
return closeTimeResolution_;
}
auto
closeAgree() const
{
return closeTimeAgree_;
}
auto
closeTime() const
{
return closeTime_;
}
auto
actualCloseTime() const
{
return actualCloseTime_;
}
auto
parentCloseTime() const
{
return parentCloseTime_;
}
auto const &
parentID() const
{
return parentID_;
}
Json::Value
getJson() const
{
Json::Value res(Json::objectValue);
res["seq"] = seq();
return res;
}
//! Apply the given transactions to this ledger
Ledger
close(TxSetType const & txs,
NetClock::duration closeTimeResolution,
NetClock::time_point const & consensusCloseTime,
bool closeTimeAgree) const
{
Ledger res{ *this };
res.id_.txs.insert(txs.begin(), txs.end());
res.id_ .seq= seq() + 1;
res.closeTimeResolution_ = closeTimeResolution;
res.actualCloseTime_ = consensusCloseTime;
res.closeTime_ = effectiveCloseTime(consensusCloseTime,
closeTimeResolution, parentCloseTime_);
res.closeTimeAgree_ = closeTimeAgree;
res.parentCloseTime_ = closeTime();
res.parentID_ = id();
return res;
}
private:
//! Unique identifier of ledger is combination of sequence number and id
ID id_;
//! Bucket resolution used to determine close time
NetClock::duration closeTimeResolution_ = ledgerDefaultTimeResolution;
//! When the ledger closed
NetClock::time_point closeTime_;
//! Whether consenssus agreed on the close time
bool closeTimeAgree_ = true;
//! Parent ledger id
ID parentID_;
//! Parent ledger close time
NetClock::time_point parentCloseTime_;
//! Close time unadjusted by closeTimeResolution
NetClock::time_point actualCloseTime_;
};
inline
std::ostream &
operator<<(std::ostream & o, Ledger::ID const & id)
{
return o << id.seq << "," << id.txs;
}
inline
std::string
to_string(Ledger::ID const & id)
{
std::stringstream ss;
ss << id;
return ss.str();
}
} // csf
} // test
} // ripple
#endif

492
src/test/csf/Peer.h Normal file
View File

@@ -0,0 +1,492 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012-2017 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.
*/
//==============================================================================
#ifndef RIPPLE_TEST_CSF_PEER_H_INCLUDED
#define RIPPLE_TEST_CSF_PEER_H_INCLUDED
#include <boost/container/flat_map.hpp>
#include <boost/container/flat_set.hpp>
#include <test/csf/Tx.h>
#include <test/csf/Ledger.h>
#include <test/csf/UNL.h>
namespace ripple {
namespace test {
namespace csf {
/** Store validations reached by peers */
struct Validation
{
PeerID id;
Ledger::ID ledger;
Ledger::ID prevLedger;
};
namespace bc = boost::container;
class Validations
{
//< Ledgers seen by peers, saved in order received (which should be order
//< created)
bc::flat_map<Ledger::ID, bc::flat_set<PeerID>> nodesFromLedger;
bc::flat_map<Ledger::ID, bc::flat_set<PeerID>> nodesFromPrevLedger;
bc::flat_map<Ledger::ID, bc::flat_map<Ledger::ID, std::size_t>> childLedgers;
public:
void
update(Validation const & v)
{
nodesFromLedger[v.ledger].insert(v.id);
if(v.ledger.seq > 0)
{
nodesFromPrevLedger[v.prevLedger].insert(v.id);
childLedgers[v.prevLedger][v.ledger]++;
}
}
//< The number of peers who have validated this ledger
std::size_t
proposersValidated(Ledger::ID const & prevLedger) const
{
auto it = nodesFromLedger.find(prevLedger);
if(it != nodesFromLedger.end())
return it->second.size();
return 0;
}
/** The number of peers that are past this ledger, i.e.
they have a newer most recent ledger, but have this ledger
as an ancestor.
*/
std::size_t
proposersFinished(Ledger::ID const & prevLedger) const
{
auto it = nodesFromPrevLedger.find(prevLedger);
if(it != nodesFromPrevLedger.end())
return it->second.size();
return 0;
}
/** Returns the ledger starting from prevLedger with the most validations.
*/
Ledger::ID
getBestLCL(Ledger::ID const & currLedger,
Ledger::ID const & prevLedger) const
{
auto it = childLedgers.find(prevLedger);
if (it != childLedgers.end() &&
! it->second.empty())
{
std::size_t bestCount = 0;
Ledger::ID bestLedger;
for (auto const & b : it->second)
{
auto currCount = b.second;
if(currLedger == b.first)
currCount++;
if(currCount > bestCount)
bestLedger = b.first;
if(currCount == bestCount && currLedger == b.first)
bestLedger = b.first;
}
return bestLedger;
}
return currLedger;
}
};
/** Proposal is a position taken in the consensus process and is represented
directly from the generic types.
*/
using Proposal = ConsensusProposal<PeerID, Ledger::ID, TxSetType>;
struct Traits
{
using Ledger_t = Ledger;
using NodeID_t = PeerID;
using TxSet_t = TxSet;
using MissingTxException_t = MissingTx;
};
/** Represents a single node participating in the consensus process.
It implements the Callbacks required by Consensus.
*/
struct Peer : public Consensus<Peer, Traits>
{
using Base = Consensus<Peer, Traits>;
//! Our unique ID
PeerID id;
//! openTxs that haven't been closed in a ledger yet
TxSetType openTxs;
//! last ledger this peer closed
Ledger lastClosedLedger;
//! Handle to network for sending messages
BasicNetwork<Peer*> & net;
//! UNL of trusted peers
UNL unl;
//! Most recent ledger completed by peers
Validations peerValidations;
// The ledgers, proposals, TxSets and Txs this peer has seen
bc::flat_map<Ledger::ID, Ledger> ledgers;
//! Map from Ledger::ID to vector of Positions with that ledger
//! as the prior ledger
bc::flat_map<Ledger::ID, std::vector<Proposal>> peerPositions_;
bc::flat_map<TxSet::ID, TxSet> txSets;
int completedLedgers = 0;
int targetLedgers = std::numeric_limits<int>::max();
//! Skew samples from the network clock; to be refactored into a
//! clock time once it is provided separately from the network.
std::chrono::seconds clockSkew{0};
//! Delay in processing validations from remote peers
std::chrono::milliseconds validationDelay{0};
//! Delay in acquiring missing ledger from the network
std::chrono::milliseconds missingLedgerDelay{0};
bool validating = true;
bool proposing = true;
//! All peers start from the default constructed ledger
Peer(PeerID i, BasicNetwork<Peer*> & n, UNL const & u)
: Consensus<Peer, Traits>( n.clock(), beast::Journal{})
, id{i}
, net{n}
, unl(u)
{
ledgers[lastClosedLedger.id()] = lastClosedLedger;
}
// @return whether we are proposing,validating
// TODO: Bit akward that this is in callbacks, would be nice to extract
std::pair<bool, bool>
getMode()
{
// in RCL this hits NetworkOps to decide whether we are proposing
// validating
return{ proposing, validating };
}
Ledger const *
acquireLedger(Ledger::ID const & ledgerHash)
{
auto it = ledgers.find(ledgerHash);
if (it != ledgers.end())
return &(it->second);
// TODO Get from network/oracle properly!
for (auto const& link : net.links(this))
{
auto const & p = *link.to;
auto it = p.ledgers.find(ledgerHash);
if (it != p.ledgers.end())
{
schedule(missingLedgerDelay,
[this, ledgerHash, ledger = it->second]()
{
ledgers.emplace(ledgerHash, ledger);
});
if(missingLedgerDelay == 0ms)
return &ledgers[ledgerHash];
break;
}
}
return nullptr;
}
auto const &
proposals(Ledger::ID const & ledgerHash)
{
return peerPositions_[ledgerHash];
}
TxSet const *
acquireTxSet(TxSet::ID const & setId)
{
auto it = txSets.find(setId);
if(it != txSets.end())
return &(it->second);
// TODO Get from network/oracle instead!
return nullptr;
}
bool
hasOpenTransactions() const
{
return !openTxs.empty();
}
std::size_t
proposersValidated(Ledger::ID const & prevLedger)
{
return peerValidations.proposersValidated(prevLedger);
}
std::size_t
proposersFinished(Ledger::ID const & prevLedger)
{
return peerValidations.proposersFinished(prevLedger);
}
void
onStartRound(Ledger const &) {}
void
onClose(Ledger const &, bool ) {}
// don't really offload
void
dispatchAccept(TxSet const & f)
{
Base::accept(f);
}
void
share(TxSet const &s)
{
relay(s);
}
Ledger::ID
getLCL(Ledger::ID const & currLedger,
Ledger::ID const & priorLedger,
bool haveCorrectLCL)
{
// TODO: Use generic validation code
if(currLedger.seq > 0 && priorLedger.seq > 0)
return peerValidations.getBestLCL(currLedger, priorLedger);
return currLedger;
}
void
propose(Proposal const & pos)
{
if(proposing)
relay(pos);
}
void
relay(DisputedTx<Tx, PeerID> const & dispute)
{
relay(dispute.tx());
}
std::pair <TxSet, Proposal>
makeInitialPosition(
Ledger const & prevLedger,
bool isProposing,
bool isCorrectLCL,
NetClock::time_point closeTime,
NetClock::time_point now)
{
TxSet res{ openTxs };
return { res,
Proposal{prevLedger.id(), Proposal::seqJoin, res.id(), closeTime, now, id} };
}
// Process the accepted transaction set, generating the newly closed ledger
// and clearing out the openTxs that were included.
// TODO: Kinda nasty it takes so many arguments . . . sign of bad coupling
bool
accept(TxSet const& set,
NetClock::time_point consensusCloseTime,
bool proposing_,
bool validating_,
bool haveCorrectLCL_,
bool consensusFail_,
Ledger::ID const & prevLedgerHash_,
Ledger const & previousLedger_,
NetClock::duration closeResolution_,
NetClock::time_point const & now,
std::chrono::milliseconds const & roundTime_,
hash_map<Tx::ID, DisputedTx <Tx, PeerID>> const & disputes_,
std::map <NetClock::time_point, int> closeTimes_,
NetClock::time_point const & closeTime)
{
auto newLedger = previousLedger_.close(set.txs_, closeResolution_,
closeTime, consensusCloseTime != NetClock::time_point{});
ledgers[newLedger.id()] = newLedger;
lastClosedLedger = newLedger;
auto it = std::remove_if(openTxs.begin(), openTxs.end(),
[&](Tx const & tx)
{
return set.exists(tx.id());
});
openTxs.erase(it, openTxs.end());
if(validating)
relay(Validation{id, newLedger.id(), newLedger.parentID()});
return validating_;
}
void
endConsensus(bool correct)
{
// kick off the next round...
// in the actual implementation, this passes back through
// network ops
++completedLedgers;
// startRound sets the LCL state, so we need to call it once after
// the last requested round completes
// TODO: reconsider this and instead just save LCL generated here?
if(completedLedgers <= targetLedgers)
{
startRound(now(), lastClosedLedger.id(),
lastClosedLedger);
}
}
//-------------------------------------------------------------------------
// non-callback helpers
void
receive(Proposal const & p)
{
if(unl.find(p.nodeID()) == unl.end())
return;
// TODO: Be sure this is a new proposal!!!!!
auto & dest = peerPositions_[p.prevLedger()];
if(std::find(dest.begin(), dest.end(), p) != dest.end())
return;
dest.push_back(p);
peerProposal(now(), p);
}
void
receive(TxSet const & txs)
{
// save and map complete?
auto it = txSets.insert(std::make_pair(txs.id(), txs));
if(it.second)
gotTxSet(now(), txs);
}
void
receive(Tx const & tx)
{
if (openTxs.find(tx.id()) == openTxs.end())
{
openTxs.insert(tx);
// relay to peers???
relay(tx);
}
}
void
receive(Validation const & v)
{
if(unl.find(v.id) != unl.end())
{
schedule(validationDelay,
[&, v]()
{
peerValidations.update(v);
});
}
}
template <class T>
void
relay(T const & t)
{
for(auto const& link : net.links(this))
net.send(this, link.to,
[msg = t, to = link.to]
{
to->receive(msg);
});
}
// Receive and relay locally submitted transaction
void
submit(Tx const & tx)
{
receive(tx);
relay(tx);
}
void
timerEntry()
{
Base::timerEntry(now());
// only reschedule if not completed
if(completedLedgers < targetLedgers)
net.timer(LEDGER_GRANULARITY, [&]() { timerEntry(); });
}
void
start()
{
net.timer(LEDGER_GRANULARITY, [&]() { timerEntry(); });
// The ID is the one we have seen the most validations for
// In practice, we might not actually have that ledger itself yet,
// so there is no gaurantee that bestLCL == lastClosedLedger.id()
auto bestLCL = peerValidations.getBestLCL(lastClosedLedger.id(),
lastClosedLedger.parentID());
startRound(now(), bestLCL,
lastClosedLedger);
}
NetClock::time_point
now() const
{
// We don't care about the actual epochs, but do want the
// generated NetClock time to be well past its epoch to ensure
// any subtractions of two NetClock::time_point in the consensu
// code are positive. (e.g. PROPOSE_FRESHNESS)
using namespace std::chrono;
return NetClock::time_point(duration_cast<NetClock::duration>
(net.now().time_since_epoch()+ 86400s + clockSkew));
}
// Schedule the provided callback in `when` duration, but if
// `when` is 0, call immediately
template <class T>
void schedule(std::chrono::nanoseconds when, T && what)
{
if(when == 0ns)
what();
else
net.timer(when, std::forward<T>(what));
}
};
} // csf
} // test
} // ripple
#endif

104
src/test/csf/Sim.h Normal file
View File

@@ -0,0 +1,104 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012-2017 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.
*/
//==============================================================================
#ifndef RIPPLE_TEST_CSF_SIM_H_INCLUDED
#define RIPPLE_TEST_CSF_SIM_H_INCLUDED
#include <test/csf/UNL.h>
#include <test/csf/BasicNetwork.h>
namespace ripple {
namespace test {
namespace csf {
class Sim
{
public:
/** Create a simulator for the given trust graph and network topology.
Create a simulator for consensus over the given trust graph and connect
the network links between nodes based on the provided topology.
Topology is is a functor with signature
boost::optional<std::chrono::duration> (NodeId i, NodeId j)
that returns the delay sending messages from node i to node j.
In general, this network graph is distinct from the trust graph, but
users can use adaptors to present a TrustGraph as a Topology by
specifying the delay between nodes.
@param g The trust graph between peers.
@param top The network topology between peers.
*/
template <class Topology>
Sim(TrustGraph const & g, Topology const & top)
{
peers.reserve(g.numPeers());
for(int i = 0; i < g.numPeers(); ++i)
peers.emplace_back(i, net, g.unl(i));
for(int i = 0; i < peers.size(); ++i)
{
for(int j = 0; j < peers.size(); ++j)
{
if( i != j)
{
auto d = top(i,j);
if (d)
{
net.connect(&peers[i], &peers[j], *d);
}
}
}
}
}
/** Run consensus protocol to generate the provided number of ledgers.
Has each peer run consensus until it creates `ledgers` more ledgers.
@param ledgers The number of additional ledgers to create
*/
void
run(int ledgers)
{
for (auto & p : peers)
{
if(p.completedLedgers == 0)
p.relay(Validation{p.id, p.LCL(), p.LCL()});
p.targetLedgers = p.completedLedgers + ledgers;
p.start();
}
net.step();
}
std::vector<Peer> peers;
BasicNetwork<Peer*> net;
};
} // csf
} // test
} // ripple
#endif

221
src/test/csf/Tx.h Normal file
View File

@@ -0,0 +1,221 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012-2017 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.
*/
//==============================================================================
#ifndef RIPPLE_TEST_CSF_TX_H_INCLUDED
#define RIPPLE_TEST_CSF_TX_H_INCLUDED
#include <ripple/beast/hash/hash_append.h>
#include <boost/container/flat_set.hpp>
#include <ostream>
#include <string>
#include <map>
namespace ripple {
namespace test {
namespace csf {
//! A single transaction
class Tx
{
public:
using ID = std::uint32_t;
Tx(ID i) : id_{ i } {}
ID
id() const
{
return id_;
}
bool
operator<(Tx const & o) const
{
return id_ < o.id_;
}
bool
operator==(Tx const & o) const
{
return id_ == o.id_;
}
private:
ID id_;
};
//!-------------------------------------------------------------------------
//! All sets of Tx are represented as a flat_set.
using TxSetType = boost::container::flat_set<Tx>;
//! TxSet is a set of transactions to consider including in the ledger
class TxSet
{
public:
using ID = TxSetType;
using Tx = csf::Tx;
using MutableTxSet = TxSet;
TxSet() = default;
TxSet(TxSetType const & s) : txs_{ s } {}
bool
insert(Tx const & t)
{
return txs_.insert(t).second;
}
bool
erase(Tx::ID const & txId)
{
return txs_.erase(Tx{ txId }) > 0;
}
bool
exists(Tx::ID const txId) const
{
auto it = txs_.find(Tx{ txId });
return it != txs_.end();
}
Tx const *
find(Tx::ID const& txId) const
{
auto it = txs_.find(Tx{ txId });
if (it != txs_.end())
return &(*it);
return nullptr;
}
auto const &
id() const
{
return txs_;
}
/** @return Map of Tx::ID that are missing. True means
it was in this set and not other. False means
it was in the other set and not this
*/
std::map<Tx::ID, bool>
compare(TxSet const& other) const
{
std::map<Tx::ID, bool> res;
auto populate_diffs = [&res](auto const & a, auto const & b, bool s)
{
auto populator = [&](auto const & tx)
{
res[tx.id()] = s;
};
std::set_difference(
a.begin(), a.end(),
b.begin(), b.end(),
boost::make_function_output_iterator(
std::ref(populator)
)
);
};
populate_diffs(txs_, other.txs_, true);
populate_diffs(other.txs_, txs_, false);
return res;
}
//! The set contains the actual transactions
TxSetType txs_;
};
/** The RCL consensus process catches missing node SHAMap error
in several points. This exception is meant to represent a similar
case for the unit test.
*/
class MissingTx : public std::runtime_error
{
public:
MissingTx()
: std::runtime_error("MissingTx")
{}
};
//------------------------------------------------------------------------------
// Helper functions for debug printing
inline
std::ostream&
operator<<(std::ostream & o, const Tx & t)
{
return o << t.id();
}
template <class T>
inline
std::ostream&
operator<<(std::ostream & o, boost::container::flat_set<T> const & ts)
{
o << "{ ";
bool do_comma = false;
for (auto const & t : ts)
{
if (do_comma)
o << ", ";
else
do_comma = true;
o << t;
}
o << " }";
return o;
}
inline
std::string
to_string(TxSetType const & txs)
{
std::stringstream ss;
ss << txs;
return ss.str();
}
template <class Hasher>
inline
void
hash_append(Hasher& h, Tx const & tx)
{
using beast::hash_append;
hash_append(h, tx.id());
}
std::ostream&
operator<<(std::ostream & o, MissingTx const &m)
{
return o << m.what();
}
} // csf
} // test
} // ripple
#endif

277
src/test/csf/UNL.h Normal file
View File

@@ -0,0 +1,277 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012-2017 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.
*/
//==============================================================================
#ifndef RIPPLE_TEST_CSF_UNL_H_INCLUDED
#define RIPPLE_TEST_CSF_UNL_H_INCLUDED
#include <boost/container/flat_set.hpp>
#include <boost/optional.hpp>
#include <vector>
#include <random>
#include <numeric>
#include <chrono>
namespace ripple {
namespace test {
namespace csf {
/** Return a randomly shuffled copy of vector based on weights w.
@param v The set of values
@param w The set of weights of each value
@param g A pseudo-random number generator
@return A vector with entries randomly sampled without replacement
from the original vector based on the provided weights.
I.e. res[0] comes from sample v[i] with weight w[i]/sum_k w[k]
*/
template <class T, class G>
std::vector<T>
random_weighted_shuffle(std::vector<T> v, std::vector<double> w, G & g)
{
using std::swap;
for (int i = 0; i < v.size() - 1; ++i)
{
// pick a random item weighted by w
std::discrete_distribution<> dd(w.begin() + i, w.end());
auto idx = dd(g);
std::swap(v[i], v[idx]);
std::swap(w[i], w[idx]);
}
return v;
}
/** Power-law distribution with PDF
P(x) = (x/xmin)^-a
for a >= 1 and xmin >= 1
*/
class PowerLawDistribution
{
double xmin_;
double a_;
double inv_;
std::uniform_real_distribution<double> uf_{0,1};
public:
PowerLawDistribution(double xmin, double a)
: xmin_{xmin}, a_{a}
{
inv_ = 1.0/(1.0 - a_);
}
template <class Generator>
inline
double
operator()(Generator & g)
{
// use inverse transform of CDF to sample
// CDF is P(X <= x): 1 - (x/xmin)^(1-a)
return xmin_ * std::pow(1 - uf_(g), inv_);
}
};
//< Unique identifier for each node in the network
using PeerID = std::uint32_t;
//< A unique node list defines a set of trusted peers used in consensus
using UNL = boost::container::flat_set<PeerID>;
/** Trust graph defining the consensus simulation
Trust is a directed relationship from a node i to node j.
If node i trusts node j, then node i has node j in its UNL.
Note that each node implicitly trusts itself but that need not be
explicitly modeled, e.g. UNLS[assignment
*/
class TrustGraph
{
//< Unique UNLs for the network
std::vector<UNL> UNLs_;
std::vector<int> assignment_;
public:
//< Constructor
TrustGraph(std::vector<UNL> UNLs, std::vector<int> assignment)
: UNLs_{UNLs}
, assignment_{assignment}
{}
//< Whether node `i` trusts node `j`
inline
bool
trusts(PeerID i, PeerID j) const
{
return unl(i).find(j) != unl(i).end();
}
//< Get the UNL for node `i`
inline
UNL const &
unl(PeerID i) const
{
return UNLs_[assignment_[i]];
}
//< Check whether this trust graph satisfies the no forking condition
bool
canFork(double quorum) const;
auto
numPeers() const
{
return assignment_.size();
}
//< Save grapviz dot file reprentation of the trust graph
void
save_dot(std::string const & fileName);
/** Generate a random trust graph based on random ranking of peers
Generate a random trust graph by
1. Randomly ranking the peers acording to RankPDF
2. Generating `numUNL` random UNLs by sampling without replacement
from the ranked nodes.
3. Restricting the size of the random UNLs according to SizePDF
@param size The number of nodes in the trust graph
@param numUNLs The number of UNLs to create
@param rankPDF Generates random positive real numbers to use as ranks
@param unlSizePDF Generates random integeres between (0,size-1) to
restrict the size of generated PDF
@param Generator The uniform random bit generator to use
@note RankPDF/SizePDF can model the full RandomDistribution concept
defined in the STL, but for the purposes of this function need
only provide:
auto operator()(Generator & g)
which should return the random sample.
*/
template <class RankPDF, class SizePDF, class Generator>
static
TrustGraph
makeRandomRanked(int size,
int numUNLs,
RankPDF rankPDF,
SizePDF unlSizePDF,
Generator & g)
{
// 1. Generate ranks
std::vector<double> weights(size);
std::generate(weights.begin(), weights.end(), [&]()
{
return rankPDF(g);
});
// 2. Generate UNLs based on sampling without replacement according
// to weights
std::vector<UNL> unls(numUNLs);
std::generate(unls.begin(), unls.end(), [&]()
{
std::vector<PeerID> ids(size);
std::iota(ids.begin(), ids.end(), 0);
auto res = random_weighted_shuffle(ids, weights, g);
return UNL(res.begin(), res.begin() + unlSizePDF(g));
});
// 3. Assign membership
std::vector<int> assignment(size);
std::uniform_int_distribution<int> u(0, numUNLs-1);
std::generate(assignment.begin(), assignment.end(),
[&]()
{
return u(g);
});
return TrustGraph(unls, assignment);
}
/** Generate a 2 UNL trust graph with some overlap.
Generates a trust graph for `size` peers formed from
two cliques with the given overlap. Nodes in the overlap
trust both all other nodes, while nodes outside the overlap
only trust nodes in their clique.
@param size The number of nodes in the trust graph
@param overlap The number of nodes trusting both cliques
*/
static
TrustGraph
makeClique(int size, int overlap);
/** Generate a complete (fully-connect) trust graph
Generatest a trust graph in which all peers trust all
other peers.
@param size The number of nodes in the trust graph
*/
static
TrustGraph
makeComplete(int size);
};
//< Make the TrustGraph into a topology with delays given by DelayModel
template <class DelayModel>
auto
topology(TrustGraph const & tg, DelayModel const & d)
{
return [&](PeerID i, PeerID j)
{
return tg.trusts(i,j) ? boost::make_optional(d(i,j)) : boost::none;
};
}
class fixed
{
std::chrono::nanoseconds d_;
public:
fixed(std::chrono::nanoseconds const & d) : d_{d} {}
inline
std::chrono::nanoseconds
operator()(PeerID const & i, PeerID const & j) const
{
return d_;
}
};
} // csf
} // test
} // ripple
#endif

135
src/test/csf/impl/UNL.cpp Normal file
View File

@@ -0,0 +1,135 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012-2017 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/UNL.h>
#include <boost/iterator/counting_iterator.hpp>
#include <fstream>
#include <algorithm>
namespace ripple {
namespace test {
namespace csf {
bool
TrustGraph::canFork(double quorum) const
{
// Check the forking condition by looking at intersection
// between all pairs of UNLs.
// First check if some nodes uses a UNL they are not members of, since
// this creates an implicit UNL with that ndoe.
auto uniqueUNLs = UNLs_;
for (int i = 0; i < assignment_.size(); ++i)
{
auto const & myUNL = UNLs_[assignment_[i]];
if(myUNL.find(i) == myUNL.end())
{
auto myUNLcopy = myUNL;
myUNLcopy.insert(i);
uniqueUNLs.push_back(std::move(myUNLcopy));
}
}
// Loop over all pairs of uniqueUNLs
for (int i = 0; i < uniqueUNLs.size(); ++i)
{
for (int j = (i+1); j < uniqueUNLs.size(); ++j)
{
auto const & unlA = uniqueUNLs[i];
auto const & unlB = uniqueUNLs[j];
double rhs = 2.0*(1.-quorum) *
std::max(unlA.size(), unlB.size() );
int intersectionSize = std::count_if(unlA.begin(), unlA.end(),
[&](PeerID id)
{
return unlB.find(id) != unlB.end();
});
if(intersectionSize < rhs)
return true;
}
}
return false;
}
TrustGraph
TrustGraph::makeClique(int size, int overlap)
{
using bci = boost::counting_iterator<PeerID>;
// Split network into two cliques with the given overlap
// Clique A has nodes [0,endA) and Clique B has [startB,numPeers)
// Note: Clique B will have an extra peer when numPeers - overlap
// is odd
int endA = (size + overlap)/2;
int startB = (size - overlap)/2;
std::vector<UNL> unls;
unls.emplace_back(bci(0), bci(endA));
unls.emplace_back(bci(startB), bci(size));
unls.emplace_back(bci(0), bci(size));
std::vector<int> assignment(size,0);
for (int i = 0; i < size; ++i)
{
if(i < startB)
assignment[i] = 0;
else if(i > endA)
assignment[i] = 1;
else
assignment[i] = 2;
}
return TrustGraph(unls, assignment);
}
TrustGraph
TrustGraph::makeComplete(int size)
{
UNL all{ boost::counting_iterator<PeerID>( 0 ),
boost::counting_iterator<PeerID>( size ) };
return TrustGraph(std::vector<UNL>(1,all),
std::vector<int>(size, 0));
}
inline void TrustGraph::save_dot(std::string const & fileName)
{
std::ofstream out(fileName);
out << "digraph {\n";
for (int i = 0; i < assignment_.size(); ++i)
{
for (auto & j : UNLs_[assignment_[i]])
{
out << i << " -> " << j << ";\n";
}
}
out << "}\n";
}
} // csf
} // test
} // ripple