Prevent consensus from getting stuck in the establish phase (#5277)

- Detects if the consensus process is "stalled". If it is, then we can declare a 
  consensus and end successfully even if we do not have 80% agreement on
  our proposal.
  - "Stalled" is defined as:
    - We have a close time consensus
    - Each disputed transaction is individually stalled:
      - It has been in the final "stuck" 95% requirement for at least 2
        (avMIN_ROUNDS) "inner rounds" of phaseEstablish,
      - and either all of the other trusted proposers or this validator, if proposing,
        have had the same vote(s) for at least 4 (avSTALLED_ROUNDS) "inner
        rounds", and at least 80% of the validators (including this one, if
        appropriate) agree about the vote (whether yes or no).
- If we have been in the establish phase for more than 10x the previous
  consensus establish phase's time, then consensus is considered "expired",
  and we will leave the round, which sends a partial validation (indicating
  that the node is moving on without validating). Two restrictions avoid
  prematurely exiting, or having an extended exit in extreme situations.
  - The 10x time is clamped to be within a range of 15s
    (ledgerMAX_CONSENSUS) to 120s (ledgerABANDON_CONSENSUS).
  - If consensus has not had an opportunity to walk through all avalanche
    states (defined as not going through 8 "inner rounds" of phaseEstablish),
    then ConsensusState::Expired is treated as ConsensusState::No.
- When enough nodes leave the round, any remaining nodes will see they've
  fallen behind, and move on, too, generally before hitting the timeout. Any
  validations or partial validations sent during this time will help the
  consensus process bring the nodes back together.
This commit is contained in:
Ed Hennis
2025-03-20 12:41:44 -04:00
committed by GitHub
parent 75a20194c5
commit d22a5057b9
10 changed files with 689 additions and 80 deletions

View File

@@ -43,6 +43,7 @@ test.consensus > xrpl.basics
test.consensus > xrpld.app
test.consensus > xrpld.consensus
test.consensus > xrpld.ledger
test.consensus > xrpl.json
test.core > test.jtx
test.core > test.toplevel
test.core > test.unit_test

View File

@@ -558,7 +558,7 @@ struct ConsensusResult
ConsensusTimer roundTime;
// Indicates state in which consensus ended. Once in the accept phase
// will be either Yes or MovedOn
// will be either Yes or MovedOn or Expired
ConsensusState state = ConsensusState::No;
};

View File

@@ -23,6 +23,7 @@
#include <xrpld/consensus/Consensus.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/json/to_string.h>
namespace ripple {
namespace test {
@@ -40,6 +41,7 @@ public:
testShouldCloseLedger()
{
using namespace std::chrono_literals;
testcase("should close ledger");
// Use default parameters
ConsensusParms const p{};
@@ -78,46 +80,102 @@ public:
testCheckConsensus()
{
using namespace std::chrono_literals;
testcase("check consensus");
// Use default parameterss
ConsensusParms const p{};
///////////////
// Disputes still in doubt
//
// Not enough time has elapsed
BEAST_EXPECT(
ConsensusState::No ==
checkConsensus(10, 2, 2, 0, 3s, 2s, p, true, journal_));
checkConsensus(10, 2, 2, 0, 3s, 2s, false, p, true, journal_));
// If not enough peers have propsed, ensure
// more time for proposals
BEAST_EXPECT(
ConsensusState::No ==
checkConsensus(10, 2, 2, 0, 3s, 4s, p, true, journal_));
checkConsensus(10, 2, 2, 0, 3s, 4s, false, p, true, journal_));
// Enough time has elapsed and we all agree
BEAST_EXPECT(
ConsensusState::Yes ==
checkConsensus(10, 2, 2, 0, 3s, 10s, p, true, journal_));
checkConsensus(10, 2, 2, 0, 3s, 10s, false, p, true, journal_));
// Enough time has elapsed and we don't yet agree
BEAST_EXPECT(
ConsensusState::No ==
checkConsensus(10, 2, 1, 0, 3s, 10s, p, true, journal_));
checkConsensus(10, 2, 1, 0, 3s, 10s, false, p, true, journal_));
// Our peers have moved on
// Enough time has elapsed and we all agree
BEAST_EXPECT(
ConsensusState::MovedOn ==
checkConsensus(10, 2, 1, 8, 3s, 10s, p, true, journal_));
checkConsensus(10, 2, 1, 8, 3s, 10s, false, p, true, journal_));
// If no peers, don't agree until time has passed.
BEAST_EXPECT(
ConsensusState::No ==
checkConsensus(0, 0, 0, 0, 3s, 10s, p, true, journal_));
checkConsensus(0, 0, 0, 0, 3s, 10s, false, p, true, journal_));
// Agree if no peers and enough time has passed.
BEAST_EXPECT(
ConsensusState::Yes ==
checkConsensus(0, 0, 0, 0, 3s, 16s, p, true, journal_));
checkConsensus(0, 0, 0, 0, 3s, 16s, false, p, true, journal_));
// Expire if too much time has passed without agreement
BEAST_EXPECT(
ConsensusState::Expired ==
checkConsensus(10, 8, 1, 0, 1s, 19s, false, p, true, journal_));
///////////////
// Stalled
//
// Not enough time has elapsed
BEAST_EXPECT(
ConsensusState::No ==
checkConsensus(10, 2, 2, 0, 3s, 2s, true, p, true, journal_));
// If not enough peers have propsed, ensure
// more time for proposals
BEAST_EXPECT(
ConsensusState::No ==
checkConsensus(10, 2, 2, 0, 3s, 4s, true, p, true, journal_));
// Enough time has elapsed and we all agree
BEAST_EXPECT(
ConsensusState::Yes ==
checkConsensus(10, 2, 2, 0, 3s, 10s, true, p, true, journal_));
// Enough time has elapsed and we don't yet agree, but there's nothing
// left to dispute
BEAST_EXPECT(
ConsensusState::Yes ==
checkConsensus(10, 2, 1, 0, 3s, 10s, true, p, true, journal_));
// Our peers have moved on
// Enough time has elapsed and we all agree, nothing left to dispute
BEAST_EXPECT(
ConsensusState::Yes ==
checkConsensus(10, 2, 1, 8, 3s, 10s, true, p, true, journal_));
// If no peers, don't agree until time has passed.
BEAST_EXPECT(
ConsensusState::No ==
checkConsensus(0, 0, 0, 0, 3s, 10s, true, p, true, journal_));
// Agree if no peers and enough time has passed.
BEAST_EXPECT(
ConsensusState::Yes ==
checkConsensus(0, 0, 0, 0, 3s, 16s, true, p, true, journal_));
// We are done if there's nothing left to dispute, no matter how much
// time has passed
BEAST_EXPECT(
ConsensusState::Yes ==
checkConsensus(10, 8, 1, 0, 1s, 19s, true, p, true, journal_));
}
void
@@ -125,6 +183,7 @@ public:
{
using namespace std::chrono_literals;
using namespace csf;
testcase("standalone");
Sim s;
PeerGroup peers = s.createGroup(1);
@@ -149,6 +208,7 @@ public:
{
using namespace csf;
using namespace std::chrono;
testcase("peers agree");
ConsensusParms const parms{};
Sim sim;
@@ -186,6 +246,7 @@ public:
{
using namespace csf;
using namespace std::chrono;
testcase("slow peers");
// Several tests of a complete trust graph with a subset of peers
// that have significantly longer network delays to the rest of the
@@ -351,6 +412,7 @@ public:
{
using namespace csf;
using namespace std::chrono;
testcase("close time disagree");
// This is a very specialized test to get ledgers to disagree on
// the close time. It unfortunately assumes knowledge about current
@@ -417,6 +479,8 @@ public:
{
using namespace csf;
using namespace std::chrono;
testcase("wrong LCL");
// Specialized test to exercise a temporary fork in which some peers
// are working on an incorrect prior ledger.
@@ -589,6 +653,7 @@ public:
{
using namespace csf;
using namespace std::chrono;
testcase("consensus close time rounding");
// This is a specialized test engineered to yield ledgers with different
// close times even though the peers believe they had close time
@@ -604,9 +669,6 @@ public:
PeerGroup fast = sim.createGroup(4);
PeerGroup network = fast + slow;
for (Peer* peer : network)
peer->consensusParms = parms;
// Connected trust graph
network.trust(network);
@@ -692,6 +754,7 @@ public:
{
using namespace csf;
using namespace std::chrono;
testcase("fork");
std::uint32_t numPeers = 10;
// Vary overlap between two UNLs
@@ -748,6 +811,7 @@ public:
{
using namespace csf;
using namespace std::chrono;
testcase("hub network");
// Simulate a set of 5 validators that aren't directly connected but
// rely on a single hub node for communication
@@ -835,6 +899,7 @@ public:
{
using namespace csf;
using namespace std::chrono;
testcase("preferred by branch");
// Simulate network splits that are prevented from forking when using
// preferred ledger by trie. This is a contrived example that involves
@@ -967,6 +1032,7 @@ public:
{
using namespace csf;
using namespace std::chrono;
testcase("pause for laggards");
// Test that validators that jump ahead of the network slow
// down.
@@ -1052,6 +1118,302 @@ public:
BEAST_EXPECT(sim.synchronized());
}
void
testDisputes()
{
testcase("disputes");
using namespace csf;
// Test dispute objects directly
using Dispute = DisputedTx<Tx, PeerID>;
Tx const txTrue{99};
Tx const txFalse{98};
Tx const txFollowingTrue{97};
Tx const txFollowingFalse{96};
int const numPeers = 100;
ConsensusParms p;
std::size_t peersUnchanged = 0;
// Three cases:
// 1 proposing, initial vote yes
// 2 proposing, initial vote no
// 3 not proposing, initial vote doesn't matter after the first update,
// use yes
{
Dispute proposingTrue{txTrue.id(), true, numPeers, journal_};
Dispute proposingFalse{txFalse.id(), false, numPeers, journal_};
Dispute followingTrue{
txFollowingTrue.id(), true, numPeers, journal_};
Dispute followingFalse{
txFollowingFalse.id(), false, numPeers, journal_};
BEAST_EXPECT(proposingTrue.ID() == 99);
BEAST_EXPECT(proposingFalse.ID() == 98);
BEAST_EXPECT(followingTrue.ID() == 97);
BEAST_EXPECT(followingFalse.ID() == 96);
// Create an even split in the peer votes
for (int i = 0; i < numPeers; ++i)
{
BEAST_EXPECT(proposingTrue.setVote(PeerID(i), i < 50));
BEAST_EXPECT(proposingFalse.setVote(PeerID(i), i < 50));
BEAST_EXPECT(followingTrue.setVote(PeerID(i), i < 50));
BEAST_EXPECT(followingFalse.setVote(PeerID(i), i < 50));
}
// Switch the middle vote to match mine
BEAST_EXPECT(proposingTrue.setVote(PeerID(50), true));
BEAST_EXPECT(proposingFalse.setVote(PeerID(49), false));
BEAST_EXPECT(followingTrue.setVote(PeerID(50), true));
BEAST_EXPECT(followingFalse.setVote(PeerID(49), false));
// no changes yet
BEAST_EXPECT(proposingTrue.getOurVote() == true);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
BEAST_EXPECT(!proposingTrue.stalled(p, true, peersUnchanged));
BEAST_EXPECT(!proposingFalse.stalled(p, true, peersUnchanged));
BEAST_EXPECT(!followingTrue.stalled(p, false, peersUnchanged));
BEAST_EXPECT(!followingFalse.stalled(p, false, peersUnchanged));
// I'm in the majority, my vote should not change
BEAST_EXPECT(!proposingTrue.updateVote(5, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(5, true, p));
BEAST_EXPECT(!followingTrue.updateVote(5, false, p));
BEAST_EXPECT(!followingFalse.updateVote(5, false, p));
BEAST_EXPECT(!proposingTrue.updateVote(10, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(10, true, p));
BEAST_EXPECT(!followingTrue.updateVote(10, false, p));
BEAST_EXPECT(!followingFalse.updateVote(10, false, p));
peersUnchanged = 2;
BEAST_EXPECT(!proposingTrue.stalled(p, true, peersUnchanged));
BEAST_EXPECT(!proposingFalse.stalled(p, true, peersUnchanged));
BEAST_EXPECT(!followingTrue.stalled(p, false, peersUnchanged));
BEAST_EXPECT(!followingFalse.stalled(p, false, peersUnchanged));
// Right now, the vote is 51%. The requirement is about to jump to
// 65%
BEAST_EXPECT(proposingTrue.updateVote(55, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(55, true, p));
BEAST_EXPECT(!followingTrue.updateVote(55, false, p));
BEAST_EXPECT(!followingFalse.updateVote(55, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == false);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
// 16 validators change their vote to match my original vote
for (int i = 0; i < 16; ++i)
{
auto pTrue = PeerID(numPeers - i - 1);
auto pFalse = PeerID(i);
BEAST_EXPECT(proposingTrue.setVote(pTrue, true));
BEAST_EXPECT(proposingFalse.setVote(pFalse, false));
BEAST_EXPECT(followingTrue.setVote(pTrue, true));
BEAST_EXPECT(followingFalse.setVote(pFalse, false));
}
// The vote should now be 66%, threshold is 65%
BEAST_EXPECT(proposingTrue.updateVote(60, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(60, true, p));
BEAST_EXPECT(!followingTrue.updateVote(60, false, p));
BEAST_EXPECT(!followingFalse.updateVote(60, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == true);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
// Threshold jumps to 70%
BEAST_EXPECT(proposingTrue.updateVote(86, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(86, true, p));
BEAST_EXPECT(!followingTrue.updateVote(86, false, p));
BEAST_EXPECT(!followingFalse.updateVote(86, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == false);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
// 5 more validators change their vote to match my original vote
for (int i = 16; i < 21; ++i)
{
auto pTrue = PeerID(numPeers - i - 1);
auto pFalse = PeerID(i);
BEAST_EXPECT(proposingTrue.setVote(pTrue, true));
BEAST_EXPECT(proposingFalse.setVote(pFalse, false));
BEAST_EXPECT(followingTrue.setVote(pTrue, true));
BEAST_EXPECT(followingFalse.setVote(pFalse, false));
}
// The vote should now be 71%, threshold is 70%
BEAST_EXPECT(proposingTrue.updateVote(90, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(90, true, p));
BEAST_EXPECT(!followingTrue.updateVote(90, false, p));
BEAST_EXPECT(!followingFalse.updateVote(90, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == true);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
// The vote should now be 71%, threshold is 70%
BEAST_EXPECT(!proposingTrue.updateVote(150, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(150, true, p));
BEAST_EXPECT(!followingTrue.updateVote(150, false, p));
BEAST_EXPECT(!followingFalse.updateVote(150, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == true);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
// The vote should now be 71%, threshold is 70%
BEAST_EXPECT(!proposingTrue.updateVote(190, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(190, true, p));
BEAST_EXPECT(!followingTrue.updateVote(190, false, p));
BEAST_EXPECT(!followingFalse.updateVote(190, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == true);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
peersUnchanged = 3;
BEAST_EXPECT(!proposingTrue.stalled(p, true, peersUnchanged));
BEAST_EXPECT(!proposingFalse.stalled(p, true, peersUnchanged));
BEAST_EXPECT(!followingTrue.stalled(p, false, peersUnchanged));
BEAST_EXPECT(!followingFalse.stalled(p, false, peersUnchanged));
// Threshold jumps to 95%
BEAST_EXPECT(proposingTrue.updateVote(220, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(220, true, p));
BEAST_EXPECT(!followingTrue.updateVote(220, false, p));
BEAST_EXPECT(!followingFalse.updateVote(220, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == false);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
// 25 more validators change their vote to match my original vote
for (int i = 21; i < 46; ++i)
{
auto pTrue = PeerID(numPeers - i - 1);
auto pFalse = PeerID(i);
BEAST_EXPECT(proposingTrue.setVote(pTrue, true));
BEAST_EXPECT(proposingFalse.setVote(pFalse, false));
BEAST_EXPECT(followingTrue.setVote(pTrue, true));
BEAST_EXPECT(followingFalse.setVote(pFalse, false));
}
// The vote should now be 96%, threshold is 95%
BEAST_EXPECT(proposingTrue.updateVote(250, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(250, true, p));
BEAST_EXPECT(!followingTrue.updateVote(250, false, p));
BEAST_EXPECT(!followingFalse.updateVote(250, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == true);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
for (peersUnchanged = 0; peersUnchanged < 6; ++peersUnchanged)
{
BEAST_EXPECT(!proposingTrue.stalled(p, true, peersUnchanged));
BEAST_EXPECT(!proposingFalse.stalled(p, true, peersUnchanged));
BEAST_EXPECT(!followingTrue.stalled(p, false, peersUnchanged));
BEAST_EXPECT(!followingFalse.stalled(p, false, peersUnchanged));
}
for (int i = 0; i < 1; ++i)
{
BEAST_EXPECT(!proposingTrue.updateVote(250 + 10 * i, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(250 + 10 * i, true, p));
BEAST_EXPECT(!followingTrue.updateVote(250 + 10 * i, false, p));
BEAST_EXPECT(
!followingFalse.updateVote(250 + 10 * i, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == true);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
// true vote has changed recently, so not stalled
BEAST_EXPECT(!proposingTrue.stalled(p, true, 0));
// remaining votes have been unchanged in so long that we only
// need to hit the second round at 95% to be stalled, regardless
// of peers
BEAST_EXPECT(proposingFalse.stalled(p, true, 0));
BEAST_EXPECT(followingTrue.stalled(p, false, 0));
BEAST_EXPECT(followingFalse.stalled(p, false, 0));
// true vote has changed recently, so not stalled
BEAST_EXPECT(!proposingTrue.stalled(p, true, peersUnchanged));
// remaining votes have been unchanged in so long that we only
// need to hit the second round at 95% to be stalled, regardless
// of peers
BEAST_EXPECT(proposingFalse.stalled(p, true, peersUnchanged));
BEAST_EXPECT(followingTrue.stalled(p, false, peersUnchanged));
BEAST_EXPECT(followingFalse.stalled(p, false, peersUnchanged));
}
for (int i = 1; i < 3; ++i)
{
BEAST_EXPECT(!proposingTrue.updateVote(250 + 10 * i, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(250 + 10 * i, true, p));
BEAST_EXPECT(!followingTrue.updateVote(250 + 10 * i, false, p));
BEAST_EXPECT(
!followingFalse.updateVote(250 + 10 * i, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == true);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
// true vote changed 2 rounds ago, and peers are changing, so
// not stalled
BEAST_EXPECT(!proposingTrue.stalled(p, true, 0));
// still stalled
BEAST_EXPECT(proposingFalse.stalled(p, true, 0));
BEAST_EXPECT(followingTrue.stalled(p, false, 0));
BEAST_EXPECT(followingFalse.stalled(p, false, 0));
// true vote changed 2 rounds ago, and peers are NOT changing,
// so stalled
BEAST_EXPECT(proposingTrue.stalled(p, true, peersUnchanged));
// still stalled
BEAST_EXPECT(proposingFalse.stalled(p, true, peersUnchanged));
BEAST_EXPECT(followingTrue.stalled(p, false, peersUnchanged));
BEAST_EXPECT(followingFalse.stalled(p, false, peersUnchanged));
}
for (int i = 3; i < 5; ++i)
{
BEAST_EXPECT(!proposingTrue.updateVote(250 + 10 * i, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(250 + 10 * i, true, p));
BEAST_EXPECT(!followingTrue.updateVote(250 + 10 * i, false, p));
BEAST_EXPECT(
!followingFalse.updateVote(250 + 10 * i, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == true);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
BEAST_EXPECT(proposingTrue.stalled(p, true, 0));
BEAST_EXPECT(proposingFalse.stalled(p, true, 0));
BEAST_EXPECT(followingTrue.stalled(p, false, 0));
BEAST_EXPECT(followingFalse.stalled(p, false, 0));
BEAST_EXPECT(proposingTrue.stalled(p, true, peersUnchanged));
BEAST_EXPECT(proposingFalse.stalled(p, true, peersUnchanged));
BEAST_EXPECT(followingTrue.stalled(p, false, peersUnchanged));
BEAST_EXPECT(followingFalse.stalled(p, false, peersUnchanged));
}
}
}
void
run() override
{
@@ -1068,6 +1430,7 @@ public:
testHubNetwork();
testPreferredByBranch();
testPauseForLaggards();
testDisputes();
}
};

View File

@@ -52,7 +52,7 @@ public:
{
}
ID
ID const&
id() const
{
return id_;

View File

@@ -35,6 +35,7 @@
#include <xrpld/app/misc/NetworkOPs.h>
#include <xrpld/app/misc/Transaction.h>
#include <xrpld/app/misc/TxQ.h>
#include <xrpld/app/misc/ValidatorKeys.h>
#include <xrpld/app/misc/ValidatorList.h>
#include <xrpld/app/misc/detail/AccountTxPaging.h>
#include <xrpld/app/rdb/backend/SQLiteDatabase.h>
@@ -249,6 +250,12 @@ public:
beast::get_abstract_clock<std::chrono::steady_clock>(),
validatorKeys,
app_.logs().journal("LedgerConsensus"))
, validatorPK_(
validatorKeys.keys ? validatorKeys.keys->publicKey
: decltype(validatorPK_){})
, validatorMasterPK_(
validatorKeys.keys ? validatorKeys.keys->masterPublicKey
: decltype(validatorMasterPK_){})
, m_ledgerMaster(ledgerMaster)
, m_job_queue(job_queue)
, m_standalone(standalone)
@@ -732,6 +739,9 @@ private:
RCLConsensus mConsensus;
std::optional<PublicKey> const validatorPK_;
std::optional<PublicKey> const validatorMasterPK_;
ConsensusPhase mLastConsensusPhase;
LedgerMaster& m_ledgerMaster;
@@ -1917,6 +1927,23 @@ NetworkOPsImp::beginConsensus(
bool
NetworkOPsImp::processTrustedProposal(RCLCxPeerPos peerPos)
{
auto const& peerKey = peerPos.publicKey();
if (validatorPK_ == peerKey || validatorMasterPK_ == peerKey)
{
// Could indicate a operator misconfiguration where two nodes are
// running with the same validator key configured, so this isn't fatal,
// and it doesn't necessarily indicate peer misbehavior. But since this
// is a trusted message, it could be a very big deal. Either way, we
// don't want to relay the proposal. Note that the byzantine behavior
// detection in handleNewValidation will notify other peers.
UNREACHABLE(
"ripple::NetworkOPsImp::processTrustedProposal : received own "
"proposal");
JLOG(m_journal.error())
<< "Received a TRUSTED proposal signed with my key from a peer";
return false;
}
return mConsensus.peerProposal(app_.timeKeeper().closeTime(), peerPos);
}

View File

@@ -109,6 +109,7 @@ checkConsensusReached(
bool count_self,
std::size_t minConsensusPct,
bool reachedMax,
bool stalled,
std::unique_ptr<std::stringstream> const& clog)
{
CLOG(clog) << "checkConsensusReached params: agreeing: " << agreeing
@@ -138,6 +139,17 @@ checkConsensusReached(
return false;
}
// We only get stalled when every disputed transaction unequivocally has 80%
// (minConsensusPct) agreement, either for or against. That is: either under
// 20% or over 80% consensus (repectively "nay" or "yay"). This prevents
// manipulation by a minority of byzantine peers of which transactions make
// the cut to get into the ledger.
if (stalled)
{
CLOG(clog) << "consensus stalled. ";
return true;
}
if (count_self)
{
++agreeing;
@@ -147,6 +159,7 @@ checkConsensusReached(
}
std::size_t currentPercentage = (agreeing * 100) / total;
CLOG(clog) << "currentPercentage: " << currentPercentage;
bool const ret = currentPercentage >= minConsensusPct;
if (ret)
@@ -168,6 +181,7 @@ checkConsensus(
std::size_t currentFinished,
std::chrono::milliseconds previousAgreeTime,
std::chrono::milliseconds currentAgreeTime,
bool stalled,
ConsensusParms const& parms,
bool proposing,
beast::Journal j,
@@ -181,7 +195,7 @@ checkConsensus(
<< " minimum duration to reach consensus: "
<< parms.ledgerMIN_CONSENSUS.count() << "ms"
<< " max consensus time " << parms.ledgerMAX_CONSENSUS.count()
<< "s"
<< "ms"
<< " minimum consensus percentage: " << parms.minCONSENSUS_PCT
<< ". ";
@@ -211,10 +225,12 @@ checkConsensus(
proposing,
parms.minCONSENSUS_PCT,
currentAgreeTime > parms.ledgerMAX_CONSENSUS,
stalled,
clog))
{
JLOG(j.debug()) << "normal consensus";
CLOG(clog) << "reached. ";
JLOG((stalled ? j.warn() : j.debug()))
<< "normal consensus" << (stalled ? ", but stalled" : "");
CLOG(clog) << "reached" << (stalled ? ", but stalled." : ".");
return ConsensusState::Yes;
}
@@ -226,6 +242,7 @@ checkConsensus(
false,
parms.minCONSENSUS_PCT,
currentAgreeTime > parms.ledgerMAX_CONSENSUS,
false,
clog))
{
JLOG(j.warn()) << "We see no consensus, but 80% of nodes have moved on";
@@ -233,6 +250,19 @@ checkConsensus(
return ConsensusState::MovedOn;
}
std::chrono::milliseconds const maxAgreeTime =
previousAgreeTime * parms.ledgerABANDON_CONSENSUS_FACTOR;
if (currentAgreeTime > std::clamp(
maxAgreeTime,
parms.ledgerMAX_CONSENSUS,
parms.ledgerABANDON_CONSENSUS))
{
JLOG(j.warn()) << "consensus taken too long";
CLOG(clog) << "Consensus taken too long. ";
// Note the Expired result may be overridden by the caller.
return ConsensusState::Expired;
}
// no consensus yet
JLOG(j.trace()) << "no consensus";
CLOG(clog) << "No consensus. ";

View File

@@ -31,6 +31,7 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/json/json_writer.h>
#include <algorithm>
#include <chrono>
#include <deque>
#include <optional>
@@ -81,6 +82,10 @@ shouldCloseLedger(
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 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
@@ -94,6 +99,7 @@ checkConsensus(
std::size_t currentFinished,
std::chrono::milliseconds previousAgreeTime,
std::chrono::milliseconds currentAgreeTime,
bool stalled,
ConsensusParms const& parms,
bool proposing,
beast::Journal j,
@@ -574,6 +580,9 @@ private:
NetClock::duration closeResolution_ = ledgerDefaultTimeResolution;
ConsensusParms::AvalancheState closeTimeAvalancheState_ =
ConsensusParms::init;
// Time it took for the last consensus round to converge
std::chrono::milliseconds prevRoundTime_;
@@ -599,6 +608,13 @@ private:
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
@@ -696,6 +712,7 @@ Consensus<Adaptor>::startRoundInternal(
previousLedger_ = prevLedger;
result_.reset();
convergePercent_ = 0;
closeTimeAvalancheState_ = ConsensusParms::init;
haveCloseTimeConsensus_ = false;
openTime_.reset(clock_.now());
currPeerPositions_.clear();
@@ -1351,6 +1368,9 @@ Consensus<Adaptor>::phaseEstablish(
// can only establish consensus if we already took a stance
XRPL_ASSERT(result_, "ripple::Consensus::phaseEstablish : result is set");
++peerUnchangedCounter_;
++establishCounter_;
using namespace std::chrono;
ConsensusParms const& parms = adaptor_.parms();
@@ -1417,6 +1437,8 @@ Consensus<Adaptor>::closeLedger(std::unique_ptr<std::stringstream> const& clog)
phase_ = ConsensusPhase::establish;
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());
@@ -1550,16 +1572,11 @@ Consensus<Adaptor>::updateOurPositions(
}
else
{
int neededWeight;
if (convergePercent_ < parms.avMID_CONSENSUS_TIME)
neededWeight = parms.avINIT_CONSENSUS_PCT;
else if (convergePercent_ < parms.avLATE_CONSENSUS_TIME)
neededWeight = parms.avMID_CONSENSUS_PCT;
else if (convergePercent_ < parms.avSTUCK_CONSENSUS_TIME)
neededWeight = parms.avLATE_CONSENSUS_PCT;
else
neededWeight = parms.avSTUCK_CONSENSUS_PCT;
// 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 << ". ";
int participants = currPeerPositions_.size();
@@ -1681,7 +1698,8 @@ Consensus<Adaptor>::haveConsensus(
}
else
{
JLOG(j_.debug()) << nodeId << " has " << peerProp.position();
JLOG(j_.debug()) << "Proposal disagreement: Peer " << nodeId
<< " has " << peerProp.position();
++disagree;
}
}
@@ -1691,6 +1709,17 @@ Consensus<Adaptor>::haveConsensus(
JLOG(j_.debug()) << "Checking for TX consensus: agree=" << agree
<< ", disagree=" << disagree;
ConsensusParms const& parms = adaptor_.parms();
// Stalling is BAD
bool const stalled = haveCloseTimeConsensus_ &&
std::ranges::all_of(result_->disputes,
[this, &parms](auto const& dispute) {
return dispute.second.stalled(
parms,
mode_.get() == ConsensusMode::proposing,
peerUnchangedCounter_);
});
// Determine if we actually have consensus or not
result_->state = checkConsensus(
prevProposers_,
@@ -1699,7 +1728,8 @@ Consensus<Adaptor>::haveConsensus(
currentFinished,
prevRoundTime_,
result_->roundTime.read(),
adaptor_.parms(),
stalled,
parms,
mode_.get() == ConsensusMode::proposing,
j_,
clog);
@@ -1710,6 +1740,33 @@ Consensus<Adaptor>::haveConsensus(
return false;
}
// Consensus has taken far too long. Drop out of the round.
if (result_->state == ConsensusState::Expired)
{
static auto const minimumCounter =
parms.avalancheCutoffs.size() * parms.avMIN_ROUNDS;
std::stringstream ss;
if (establishCounter_ < minimumCounter)
{
// 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 " << minimumCounter << ". "
<< 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)
@@ -1802,8 +1859,9 @@ Consensus<Adaptor>::createDisputes(
{
Proposal_t const& peerProp = peerPos.proposal();
auto const cit = acquired_.find(peerProp.position());
if (cit != acquired_.end())
dtx.setVote(nodeId, cit->second.exists(txID));
if (cit != acquired_.end() &&
dtx.setVote(nodeId, cit->second.exists(txID)))
peerUnchangedCounter_ = 0;
}
adaptor_.share(dtx.tx());
@@ -1828,7 +1886,8 @@ Consensus<Adaptor>::updateDisputes(NodeID_t const& node, TxSet_t const& other)
for (auto& it : result_->disputes)
{
auto& d = it.second;
d.setVote(node, other.exists(d.tx().id()));
if (d.setVote(node, other.exists(d.tx().id())))
peerUnchangedCounter_ = 0;
}
}

View File

@@ -20,8 +20,13 @@
#ifndef RIPPLE_CONSENSUS_CONSENSUS_PARMS_H_INCLUDED
#define RIPPLE_CONSENSUS_CONSENSUS_PARMS_H_INCLUDED
#include <xrpl/beast/utility/instrumentation.h>
#include <chrono>
#include <cstddef>
#include <functional>
#include <map>
#include <optional>
namespace ripple {
@@ -43,7 +48,7 @@ struct ConsensusParms
This is a safety to protect against very old validations and the time
it takes to adjust the close time accuracy window.
*/
std::chrono::seconds validationVALID_WALL = std::chrono::minutes{5};
std::chrono::seconds const validationVALID_WALL = std::chrono::minutes{5};
/** Duration a validation remains current after first observed.
@@ -51,33 +56,34 @@ struct ConsensusParms
first saw it. This provides faster recovery in very rare cases where the
number of validations produced by the network is lower than normal
*/
std::chrono::seconds validationVALID_LOCAL = std::chrono::minutes{3};
std::chrono::seconds const validationVALID_LOCAL = std::chrono::minutes{3};
/** Duration pre-close in which validations are acceptable.
The number of seconds before a close time that we consider a validation
acceptable. This protects against extreme clock errors
*/
std::chrono::seconds validationVALID_EARLY = std::chrono::minutes{3};
std::chrono::seconds const validationVALID_EARLY = std::chrono::minutes{3};
//! How long we consider a proposal fresh
std::chrono::seconds proposeFRESHNESS = std::chrono::seconds{20};
std::chrono::seconds const proposeFRESHNESS = std::chrono::seconds{20};
//! How often we force generating a new proposal to keep ours fresh
std::chrono::seconds proposeINTERVAL = std::chrono::seconds{12};
std::chrono::seconds const proposeINTERVAL = std::chrono::seconds{12};
//-------------------------------------------------------------------------
// Consensus durations are relative to the internal Consensus clock and use
// millisecond resolution.
//! The percentage threshold above which we can declare consensus.
std::size_t minCONSENSUS_PCT = 80;
std::size_t const minCONSENSUS_PCT = 80;
//! The duration a ledger may remain idle before closing
std::chrono::milliseconds ledgerIDLE_INTERVAL = std::chrono::seconds{15};
std::chrono::milliseconds const ledgerIDLE_INTERVAL =
std::chrono::seconds{15};
//! The number of seconds we wait minimum to ensure participation
std::chrono::milliseconds ledgerMIN_CONSENSUS =
std::chrono::milliseconds const ledgerMIN_CONSENSUS =
std::chrono::milliseconds{1950};
/** The maximum amount of time to spend pausing for laggards.
@@ -86,13 +92,26 @@ struct ConsensusParms
* validators don't appear to be offline that are merely waiting for
* laggards.
*/
std::chrono::milliseconds ledgerMAX_CONSENSUS = std::chrono::seconds{15};
std::chrono::milliseconds const ledgerMAX_CONSENSUS =
std::chrono::seconds{15};
//! Minimum number of seconds to wait to ensure others have computed the LCL
std::chrono::milliseconds ledgerMIN_CLOSE = std::chrono::seconds{2};
std::chrono::milliseconds const ledgerMIN_CLOSE = std::chrono::seconds{2};
//! How often we check state or change positions
std::chrono::milliseconds ledgerGRANULARITY = std::chrono::seconds{1};
std::chrono::milliseconds const ledgerGRANULARITY = std::chrono::seconds{1};
//! How long to wait before completely abandoning consensus
std::size_t const ledgerABANDON_CONSENSUS_FACTOR = 10;
/**
* Maximum amount of time to give a consensus round
*
* Does not include the time to build the LCL, so there is no reason for a
* round to go this long, regardless of how big the ledger is.
*/
std::chrono::milliseconds const ledgerABANDON_CONSENSUS =
std::chrono::seconds{120};
/** The minimum amount of time to consider the previous round
to have taken.
@@ -104,38 +123,80 @@ struct ConsensusParms
twice the interval between proposals (0.7s) divided by
the interval between mid and late consensus ([85-50]/100).
*/
std::chrono::milliseconds avMIN_CONSENSUS_TIME = std::chrono::seconds{5};
std::chrono::milliseconds const avMIN_CONSENSUS_TIME =
std::chrono::seconds{5};
//------------------------------------------------------------------------------
// Avalanche tuning
// As a function of the percent this round's duration is of the prior round,
// we increase the threshold for yes votes to add a transaction to our
// position.
//! Percentage of nodes on our UNL that must vote yes
std::size_t avINIT_CONSENSUS_PCT = 50;
//! Percentage of previous round duration before we advance
std::size_t avMID_CONSENSUS_TIME = 50;
//! Percentage of nodes that most vote yes after advancing
std::size_t avMID_CONSENSUS_PCT = 65;
//! Percentage of previous round duration before we advance
std::size_t avLATE_CONSENSUS_TIME = 85;
//! Percentage of nodes that most vote yes after advancing
std::size_t avLATE_CONSENSUS_PCT = 70;
//! Percentage of previous round duration before we are stuck
std::size_t avSTUCK_CONSENSUS_TIME = 200;
//! Percentage of nodes that must vote yes after we are stuck
std::size_t avSTUCK_CONSENSUS_PCT = 95;
enum AvalancheState { init, mid, late, stuck };
struct AvalancheCutoff
{
int const consensusTime;
std::size_t const consensusPct;
AvalancheState const next;
};
//! Map the consensus requirement avalanche state to the amount of time that
//! must pass before moving to that state, the agreement percentage required
//! at that state, and the next state. "stuck" loops back on itself because
//! once we're stuck, we're stuck.
//! This structure allows for "looping" of states if needed.
std::map<AvalancheState, AvalancheCutoff> const avalancheCutoffs{
// {state, {time, percent, nextState}},
// Initial state: 50% of nodes must vote yes
{init, {0, 50, mid}},
// mid-consensus starts after 50% of the previous round time, and
// requires 65% yes
{mid, {50, 65, late}},
// late consensus starts after 85% time, and requires 70% yes
{late, {85, 70, stuck}},
// we're stuck after 2x time, requires 95% yes votes
{stuck, {200, 95, stuck}},
};
//! Percentage of nodes required to reach agreement on ledger close time
std::size_t avCT_CONSENSUS_PCT = 75;
std::size_t const avCT_CONSENSUS_PCT = 75;
//! Number of rounds before certain actions can happen.
// (Moving to the next avalanche level, considering that votes are stalled
// without consensus.)
std::size_t const avMIN_ROUNDS = 2;
//! Number of rounds before a stuck vote is considered unlikely to change
//! because voting stalled
std::size_t const avSTALLED_ROUNDS = 4;
};
inline std::pair<std::size_t, std::optional<ConsensusParms::AvalancheState>>
getNeededWeight(
ConsensusParms const& p,
ConsensusParms::AvalancheState currentState,
int percentTime,
std::size_t currentRounds,
std::size_t minimumRounds)
{
// at() can throw, but the map is built by hand to ensure all valid
// values are available.
auto const& currentCutoff = p.avalancheCutoffs.at(currentState);
// Should we consider moving to the next state?
if (currentCutoff.next != currentState && currentRounds >= minimumRounds)
{
// at() can throw, but the map is built by hand to ensure all
// valid values are available.
auto const& nextCutoff = p.avalancheCutoffs.at(currentCutoff.next);
// See if enough time has passed to move on to the next.
XRPL_ASSERT(
nextCutoff.consensusTime >= currentCutoff.consensusTime,
"ripple::getNeededWeight : next state valid");
if (percentTime >= nextCutoff.consensusTime)
{
return {nextCutoff.consensusPct, currentCutoff.next};
}
}
return {currentCutoff.consensusPct, {}};
}
} // namespace ripple
#endif

View File

@@ -188,6 +188,7 @@ struct ConsensusCloseTimes
enum class ConsensusState {
No, //!< We do not have consensus
MovedOn, //!< The network has consensus without us
Expired, //!< Consensus time limit has hard-expired
Yes //!< We have consensus along with the network
};
@@ -237,7 +238,7 @@ struct ConsensusResult
ConsensusTimer roundTime;
// Indicates state in which consensus ended. Once in the accept phase
// will be either Yes or MovedOn
// will be either Yes or MovedOn or Expired
ConsensusState state = ConsensusState::No;
// The number of peers proposing during the round

View File

@@ -82,6 +82,51 @@ public:
return ourVote_;
}
//! Are we and our peers "stalled" where we probably won't change
//! our vote?
bool
stalled(ConsensusParms const& p, bool proposing, int peersUnchanged) const
{
// at() can throw, but the map is built by hand to ensure all valid
// values are available.
auto const& currentCutoff = p.avalancheCutoffs.at(avalancheState_);
auto const& nextCutoff = p.avalancheCutoffs.at(currentCutoff.next);
// We're have not reached the final avalanche state, or been there long
// enough, so there's room for change. Check the times in case the state
// machine is altered to allow states to loop.
if (nextCutoff.consensusTime > currentCutoff.consensusTime ||
avalancheCounter_ < p.avMIN_ROUNDS)
return false;
// We've haven't had this vote for minimum rounds yet. Things could
// change.
if (proposing && currentVoteCounter_ < p.avMIN_ROUNDS)
return false;
// If we or any peers have changed a vote in several rounds, then
// things could still change. But if _either_ has not changed in that
// long, we're unlikely to change our vote any time soon. (This prevents
// a malicious peer from flip-flopping a vote to prevent consensus.)
if (peersUnchanged < p.avSTALLED_ROUNDS &&
(proposing && currentVoteCounter_ < p.avSTALLED_ROUNDS))
return false;
// Does this transaction have more than 80% agreement
// Compute the percentage of nodes voting 'yes' (possibly including us)
int const support = (yays_ + (proposing && ourVote_ ? 1 : 0)) * 100;
int total = nays_ + yays_ + (proposing ? 1 : 0);
if (!total)
// There are no votes, so we know nothing
return false;
int const weight = support / total;
// Returns true if the tx has more than minCONSENSUS_PCT (80) percent
// agreement. Either voting for _or_ voting against the tx.
return weight > p.minCONSENSUS_PCT ||
weight < (100 - p.minCONSENSUS_PCT);
}
//! The disputed transaction.
Tx_t const&
tx() const
@@ -100,8 +145,12 @@ public:
@param peer Identifier of peer.
@param votesYes Whether peer votes to include the disputed transaction.
@return bool Whether the peer changed its vote. (A new vote counts as a
change.)
*/
void
[[nodiscard]]
bool
setVote(NodeID_t const& peer, bool votesYes);
/** Remove a peer's vote
@@ -135,12 +184,18 @@ private:
bool ourVote_; //< Our vote (true is yes)
Tx_t tx_; //< Transaction under dispute
Map_t votes_; //< Map from NodeID to vote
//! The number of rounds we've gone without changing our vote
std::size_t currentVoteCounter_ = 0;
//! Which minimum acceptance percentage phase we are currently in
ConsensusParms::AvalancheState avalancheState_ = ConsensusParms::init;
//! How long we have been in the current acceptance phase
std::size_t avalancheCounter_ = 0;
beast::Journal const j_;
};
// Track a peer's yes/no vote on a particular disputed tx_
template <class Tx_t, class NodeID_t>
void
bool
DisputedTx<Tx_t, NodeID_t>::setVote(NodeID_t const& peer, bool votesYes)
{
auto const [it, inserted] = votes_.insert(std::make_pair(peer, votesYes));
@@ -158,6 +213,7 @@ DisputedTx<Tx_t, NodeID_t>::setVote(NodeID_t const& peer, bool votesYes)
JLOG(j_.debug()) << "Peer " << peer << " votes NO on " << tx_.id();
++nays_;
}
return true;
}
// changes vote to yes
else if (votesYes && !it->second)
@@ -166,6 +222,7 @@ DisputedTx<Tx_t, NodeID_t>::setVote(NodeID_t const& peer, bool votesYes)
--nays_;
++yays_;
it->second = true;
return true;
}
// changes vote to no
else if (!votesYes && it->second)
@@ -174,7 +231,9 @@ DisputedTx<Tx_t, NodeID_t>::setVote(NodeID_t const& peer, bool votesYes)
++nays_;
--yays_;
it->second = false;
return true;
}
return false;
}
// Remove a peer's vote on this disputed transaction
@@ -211,21 +270,26 @@ DisputedTx<Tx_t, NodeID_t>::updateVote(
bool newPosition;
int weight;
// When proposing, to prevent avalanche stalls, we increase the needed
// weight slightly over time. We also need to ensure that the consensus has
// made a minimum number of attempts at each "state" before moving
// to the next.
// Proposing or not, we need to keep track of which state we've reached so
// we can determine if the vote has stalled.
auto const [requiredPct, newState] = getNeededWeight(
p, avalancheState_, percentTime, ++avalancheCounter_, p.avMIN_ROUNDS);
if (newState)
{
avalancheState_ = *newState;
avalancheCounter_ = 0;
}
if (proposing) // give ourselves full weight
{
// This is basically the percentage of nodes voting 'yes' (including us)
weight = (yays_ * 100 + (ourVote_ ? 100 : 0)) / (nays_ + yays_ + 1);
// To prevent avalanche stalls, we increase the needed weight slightly
// over time.
if (percentTime < p.avMID_CONSENSUS_TIME)
newPosition = weight > p.avINIT_CONSENSUS_PCT;
else if (percentTime < p.avLATE_CONSENSUS_TIME)
newPosition = weight > p.avMID_CONSENSUS_PCT;
else if (percentTime < p.avSTUCK_CONSENSUS_TIME)
newPosition = weight > p.avLATE_CONSENSUS_PCT;
else
newPosition = weight > p.avSTUCK_CONSENSUS_PCT;
newPosition = weight > requiredPct;
}
else
{
@@ -236,13 +300,16 @@ DisputedTx<Tx_t, NodeID_t>::updateVote(
if (newPosition == ourVote_)
{
JLOG(j_.info()) << "No change (" << (ourVote_ ? "YES" : "NO")
<< ") : weight " << weight << ", percent "
<< percentTime;
++currentVoteCounter_;
JLOG(j_.info()) << "No change (" << (ourVote_ ? "YES" : "NO") << ") on "
<< tx_.id() << " : weight " << weight << ", percent "
<< percentTime
<< ", round(s) with this vote: " << currentVoteCounter_;
JLOG(j_.debug()) << Json::Compact{getJson()};
return false;
}
currentVoteCounter_ = 0;
ourVote_ = newPosition;
JLOG(j_.debug()) << "We now vote " << (ourVote_ ? "YES" : "NO") << " on "
<< tx_.id();