mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-23 12:35:50 +00:00
Merge branch 'develop' into vault
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ using TERUnderlyingType = int;
|
||||
enum TELcodes : TERUnderlyingType {
|
||||
// Note: Range is stable.
|
||||
// Exact numbers are used in ripple-binary-codec:
|
||||
// https://github.com/ripple/ripple-binary-codec/blob/master/src/enums/definitions.json
|
||||
// https://github.com/XRPLF/xrpl.js/blob/main/packages/ripple-binary-codec/src/enums/definitions.json
|
||||
// Use tokens.
|
||||
|
||||
// -399 .. -300: L Local error (transaction fee inadequate, exceeds local
|
||||
@@ -73,7 +73,7 @@ enum TELcodes : TERUnderlyingType {
|
||||
enum TEMcodes : TERUnderlyingType {
|
||||
// Note: Range is stable.
|
||||
// Exact numbers are used in ripple-binary-codec:
|
||||
// https://github.com/ripple/ripple-binary-codec/blob/master/src/enums/definitions.json
|
||||
// https://github.com/XRPLF/xrpl.js/blob/main/packages/ripple-binary-codec/src/enums/definitions.json
|
||||
// Use tokens.
|
||||
|
||||
// -299 .. -200: M Malformed (bad signature)
|
||||
@@ -148,7 +148,7 @@ enum TEMcodes : TERUnderlyingType {
|
||||
enum TEFcodes : TERUnderlyingType {
|
||||
// Note: Range is stable.
|
||||
// Exact numbers are used in ripple-binary-codec:
|
||||
// https://github.com/ripple/ripple-binary-codec/blob/master/src/enums/definitions.json
|
||||
// https://github.com/XRPLF/xrpl.js/blob/main/packages/ripple-binary-codec/src/enums/definitions.json
|
||||
// Use tokens.
|
||||
|
||||
// -199 .. -100: F
|
||||
@@ -192,7 +192,7 @@ enum TEFcodes : TERUnderlyingType {
|
||||
enum TERcodes : TERUnderlyingType {
|
||||
// Note: Range is stable.
|
||||
// Exact numbers are used in ripple-binary-codec:
|
||||
// https://github.com/ripple/ripple-binary-codec/blob/master/src/enums/definitions.json
|
||||
// https://github.com/XRPLF/xrpl.js/blob/main/packages/ripple-binary-codec/src/enums/definitions.json
|
||||
// Use tokens.
|
||||
|
||||
// -99 .. -1: R Retry
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
ID
|
||||
ID const&
|
||||
id() const
|
||||
{
|
||||
return id_;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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. ";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user