mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-29 10:00:30 +00:00
2143 lines
74 KiB
C++
2143 lines
74 KiB
C++
#pragma once
|
|
|
|
#include <xrpld/consensus/ConsensusParms.h>
|
|
#include <xrpld/consensus/ConsensusProposal.h>
|
|
#include <xrpld/consensus/ConsensusSpanNames.h>
|
|
#include <xrpld/consensus/ConsensusTypes.h>
|
|
|
|
#include <xrpl/basics/Log.h>
|
|
#include <xrpl/basics/UnorderedContainers.h>
|
|
#include <xrpl/basics/chrono.h>
|
|
#include <xrpl/beast/clock/abstract_clock.h>
|
|
#include <xrpl/beast/utility/Journal.h>
|
|
#include <xrpl/beast/utility/instrumentation.h>
|
|
#include <xrpl/consensus/ConsensusParms.h>
|
|
#include <xrpl/consensus/ConsensusProposal.h>
|
|
#include <xrpl/consensus/ConsensusTypes.h>
|
|
#include <xrpl/json/json_value.h>
|
|
#include <xrpl/json/json_writer.h>
|
|
#include <xrpl/ledger/LedgerTiming.h>
|
|
#include <xrpl/telemetry/SpanGuard.h>
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <deque>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <utility>
|
|
|
|
namespace xrpl {
|
|
|
|
/**
|
|
* Determines whether the current ledger should close at this time.
|
|
*
|
|
* This function should be called when a ledger is open and there is no close
|
|
* in progress, or when a transaction is received and no close is in progress.
|
|
*
|
|
* @param anyTransactions indicates whether any transactions have been received
|
|
* @param prevProposers proposers in the last closing
|
|
* @param proposersClosed proposers who have currently closed this ledger
|
|
* @param proposersValidated proposers who have validated the last closed
|
|
* ledger
|
|
* @param prevRoundTime time for the previous ledger to reach consensus
|
|
* @param timeSincePrevClose time since the previous ledger's (possibly
|
|
* rounded) close time
|
|
* @param openTime duration this ledger has been open
|
|
* @param idleInterval the network's desired idle interval
|
|
* @param parms Consensus constant parameters
|
|
* @param j journal for logging
|
|
* @param clog log object to which to append
|
|
*/
|
|
bool
|
|
shouldCloseLedger(
|
|
bool anyTransactions,
|
|
std::size_t prevProposers,
|
|
std::size_t proposersClosed,
|
|
std::size_t proposersValidated,
|
|
std::chrono::milliseconds prevRoundTime,
|
|
std::chrono::milliseconds timeSincePrevClose,
|
|
std::chrono::milliseconds openTime,
|
|
std::chrono::milliseconds idleInterval,
|
|
ConsensusParms const& parms,
|
|
beast::Journal j,
|
|
std::unique_ptr<std::stringstream> const& clog = {});
|
|
|
|
/**
|
|
* Determine whether the network reached consensus and whether we joined.
|
|
*
|
|
* @param prevProposers proposers in the last closing (not including us)
|
|
* @param currentProposers proposers in this closing so far (not including us)
|
|
* @param currentAgree proposers who agree with us
|
|
* @param currentFinished proposers who have validated a ledger after this one
|
|
* @param previousAgreeTime how long, in milliseconds, it took to agree on the
|
|
* last ledger
|
|
* @param currentAgreeTime how long, in milliseconds, we've been trying to
|
|
* agree
|
|
* @param stalled the network appears to be stalled, where
|
|
* neither we nor our peers have changed their vote on any disputes in a
|
|
* while. This is undesirable, and should be rare, and will cause us to
|
|
* end consensus without 80% agreement.
|
|
* @param parms Consensus constant parameters
|
|
* @param proposing whether we should count ourselves
|
|
* @param j journal for logging
|
|
* @param clog log object to which to append
|
|
*/
|
|
ConsensusState
|
|
checkConsensus(
|
|
std::size_t prevProposers,
|
|
std::size_t currentProposers,
|
|
std::size_t currentAgree,
|
|
std::size_t currentFinished,
|
|
std::chrono::milliseconds previousAgreeTime,
|
|
std::chrono::milliseconds currentAgreeTime,
|
|
bool stalled,
|
|
ConsensusParms const& parms,
|
|
bool proposing,
|
|
beast::Journal j,
|
|
std::unique_ptr<std::stringstream> const& clog = {});
|
|
|
|
/**
|
|
* Generic implementation of consensus algorithm.
|
|
*
|
|
* Achieves consensus on the next ledger.
|
|
*
|
|
* Two things need consensus:
|
|
*
|
|
* 1. The set of transactions included in the ledger.
|
|
* 2. The close time for the ledger.
|
|
*
|
|
* The basic flow:
|
|
*
|
|
* 1. A call to `startRound` places the node in the `Open` phase. In this
|
|
* phase, the node is waiting for transactions to include in its open
|
|
* ledger.
|
|
* 2. Successive calls to `timerEntry` check if the node can close the ledger.
|
|
* Once the node `Close`s the open ledger, it transitions to the
|
|
* `Establish` phase. In this phase, the node shares/receives peer
|
|
* proposals on which transactions should be accepted in the closed ledger.
|
|
* 3. During a subsequent call to `timerEntry`, the node determines it has
|
|
* reached consensus with its peers on which transactions to include. It
|
|
* transitions to the `Accept` phase. In this phase, the node works on
|
|
* applying the transactions to the prior ledger to generate a new closed
|
|
* ledger. Once the new ledger is completed, the node shares the validated
|
|
* ledger with the network, does some book-keeping, then makes a call to
|
|
* `startRound` to start the cycle again.
|
|
*
|
|
* This class uses a generic interface to allow adapting Consensus for specific
|
|
* applications. The Adaptor template implements a set of helper functions that
|
|
* plug the consensus algorithm into a specific application. It also identifies
|
|
* the types that play important roles in Consensus (transactions, ledgers, ...).
|
|
* The code stubs below outline the interface and type requirements. The traits
|
|
* types must be copy constructible and assignable.
|
|
*
|
|
* @warning The generic implementation is not thread safe and the public methods
|
|
* are not intended to be run concurrently. When in a concurrent environment,
|
|
* the application is responsible for ensuring thread-safety. Simply locking
|
|
* whenever touching the Consensus instance is one option.
|
|
*
|
|
* @code
|
|
* // A single transaction
|
|
* struct Tx
|
|
* {
|
|
* // Unique identifier of transaction
|
|
* using ID = ...;
|
|
*
|
|
* ID id() const;
|
|
*
|
|
* };
|
|
*
|
|
* // A set of transactions
|
|
* struct TxSet
|
|
* {
|
|
* // Unique ID of TxSet (not of Tx)
|
|
* using ID = ...;
|
|
* // Type of individual transaction comprising the TxSet
|
|
* using Tx = Tx;
|
|
*
|
|
* bool exists(Tx::ID const &) const;
|
|
* // Return value should have semantics like Tx const *
|
|
* Tx const * find(Tx::ID const &) const ;
|
|
* ID const & id() const;
|
|
*
|
|
* // Return set of transactions that are not common to this set or other
|
|
* // boolean indicates which set it was in
|
|
* std::map<Tx::ID, bool> compare(TxSet const & other) const;
|
|
*
|
|
* // A mutable view of transactions
|
|
* struct MutableTxSet
|
|
* {
|
|
* MutableTxSet(TxSet const &);
|
|
* bool insert(Tx const &);
|
|
* bool erase(Tx::ID const &);
|
|
* };
|
|
*
|
|
* // Construct from a mutable view.
|
|
* TxSet(MutableTxSet const &);
|
|
*
|
|
* // Alternatively, if the TxSet is itself mutable
|
|
* // just alias MutableTxSet = TxSet
|
|
*
|
|
* };
|
|
*
|
|
* // Agreed upon state that consensus transactions will modify
|
|
* struct Ledger
|
|
* {
|
|
* using ID = ...;
|
|
* using Seq = ...;
|
|
*
|
|
* // Unique identifier of ledger
|
|
* ID const id() const;
|
|
* Seq seq() const;
|
|
* auto closeTimeResolution() const;
|
|
* auto closeAgree() const;
|
|
* auto closeTime() const;
|
|
* auto parentCloseTime() const;
|
|
* json::Value getJson() const;
|
|
* };
|
|
*
|
|
* // Wraps a peer's ConsensusProposal
|
|
* struct PeerPosition
|
|
* {
|
|
* ConsensusProposal<
|
|
* std::uint32_t, //NodeID,
|
|
* typename Ledger::ID,
|
|
* typename TxSet::ID> const &
|
|
* proposal() const;
|
|
*
|
|
* };
|
|
*
|
|
*
|
|
* class Adaptor
|
|
* {
|
|
* public:
|
|
* //-----------------------------------------------------------------------
|
|
* // Define consensus types
|
|
* using Ledger_t = Ledger;
|
|
* using NodeID_t = std::uint32_t;
|
|
* using TxSet_t = TxSet;
|
|
* using PeerPosition_t = PeerPosition;
|
|
*
|
|
* //-----------------------------------------------------------------------
|
|
* //
|
|
* // Attempt to acquire a specific ledger.
|
|
* std::optional<Ledger> acquireLedger(Ledger::ID const & ledgerID);
|
|
*
|
|
* // Acquire the transaction set associated with a proposed position.
|
|
* std::optional<TxSet> acquireTxSet(TxSet::ID const & setID);
|
|
*
|
|
* // Whether any transactions are in the open ledger
|
|
* bool hasOpenTransactions() const;
|
|
*
|
|
* // Number of proposers that have validated the given ledger
|
|
* std::size_t proposersValidated(Ledger::ID const & prevLedger) const;
|
|
*
|
|
* // Number of proposers that have validated a ledger descended from the
|
|
* // given ledger; if prevLedger.id() != prevLedgerID, use prevLedgerID
|
|
* // for the determination
|
|
* std::size_t proposersFinished(Ledger const & prevLedger,
|
|
* Ledger::ID const & prevLedger) const;
|
|
*
|
|
* // Return the ID of the last closed (and validated) ledger that the
|
|
* // application thinks consensus should use as the prior ledger.
|
|
* Ledger::ID getPrevLedger(Ledger::ID const & prevLedgerID,
|
|
* Ledger const & prevLedger,
|
|
* Mode mode);
|
|
*
|
|
* // Called whenever consensus operating mode changes
|
|
* void onModeChange(ConsensusMode before, ConsensusMode after);
|
|
*
|
|
* // Called when ledger closes
|
|
* Result onClose(Ledger const &, Ledger const & prev, Mode mode);
|
|
*
|
|
* // Called when ledger is accepted by consensus
|
|
* void onAccept(Result const & result,
|
|
* RCLCxLedger const & prevLedger,
|
|
* NetClock::duration closeResolution,
|
|
* CloseTimes const & rawCloseTimes,
|
|
* Mode const & mode);
|
|
*
|
|
* // Called when ledger was forcibly accepted by consensus via the simulate
|
|
* // function.
|
|
* void onForceAccept(Result const & result,
|
|
* RCLCxLedger const & prevLedger,
|
|
* NetClock::duration closeResolution,
|
|
* CloseTimes const & rawCloseTimes,
|
|
* Mode const & mode);
|
|
*
|
|
* // Propose the position to peers.
|
|
* void propose(ConsensusProposal<...> const & pos);
|
|
*
|
|
* // Share a received peer proposal with other peer's.
|
|
* void share(PeerPosition_t const & prop);
|
|
*
|
|
* // Share a disputed transaction with peers
|
|
* void share(Txn const & tx);
|
|
*
|
|
* // Share given transaction set with peers
|
|
* void share(TxSet const &s);
|
|
*
|
|
* // Consensus timing parameters and constants
|
|
* ConsensusParms const &
|
|
* parms() const;
|
|
* };
|
|
* @endcode
|
|
*
|
|
* @tparam Adaptor Defines types and provides helper functions needed to adapt
|
|
* Consensus to the larger application.
|
|
*/
|
|
template <class Adaptor>
|
|
class Consensus
|
|
{
|
|
using Ledger_t = Adaptor::Ledger_t;
|
|
using TxSet_t = Adaptor::TxSet_t;
|
|
using NodeID_t = Adaptor::NodeID_t;
|
|
using Tx_t = TxSet_t::Tx;
|
|
using PeerPosition_t = Adaptor::PeerPosition_t;
|
|
using Proposal_t = ConsensusProposal<NodeID_t, typename Ledger_t::ID, typename TxSet_t::ID>;
|
|
|
|
using Result = ConsensusResult<Adaptor>;
|
|
|
|
// Helper class to ensure adaptor is notified whenever the ConsensusMode
|
|
// changes
|
|
class MonitoredMode
|
|
{
|
|
ConsensusMode mode_;
|
|
|
|
public:
|
|
MonitoredMode(ConsensusMode m) : mode_{m}
|
|
{
|
|
}
|
|
[[nodiscard]] ConsensusMode
|
|
get() const
|
|
{
|
|
return mode_;
|
|
}
|
|
|
|
void
|
|
set(ConsensusMode mode, Adaptor& a)
|
|
{
|
|
a.onModeChange(mode_, mode);
|
|
mode_ = mode;
|
|
}
|
|
};
|
|
|
|
public:
|
|
/**
|
|
* Clock type for measuring time within the consensus code
|
|
*/
|
|
using clock_type = beast::AbstractClock<std::chrono::steady_clock>;
|
|
|
|
Consensus(Consensus&&) noexcept = default;
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @param clock The clock used to internally sample consensus progress
|
|
* @param adaptor The instance of the adaptor class
|
|
* @param j The journal to log debug output
|
|
*/
|
|
Consensus(clock_type const& clock, Adaptor& adaptor, beast::Journal j);
|
|
|
|
/**
|
|
* Kick-off the next round of consensus.
|
|
*
|
|
* Called by the client code to start each round of consensus.
|
|
*
|
|
* @param now The network adjusted time
|
|
* @param prevLedgerID the ID of the last ledger
|
|
* @param prevLedger The last ledger
|
|
* @param nowUntrusted ID of nodes that are newly untrusted this round
|
|
* @param proposing Whether we want to send proposals to peers this
|
|
* round.
|
|
* @param clog log object to which to append
|
|
*
|
|
* @note @b prevLedgerID is not required to the ID of @b prevLedger since
|
|
* the ID may be known locally before the contents of the ledger arrive
|
|
*/
|
|
void
|
|
startRound(
|
|
NetClock::time_point const& now,
|
|
Ledger_t::ID const& prevLedgerID,
|
|
Ledger_t prevLedger,
|
|
hash_set<NodeID_t> const& nowUntrusted,
|
|
bool proposing,
|
|
std::unique_ptr<std::stringstream> const& clog = {});
|
|
|
|
/**
|
|
* A peer has proposed a new position, adjust our tracking.
|
|
*
|
|
* @param now The network adjusted time
|
|
* @param newProposal The new proposal from a peer
|
|
* @return Whether we should do delayed relay of this proposal.
|
|
*/
|
|
bool
|
|
peerProposal(NetClock::time_point const& now, PeerPosition_t const& newProposal);
|
|
|
|
/**
|
|
* Call periodically to drive consensus forward.
|
|
*
|
|
* @param now The network adjusted time
|
|
* @param clog log object to which to append
|
|
*/
|
|
void
|
|
timerEntry(
|
|
NetClock::time_point const& now,
|
|
std::unique_ptr<std::stringstream> const& clog = {});
|
|
|
|
/**
|
|
* Process a transaction set acquired from the network
|
|
*
|
|
* @param now The network adjusted time
|
|
* @param txSet the transaction set
|
|
*/
|
|
void
|
|
gotTxSet(NetClock::time_point const& now, TxSet_t const& txSet);
|
|
|
|
/**
|
|
* Simulate the consensus process without any network traffic.
|
|
*
|
|
* The end result, is that consensus begins and completes as if everyone
|
|
* had agreed with whatever we propose.
|
|
*
|
|
* This function is only called from the rpc "ledger_accept" path with the
|
|
* server in standalone mode and SHOULD NOT be used during the normal
|
|
* consensus process.
|
|
*
|
|
* Simulate will call onForceAccept since clients are manually driving
|
|
* consensus to the accept phase.
|
|
*
|
|
* @param now The current network adjusted time.
|
|
* @param consensusDelay Duration to delay between closing and accepting the
|
|
* ledger. Uses 100ms if unspecified.
|
|
*/
|
|
void
|
|
simulate(
|
|
NetClock::time_point const& now,
|
|
std::optional<std::chrono::milliseconds> consensusDelay);
|
|
|
|
/**
|
|
* Get the previous ledger ID.
|
|
*
|
|
* The previous ledger is the last ledger seen by the consensus code and
|
|
* should correspond to the most recent validated ledger seen by this peer.
|
|
*
|
|
* @return ID of previous ledger
|
|
*/
|
|
Ledger_t::ID
|
|
prevLedgerID() const
|
|
{
|
|
return prevLedgerID_;
|
|
}
|
|
|
|
[[nodiscard]] ConsensusPhase
|
|
phase() const
|
|
{
|
|
return phase_;
|
|
}
|
|
|
|
/**
|
|
* Get the Json state of the consensus process.
|
|
*
|
|
* Called by the consensus_info RPC.
|
|
*
|
|
* @param full True if verbose response desired.
|
|
* @return The Json state.
|
|
*/
|
|
[[nodiscard]] json::Value
|
|
getJson(bool full) const;
|
|
|
|
private:
|
|
/**
|
|
* Why startRoundInternal is being entered.
|
|
*
|
|
* Distinguishes the normal Initial entry (from public startRound)
|
|
* from a Recovered re-entry (from handleWrongLedger after the
|
|
* correct prior ledger was acquired mid-round). The Recovered path
|
|
* resets phase to Open within the SAME round, which is a state
|
|
* transition that would otherwise be invisible in traces — the
|
|
* recovery flag drives a `phase.recovery` event on the round span.
|
|
*/
|
|
enum class StartRoundReason : std::uint8_t {
|
|
Initial,
|
|
Recovered,
|
|
};
|
|
|
|
void
|
|
startRoundInternal(
|
|
NetClock::time_point const& now,
|
|
Ledger_t::ID const& prevLedgerID,
|
|
Ledger_t const& prevLedger,
|
|
ConsensusMode mode,
|
|
std::unique_ptr<std::stringstream> const& clog,
|
|
StartRoundReason reason = StartRoundReason::Initial);
|
|
|
|
// Change our view of the previous ledger
|
|
void
|
|
handleWrongLedger(Ledger_t::ID const& lgrId, std::unique_ptr<std::stringstream> const& clog);
|
|
|
|
/**
|
|
* Check if our previous ledger matches the network's.
|
|
*
|
|
* If the previous ledger differs, we are no longer in sync with
|
|
* the network and need to bow out/switch modes.
|
|
*/
|
|
void
|
|
checkLedger(std::unique_ptr<std::stringstream> const& clog);
|
|
|
|
/**
|
|
* If we radically changed our consensus context for some reason,
|
|
* we need to replay recent proposals so that they're not lost.
|
|
*/
|
|
void
|
|
playbackProposals();
|
|
|
|
/**
|
|
* Handle a replayed or a new peer proposal.
|
|
*/
|
|
bool
|
|
peerProposalInternal(NetClock::time_point const& now, PeerPosition_t const& newProposal);
|
|
|
|
/**
|
|
* Handle pre-close phase.
|
|
*
|
|
* In the pre-close phase, the ledger is open as we wait for new
|
|
* transactions. After enough time has elapsed, we will close the ledger,
|
|
* switch to the establish phase and start the consensus process.
|
|
*/
|
|
void
|
|
phaseOpen(std::unique_ptr<std::stringstream> const& clog);
|
|
|
|
/**
|
|
* Handle establish phase.
|
|
*
|
|
* In the establish phase, the ledger has closed and we work with peers
|
|
* to reach consensus. Update our position only on the timer, and in this
|
|
* phase.
|
|
*
|
|
* If we have consensus, move to the accepted phase.
|
|
*/
|
|
void
|
|
phaseEstablish(std::unique_ptr<std::stringstream> const& clog);
|
|
|
|
/**
|
|
* Evaluate whether pausing increases likelihood of validation.
|
|
*
|
|
* As a validator that has previously synced to the network, if our most
|
|
* recent locally-validated ledger did not also achieve
|
|
* full validation, then consider pausing for awhile based on
|
|
* the state of other validators.
|
|
*
|
|
* Pausing may be beneficial in this situation if at least one validator
|
|
* is known to be on a sequence number earlier than ours. The minimum
|
|
* number of validators on the same sequence number does not guarantee
|
|
* consensus, and waiting for all validators may be too time-consuming.
|
|
* Therefore, a variable threshold is enacted based on the number
|
|
* of ledgers past network validation that we are on. For the first phase,
|
|
* the threshold is also the minimum required for quorum. For the last,
|
|
* no online validators can have a lower sequence number. For intermediate
|
|
* phases, the threshold is linear between the minimum required for
|
|
* quorum and 100%. For example, with 3 total phases and a quorum of
|
|
* 80%, the 2nd phase would be 90%. Once the final phase is reached,
|
|
* if consensus still fails to occur, the cycle is begun again at phase 1.
|
|
*
|
|
* @return Whether to pause to wait for lagging proposers.
|
|
*/
|
|
[[nodiscard]] bool
|
|
shouldPause(std::unique_ptr<std::stringstream> const& clog) const;
|
|
|
|
// Close the open ledger and establish initial position.
|
|
void
|
|
closeLedger(std::unique_ptr<std::stringstream> const& clog);
|
|
|
|
// Adjust our positions to try to agree with other validators.
|
|
void
|
|
updateOurPositions(std::unique_ptr<std::stringstream> const& clog);
|
|
|
|
bool
|
|
haveConsensus(std::unique_ptr<std::stringstream> const& clog);
|
|
|
|
// Create disputes between our position and the provided one.
|
|
void
|
|
createDisputes(TxSet_t const& o, std::unique_ptr<std::stringstream> const& clog = {});
|
|
|
|
// Update our disputes given that this node has adopted a new position.
|
|
// Will call createDisputes as needed.
|
|
void
|
|
updateDisputes(NodeID_t const& node, TxSet_t const& other);
|
|
|
|
// Revoke our outstanding proposal, if any, and cease proposing
|
|
// until this round ends.
|
|
void
|
|
leaveConsensus(std::unique_ptr<std::stringstream> const& clog);
|
|
|
|
// The rounded or effective close time estimate from a proposer
|
|
[[nodiscard]] NetClock::time_point
|
|
asCloseTime(NetClock::time_point raw) const;
|
|
|
|
private:
|
|
Adaptor& adaptor_;
|
|
|
|
ConsensusPhase phase_{ConsensusPhase::Accepted};
|
|
MonitoredMode mode_{ConsensusMode::Observing};
|
|
bool firstRound_ = true;
|
|
bool haveCloseTimeConsensus_ = false;
|
|
|
|
clock_type const& clock_;
|
|
|
|
// How long the consensus convergence has taken, expressed as
|
|
// a percentage of the time that we expected it to take.
|
|
int convergePercent_{0};
|
|
|
|
// How long has this round been open
|
|
ConsensusTimer openTime_;
|
|
|
|
NetClock::duration closeResolution_ = kLedgerDefaultTimeResolution;
|
|
|
|
ConsensusParms::AvalancheState closeTimeAvalancheState_ = ConsensusParms::AvalancheState::Init;
|
|
|
|
// Time it took for the last consensus round to converge
|
|
std::chrono::milliseconds prevRoundTime_{};
|
|
|
|
//-------------------------------------------------------------------------
|
|
// Network time measurements of consensus progress
|
|
|
|
// The current network adjusted time. This is the network time the
|
|
// ledger would close if it closed now
|
|
NetClock::time_point now_;
|
|
NetClock::time_point prevCloseTime_;
|
|
|
|
//-------------------------------------------------------------------------
|
|
// Non-peer (self) consensus data
|
|
|
|
// Last validated ledger ID provided to consensus
|
|
Ledger_t::ID prevLedgerID_;
|
|
// Last validated ledger seen by consensus
|
|
Ledger_t previousLedger_;
|
|
|
|
// Transaction Sets, indexed by hash of transaction tree
|
|
hash_map<typename TxSet_t::ID, TxSet_t const> acquired_;
|
|
|
|
std::optional<Result> result_;
|
|
ConsensusCloseTimes rawCloseTimes_;
|
|
|
|
// The number of calls to phaseEstablish where none of our peers
|
|
// have changed any votes on disputed transactions.
|
|
std::size_t peerUnchangedCounter_ = 0;
|
|
|
|
// The total number of times we have called phaseEstablish
|
|
std::size_t establishCounter_ = 0;
|
|
|
|
//-------------------------------------------------------------------------
|
|
// Peer related consensus data
|
|
|
|
// Peer proposed positions for the current round
|
|
hash_map<NodeID_t, PeerPosition_t> currPeerPositions_;
|
|
|
|
// Recently received peer positions, available when transitioning between
|
|
// ledgers or rounds
|
|
hash_map<NodeID_t, std::deque<PeerPosition_t>> recentPeerPositions_;
|
|
|
|
// The number of proposers who participated in the last consensus round
|
|
std::size_t prevProposers_ = 0;
|
|
|
|
// nodes that have bowed out of this consensus process
|
|
hash_set<NodeID_t> deadNodes_;
|
|
|
|
/**
|
|
* Span for the establish phase of consensus.
|
|
* Created when the ledger closes and we enter phaseEstablish;
|
|
* cleared (ended) when consensus is reached. A thread-free SpanGuard,
|
|
* emplaced and reset() on different job workers.
|
|
*/
|
|
std::optional<xrpl::telemetry::SpanGuard> establishSpan_;
|
|
|
|
/**
|
|
* Captured context of establishSpan_ (its own span context). Children
|
|
* (update_positions, check) build from this explicit context.
|
|
*/
|
|
xrpl::telemetry::SpanContext establishSpanContext_;
|
|
|
|
/**
|
|
* Span for the open phase of consensus.
|
|
* Created in startRoundInternal(); cleared (ended) in closeLedger().
|
|
* A thread-free SpanGuard, emplaced and reset() on different job workers.
|
|
*/
|
|
std::optional<xrpl::telemetry::SpanGuard> openSpan_;
|
|
|
|
/**
|
|
* Create the establish-phase span if not yet active.
|
|
* Called on each phaseEstablish() invocation; no-op while span is live.
|
|
*/
|
|
void
|
|
startEstablishTracing();
|
|
|
|
/**
|
|
* Overwrite convergence metrics on the establish span each iteration.
|
|
* Final span attributes always reflect the last state before consensus.
|
|
*/
|
|
void
|
|
updateEstablishTracing();
|
|
|
|
/**
|
|
* End the establish span when transitioning to the accepted phase.
|
|
*/
|
|
void
|
|
endEstablishTracing();
|
|
|
|
// Journal for debugging
|
|
beast::Journal const j_;
|
|
};
|
|
|
|
template <class Adaptor>
|
|
Consensus<Adaptor>::Consensus(clock_type const& clock, Adaptor& adaptor, beast::Journal journal)
|
|
: adaptor_(adaptor), clock_(clock), j_{journal}
|
|
{
|
|
JLOG(j_.debug()) << "Creating consensus object";
|
|
}
|
|
|
|
template <class Adaptor>
|
|
void
|
|
Consensus<Adaptor>::startRound(
|
|
NetClock::time_point const& now,
|
|
Ledger_t::ID const& prevLedgerID,
|
|
Ledger_t prevLedger,
|
|
hash_set<NodeID_t> const& nowUntrusted,
|
|
bool proposing,
|
|
std::unique_ptr<std::stringstream> const& clog)
|
|
{
|
|
if (firstRound_)
|
|
{
|
|
// take our initial view of closeTime_ from the seed ledger
|
|
prevRoundTime_ = adaptor_.parms().ledgerIdleInterval;
|
|
prevCloseTime_ = prevLedger.closeTime();
|
|
firstRound_ = false;
|
|
}
|
|
else
|
|
{
|
|
prevCloseTime_ = rawCloseTimes_.self;
|
|
}
|
|
|
|
for (NodeID_t const& n : nowUntrusted)
|
|
recentPeerPositions_.erase(n);
|
|
|
|
ConsensusMode startMode = proposing ? ConsensusMode::Proposing : ConsensusMode::Observing;
|
|
|
|
// We were handed the wrong ledger
|
|
if (prevLedger.id() != prevLedgerID)
|
|
{
|
|
// try to acquire the correct one
|
|
if (auto newLedger = adaptor_.acquireLedger(prevLedgerID))
|
|
{
|
|
prevLedger = *newLedger;
|
|
}
|
|
else // Unable to acquire the correct ledger
|
|
{
|
|
startMode = ConsensusMode::WrongLedger;
|
|
JLOG(j_.info()) << "Entering consensus with: " << previousLedger_.id();
|
|
JLOG(j_.info()) << "Correct LCL is: " << prevLedgerID;
|
|
}
|
|
}
|
|
|
|
startRoundInternal(now, prevLedgerID, prevLedger, startMode, clog);
|
|
}
|
|
template <class Adaptor>
|
|
void
|
|
Consensus<Adaptor>::startRoundInternal(
|
|
NetClock::time_point const& now,
|
|
Ledger_t::ID const& prevLedgerID,
|
|
Ledger_t const& prevLedger,
|
|
ConsensusMode mode,
|
|
std::unique_ptr<std::stringstream> const& clog,
|
|
StartRoundReason const reason)
|
|
{
|
|
// Recovery path: handleWrongLedger acquired the correct prior ledger
|
|
// and re-entered startRoundInternal mid-round. The roundSpan_ owned by
|
|
// the adaptor is still the SAME span as before — startRoundTracing is
|
|
// not called on the recovery path, so we record the recovery as an
|
|
// event on the surviving round span. Pass empty phaseLabel to leave
|
|
// consensus_phase unchanged (the actual phase reset to open is marked
|
|
// separately by the new openSpan_ being emplaced below).
|
|
if (reason == StartRoundReason::Recovered)
|
|
{
|
|
adaptor_.onPhaseEvent(telemetry::consensus::span::event::phaseRecovery, "");
|
|
}
|
|
|
|
phase_ = ConsensusPhase::Open;
|
|
JLOG(j_.debug()) << "transitioned to ConsensusPhase::Open ";
|
|
CLOG(clog) << "startRoundInternal transitioned to ConsensusPhase::Open, "
|
|
"previous ledgerID: "
|
|
<< prevLedgerID << ", seq: " << prevLedger.seq() << ". ";
|
|
// Reset establishSpan_ so a wrongLedger recovery mid-establish doesn't
|
|
// leak the prior round's span into the new one (startEstablishTracing
|
|
// early-returns when establishSpan_ is populated).
|
|
establishSpan_.reset();
|
|
establishSpanContext_ = telemetry::SpanContext{};
|
|
// Child of the round span via its captured context: parent phase.open
|
|
// explicitly under roundSpanContext_. An invalid round context (round span
|
|
// not yet created) yields a null guard. openSpan_ is a thread-free
|
|
// SpanGuard emplaced here on one job worker and reset() on another; no
|
|
// scope to strip.
|
|
openSpan_.emplace(
|
|
telemetry::SpanGuard::childSpan(
|
|
telemetry::consensus::span::phaseOpen, adaptor_.roundSpanContext()));
|
|
// On the Recovered path, fire phase.open here because startRoundTracing
|
|
// (which fires it for the Initial path) is not called on re-entry. On
|
|
// the Initial path this is a no-op because the round span hasn't been
|
|
// created yet — the phase.open event is fired later by startRoundTracing
|
|
// after the new round span is in place.
|
|
if (reason == StartRoundReason::Recovered)
|
|
{
|
|
adaptor_.onPhaseEvent(
|
|
telemetry::consensus::span::event::phaseOpen,
|
|
telemetry::consensus::span::val::phaseOpen);
|
|
}
|
|
mode_.set(mode, adaptor_);
|
|
now_ = now;
|
|
prevLedgerID_ = prevLedgerID;
|
|
previousLedger_ = prevLedger;
|
|
result_.reset();
|
|
convergePercent_ = 0;
|
|
closeTimeAvalancheState_ = ConsensusParms::AvalancheState::Init;
|
|
haveCloseTimeConsensus_ = false;
|
|
openTime_.reset(clock_.now());
|
|
currPeerPositions_.clear();
|
|
acquired_.clear();
|
|
rawCloseTimes_.peers.clear();
|
|
rawCloseTimes_.self = {};
|
|
deadNodes_.clear();
|
|
|
|
closeResolution_ = getNextLedgerTimeResolution(
|
|
previousLedger_.closeTimeResolution(),
|
|
previousLedger_.closeAgree(),
|
|
previousLedger_.seq() + typename Ledger_t::Seq{1});
|
|
|
|
playbackProposals();
|
|
CLOG(clog) << "number of peer proposals,previous proposers: " << currPeerPositions_.size()
|
|
<< ',' << prevProposers_ << ". ";
|
|
if (currPeerPositions_.size() > (prevProposers_ / 2))
|
|
{
|
|
// We may be falling behind, don't wait for the timer
|
|
// consider closing the ledger immediately
|
|
CLOG(clog) << "consider closing the ledger immediately. ";
|
|
timerEntry(now_, clog);
|
|
}
|
|
}
|
|
|
|
template <class Adaptor>
|
|
bool
|
|
Consensus<Adaptor>::peerProposal(NetClock::time_point const& now, PeerPosition_t const& newPeerPos)
|
|
{
|
|
JLOG(j_.debug()) << "PROPOSAL " << newPeerPos.render();
|
|
auto const& peerID = newPeerPos.proposal().nodeID();
|
|
|
|
// Always need to store recent positions
|
|
{
|
|
auto& props = recentPeerPositions_[peerID];
|
|
|
|
if (props.size() >= 10)
|
|
props.pop_front();
|
|
|
|
props.push_back(newPeerPos);
|
|
}
|
|
return peerProposalInternal(now, newPeerPos);
|
|
}
|
|
|
|
template <class Adaptor>
|
|
bool
|
|
Consensus<Adaptor>::peerProposalInternal(
|
|
NetClock::time_point const& now,
|
|
PeerPosition_t const& newPeerPos)
|
|
{
|
|
// Nothing to do for now if we are currently working on a ledger
|
|
if (phase_ == ConsensusPhase::Accepted)
|
|
return false;
|
|
|
|
now_ = now;
|
|
|
|
auto const& newPeerProp = newPeerPos.proposal();
|
|
|
|
if (newPeerProp.prevLedger() != prevLedgerID_)
|
|
{
|
|
JLOG(j_.debug()) << "Got proposal for " << newPeerProp.prevLedger() << " but we are on "
|
|
<< prevLedgerID_;
|
|
return false;
|
|
}
|
|
|
|
auto const& peerID = newPeerProp.nodeID();
|
|
|
|
if (deadNodes_.find(peerID) != deadNodes_.end())
|
|
{
|
|
JLOG(j_.info()) << "Position from dead node: " << peerID;
|
|
return false;
|
|
}
|
|
|
|
{
|
|
// update current position
|
|
auto peerPosIt = currPeerPositions_.find(peerID);
|
|
|
|
if (peerPosIt != currPeerPositions_.end())
|
|
{
|
|
if (newPeerProp.proposeSeq() <= peerPosIt->second.proposal().proposeSeq())
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (newPeerProp.isBowOut())
|
|
{
|
|
JLOG(j_.info()) << "Peer " << peerID << " bows out";
|
|
if (result_)
|
|
{
|
|
for (auto& it : result_->disputes)
|
|
it.second.unVote(peerID);
|
|
}
|
|
if (peerPosIt != currPeerPositions_.end())
|
|
currPeerPositions_.erase(peerID);
|
|
deadNodes_.insert(peerID);
|
|
|
|
return true;
|
|
}
|
|
|
|
if (peerPosIt != currPeerPositions_.end())
|
|
{
|
|
peerPosIt->second = newPeerPos;
|
|
}
|
|
else
|
|
{
|
|
currPeerPositions_.emplace(peerID, newPeerPos);
|
|
}
|
|
}
|
|
|
|
if (newPeerProp.isInitial())
|
|
{
|
|
// Record the close time estimate
|
|
JLOG(j_.trace()) << "Peer reports close time as "
|
|
<< newPeerProp.closeTime().time_since_epoch().count();
|
|
++rawCloseTimes_.peers[newPeerProp.closeTime()];
|
|
}
|
|
|
|
JLOG(j_.trace()) << "Processing peer proposal " << newPeerProp.proposeSeq() << "/"
|
|
<< newPeerProp.position();
|
|
|
|
{
|
|
auto const ait = acquired_.find(newPeerProp.position());
|
|
if (ait == acquired_.end())
|
|
{
|
|
// acquireTxSet will return the set if it is available, or
|
|
// spawn a request for it and return nullopt/nullptr. It will call
|
|
// gotTxSet once it arrives
|
|
if (auto set = adaptor_.acquireTxSet(newPeerProp.position()))
|
|
{
|
|
gotTxSet(now_, *set);
|
|
}
|
|
else
|
|
{
|
|
JLOG(j_.debug()) << "Don't have tx set for peer";
|
|
}
|
|
}
|
|
else if (result_)
|
|
{
|
|
updateDisputes(newPeerProp.nodeID(), ait->second);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
template <class Adaptor>
|
|
void
|
|
Consensus<Adaptor>::timerEntry(
|
|
NetClock::time_point const& now,
|
|
std::unique_ptr<std::stringstream> const& clog)
|
|
{
|
|
CLOG(clog) << "Consensus<Adaptor>::timerEntry. ";
|
|
// Nothing to do if we are currently working on a ledger
|
|
if (phase_ == ConsensusPhase::Accepted)
|
|
{
|
|
CLOG(clog) << "Nothing to do during accepted phase. ";
|
|
return;
|
|
}
|
|
|
|
now_ = now;
|
|
CLOG(clog) << "Set network adjusted time to " << to_string(now) << ". ";
|
|
|
|
// Check we are on the proper ledger (this may change phase_)
|
|
auto const phaseOrig = phase_;
|
|
CLOG(clog) << "Phase " << to_string(phaseOrig) << ". ";
|
|
checkLedger(clog);
|
|
if (phaseOrig != phase_)
|
|
{
|
|
CLOG(clog) << "Changed phase to << " << to_string(phase_) << ". ";
|
|
}
|
|
|
|
if (phase_ == ConsensusPhase::Open)
|
|
{
|
|
phaseOpen(clog);
|
|
}
|
|
else if (phase_ == ConsensusPhase::Establish)
|
|
{
|
|
phaseEstablish(clog);
|
|
}
|
|
CLOG(clog) << "timerEntry finishing in phase " << to_string(phase_) << ". ";
|
|
}
|
|
|
|
template <class Adaptor>
|
|
void
|
|
Consensus<Adaptor>::gotTxSet(NetClock::time_point const& now, TxSet_t const& txSet)
|
|
{
|
|
// Nothing to do if we've finished work on a ledger
|
|
if (phase_ == ConsensusPhase::Accepted)
|
|
return;
|
|
|
|
now_ = now;
|
|
|
|
auto id = txSet.id();
|
|
|
|
// If we've already processed this transaction set since requesting
|
|
// it from the network, there is nothing to do now
|
|
if (!acquired_.emplace(id, txSet).second)
|
|
return;
|
|
|
|
if (!result_)
|
|
{
|
|
JLOG(j_.debug()) << "Not creating disputes: no position yet.";
|
|
}
|
|
else
|
|
{
|
|
// Our position is added to acquired_ as soon as we create it,
|
|
// so this txSet must differ
|
|
XRPL_ASSERT(
|
|
id != result_->position.position(),
|
|
"xrpl::Consensus::gotTxSet : updated transaction set");
|
|
bool any = false;
|
|
for (auto const& [nodeId, peerPos] : currPeerPositions_)
|
|
{
|
|
if (peerPos.proposal().position() == id)
|
|
{
|
|
updateDisputes(nodeId, txSet);
|
|
any = true;
|
|
}
|
|
}
|
|
|
|
if (!any)
|
|
{
|
|
JLOG(j_.warn()) << "By the time we got " << id << " no peers were proposing it";
|
|
}
|
|
}
|
|
}
|
|
|
|
template <class Adaptor>
|
|
void
|
|
Consensus<Adaptor>::simulate(
|
|
NetClock::time_point const& now,
|
|
std::optional<std::chrono::milliseconds> consensusDelay)
|
|
{
|
|
using namespace std::chrono_literals;
|
|
JLOG(j_.info()) << "Simulating consensus";
|
|
now_ = now;
|
|
closeLedger({});
|
|
// NOLINTBEGIN(bugprone-unchecked-optional-access) closeLedger sets result_
|
|
result_->roundTime.tick(consensusDelay.value_or(100ms));
|
|
result_->proposers = prevProposers_ = currPeerPositions_.size();
|
|
prevRoundTime_ = result_->roundTime.read();
|
|
phase_ = ConsensusPhase::Accepted;
|
|
adaptor_.onPhaseEvent(
|
|
telemetry::consensus::span::event::phaseAccepted,
|
|
telemetry::consensus::span::val::phaseAccepted);
|
|
adaptor_.onForceAccept(
|
|
*result_, previousLedger_, closeResolution_, rawCloseTimes_, mode_.get(), getJson(true));
|
|
// NOLINTEND(bugprone-unchecked-optional-access)
|
|
JLOG(j_.info()) << "Simulation complete";
|
|
}
|
|
|
|
template <class Adaptor>
|
|
json::Value
|
|
Consensus<Adaptor>::getJson(bool full) const
|
|
{
|
|
using std::to_string;
|
|
using Int = json::Value::Int;
|
|
|
|
json::Value ret(json::ValueType::Object);
|
|
|
|
ret["proposing"] = (mode_.get() == ConsensusMode::Proposing);
|
|
ret["proposers"] = static_cast<int>(currPeerPositions_.size());
|
|
|
|
if (mode_.get() != ConsensusMode::WrongLedger)
|
|
{
|
|
ret["synched"] = true;
|
|
ret["ledger_seq"] = static_cast<std::uint32_t>(previousLedger_.seq()) + 1;
|
|
ret["close_granularity"] = static_cast<Int>(closeResolution_.count());
|
|
}
|
|
else
|
|
{
|
|
ret["synched"] = false;
|
|
}
|
|
|
|
ret["phase"] = to_string(phase_);
|
|
|
|
if (result_ && !result_->disputes.empty() && !full)
|
|
ret["disputes"] = static_cast<Int>(result_->disputes.size());
|
|
|
|
if (result_)
|
|
ret["our_position"] = result_->position.getJson();
|
|
|
|
if (full)
|
|
{
|
|
if (result_)
|
|
ret["current_ms"] = static_cast<Int>(result_->roundTime.read().count());
|
|
ret["converge_percent"] = convergePercent_;
|
|
ret["close_resolution"] = static_cast<Int>(closeResolution_.count());
|
|
ret["have_time_consensus"] = haveCloseTimeConsensus_;
|
|
ret["previous_proposers"] = static_cast<Int>(prevProposers_);
|
|
ret["previous_mseconds"] = static_cast<Int>(prevRoundTime_.count());
|
|
|
|
if (!currPeerPositions_.empty())
|
|
{
|
|
json::Value ppj(json::ValueType::Object);
|
|
|
|
for (auto const& [nodeId, peerPos] : currPeerPositions_)
|
|
{
|
|
ppj[to_string(nodeId)] = peerPos.getJson();
|
|
}
|
|
ret["peer_positions"] = std::move(ppj);
|
|
}
|
|
|
|
if (!acquired_.empty())
|
|
{
|
|
json::Value acq(json::ValueType::Array);
|
|
for (auto const& at : acquired_)
|
|
{
|
|
acq.append(to_string(at.first));
|
|
}
|
|
ret["acquired"] = std::move(acq);
|
|
}
|
|
|
|
if (result_ && !result_->disputes.empty())
|
|
{
|
|
json::Value dsj(json::ValueType::Object);
|
|
for (auto const& [txId, dispute] : result_->disputes)
|
|
{
|
|
dsj[to_string(txId)] = dispute.getJson();
|
|
}
|
|
ret["disputes"] = std::move(dsj);
|
|
}
|
|
|
|
if (!rawCloseTimes_.peers.empty())
|
|
{
|
|
json::Value ctj(json::ValueType::Object);
|
|
for (auto const& ct : rawCloseTimes_.peers)
|
|
{
|
|
ctj[std::to_string(ct.first.time_since_epoch().count())] = ct.second;
|
|
}
|
|
ret["close_times"] = std::move(ctj);
|
|
}
|
|
|
|
if (!deadNodes_.empty())
|
|
{
|
|
json::Value dnj(json::ValueType::Array);
|
|
for (auto const& dn : deadNodes_)
|
|
{
|
|
dnj.append(to_string(dn));
|
|
}
|
|
ret["dead_nodes"] = std::move(dnj);
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
// Handle a change in the prior ledger during a consensus round
|
|
template <class Adaptor>
|
|
void
|
|
Consensus<Adaptor>::handleWrongLedger(
|
|
Ledger_t::ID const& lgrId,
|
|
std::unique_ptr<std::stringstream> const& clog)
|
|
{
|
|
CLOG(clog) << "handleWrongLedger. ";
|
|
XRPL_ASSERT(
|
|
lgrId != prevLedgerID_ || previousLedger_.id() != lgrId,
|
|
"xrpl::Consensus::handleWrongLedger : have wrong ledger");
|
|
|
|
// Stop proposing because we are out of sync
|
|
leaveConsensus(clog);
|
|
|
|
// First time switching to this ledger
|
|
if (prevLedgerID_ != lgrId)
|
|
{
|
|
prevLedgerID_ = lgrId;
|
|
|
|
// Clear out state
|
|
if (result_)
|
|
{
|
|
result_->disputes.clear();
|
|
result_->compares.clear();
|
|
}
|
|
|
|
currPeerPositions_.clear();
|
|
rawCloseTimes_.peers.clear();
|
|
deadNodes_.clear();
|
|
|
|
// Get back in sync, this will also recreate disputes
|
|
playbackProposals();
|
|
}
|
|
|
|
if (previousLedger_.id() == prevLedgerID_)
|
|
{
|
|
CLOG(clog) << "previousLedger_.id() == prevLeverID_ " << prevLedgerID_ << ". ";
|
|
return;
|
|
}
|
|
|
|
// we need to switch the ledger we're working from
|
|
if (auto newLedger = adaptor_.acquireLedger(prevLedgerID_))
|
|
{
|
|
JLOG(j_.info()) << "Have the consensus ledger " << prevLedgerID_;
|
|
CLOG(clog) << "Have the consensus ledger " << prevLedgerID_ << ". ";
|
|
startRoundInternal(
|
|
now_,
|
|
lgrId,
|
|
*newLedger,
|
|
ConsensusMode::SwitchedLedger,
|
|
clog,
|
|
StartRoundReason::Recovered);
|
|
}
|
|
else
|
|
{
|
|
CLOG(clog) << "Still on wrong ledger. ";
|
|
mode_.set(ConsensusMode::WrongLedger, adaptor_);
|
|
}
|
|
}
|
|
|
|
template <class Adaptor>
|
|
void
|
|
Consensus<Adaptor>::checkLedger(std::unique_ptr<std::stringstream> const& clog)
|
|
{
|
|
CLOG(clog) << "checkLedger. ";
|
|
|
|
auto netLgr = adaptor_.getPrevLedger(prevLedgerID_, previousLedger_, mode_.get());
|
|
CLOG(clog) << "network ledgerid " << netLgr << ", "
|
|
<< "previous ledger " << prevLedgerID_ << ". ";
|
|
|
|
if (netLgr != prevLedgerID_)
|
|
{
|
|
std::stringstream ss;
|
|
ss << "View of consensus changed during " << to_string(phase_)
|
|
<< " mode=" << to_string(mode_.get()) << ", " << prevLedgerID_ << " to " << netLgr
|
|
<< ", " << json::Compact{previousLedger_.getJson()} << ". ";
|
|
JLOG(j_.warn()) << ss.str();
|
|
CLOG(clog) << ss.str();
|
|
CLOG(clog) << "State on consensus change " << json::Compact{getJson(true)} << ". ";
|
|
handleWrongLedger(netLgr, clog);
|
|
}
|
|
else if (previousLedger_.id() != prevLedgerID_)
|
|
{
|
|
CLOG(clog) << "previousLedger_.id() != prevLedgerID_: " << previousLedger_.id() << ','
|
|
<< to_string(prevLedgerID_) << ". ";
|
|
handleWrongLedger(netLgr, clog);
|
|
}
|
|
}
|
|
|
|
template <class Adaptor>
|
|
void
|
|
Consensus<Adaptor>::playbackProposals()
|
|
{
|
|
for (auto const& it : recentPeerPositions_)
|
|
{
|
|
for (auto const& pos : it.second)
|
|
{
|
|
if (pos.proposal().prevLedger() == prevLedgerID_)
|
|
{
|
|
if (peerProposalInternal(now_, pos))
|
|
adaptor_.share(pos);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
template <class Adaptor>
|
|
void
|
|
Consensus<Adaptor>::phaseOpen(std::unique_ptr<std::stringstream> const& clog)
|
|
{
|
|
CLOG(clog) << "phaseOpen. ";
|
|
using namespace std::chrono;
|
|
|
|
// it is shortly before ledger close time
|
|
bool const anyTransactions = adaptor_.hasOpenTransactions();
|
|
auto proposersClosed = currPeerPositions_.size();
|
|
auto proposersValidated = adaptor_.proposersValidated(prevLedgerID_);
|
|
|
|
openTime_.tick(clock_.now());
|
|
|
|
// This computes how long since last ledger's close time
|
|
milliseconds sinceClose;
|
|
{
|
|
auto const mode = mode_.get();
|
|
bool const closeAgree = previousLedger_.closeAgree();
|
|
auto const prevCloseTime = previousLedger_.closeTime();
|
|
auto const prevParentCloseTimePlus1 = previousLedger_.parentCloseTime() + 1s;
|
|
bool const previousCloseCorrect = (mode != ConsensusMode::WrongLedger) && closeAgree &&
|
|
(prevCloseTime != prevParentCloseTimePlus1);
|
|
|
|
auto const lastCloseTime = previousCloseCorrect
|
|
? prevCloseTime // use consensus timing
|
|
: prevCloseTime_; // use the time we saw internally
|
|
|
|
if (now_ >= lastCloseTime)
|
|
{
|
|
sinceClose = duration_cast<milliseconds>(now_ - lastCloseTime);
|
|
}
|
|
else
|
|
{
|
|
sinceClose = -duration_cast<milliseconds>(lastCloseTime - now_);
|
|
}
|
|
CLOG(clog) << "calculating how long since last ledger's close time "
|
|
"based on mode : "
|
|
<< to_string(mode) << ", previous closeAgree: " << closeAgree
|
|
<< ", previous close time: " << to_string(prevCloseTime)
|
|
<< ", previous parent close time + 1s: " << to_string(prevParentCloseTimePlus1)
|
|
<< ", previous close time seen internally: " << to_string(prevCloseTime_)
|
|
<< ", last close time: " << to_string(lastCloseTime)
|
|
<< ", since close: " << sinceClose.count() << ". ";
|
|
}
|
|
|
|
auto const idleInterval = std::max<milliseconds>(
|
|
adaptor_.parms().ledgerIdleInterval, 2 * previousLedger_.closeTimeResolution());
|
|
CLOG(clog) << "idle interval set to " << idleInterval.count() << "ms based on "
|
|
<< "ledgerIDLE_INTERVAL: " << adaptor_.parms().ledgerIdleInterval.count()
|
|
<< ", previous ledger close time resolution: "
|
|
<< previousLedger_.closeTimeResolution().count() << "ms. ";
|
|
|
|
// Decide if we should close the ledger
|
|
if (shouldCloseLedger(
|
|
anyTransactions,
|
|
prevProposers_,
|
|
proposersClosed,
|
|
proposersValidated,
|
|
prevRoundTime_,
|
|
sinceClose,
|
|
openTime_.read(),
|
|
idleInterval,
|
|
adaptor_.parms(),
|
|
j_,
|
|
clog))
|
|
{
|
|
CLOG(clog) << "closing ledger. ";
|
|
closeLedger(clog);
|
|
}
|
|
}
|
|
|
|
template <class Adaptor>
|
|
bool
|
|
Consensus<Adaptor>::shouldPause(std::unique_ptr<std::stringstream> const& clog) const
|
|
{
|
|
CLOG(clog) << "shouldPause? ";
|
|
auto const& parms = adaptor_.parms();
|
|
std::uint32_t const ahead(
|
|
previousLedger_.seq() - std::min(adaptor_.getValidLedgerIndex(), previousLedger_.seq()));
|
|
auto [quorum, trustedKeys] = adaptor_.getQuorumKeys();
|
|
std::size_t const totalValidators = trustedKeys.size();
|
|
std::size_t const laggards = adaptor_.laggards(previousLedger_.seq(), trustedKeys);
|
|
std::size_t const offline = trustedKeys.size();
|
|
|
|
std::stringstream vars;
|
|
vars << " consensuslog (working seq: " << previousLedger_.seq() << ", "
|
|
<< "validated seq: " << adaptor_.getValidLedgerIndex() << ", "
|
|
<< "am validator: " << adaptor_.validator() << ", "
|
|
<< "have validated: " << adaptor_.haveValidated()
|
|
<< ", "
|
|
// NOLINTBEGIN(bugprone-unchecked-optional-access) result_ is always set when shouldPause
|
|
// is called (from phaseEstablish after assert)
|
|
<< "roundTime: " << result_->roundTime.read().count()
|
|
<< ", "
|
|
// NOLINTEND(bugprone-unchecked-optional-access)
|
|
<< "max consensus time: " << parms.ledgerMaxConsensus.count() << ", "
|
|
<< "validators: " << totalValidators << ", "
|
|
<< "laggards: " << laggards << ", "
|
|
<< "offline: " << offline << ", "
|
|
<< "quorum: " << quorum << ")";
|
|
|
|
if ((ahead == 0u) || (laggards == 0u) || (totalValidators == 0u) || !adaptor_.validator() ||
|
|
!adaptor_.haveValidated() ||
|
|
// NOLINTNEXTLINE(bugprone-unchecked-optional-access) result_ set as shouldPause called
|
|
result_->roundTime.read() > parms.ledgerMaxConsensus)
|
|
{
|
|
j_.debug() << "not pausing (early)" << vars.str();
|
|
CLOG(clog) << "Not pausing (early). ";
|
|
return false;
|
|
}
|
|
|
|
bool willPause = false;
|
|
|
|
/**
|
|
* Maximum phase with distinct thresholds to determine how
|
|
* many validators must be on our same ledger sequence number.
|
|
* The threshold for the 1st (0) phase is >= the minimum number that
|
|
* can achieve quorum. Threshold for the maximum phase is 100%
|
|
* of all trusted validators. Progression from min to max phase is
|
|
* simply linear. If there are 5 phases (maxPausePhase = 4)
|
|
* and minimum quorum is 80%, then thresholds progress as follows:
|
|
* 0: >=80%
|
|
* 1: >=85%
|
|
* 2: >=90%
|
|
* 3: >=95%
|
|
* 4: =100%
|
|
*/
|
|
static constexpr std::size_t kMaxPausePhase = 4;
|
|
|
|
/**
|
|
* No particular threshold guarantees consensus. Lower thresholds
|
|
* are easier to achieve than higher, but higher thresholds are
|
|
* more likely to reach consensus. Cycle through the phases if
|
|
* lack of synchronization continues.
|
|
*
|
|
* Current information indicates that no phase is likely to be intrinsically
|
|
* better than any other: the lower the threshold, the less likely that
|
|
* up-to-date nodes will be able to reach consensus without the laggards.
|
|
* But the higher the threshold, the longer the likely resulting pause.
|
|
* 100% is slightly less desirable in the long run because the potential
|
|
* of a single dawdling peer to slow down everything else. So if we
|
|
* accept that no phase is any better than any other phase, but that
|
|
* all of them will potentially enable us to arrive at consensus, cycling
|
|
* through them seems to be appropriate. Further, if we do reach the
|
|
* point of having to cycle back around, then it's likely that something
|
|
* else out of the scope of this delay mechanism is wrong with the
|
|
* network.
|
|
*/
|
|
std::size_t const phase = (ahead - 1) % (kMaxPausePhase + 1);
|
|
|
|
// validators that remain after the laggards() function are considered
|
|
// offline, and should be considered as laggards for purposes of
|
|
// evaluating whether the threshold for non-laggards has been reached.
|
|
switch (phase)
|
|
{
|
|
case 0:
|
|
// Laggards and offline shouldn't preclude consensus.
|
|
if (laggards + offline > totalValidators - quorum)
|
|
willPause = true;
|
|
break;
|
|
case kMaxPausePhase:
|
|
// No tolerance.
|
|
willPause = true;
|
|
break;
|
|
default:
|
|
// Ensure that sufficient validators are known to be not lagging.
|
|
// Their sufficiently most recent validation sequence was equal to
|
|
// or greater than our own.
|
|
//
|
|
// The threshold is the amount required for quorum plus
|
|
// the proportion of the remainder based on number of intermediate
|
|
// phases between 0 and max.
|
|
float const nonLaggards = totalValidators - (laggards + offline);
|
|
float const quorumRatio = static_cast<float>(quorum) / totalValidators;
|
|
float const allowedDissent = 1.0f - quorumRatio;
|
|
float const phaseFactor = static_cast<float>(phase) / kMaxPausePhase;
|
|
|
|
if (nonLaggards / totalValidators < quorumRatio + (allowedDissent * phaseFactor))
|
|
{
|
|
willPause = true;
|
|
}
|
|
}
|
|
|
|
if (willPause)
|
|
{
|
|
j_.warn() << "pausing" << vars.str();
|
|
CLOG(clog) << "pausing " << vars.str() << ". ";
|
|
}
|
|
else
|
|
{
|
|
j_.debug() << "not pausing" << vars.str();
|
|
CLOG(clog) << "not pausing. ";
|
|
}
|
|
return willPause;
|
|
}
|
|
|
|
template <class Adaptor>
|
|
void
|
|
Consensus<Adaptor>::phaseEstablish(std::unique_ptr<std::stringstream> const& clog)
|
|
{
|
|
CLOG(clog) << "phaseEstablish. ";
|
|
// can only establish consensus if we already took a stance
|
|
XRPL_ASSERT(result_, "xrpl::Consensus::phaseEstablish : result is set");
|
|
// NOLINTBEGIN(bugprone-unchecked-optional-access) assert above
|
|
|
|
startEstablishTracing();
|
|
|
|
++peerUnchangedCounter_;
|
|
++establishCounter_;
|
|
|
|
using namespace std::chrono;
|
|
ConsensusParms const& parms = adaptor_.parms();
|
|
|
|
result_->roundTime.tick(clock_.now());
|
|
result_->proposers = currPeerPositions_.size();
|
|
|
|
convergePercent_ = result_->roundTime.read() * 100 /
|
|
std::max<milliseconds>(prevRoundTime_, parms.avMinConsensusTime);
|
|
CLOG(clog) << "convergePercent_ " << convergePercent_
|
|
<< " is based on round duration so far: " << result_->roundTime.read().count()
|
|
<< "ms, "
|
|
<< "previous round duration: " << prevRoundTime_.count() << "ms, "
|
|
<< "avMIN_CONSENSUS_TIME: " << parms.avMinConsensusTime.count() << "ms. ";
|
|
|
|
// Give everyone a chance to take an initial position
|
|
if (result_->roundTime.read() < parms.ledgerMinConsensus)
|
|
{
|
|
CLOG(clog) << "ledgerMIN_CONSENSUS not reached: " << parms.ledgerMinConsensus.count()
|
|
<< "ms. ";
|
|
return;
|
|
}
|
|
|
|
updateOurPositions(clog);
|
|
|
|
updateEstablishTracing();
|
|
|
|
// Nothing to do if too many laggards or we don't have consensus.
|
|
if (shouldPause(clog) || !haveConsensus(clog))
|
|
return;
|
|
|
|
if (!haveCloseTimeConsensus_)
|
|
{
|
|
JLOG(j_.info()) << "We have TX consensus but not CT consensus";
|
|
CLOG(clog) << "We have TX consensus but not CT consensus. ";
|
|
return;
|
|
}
|
|
|
|
JLOG(j_.info()) << "Converge cutoff (" << currPeerPositions_.size() << " participants)";
|
|
CLOG(clog) << "Converge cutoff (" << currPeerPositions_.size()
|
|
<< " participants). Transitioned to ConsensusPhase::Accepted. ";
|
|
adaptor_.updateOperatingMode(currPeerPositions_.size());
|
|
prevProposers_ = currPeerPositions_.size();
|
|
prevRoundTime_ = result_->roundTime.read();
|
|
endEstablishTracing();
|
|
{
|
|
namespace cs = telemetry::consensus::span;
|
|
if (result_->state == ConsensusState::Yes)
|
|
{
|
|
adaptor_.onOutcomeEvent(cs::event::outcomeYes);
|
|
}
|
|
else if (result_->state == ConsensusState::MovedOn)
|
|
{
|
|
adaptor_.onOutcomeEvent(cs::event::outcomeMovedOn);
|
|
}
|
|
else if (result_->state == ConsensusState::Expired)
|
|
{
|
|
adaptor_.onOutcomeEvent(cs::event::outcomeExpired);
|
|
}
|
|
}
|
|
phase_ = ConsensusPhase::Accepted;
|
|
adaptor_.onPhaseEvent(
|
|
telemetry::consensus::span::event::phaseAccepted,
|
|
telemetry::consensus::span::val::phaseAccepted);
|
|
JLOG(j_.debug()) << "transitioned to ConsensusPhase::Accepted";
|
|
adaptor_.onAccept(
|
|
*result_,
|
|
previousLedger_,
|
|
closeResolution_,
|
|
rawCloseTimes_,
|
|
mode_.get(),
|
|
getJson(true),
|
|
adaptor_.validating());
|
|
// NOLINTEND(bugprone-unchecked-optional-access)
|
|
}
|
|
|
|
template <class Adaptor>
|
|
void
|
|
Consensus<Adaptor>::closeLedger(std::unique_ptr<std::stringstream> const& clog)
|
|
{
|
|
// We should not be closing if we already have a position
|
|
XRPL_ASSERT(!result_, "xrpl::Consensus::closeLedger : result is not set");
|
|
|
|
// Annotate the open-phase span with end-of-phase metadata before
|
|
// ending it, so a Tempo/Jaeger query for consensus.phase.open shows
|
|
// how long the phase ran and how many peer positions arrived during it.
|
|
openTime_.tick(clock_.now());
|
|
if (openSpan_ && *openSpan_)
|
|
{
|
|
namespace cs = telemetry::consensus::span;
|
|
openSpan_->setAttribute(
|
|
cs::attr::openDurationMs, static_cast<int64_t>(openTime_.read().count()));
|
|
openSpan_->setAttribute(
|
|
cs::attr::peerPositionsAtClose, static_cast<int64_t>(currPeerPositions_.size()));
|
|
}
|
|
openSpan_.reset();
|
|
phase_ = ConsensusPhase::Establish;
|
|
adaptor_.onPhaseEvent(
|
|
telemetry::consensus::span::event::phaseEstablish,
|
|
telemetry::consensus::span::val::phaseEstablish);
|
|
JLOG(j_.debug()) << "transitioned to ConsensusPhase::Establish";
|
|
rawCloseTimes_.self = now_;
|
|
peerUnchangedCounter_ = 0;
|
|
establishCounter_ = 0;
|
|
|
|
result_.emplace(adaptor_.onClose(previousLedger_, now_, mode_.get()));
|
|
result_->roundTime.reset(clock_.now());
|
|
// Share the newly created transaction set if we haven't already
|
|
// received it from a peer
|
|
if (acquired_.emplace(result_->txns.id(), result_->txns).second)
|
|
adaptor_.share(result_->txns);
|
|
|
|
auto const mode = mode_.get();
|
|
CLOG(clog) << "closeLedger transitioned to ConsensusPhase::Establish, mode: " << to_string(mode)
|
|
<< ", number of peer positions: " << currPeerPositions_.size() << ". ";
|
|
if (mode == ConsensusMode::Proposing)
|
|
adaptor_.propose(result_->position);
|
|
|
|
// Create disputes with any peer positions we have transactions for
|
|
for (auto const& pit : currPeerPositions_)
|
|
{
|
|
auto const& pos = pit.second.proposal().position();
|
|
auto const it = acquired_.find(pos);
|
|
if (it != acquired_.end())
|
|
createDisputes(it->second, clog);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* How many of the participants must agree to reach a given threshold?
|
|
*
|
|
* Note that the number may not precisely yield the requested percentage.
|
|
* For example, with with size = 5 and percent = 70, we return 3, but
|
|
* 3 out of 5 works out to 60%. There are no security implications to
|
|
* this.
|
|
*
|
|
* @param participants The number of participants (i.e. validators)
|
|
* @param percent The percent that we want to reach
|
|
*
|
|
* @return the number of participants which must agree
|
|
*/
|
|
inline int
|
|
participantsNeeded(int participants, int percent)
|
|
{
|
|
int const result = ((participants * percent) + (percent / 2)) / 100;
|
|
|
|
return (result == 0) ? 1 : result;
|
|
}
|
|
|
|
template <class Adaptor>
|
|
void
|
|
Consensus<Adaptor>::updateOurPositions(std::unique_ptr<std::stringstream> const& clog)
|
|
{
|
|
// We must have a position if we are updating it
|
|
XRPL_ASSERT(result_, "xrpl::Consensus::updateOurPositions : result is set");
|
|
// NOLINTBEGIN(bugprone-unchecked-optional-access) assert above
|
|
using namespace telemetry;
|
|
// Child of the establish span via its captured context (establishSpan_ is
|
|
// a thread-free SpanGuard, so parent explicitly via its context). Null
|
|
// context (establish not started) yields a null guard, same as before.
|
|
auto span = SpanGuard::childSpan(consensus::span::updatePositions, establishSpanContext_);
|
|
span.setAttribute(
|
|
consensus::span::attr::convergePercent, static_cast<int64_t>(convergePercent_));
|
|
span.setAttribute(
|
|
consensus::span::attr::proposers, static_cast<int64_t>(currPeerPositions_.size()));
|
|
span.setAttribute(
|
|
consensus::span::attr::disputesCount, static_cast<int64_t>(result_->disputes.size()));
|
|
ConsensusParms const& parms = adaptor_.parms();
|
|
|
|
// Compute a cutoff time
|
|
auto const peerCutoff = now_ - parms.proposeFRESHNESS;
|
|
auto const ourCutoff = now_ - parms.proposeINTERVAL;
|
|
CLOG(clog) << "updateOurPositions. peerCutoff " << to_string(peerCutoff) << ", ourCutoff "
|
|
<< to_string(ourCutoff) << ". ";
|
|
|
|
// Verify freshness of peer positions and compute close times
|
|
std::map<NetClock::time_point, int> closeTimeVotes;
|
|
{
|
|
auto it = currPeerPositions_.begin();
|
|
while (it != currPeerPositions_.end())
|
|
{
|
|
Proposal_t const& peerProp = it->second.proposal();
|
|
if (peerProp.isStale(peerCutoff))
|
|
{
|
|
// peer's proposal is stale, so remove it
|
|
NodeID_t const& peerID = peerProp.nodeID();
|
|
JLOG(j_.warn()) << "Removing stale proposal from " << peerID;
|
|
for (auto& dt : result_->disputes)
|
|
dt.second.unVote(peerID);
|
|
it = currPeerPositions_.erase(it);
|
|
}
|
|
else
|
|
{
|
|
// proposal is still fresh
|
|
++closeTimeVotes[asCloseTime(peerProp.closeTime())];
|
|
++it;
|
|
}
|
|
}
|
|
}
|
|
|
|
// This will stay unseated unless there are any changes
|
|
std::optional<TxSet_t> ourNewSet;
|
|
|
|
// Update votes on disputed transactions
|
|
{
|
|
std::optional<typename TxSet_t::MutableTxSet> mutableSet;
|
|
for (auto& [txId, dispute] : result_->disputes)
|
|
{
|
|
// Because the threshold for inclusion increases,
|
|
// time can change our position on a dispute
|
|
if (dispute.updateVote(
|
|
convergePercent_, mode_.get() == ConsensusMode::Proposing, parms))
|
|
{
|
|
if (!mutableSet)
|
|
mutableSet.emplace(result_->txns);
|
|
|
|
if (dispute.getOurVote())
|
|
{
|
|
// now a yes
|
|
mutableSet->insert(dispute.tx());
|
|
}
|
|
else
|
|
{
|
|
// now a no
|
|
mutableSet->erase(txId);
|
|
}
|
|
|
|
auto const yaysStr = std::to_string(dispute.getYays());
|
|
auto const naysStr = std::to_string(dispute.getNays());
|
|
span.addEvent(
|
|
consensus::span::event::disputeResolve,
|
|
{{consensus::span::attr::txId, to_string(txId)},
|
|
{consensus::span::attr::disputeOurVote,
|
|
dispute.getOurVote() ? std::string_view{consensus::span::val::yes}
|
|
: std::string_view{consensus::span::val::no}},
|
|
{consensus::span::attr::disputeYays, yaysStr},
|
|
{consensus::span::attr::disputeNays, naysStr}});
|
|
}
|
|
}
|
|
|
|
if (mutableSet)
|
|
ourNewSet.emplace(std::move(*mutableSet));
|
|
}
|
|
|
|
NetClock::time_point consensusCloseTime = {};
|
|
haveCloseTimeConsensus_ = false;
|
|
|
|
if (currPeerPositions_.empty())
|
|
{
|
|
// no other times
|
|
haveCloseTimeConsensus_ = true;
|
|
consensusCloseTime = asCloseTime(result_->position.closeTime());
|
|
}
|
|
else
|
|
{
|
|
// We don't track rounds for close time, so just pass 0s
|
|
auto const [neededWeight, newState] =
|
|
getNeededWeight(parms, closeTimeAvalancheState_, convergePercent_, 0, 0);
|
|
if (newState)
|
|
closeTimeAvalancheState_ = *newState;
|
|
CLOG(clog) << "neededWeight " << neededWeight << ". ";
|
|
span.setAttribute(
|
|
consensus::span::attr::avalancheThreshold, static_cast<int64_t>(neededWeight));
|
|
|
|
int participants = currPeerPositions_.size();
|
|
if (mode_.get() == ConsensusMode::Proposing)
|
|
{
|
|
++closeTimeVotes[asCloseTime(result_->position.closeTime())];
|
|
++participants;
|
|
}
|
|
|
|
// Threshold for non-zero vote
|
|
int threshVote = participantsNeeded(participants, neededWeight);
|
|
|
|
// Threshold to declare consensus
|
|
int const threshConsensus = participantsNeeded(participants, parms.avCtConsensusPct);
|
|
|
|
std::stringstream ss;
|
|
ss << "Proposers:" << currPeerPositions_.size() << " nw:" << neededWeight
|
|
<< " thrV:" << threshVote << " thrC:" << threshConsensus;
|
|
JLOG(j_.info()) << ss.str();
|
|
CLOG(clog) << ss.str();
|
|
|
|
for (auto const& [t, v] : closeTimeVotes)
|
|
{
|
|
JLOG(j_.debug()) << "CCTime: seq "
|
|
<< static_cast<std::uint32_t>(previousLedger_.seq()) + 1 << ": "
|
|
<< t.time_since_epoch().count() << " has " << v << ", " << threshVote
|
|
<< " required";
|
|
|
|
if (v >= threshVote)
|
|
{
|
|
// A close time has enough votes for us to try to agree
|
|
consensusCloseTime = t;
|
|
threshVote = v;
|
|
|
|
if (threshVote >= threshConsensus)
|
|
haveCloseTimeConsensus_ = true;
|
|
}
|
|
}
|
|
|
|
if (!haveCloseTimeConsensus_)
|
|
{
|
|
JLOG(j_.debug()) << "No CT consensus:"
|
|
<< " Proposers:" << currPeerPositions_.size()
|
|
<< " Mode:" << to_string(mode_.get()) << " Thresh:" << threshConsensus
|
|
<< " Pos:" << consensusCloseTime.time_since_epoch().count();
|
|
CLOG(clog) << "No close time consensus. ";
|
|
}
|
|
}
|
|
|
|
span.setAttribute(consensus::span::attr::haveCloseTimeConsensus, haveCloseTimeConsensus_);
|
|
span.setAttribute(
|
|
consensus::span::attr::closeTimeThreshold, static_cast<int64_t>(parms.avCtConsensusPct));
|
|
|
|
if (!ourNewSet &&
|
|
((consensusCloseTime != asCloseTime(result_->position.closeTime())) ||
|
|
result_->position.isStale(ourCutoff)))
|
|
{
|
|
// close time changed or our position is stale
|
|
ourNewSet.emplace(result_->txns);
|
|
}
|
|
|
|
if (ourNewSet)
|
|
{
|
|
auto newID = ourNewSet->id();
|
|
|
|
result_->txns = std::move(*ourNewSet);
|
|
|
|
std::stringstream ss;
|
|
ss << "Position change: CTime " << consensusCloseTime.time_since_epoch().count() << ", tx "
|
|
<< newID;
|
|
JLOG(j_.info()) << ss.str();
|
|
CLOG(clog) << ss.str();
|
|
|
|
result_->position.changePosition(newID, consensusCloseTime, now_);
|
|
|
|
// Share our new transaction set and update disputes
|
|
// if we haven't already received it
|
|
if (acquired_.emplace(newID, result_->txns).second)
|
|
{
|
|
if (!result_->position.isBowOut())
|
|
adaptor_.share(result_->txns);
|
|
|
|
for (auto const& [nodeId, peerPos] : currPeerPositions_)
|
|
{
|
|
Proposal_t const& p = peerPos.proposal();
|
|
if (p.position() == newID)
|
|
updateDisputes(nodeId, result_->txns);
|
|
}
|
|
}
|
|
|
|
// Share our new position if we are still participating this round
|
|
if (!result_->position.isBowOut() && (mode_.get() == ConsensusMode::Proposing))
|
|
adaptor_.propose(result_->position);
|
|
}
|
|
// NOLINTEND(bugprone-unchecked-optional-access)
|
|
}
|
|
|
|
template <class Adaptor>
|
|
bool
|
|
Consensus<Adaptor>::haveConsensus(std::unique_ptr<std::stringstream> const& clog)
|
|
{
|
|
// Must have a stance if we are checking for consensus
|
|
XRPL_ASSERT(result_, "xrpl::Consensus::haveConsensus : has result");
|
|
// NOLINTBEGIN(bugprone-unchecked-optional-access) assert above
|
|
using namespace telemetry;
|
|
// Child of the establish span via its captured context (establishSpan_ is
|
|
// a thread-free SpanGuard, so parent explicitly via its context).
|
|
auto span = SpanGuard::childSpan(consensus::span::check, establishSpanContext_);
|
|
|
|
// CHECKME: should possibly count unacquired TX sets as disagreeing
|
|
int agree = 0, disagree = 0;
|
|
|
|
auto ourPosition = result_->position.position();
|
|
|
|
// Count number of agreements/disagreements with our position
|
|
for (auto const& [nodeId, peerPos] : currPeerPositions_)
|
|
{
|
|
Proposal_t const& peerProp = peerPos.proposal();
|
|
if (peerProp.position() == ourPosition)
|
|
{
|
|
++agree;
|
|
}
|
|
else
|
|
{
|
|
JLOG(j_.debug()) << "Proposal disagreement: Peer " << nodeId << " has "
|
|
<< peerProp.position();
|
|
++disagree;
|
|
}
|
|
}
|
|
auto currentFinished = adaptor_.proposersFinished(previousLedger_, prevLedgerID_);
|
|
|
|
JLOG(j_.debug()) << "Checking for TX consensus: agree=" << agree << ", disagree=" << disagree;
|
|
|
|
ConsensusParms const& parms = adaptor_.parms();
|
|
// Stalling is BAD. It means that we have a consensus on the close time, so
|
|
// peers are talking, but we have disputed transactions that peers are
|
|
// unable or unwilling to come to agreement on one way or the other.
|
|
bool const stalled =
|
|
haveCloseTimeConsensus_ && !result_->disputes.empty() &&
|
|
std::ranges::all_of(result_->disputes, [this, &parms, &clog](auto const& dispute) {
|
|
return dispute.second.stalled(
|
|
parms, mode_.get() == ConsensusMode::Proposing, peerUnchangedCounter_, j_, clog);
|
|
});
|
|
if (stalled)
|
|
{
|
|
std::stringstream ss;
|
|
ss << "Consensus detects as stalled with " << (agree + disagree) << "/" << prevProposers_
|
|
<< " proposers, and " << result_->disputes.size() << " stalled disputed transactions.";
|
|
JLOG(j_.error()) << ss.str();
|
|
CLOG(clog) << ss.str();
|
|
}
|
|
|
|
// Determine if we actually have consensus or not
|
|
result_->state = checkConsensus(
|
|
prevProposers_,
|
|
agree + disagree,
|
|
agree,
|
|
currentFinished,
|
|
prevRoundTime_,
|
|
result_->roundTime.read(),
|
|
stalled,
|
|
parms,
|
|
mode_.get() == ConsensusMode::Proposing,
|
|
j_,
|
|
clog);
|
|
|
|
// Set span attributes before the early-return branches below so the
|
|
// consensus.check span carries diagnostic data even when consensus is
|
|
// not reached (the No / Expired paths return early).
|
|
span.setAttribute(consensus::span::attr::agreeCount, static_cast<int64_t>(agree));
|
|
span.setAttribute(consensus::span::attr::disagreeCount, static_cast<int64_t>(disagree));
|
|
span.setAttribute(
|
|
consensus::span::attr::convergePercent, static_cast<int64_t>(convergePercent_));
|
|
span.setAttribute(consensus::span::attr::haveCloseTimeConsensus, haveCloseTimeConsensus_);
|
|
span.setAttribute(
|
|
consensus::span::attr::thresholdPercent,
|
|
static_cast<int64_t>(adaptor_.parms().avCtConsensusPct));
|
|
span.setAttribute(
|
|
consensus::span::attr::proposersFinished, static_cast<int64_t>(currentFinished));
|
|
span.setAttribute(consensus::span::attr::consensusStalled, stalled);
|
|
span.setAttribute(
|
|
consensus::span::attr::establishCount, static_cast<int64_t>(establishCounter_));
|
|
|
|
std::string_view stateStr = consensus::span::val::no;
|
|
if (result_->state == ConsensusState::Yes)
|
|
{
|
|
stateStr = consensus::span::val::yes;
|
|
}
|
|
else if (result_->state == ConsensusState::MovedOn)
|
|
{
|
|
stateStr = consensus::span::val::movedOn;
|
|
}
|
|
else if (result_->state == ConsensusState::Expired)
|
|
{
|
|
stateStr = consensus::span::val::expired;
|
|
}
|
|
span.setAttribute(consensus::span::attr::consensusResult, stateStr);
|
|
|
|
if (result_->state == ConsensusState::No)
|
|
{
|
|
CLOG(clog) << "No consensus. ";
|
|
return false;
|
|
}
|
|
|
|
// Consensus has taken far too long. Drop out of the round.
|
|
if (result_->state == ConsensusState::Expired)
|
|
{
|
|
static auto const kMinimumCounter = parms.avalancheCutoffs.size() * parms.avMinRounds;
|
|
std::stringstream ss;
|
|
if (establishCounter_ < kMinimumCounter)
|
|
{
|
|
// If each round of phaseEstablish takes a very long time, we may
|
|
// "expire" before we've given consensus enough time at each
|
|
// avalanche level to actually come to a consensus. In that case,
|
|
// keep trying. This should only happen if there are an extremely
|
|
// large number of disputes such that each round takes an inordinate
|
|
// amount of time.
|
|
|
|
ss << "Consensus time has expired in round " << establishCounter_
|
|
<< "; continue until round " << kMinimumCounter << ". "
|
|
<< json::Compact{getJson(false)};
|
|
JLOG(j_.error()) << ss.str();
|
|
CLOG(clog) << ss.str() << ". ";
|
|
return false;
|
|
}
|
|
ss << "Consensus expired. " << json::Compact{getJson(true)};
|
|
JLOG(j_.error()) << ss.str();
|
|
CLOG(clog) << ss.str() << ". ";
|
|
leaveConsensus(clog);
|
|
}
|
|
// There is consensus, but we need to track if the network moved on
|
|
// without us.
|
|
if (result_->state == ConsensusState::MovedOn)
|
|
{
|
|
JLOG(j_.error()) << "Unable to reach consensus";
|
|
JLOG(j_.error()) << json::Compact{getJson(true)};
|
|
CLOG(clog) << "Unable to reach consensus " << json::Compact{getJson(true)} << ". ";
|
|
}
|
|
|
|
CLOG(clog) << "Consensus has been reached. ";
|
|
// NOLINTEND(bugprone-unchecked-optional-access)
|
|
return true;
|
|
}
|
|
|
|
template <class Adaptor>
|
|
void
|
|
Consensus<Adaptor>::leaveConsensus(std::unique_ptr<std::stringstream> const& clog)
|
|
{
|
|
if (mode_.get() == ConsensusMode::Proposing)
|
|
{
|
|
if (result_ && !result_->position.isBowOut())
|
|
{
|
|
result_->position.bowOut(now_);
|
|
adaptor_.propose(result_->position);
|
|
}
|
|
|
|
mode_.set(ConsensusMode::Observing, adaptor_);
|
|
JLOG(j_.info()) << "Bowing out of consensus";
|
|
CLOG(clog) << "Bowing out of consensus. ";
|
|
}
|
|
}
|
|
|
|
template <class Adaptor>
|
|
void
|
|
Consensus<Adaptor>::createDisputes(TxSet_t const& o, std::unique_ptr<std::stringstream> const& clog)
|
|
{
|
|
// Cannot create disputes without our stance
|
|
XRPL_ASSERT(result_, "xrpl::Consensus::createDisputes : result is set");
|
|
// NOLINTBEGIN(bugprone-unchecked-optional-access) assert above
|
|
|
|
// Only create disputes if this is a new set
|
|
auto const emplaced = result_->compares.emplace(o.id()).second;
|
|
CLOG(clog) << "createDisputes: new set? " << !emplaced << ". ";
|
|
if (!emplaced)
|
|
return;
|
|
|
|
// Nothing to dispute if we agree
|
|
if (result_->txns.id() == o.id())
|
|
{
|
|
CLOG(clog) << "both sets are identical. ";
|
|
return;
|
|
}
|
|
|
|
CLOG(clog) << "comparing existing with new set: " << result_->txns.id() << ',' << o.id()
|
|
<< ". ";
|
|
JLOG(j_.debug()) << "createDisputes " << result_->txns.id() << " to " << o.id();
|
|
|
|
auto differences = result_->txns.compare(o);
|
|
|
|
int dc = 0;
|
|
|
|
for (auto const& [txId, inThisSet] : differences)
|
|
{
|
|
++dc;
|
|
// create disputed transactions (from the ledger that has them)
|
|
XRPL_ASSERT(
|
|
(inThisSet && result_->txns.find(txId) && !o.find(txId)) ||
|
|
(!inThisSet && !result_->txns.find(txId) && o.find(txId)),
|
|
"xrpl::Consensus::createDisputes : has disputed transactions");
|
|
|
|
Tx_t const tx = inThisSet ? result_->txns.find(txId) : o.find(txId);
|
|
auto txID = tx.id();
|
|
|
|
if (result_->disputes.find(txID) != result_->disputes.end())
|
|
continue;
|
|
|
|
JLOG(j_.debug()) << "Transaction " << txID << " is disputed";
|
|
|
|
typename Result::Dispute_t dtx{
|
|
tx,
|
|
result_->txns.exists(txID),
|
|
std::max(prevProposers_, currPeerPositions_.size()),
|
|
j_};
|
|
|
|
// Update all of the available peer's votes on the disputed transaction
|
|
for (auto const& [nodeId, peerPos] : currPeerPositions_)
|
|
{
|
|
Proposal_t const& peerProp = peerPos.proposal();
|
|
auto const cit = acquired_.find(peerProp.position());
|
|
if (cit != acquired_.end() && dtx.setVote(nodeId, cit->second.exists(txID)))
|
|
peerUnchangedCounter_ = 0;
|
|
}
|
|
adaptor_.share(dtx.tx());
|
|
|
|
result_->disputes.emplace(txID, std::move(dtx));
|
|
}
|
|
JLOG(j_.debug()) << dc << " differences found";
|
|
CLOG(clog) << "disputes: " << dc << ". ";
|
|
// NOLINTEND(bugprone-unchecked-optional-access)
|
|
}
|
|
|
|
template <class Adaptor>
|
|
void
|
|
Consensus<Adaptor>::updateDisputes(NodeID_t const& node, TxSet_t const& other)
|
|
{
|
|
// Cannot updateDisputes without our stance
|
|
XRPL_ASSERT(result_, "xrpl::Consensus::updateDisputes : result is set");
|
|
// NOLINTBEGIN(bugprone-unchecked-optional-access) assert above
|
|
|
|
// Ensure we have created disputes against this set if we haven't seen
|
|
// it before
|
|
if (result_->compares.find(other.id()) == result_->compares.end())
|
|
createDisputes(other);
|
|
|
|
for (auto& it : result_->disputes)
|
|
{
|
|
auto& d = it.second;
|
|
if (d.setVote(node, other.exists(d.tx().id())))
|
|
peerUnchangedCounter_ = 0;
|
|
}
|
|
// NOLINTEND(bugprone-unchecked-optional-access)
|
|
}
|
|
|
|
template <class Adaptor>
|
|
NetClock::time_point
|
|
Consensus<Adaptor>::asCloseTime(NetClock::time_point raw) const
|
|
{
|
|
return roundCloseTime(raw, closeResolution_);
|
|
}
|
|
|
|
template <class Adaptor>
|
|
void
|
|
Consensus<Adaptor>::startEstablishTracing()
|
|
{
|
|
if (establishSpan_)
|
|
return;
|
|
// Child of the round span via its captured context: parent establish
|
|
// explicitly under roundSpanContext_. An invalid round context (round span
|
|
// not yet created) yields a null guard.
|
|
establishSpan_.emplace(
|
|
telemetry::SpanGuard::childSpan(
|
|
telemetry::consensus::span::establish, adaptor_.roundSpanContext()));
|
|
// Capture the establish context; children (update_positions, check) parent
|
|
// to it explicitly via establishSpanContext_. establishSpan_ is a
|
|
// thread-free SpanGuard reset() on a different worker than it is emplaced
|
|
// on -- no scope work.
|
|
if (*establishSpan_)
|
|
{
|
|
establishSpanContext_ = establishSpan_->spanContext();
|
|
}
|
|
}
|
|
|
|
template <class Adaptor>
|
|
void
|
|
Consensus<Adaptor>::updateEstablishTracing()
|
|
{
|
|
if (!establishSpan_)
|
|
return;
|
|
namespace cs = telemetry::consensus::span;
|
|
establishSpan_->setAttribute(cs::attr::convergePercent, static_cast<int64_t>(convergePercent_));
|
|
establishSpan_->setAttribute(cs::attr::establishCount, static_cast<int64_t>(establishCounter_));
|
|
establishSpan_->setAttribute(
|
|
cs::attr::proposers, static_cast<int64_t>(currPeerPositions_.size()));
|
|
if (result_)
|
|
{
|
|
establishSpan_->setAttribute(
|
|
cs::attr::disputesCount, static_cast<int64_t>(result_->disputes.size()));
|
|
}
|
|
}
|
|
|
|
template <class Adaptor>
|
|
void
|
|
Consensus<Adaptor>::endEstablishTracing()
|
|
{
|
|
establishSpan_.reset();
|
|
establishSpanContext_ = telemetry::SpanContext{};
|
|
}
|
|
|
|
} // namespace xrpl
|