Merge branch 'develop' into vault

This commit is contained in:
Bronek Kozicki
2025-03-24 11:25:35 +00:00
11 changed files with 693 additions and 84 deletions

View File

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

View File

@@ -558,7 +558,7 @@ struct ConsensusResult
ConsensusTimer roundTime; ConsensusTimer roundTime;
// Indicates state in which consensus ended. Once in the accept phase // 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; ConsensusState state = ConsensusState::No;
}; };

View File

@@ -42,7 +42,7 @@ using TERUnderlyingType = int;
enum TELcodes : TERUnderlyingType { enum TELcodes : TERUnderlyingType {
// Note: Range is stable. // Note: Range is stable.
// Exact numbers are used in ripple-binary-codec: // 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. // Use tokens.
// -399 .. -300: L Local error (transaction fee inadequate, exceeds local // -399 .. -300: L Local error (transaction fee inadequate, exceeds local
@@ -73,7 +73,7 @@ enum TELcodes : TERUnderlyingType {
enum TEMcodes : TERUnderlyingType { enum TEMcodes : TERUnderlyingType {
// Note: Range is stable. // Note: Range is stable.
// Exact numbers are used in ripple-binary-codec: // 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. // Use tokens.
// -299 .. -200: M Malformed (bad signature) // -299 .. -200: M Malformed (bad signature)
@@ -148,7 +148,7 @@ enum TEMcodes : TERUnderlyingType {
enum TEFcodes : TERUnderlyingType { enum TEFcodes : TERUnderlyingType {
// Note: Range is stable. // Note: Range is stable.
// Exact numbers are used in ripple-binary-codec: // 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. // Use tokens.
// -199 .. -100: F // -199 .. -100: F
@@ -192,7 +192,7 @@ enum TEFcodes : TERUnderlyingType {
enum TERcodes : TERUnderlyingType { enum TERcodes : TERUnderlyingType {
// Note: Range is stable. // Note: Range is stable.
// Exact numbers are used in ripple-binary-codec: // 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. // Use tokens.
// -99 .. -1: R Retry // -99 .. -1: R Retry

View File

@@ -23,6 +23,7 @@
#include <xrpld/consensus/Consensus.h> #include <xrpld/consensus/Consensus.h>
#include <xrpl/beast/unit_test.h> #include <xrpl/beast/unit_test.h>
#include <xrpl/json/to_string.h>
namespace ripple { namespace ripple {
namespace test { namespace test {
@@ -40,6 +41,7 @@ public:
testShouldCloseLedger() testShouldCloseLedger()
{ {
using namespace std::chrono_literals; using namespace std::chrono_literals;
testcase("should close ledger");
// Use default parameters // Use default parameters
ConsensusParms const p{}; ConsensusParms const p{};
@@ -78,46 +80,102 @@ public:
testCheckConsensus() testCheckConsensus()
{ {
using namespace std::chrono_literals; using namespace std::chrono_literals;
testcase("check consensus");
// Use default parameterss // Use default parameterss
ConsensusParms const p{}; ConsensusParms const p{};
///////////////
// Disputes still in doubt
//
// Not enough time has elapsed // Not enough time has elapsed
BEAST_EXPECT( BEAST_EXPECT(
ConsensusState::No == 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 // If not enough peers have propsed, ensure
// more time for proposals // more time for proposals
BEAST_EXPECT( BEAST_EXPECT(
ConsensusState::No == 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 // Enough time has elapsed and we all agree
BEAST_EXPECT( BEAST_EXPECT(
ConsensusState::Yes == 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 // Enough time has elapsed and we don't yet agree
BEAST_EXPECT( BEAST_EXPECT(
ConsensusState::No == 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 // Our peers have moved on
// Enough time has elapsed and we all agree // Enough time has elapsed and we all agree
BEAST_EXPECT( BEAST_EXPECT(
ConsensusState::MovedOn == 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. // If no peers, don't agree until time has passed.
BEAST_EXPECT( BEAST_EXPECT(
ConsensusState::No == 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. // Agree if no peers and enough time has passed.
BEAST_EXPECT( BEAST_EXPECT(
ConsensusState::Yes == 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 void
@@ -125,6 +183,7 @@ public:
{ {
using namespace std::chrono_literals; using namespace std::chrono_literals;
using namespace csf; using namespace csf;
testcase("standalone");
Sim s; Sim s;
PeerGroup peers = s.createGroup(1); PeerGroup peers = s.createGroup(1);
@@ -149,6 +208,7 @@ public:
{ {
using namespace csf; using namespace csf;
using namespace std::chrono; using namespace std::chrono;
testcase("peers agree");
ConsensusParms const parms{}; ConsensusParms const parms{};
Sim sim; Sim sim;
@@ -186,6 +246,7 @@ public:
{ {
using namespace csf; using namespace csf;
using namespace std::chrono; using namespace std::chrono;
testcase("slow peers");
// Several tests of a complete trust graph with a subset of peers // Several tests of a complete trust graph with a subset of peers
// that have significantly longer network delays to the rest of the // that have significantly longer network delays to the rest of the
@@ -351,6 +412,7 @@ public:
{ {
using namespace csf; using namespace csf;
using namespace std::chrono; using namespace std::chrono;
testcase("close time disagree");
// This is a very specialized test to get ledgers to disagree on // This is a very specialized test to get ledgers to disagree on
// the close time. It unfortunately assumes knowledge about current // the close time. It unfortunately assumes knowledge about current
@@ -417,6 +479,8 @@ public:
{ {
using namespace csf; using namespace csf;
using namespace std::chrono; using namespace std::chrono;
testcase("wrong LCL");
// Specialized test to exercise a temporary fork in which some peers // Specialized test to exercise a temporary fork in which some peers
// are working on an incorrect prior ledger. // are working on an incorrect prior ledger.
@@ -589,6 +653,7 @@ public:
{ {
using namespace csf; using namespace csf;
using namespace std::chrono; using namespace std::chrono;
testcase("consensus close time rounding");
// This is a specialized test engineered to yield ledgers with different // This is a specialized test engineered to yield ledgers with different
// close times even though the peers believe they had close time // close times even though the peers believe they had close time
@@ -604,9 +669,6 @@ public:
PeerGroup fast = sim.createGroup(4); PeerGroup fast = sim.createGroup(4);
PeerGroup network = fast + slow; PeerGroup network = fast + slow;
for (Peer* peer : network)
peer->consensusParms = parms;
// Connected trust graph // Connected trust graph
network.trust(network); network.trust(network);
@@ -692,6 +754,7 @@ public:
{ {
using namespace csf; using namespace csf;
using namespace std::chrono; using namespace std::chrono;
testcase("fork");
std::uint32_t numPeers = 10; std::uint32_t numPeers = 10;
// Vary overlap between two UNLs // Vary overlap between two UNLs
@@ -748,6 +811,7 @@ public:
{ {
using namespace csf; using namespace csf;
using namespace std::chrono; using namespace std::chrono;
testcase("hub network");
// Simulate a set of 5 validators that aren't directly connected but // Simulate a set of 5 validators that aren't directly connected but
// rely on a single hub node for communication // rely on a single hub node for communication
@@ -835,6 +899,7 @@ public:
{ {
using namespace csf; using namespace csf;
using namespace std::chrono; using namespace std::chrono;
testcase("preferred by branch");
// Simulate network splits that are prevented from forking when using // Simulate network splits that are prevented from forking when using
// preferred ledger by trie. This is a contrived example that involves // preferred ledger by trie. This is a contrived example that involves
@@ -967,6 +1032,7 @@ public:
{ {
using namespace csf; using namespace csf;
using namespace std::chrono; using namespace std::chrono;
testcase("pause for laggards");
// Test that validators that jump ahead of the network slow // Test that validators that jump ahead of the network slow
// down. // down.
@@ -1052,6 +1118,302 @@ public:
BEAST_EXPECT(sim.synchronized()); 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 void
run() override run() override
{ {
@@ -1068,6 +1430,7 @@ public:
testHubNetwork(); testHubNetwork();
testPreferredByBranch(); testPreferredByBranch();
testPauseForLaggards(); testPauseForLaggards();
testDisputes();
} }
}; };

View File

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

View File

@@ -35,6 +35,7 @@
#include <xrpld/app/misc/NetworkOPs.h> #include <xrpld/app/misc/NetworkOPs.h>
#include <xrpld/app/misc/Transaction.h> #include <xrpld/app/misc/Transaction.h>
#include <xrpld/app/misc/TxQ.h> #include <xrpld/app/misc/TxQ.h>
#include <xrpld/app/misc/ValidatorKeys.h>
#include <xrpld/app/misc/ValidatorList.h> #include <xrpld/app/misc/ValidatorList.h>
#include <xrpld/app/misc/detail/AccountTxPaging.h> #include <xrpld/app/misc/detail/AccountTxPaging.h>
#include <xrpld/app/rdb/backend/SQLiteDatabase.h> #include <xrpld/app/rdb/backend/SQLiteDatabase.h>
@@ -249,6 +250,12 @@ public:
beast::get_abstract_clock<std::chrono::steady_clock>(), beast::get_abstract_clock<std::chrono::steady_clock>(),
validatorKeys, validatorKeys,
app_.logs().journal("LedgerConsensus")) app_.logs().journal("LedgerConsensus"))
, validatorPK_(
validatorKeys.keys ? validatorKeys.keys->publicKey
: decltype(validatorPK_){})
, validatorMasterPK_(
validatorKeys.keys ? validatorKeys.keys->masterPublicKey
: decltype(validatorMasterPK_){})
, m_ledgerMaster(ledgerMaster) , m_ledgerMaster(ledgerMaster)
, m_job_queue(job_queue) , m_job_queue(job_queue)
, m_standalone(standalone) , m_standalone(standalone)
@@ -732,6 +739,9 @@ private:
RCLConsensus mConsensus; RCLConsensus mConsensus;
std::optional<PublicKey> const validatorPK_;
std::optional<PublicKey> const validatorMasterPK_;
ConsensusPhase mLastConsensusPhase; ConsensusPhase mLastConsensusPhase;
LedgerMaster& m_ledgerMaster; LedgerMaster& m_ledgerMaster;
@@ -1917,6 +1927,23 @@ NetworkOPsImp::beginConsensus(
bool bool
NetworkOPsImp::processTrustedProposal(RCLCxPeerPos peerPos) 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); return mConsensus.peerProposal(app_.timeKeeper().closeTime(), peerPos);
} }

View File

@@ -109,6 +109,7 @@ checkConsensusReached(
bool count_self, bool count_self,
std::size_t minConsensusPct, std::size_t minConsensusPct,
bool reachedMax, bool reachedMax,
bool stalled,
std::unique_ptr<std::stringstream> const& clog) std::unique_ptr<std::stringstream> const& clog)
{ {
CLOG(clog) << "checkConsensusReached params: agreeing: " << agreeing CLOG(clog) << "checkConsensusReached params: agreeing: " << agreeing
@@ -138,6 +139,17 @@ checkConsensusReached(
return false; 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) if (count_self)
{ {
++agreeing; ++agreeing;
@@ -147,6 +159,7 @@ checkConsensusReached(
} }
std::size_t currentPercentage = (agreeing * 100) / total; std::size_t currentPercentage = (agreeing * 100) / total;
CLOG(clog) << "currentPercentage: " << currentPercentage; CLOG(clog) << "currentPercentage: " << currentPercentage;
bool const ret = currentPercentage >= minConsensusPct; bool const ret = currentPercentage >= minConsensusPct;
if (ret) if (ret)
@@ -168,6 +181,7 @@ checkConsensus(
std::size_t currentFinished, std::size_t currentFinished,
std::chrono::milliseconds previousAgreeTime, std::chrono::milliseconds previousAgreeTime,
std::chrono::milliseconds currentAgreeTime, std::chrono::milliseconds currentAgreeTime,
bool stalled,
ConsensusParms const& parms, ConsensusParms const& parms,
bool proposing, bool proposing,
beast::Journal j, beast::Journal j,
@@ -181,7 +195,7 @@ checkConsensus(
<< " minimum duration to reach consensus: " << " minimum duration to reach consensus: "
<< parms.ledgerMIN_CONSENSUS.count() << "ms" << parms.ledgerMIN_CONSENSUS.count() << "ms"
<< " max consensus time " << parms.ledgerMAX_CONSENSUS.count() << " max consensus time " << parms.ledgerMAX_CONSENSUS.count()
<< "s" << "ms"
<< " minimum consensus percentage: " << parms.minCONSENSUS_PCT << " minimum consensus percentage: " << parms.minCONSENSUS_PCT
<< ". "; << ". ";
@@ -211,10 +225,12 @@ checkConsensus(
proposing, proposing,
parms.minCONSENSUS_PCT, parms.minCONSENSUS_PCT,
currentAgreeTime > parms.ledgerMAX_CONSENSUS, currentAgreeTime > parms.ledgerMAX_CONSENSUS,
stalled,
clog)) clog))
{ {
JLOG(j.debug()) << "normal consensus"; JLOG((stalled ? j.warn() : j.debug()))
CLOG(clog) << "reached. "; << "normal consensus" << (stalled ? ", but stalled" : "");
CLOG(clog) << "reached" << (stalled ? ", but stalled." : ".");
return ConsensusState::Yes; return ConsensusState::Yes;
} }
@@ -226,6 +242,7 @@ checkConsensus(
false, false,
parms.minCONSENSUS_PCT, parms.minCONSENSUS_PCT,
currentAgreeTime > parms.ledgerMAX_CONSENSUS, currentAgreeTime > parms.ledgerMAX_CONSENSUS,
false,
clog)) clog))
{ {
JLOG(j.warn()) << "We see no consensus, but 80% of nodes have moved on"; JLOG(j.warn()) << "We see no consensus, but 80% of nodes have moved on";
@@ -233,6 +250,19 @@ checkConsensus(
return ConsensusState::MovedOn; 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 // no consensus yet
JLOG(j.trace()) << "no consensus"; JLOG(j.trace()) << "no consensus";
CLOG(clog) << "No consensus. "; CLOG(clog) << "No consensus. ";

View File

@@ -31,6 +31,7 @@
#include <xrpl/beast/utility/Journal.h> #include <xrpl/beast/utility/Journal.h>
#include <xrpl/json/json_writer.h> #include <xrpl/json/json_writer.h>
#include <algorithm>
#include <chrono> #include <chrono>
#include <deque> #include <deque>
#include <optional> #include <optional>
@@ -81,6 +82,10 @@ shouldCloseLedger(
last ledger last ledger
@param currentAgreeTime how long, in milliseconds, we've been trying to @param currentAgreeTime how long, in milliseconds, we've been trying to
agree 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 parms Consensus constant parameters
@param proposing whether we should count ourselves @param proposing whether we should count ourselves
@param j journal for logging @param j journal for logging
@@ -94,6 +99,7 @@ checkConsensus(
std::size_t currentFinished, std::size_t currentFinished,
std::chrono::milliseconds previousAgreeTime, std::chrono::milliseconds previousAgreeTime,
std::chrono::milliseconds currentAgreeTime, std::chrono::milliseconds currentAgreeTime,
bool stalled,
ConsensusParms const& parms, ConsensusParms const& parms,
bool proposing, bool proposing,
beast::Journal j, beast::Journal j,
@@ -574,6 +580,9 @@ private:
NetClock::duration closeResolution_ = ledgerDefaultTimeResolution; NetClock::duration closeResolution_ = ledgerDefaultTimeResolution;
ConsensusParms::AvalancheState closeTimeAvalancheState_ =
ConsensusParms::init;
// Time it took for the last consensus round to converge // Time it took for the last consensus round to converge
std::chrono::milliseconds prevRoundTime_; std::chrono::milliseconds prevRoundTime_;
@@ -599,6 +608,13 @@ private:
std::optional<Result> result_; std::optional<Result> result_;
ConsensusCloseTimes rawCloseTimes_; ConsensusCloseTimes rawCloseTimes_;
// The number of calls to phaseEstablish where none of our peers
// have changed any votes on disputed transactions.
std::size_t peerUnchangedCounter_ = 0;
// The total number of times we have called phaseEstablish
std::size_t establishCounter_ = 0;
//------------------------------------------------------------------------- //-------------------------------------------------------------------------
// Peer related consensus data // Peer related consensus data
@@ -696,6 +712,7 @@ Consensus<Adaptor>::startRoundInternal(
previousLedger_ = prevLedger; previousLedger_ = prevLedger;
result_.reset(); result_.reset();
convergePercent_ = 0; convergePercent_ = 0;
closeTimeAvalancheState_ = ConsensusParms::init;
haveCloseTimeConsensus_ = false; haveCloseTimeConsensus_ = false;
openTime_.reset(clock_.now()); openTime_.reset(clock_.now());
currPeerPositions_.clear(); currPeerPositions_.clear();
@@ -1351,6 +1368,9 @@ Consensus<Adaptor>::phaseEstablish(
// can only establish consensus if we already took a stance // can only establish consensus if we already took a stance
XRPL_ASSERT(result_, "ripple::Consensus::phaseEstablish : result is set"); XRPL_ASSERT(result_, "ripple::Consensus::phaseEstablish : result is set");
++peerUnchangedCounter_;
++establishCounter_;
using namespace std::chrono; using namespace std::chrono;
ConsensusParms const& parms = adaptor_.parms(); ConsensusParms const& parms = adaptor_.parms();
@@ -1417,6 +1437,8 @@ Consensus<Adaptor>::closeLedger(std::unique_ptr<std::stringstream> const& clog)
phase_ = ConsensusPhase::establish; phase_ = ConsensusPhase::establish;
JLOG(j_.debug()) << "transitioned to ConsensusPhase::establish"; JLOG(j_.debug()) << "transitioned to ConsensusPhase::establish";
rawCloseTimes_.self = now_; rawCloseTimes_.self = now_;
peerUnchangedCounter_ = 0;
establishCounter_ = 0;
result_.emplace(adaptor_.onClose(previousLedger_, now_, mode_.get())); result_.emplace(adaptor_.onClose(previousLedger_, now_, mode_.get()));
result_->roundTime.reset(clock_.now()); result_->roundTime.reset(clock_.now());
@@ -1550,16 +1572,11 @@ Consensus<Adaptor>::updateOurPositions(
} }
else else
{ {
int neededWeight; // We don't track rounds for close time, so just pass 0s
auto const [neededWeight, newState] = getNeededWeight(
if (convergePercent_ < parms.avMID_CONSENSUS_TIME) parms, closeTimeAvalancheState_, convergePercent_, 0, 0);
neededWeight = parms.avINIT_CONSENSUS_PCT; if (newState)
else if (convergePercent_ < parms.avLATE_CONSENSUS_TIME) closeTimeAvalancheState_ = *newState;
neededWeight = parms.avMID_CONSENSUS_PCT;
else if (convergePercent_ < parms.avSTUCK_CONSENSUS_TIME)
neededWeight = parms.avLATE_CONSENSUS_PCT;
else
neededWeight = parms.avSTUCK_CONSENSUS_PCT;
CLOG(clog) << "neededWeight " << neededWeight << ". "; CLOG(clog) << "neededWeight " << neededWeight << ". ";
int participants = currPeerPositions_.size(); int participants = currPeerPositions_.size();
@@ -1681,7 +1698,8 @@ Consensus<Adaptor>::haveConsensus(
} }
else else
{ {
JLOG(j_.debug()) << nodeId << " has " << peerProp.position(); JLOG(j_.debug()) << "Proposal disagreement: Peer " << nodeId
<< " has " << peerProp.position();
++disagree; ++disagree;
} }
} }
@@ -1691,6 +1709,17 @@ Consensus<Adaptor>::haveConsensus(
JLOG(j_.debug()) << "Checking for TX consensus: agree=" << agree JLOG(j_.debug()) << "Checking for TX consensus: agree=" << agree
<< ", disagree=" << disagree; << ", 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 // Determine if we actually have consensus or not
result_->state = checkConsensus( result_->state = checkConsensus(
prevProposers_, prevProposers_,
@@ -1699,7 +1728,8 @@ Consensus<Adaptor>::haveConsensus(
currentFinished, currentFinished,
prevRoundTime_, prevRoundTime_,
result_->roundTime.read(), result_->roundTime.read(),
adaptor_.parms(), stalled,
parms,
mode_.get() == ConsensusMode::proposing, mode_.get() == ConsensusMode::proposing,
j_, j_,
clog); clog);
@@ -1710,6 +1740,33 @@ Consensus<Adaptor>::haveConsensus(
return false; 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 // There is consensus, but we need to track if the network moved on
// without us. // without us.
if (result_->state == ConsensusState::MovedOn) if (result_->state == ConsensusState::MovedOn)
@@ -1802,8 +1859,9 @@ Consensus<Adaptor>::createDisputes(
{ {
Proposal_t const& peerProp = peerPos.proposal(); Proposal_t const& peerProp = peerPos.proposal();
auto const cit = acquired_.find(peerProp.position()); auto const cit = acquired_.find(peerProp.position());
if (cit != acquired_.end()) if (cit != acquired_.end() &&
dtx.setVote(nodeId, cit->second.exists(txID)); dtx.setVote(nodeId, cit->second.exists(txID)))
peerUnchangedCounter_ = 0;
} }
adaptor_.share(dtx.tx()); adaptor_.share(dtx.tx());
@@ -1828,7 +1886,8 @@ Consensus<Adaptor>::updateDisputes(NodeID_t const& node, TxSet_t const& other)
for (auto& it : result_->disputes) for (auto& it : result_->disputes)
{ {
auto& d = it.second; 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 #ifndef RIPPLE_CONSENSUS_CONSENSUS_PARMS_H_INCLUDED
#define RIPPLE_CONSENSUS_CONSENSUS_PARMS_H_INCLUDED #define RIPPLE_CONSENSUS_CONSENSUS_PARMS_H_INCLUDED
#include <xrpl/beast/utility/instrumentation.h>
#include <chrono> #include <chrono>
#include <cstddef> #include <cstddef>
#include <functional>
#include <map>
#include <optional>
namespace ripple { namespace ripple {
@@ -43,7 +48,7 @@ struct ConsensusParms
This is a safety to protect against very old validations and the time This is a safety to protect against very old validations and the time
it takes to adjust the close time accuracy window. 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. /** 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 first saw it. This provides faster recovery in very rare cases where the
number of validations produced by the network is lower than normal 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. /** Duration pre-close in which validations are acceptable.
The number of seconds before a close time that we consider a validation The number of seconds before a close time that we consider a validation
acceptable. This protects against extreme clock errors 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 //! 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 //! 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 // Consensus durations are relative to the internal Consensus clock and use
// millisecond resolution. // millisecond resolution.
//! The percentage threshold above which we can declare consensus. //! 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 //! 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 //! 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}; std::chrono::milliseconds{1950};
/** The maximum amount of time to spend pausing for laggards. /** 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 * validators don't appear to be offline that are merely waiting for
* laggards. * 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 //! 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 //! 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 /** The minimum amount of time to consider the previous round
to have taken. to have taken.
@@ -104,38 +123,80 @@ struct ConsensusParms
twice the interval between proposals (0.7s) divided by twice the interval between proposals (0.7s) divided by
the interval between mid and late consensus ([85-50]/100). 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 // Avalanche tuning
// As a function of the percent this round's duration is of the prior round, // 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 // we increase the threshold for yes votes to add a transaction to our
// position. // position.
enum AvalancheState { init, mid, late, stuck };
//! Percentage of nodes on our UNL that must vote yes struct AvalancheCutoff
std::size_t avINIT_CONSENSUS_PCT = 50; {
int const consensusTime;
//! Percentage of previous round duration before we advance std::size_t const consensusPct;
std::size_t avMID_CONSENSUS_TIME = 50; AvalancheState const next;
};
//! Percentage of nodes that most vote yes after advancing //! Map the consensus requirement avalanche state to the amount of time that
std::size_t avMID_CONSENSUS_PCT = 65; //! must pass before moving to that state, the agreement percentage required
//! at that state, and the next state. "stuck" loops back on itself because
//! Percentage of previous round duration before we advance //! once we're stuck, we're stuck.
std::size_t avLATE_CONSENSUS_TIME = 85; //! This structure allows for "looping" of states if needed.
std::map<AvalancheState, AvalancheCutoff> const avalancheCutoffs{
//! Percentage of nodes that most vote yes after advancing // {state, {time, percent, nextState}},
std::size_t avLATE_CONSENSUS_PCT = 70; // Initial state: 50% of nodes must vote yes
{init, {0, 50, mid}},
//! Percentage of previous round duration before we are stuck // mid-consensus starts after 50% of the previous round time, and
std::size_t avSTUCK_CONSENSUS_TIME = 200; // requires 65% yes
{mid, {50, 65, late}},
//! Percentage of nodes that must vote yes after we are stuck // late consensus starts after 85% time, and requires 70% yes
std::size_t avSTUCK_CONSENSUS_PCT = 95; {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 //! 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 } // namespace ripple
#endif #endif

View File

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

View File

@@ -82,6 +82,51 @@ public:
return ourVote_; 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. //! The disputed transaction.
Tx_t const& Tx_t const&
tx() const tx() const
@@ -100,8 +145,12 @@ public:
@param peer Identifier of peer. @param peer Identifier of peer.
@param votesYes Whether peer votes to include the disputed transaction. @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); setVote(NodeID_t const& peer, bool votesYes);
/** Remove a peer's vote /** Remove a peer's vote
@@ -135,12 +184,18 @@ private:
bool ourVote_; //< Our vote (true is yes) bool ourVote_; //< Our vote (true is yes)
Tx_t tx_; //< Transaction under dispute Tx_t tx_; //< Transaction under dispute
Map_t votes_; //< Map from NodeID to vote 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_; beast::Journal const j_;
}; };
// Track a peer's yes/no vote on a particular disputed tx_ // Track a peer's yes/no vote on a particular disputed tx_
template <class Tx_t, class NodeID_t> template <class Tx_t, class NodeID_t>
void bool
DisputedTx<Tx_t, NodeID_t>::setVote(NodeID_t const& peer, bool votesYes) DisputedTx<Tx_t, NodeID_t>::setVote(NodeID_t const& peer, bool votesYes)
{ {
auto const [it, inserted] = votes_.insert(std::make_pair(peer, 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(); JLOG(j_.debug()) << "Peer " << peer << " votes NO on " << tx_.id();
++nays_; ++nays_;
} }
return true;
} }
// changes vote to yes // changes vote to yes
else if (votesYes && !it->second) else if (votesYes && !it->second)
@@ -166,6 +222,7 @@ DisputedTx<Tx_t, NodeID_t>::setVote(NodeID_t const& peer, bool votesYes)
--nays_; --nays_;
++yays_; ++yays_;
it->second = true; it->second = true;
return true;
} }
// changes vote to no // changes vote to no
else if (!votesYes && it->second) else if (!votesYes && it->second)
@@ -174,7 +231,9 @@ DisputedTx<Tx_t, NodeID_t>::setVote(NodeID_t const& peer, bool votesYes)
++nays_; ++nays_;
--yays_; --yays_;
it->second = false; it->second = false;
return true;
} }
return false;
} }
// Remove a peer's vote on this disputed transaction // Remove a peer's vote on this disputed transaction
@@ -211,21 +270,26 @@ DisputedTx<Tx_t, NodeID_t>::updateVote(
bool newPosition; bool newPosition;
int weight; 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 if (proposing) // give ourselves full weight
{ {
// This is basically the percentage of nodes voting 'yes' (including us) // This is basically the percentage of nodes voting 'yes' (including us)
weight = (yays_ * 100 + (ourVote_ ? 100 : 0)) / (nays_ + yays_ + 1); weight = (yays_ * 100 + (ourVote_ ? 100 : 0)) / (nays_ + yays_ + 1);
// To prevent avalanche stalls, we increase the needed weight slightly newPosition = weight > requiredPct;
// 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;
} }
else else
{ {
@@ -236,13 +300,16 @@ DisputedTx<Tx_t, NodeID_t>::updateVote(
if (newPosition == ourVote_) if (newPosition == ourVote_)
{ {
JLOG(j_.info()) << "No change (" << (ourVote_ ? "YES" : "NO") ++currentVoteCounter_;
<< ") : weight " << weight << ", percent " JLOG(j_.info()) << "No change (" << (ourVote_ ? "YES" : "NO") << ") on "
<< percentTime; << tx_.id() << " : weight " << weight << ", percent "
<< percentTime
<< ", round(s) with this vote: " << currentVoteCounter_;
JLOG(j_.debug()) << Json::Compact{getJson()}; JLOG(j_.debug()) << Json::Compact{getJson()};
return false; return false;
} }
currentVoteCounter_ = 0;
ourVote_ = newPosition; ourVote_ = newPosition;
JLOG(j_.debug()) << "We now vote " << (ourVote_ ? "YES" : "NO") << " on " JLOG(j_.debug()) << "We now vote " << (ourVote_ ? "YES" : "NO") << " on "
<< tx_.id(); << tx_.id();