-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make minimum quorum Byzantine fault tolerant (RIPD-1461) #2093
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -308,9 +308,6 @@ class ValidatorList | |
for_each_listed ( | ||
std::function<void(PublicKey const&, bool)> func) const; | ||
|
||
static std::size_t | ||
calculateQuorum (std::size_t nTrustedKeys); | ||
|
||
private: | ||
/** Check response for trusted valid published list | ||
|
||
|
@@ -341,6 +338,15 @@ class ValidatorList | |
bool | ||
removePublisherList (PublicKey const& publisherKey); | ||
|
||
/** Return safe minimum quorum for listed validator set | ||
|
||
@param nListedKeys Number of list validator keys | ||
|
||
@param unListedLocal Whether the local node is an unlisted validator | ||
*/ | ||
static std::size_t | ||
calculateMinimumQuorum ( | ||
std::size_t nListedKeys, bool unlistedLocal=false); | ||
}; | ||
|
||
//------------------------------------------------------------------------------ | ||
|
@@ -399,41 +405,42 @@ ValidatorList::onConsensusStart ( | |
} | ||
} | ||
|
||
// This quorum guarantees sufficient overlap with the trusted sets of other | ||
// nodes using the same set of published lists. | ||
std::size_t quorum = keyListings_.size()/2 + 1; | ||
|
||
// Increment the quorum to prevent two unlisted validators using the same | ||
// even number of listed validators from forking. | ||
if (localPubKey_.size() && ! localKeyListed && | ||
rankedKeys.size () > 1 && keyListings_.size () % 2 != 0) | ||
++quorum; | ||
// This minimum quorum guarantees safe overlap with the trusted sets of | ||
// other nodes using the same set of published lists. | ||
std::size_t quorum = calculateMinimumQuorum (keyListings_.size(), | ||
localPubKey_.size() && !localKeyListed); | ||
|
||
JLOG (j_.debug()) << | ||
rankedKeys.size() << " of " << keyListings_.size() << | ||
" listed validators eligible for inclusion in the trusted set"; | ||
|
||
auto size = rankedKeys.size(); | ||
|
||
// Use all eligible keys if there is less than 10 listed validators or | ||
// only one trusted list | ||
if (size < 10 || publisherLists_.size() == 1) | ||
{ | ||
// Try to raise the quorum toward or above 80% of the trusted set | ||
std::size_t const targetQuorum = ValidatorList::calculateQuorum (size); | ||
if (targetQuorum > quorum) | ||
quorum = targetQuorum; | ||
} | ||
else | ||
// Do not require 80% quorum for less than 10 trusted validators | ||
if (rankedKeys.size() >= 10) | ||
{ | ||
// reduce the trusted set size so that the quorum represents | ||
// at least 80% | ||
size = quorum * 1.25; | ||
// Use all eligible keys if there is only one trusted list | ||
if (publisherLists_.size() == 1) | ||
{ | ||
// Try to raise the quorum to at least 80% of the trusted set | ||
quorum = std::max(quorum, size - size / 5); | ||
} | ||
else | ||
{ | ||
// Reduce the trusted set size so that the quorum represents | ||
// at least 80% | ||
size = quorum * 1.25; | ||
} | ||
} | ||
|
||
if (minimumQuorum_ && (seenValidators.empty() || | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar question to the above, but is setting Suppose
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Setting There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should consider logging at There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now that we're not bumping the quorum as an unlisted validator if there are less than 5, we might not need |
||
rankedKeys.size() < quorum)) | ||
{ | ||
quorum = *minimumQuorum_; | ||
JLOG (j_.warn()) << | ||
"Using unsafe quorum of " << quorum_ << | ||
" as specified in the command line"; | ||
} | ||
|
||
// Do not use achievable quorum until lists from all configured | ||
// publishers are available | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -409,15 +409,33 @@ ValidatorList::for_each_listed ( | |
} | ||
|
||
std::size_t | ||
ValidatorList::calculateQuorum (std::size_t nTrustedKeys) | ||
ValidatorList::calculateMinimumQuorum ( | ||
std::size_t nListedKeys, bool unlistedLocal) | ||
{ | ||
// Use 80% for large values of n, but have special cases for small numbers. | ||
constexpr std::array<std::size_t, 10> quorum{{ 0, 1, 2, 2, 3, 3, 4, 5, 6, 7 }}; | ||
|
||
if (nTrustedKeys < quorum.size()) | ||
return quorum[nTrustedKeys]; | ||
|
||
return nTrustedKeys - nTrustedKeys / 5; | ||
// Only require 51% quorum for small number of validators to facilitate | ||
// bootstrapping a network. | ||
if (nListedKeys <= 5) | ||
return nListedKeys/2 + 1; | ||
|
||
// The number of listed validators is increased to preserve the safety | ||
// guarantee for two unlisted validators using the same set of listed | ||
// validators. | ||
if (unlistedLocal) | ||
++nListedKeys; | ||
|
||
// Guarantee safety with up to 1/3 listed validators being malicious. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it worth documenting the reason for |
||
// This prioritizes safety (Byzantine fault tolerance) over liveness. | ||
// It takes at least as many malicious nodes to split/fork the network as | ||
// to stall the network. | ||
// At 67%, the overlap of two quorums is 34% | ||
// 67 + 67 - 100 = 34 | ||
// So under certain conditions, 34% of validators could vote for two | ||
// different ledgers and split the network. | ||
// Similarly 34% could prevent quorum from being met (by not voting) and | ||
// stall the network. | ||
// If/when the quorum is subsequently raised to/towards 80%, it becomes | ||
// harder to split the network (more safe) and easier to stall it (less live). | ||
return nListedKeys * 2/3 + 1; | ||
} | ||
|
||
} // ripple |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is new with these changes, but can't
size
end up greater thanrankedKeys_.size()
?For example, suppose there are
Then isn't the minimum quorum 14, but the calculated
size = 14 * 1.25 = 17
? If so, I think that would make for an infinite loop down on line 450.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
size
can be greater thanrankedKeys_.size()
That loop should be fine in that case since it is iterating
rankedKeys
. The result is just that we end up with fewer trusted keys thansize
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
D'oh, misread that loop. Thanks!