-
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
Amendment table clean up (Improve amendment processing and activation logic) #3428
Conversation
Add majority timer configuration FIXES: XRPLF#3396
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.
This looks good, but I left a couple of suggestions to request one change: do not allow the wait time to be specified as seconds
in the config file and impose a 15-minute floor in the selected value. The other change is at your discretion.
"Invalid " SECTION_AMENDMENT_MAJORITY_TIME | ||
", must be: [0-9]+ [seconds|minutes|hours|days|weeks]"); | ||
std::uint32_t duration = | ||
beast::lexicalCastThrow<std::uint32_t>(match[1].str()); |
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 would not allow seconds
: there's no real plausible scenario for such a short interval. In fact, I think I'd reject any interval smaller than 15 minutes.
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.
Done
src/ripple/app/main/Application.cpp
Outdated
static int const MAJORITY_FRACTION_OLD(204); | ||
// 8/10 = 80% | ||
static int const MAJORITY_FRACTION(8); |
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.
These values, along with the default value for the majority time should probably migrate into SystemParametersInfo.h
:
/** The minimum amount of support an amendment should have.
@note This value is used by legacy code and will become obsolete
once the fix3396 amendment activates.
*/
constexpr int const amendmentMajorityFractionOld = 204;
/** The minimum amount of support an amendment must have */
constexpr int const amendmentMajorityPercent = 80;
/** The minimum amount of time an amendment must hold a majority */
constexpr std::chrono::seconds const defaultAmendmentMajorityTime = std::chrono::weeks{2};
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.
Done, except for amendmentMajorityPercent
, which is currently not used. Added instead amendmentMajorityFraction
.
Make 15 minutes the min amendment majority time Move amendment related const to SystemParameters.h
src/ripple/core/impl/Config.cpp
Outdated
AMENDMENT_MAJORITY_TIME = minutes(duration); | ||
else if (boost::iequals(match[2], "hours")) | ||
AMENDMENT_MAJORITY_TIME = hours(duration); | ||
else if (boost::iequals(match[2], "days")) | ||
AMENDMENT_MAJORITY_TIME = days(duration); | ||
else if (boost::iequals(match[2], "weeks")) | ||
AMENDMENT_MAJORITY_TIME = weeks(duration); | ||
|
||
if (AMENDMENT_MAJORITY_TIME < defaultAmendmentMajorityTime) |
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 think this check is wrong. It will prevent anything less than 2 weeks. I'd simply hard-code std::chrono::minutes(15)
here.
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.
Right!!!
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 left some ideas for you to think about. All of them are open for discussion.
src/ripple/app/misc/AmendmentTable.h
Outdated
int old_ = 0; | ||
int new_ = 0; |
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'm not keen on the names old_
and new_
. If I'm reading the code correctly, the way the numbers are interpreted is significantly different. Perhaps...
old_
->per256
new_
->per10fix3396
Or perhaps you have a better idea for more descriptive names...
Also, in general, around this code base, if a member variable is public (like in struct MajorityFraction
) then we don't put trailing underscores on it. The trailing underscores are usually reserved for private or protected data.
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 think it's best to use std::ratio
here:
struct AmendmentSupermajorityThreshold
{
std::ratio<204, 256> const pre3396;
std::ratio<80, 100> const post3396;
};
The nice thing is that std::ratio
tells you exactly what's happening and reduces the that risk that someone changes the numerator, while leaving the denominator (which is currently hidden in AmendmentTableImpl
) unchanged.
Of course, it's important to note that the compiler actually reduces std::ratio<204, 256>
from 204/256
to 51/64
and std::ratio<80,100>
from 80/100
to 4/5
but even with those reductions the arithmetic should be precise and match the existing math. See this gist.
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.
Done
@@ -148,8 +149,8 @@ class AmendmentTableImpl final : public AmendmentTable | |||
std::chrono::seconds const majorityTime_; | |||
|
|||
// The amount of support that an amendment must receive | |||
// 0 = 0% and 256 = 100% | |||
int const majorityFraction_; | |||
// 0 = 0% and 256 = 100%(old) 8 = 100%(new) |
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 think this comment is wrong? Doesn't the current implementation result in "10 = 100%(new) "?
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.
That's right. But I think the comment with the arithmetic is unnecessary here, especially if we use the std::ratio
thing above.
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.
Removed the comment
vote->mThreshold = | ||
std::max(1, (vote->mTrustedValidations * majorityFraction_) / 256); | ||
vote->mThreshold = rules.enabled(ripple::fix3396) | ||
? std::max(1, (vote->mTrustedValidations * majorityFraction_.new_) / 10) |
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.
There are two different places where fix3396
comes into play. Here. And down on line 515 where a >=
becomes a >
.
I'd like to suggest consolidating those two places into a single place; here. You can do that by rounding up the division instead of doing the usual C/C++ truncate toward zero.
It's worth noting that this change, either way it is done, has non-trivial impact on how amendments behave on a network with a small number of validators. It means that with small numbers of trusted validators there must be 100% agreement before any amendment will pass. For example, if we require at least 80% agreement between validators, then with 2, 3, or 4 trusted validators there must be 100% agreement for any amendment to pass.
I agree that's the right call to make given the current state of the network.
I think by doing the rounding up here, rather than tweaking the comparison on line 515, you'll make the intent easier to understand for future maintainers. There are a couple of different ways to achieve rounding up. Here's one way to do it that avoids floating point:
auto amendmentThreshold = [this] (int trustedValidations)
{
std::div_t const d =
std::div (trustedValidations * majorityFraction_.new_, 10);
return std::clamp (d.quot + (d.rem > 0 ? 1 : 0), 1, trustedValidations);
};
There are other ways to code it as well.
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.
While we're here, I have a mild preference for using 100 as the divisor rather than 10. That would allow you to rename amendmentMajorityFraction = 8 to amendmentMajorityPercent = 80; which is way easier to read and understand.
I think people accustomed to western math will also recognize percentages much faster than they will recognize fractions of 10.
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 appreciate the well-thought out comment (and agree with the 8 -> 80 change), but I find the division code confusing to read. Once I read it carefully, I understand what's happening, but I'm not confident it behaves identically pre-amendment. I am hoping we can do something even better:
We should push all this logic into AmendmentSet
and add bool passes(uint256) const
to its interface:
struct AmendmentSet
{
private:
// How many yes votes each amendment received
hash_map<uint256, int> votes_;
// number of trusted validations
int mTrustedValidations = 0;
// number of votes needed
int mThreshold;
Rules const& rules;
public:
AmendmentSet(
Rules const& rules,
MajorityFraction const& majorityFraction,
std::vector<std::shared_ptr<STValidation>> const& valSet)
{
for (auto const& val : valSet)
{
if (val->isTrusted())
{
std::set<uint256> ballot;
if (val->isFieldPresent(sfAmendments))
{
auto const choices = val->getFieldV256(sfAmendments);
ballot.insert(choices.begin(), choices.end());
}
++mTrustedValidations;
for (auto const& amendment : ballot)
++votes_[amendment];
}
}
mThreshold = rules.enabled(fix3396)
? std::max(1, (vote->mTrustedValidations * majorityFraction.pre3396.num) / majorityFraction.pre3396.den)
: std::max(1, (vote->mTrustedValidations * majorityFraction.post3396.num) / majorityFraction.post3396.den);
}
bool
passes(uint256 const& amendment) const
{
auto const& it = votes_.find(amendment);
if (it == votes_.end())
return false;
// Before this fix, it was possible for an amendment to activate with a percentage
// slightly less than 80% because we compared for "greater than or equal to"
// instead of strictly "greater than".
if (!rules.enabled(fix3396))
return it->second >= fix3396;
return it->second > fix3396;
}
... other stuff
};
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.
Done
@@ -148,8 +149,8 @@ class AmendmentTableImpl final : public AmendmentTable | |||
std::chrono::seconds const majorityTime_; |
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 can't comment on a line that hasn't been modified. And I know this is not your doing. I'm looking at the protected:
on line 141. AmendmentTableImpl
is final
, yes? So protected
data really doesn't make much sense. Perhaps you can make that private
while you're in here?
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.
Done
bool const hasValMajority = rules.enabled(ripple::fix3396) | ||
? (vote->votes(entry.first) > vote->mThreshold) | ||
: (vote->votes(entry.first) >= vote->mThreshold); |
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.
If you go with rounding up when calculating the threshold, then this change should be reverted.
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.
Updated per Nik's suggestion of pushing the logic into AmendmentSet.
src/test/app/AmendmentTable_test.cpp
Outdated
{ | ||
if ((256 * i) < (validators.size() * amendment.second)) | ||
if (auto yes = enabled3396() ? nVotes >= i : nVotes > i; yes) |
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 think you can lose yes
. The following is working for me:
if (enabled3396() ? nVotes >= i : nVotes > i)
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.
Done.
@@ -630,7 +643,7 @@ class AmendmentTable_test final : public beast::unit_test::suite | |||
enabled, | |||
majority); | |||
|
|||
if (i < 13) | |||
if (i < 13) // 13 => 13/16 = 0.8125 => > 80% |
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.
Thanks for adding this comment. It helps.
src/test/app/AmendmentTable_test.cpp
Outdated
@@ -788,6 +802,9 @@ class AmendmentTable_test final : public beast::unit_test::suite | |||
testDetectMajority(); | |||
testLostMajority(); | |||
testHasUnsupported(); | |||
enable3396(true); | |||
testDetectMajority(); | |||
testLostMajority(); |
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'd like to suggest a different way of enabling and disabling the amendment you want to test with/without. Here are a couple of unit tests files that have a similar problem:
https://github.com/ripple/rippled/blob/develop/src/test/app/Offer_test.cpp#L5155-L5227
https://github.com/ripple/rippled/blob/develop/src/test/app/Flow_test.cpp#L1354-L1388
These tests, and other similar tests in the code base, pass the amendment of interest into the test as a parameter. Those of us who structure our tests this way feel like it's easier to understand and maintain than if the current amendments are kept in a member variable of the test.
If you were to take this model, then your test file might change to look something like this:
void testAll (uint256 const& feat)
{
testConstruct(feat);
testGet(feat);
testBadConfig(feat);
testEnableVeto(feat);
testNoOnUnknown(feat);
testNoOnVetoed(feat);
testVoteEnable(feat);
testDetectMajority(feat);
testLostMajority(feat);
testHasUnsupported(feat);
}
void
run() override
{
testAll ({});
testAll (fix3396);
}
And doRound
might look like this:
// Execute a pretend consensus round for a flag ledger
void
doRound(
uint256 const& feat,
AmendmentTable& table,
...
if (feat == fix3396 ? nVotes >= i : nVotes > i)
...
auto actions = table.doVoting(
Rules({feat}), roundTime, enabled, majority, validations);
...
I think this change might improve the maintainability of your tests. Thanks for listening.
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.
Done.
src/test/core/Config_test.cpp
Outdated
testcase("amendment"); | ||
|
||
std::vector<std::pair<std::string, std::uint32_t>> units = { | ||
{"seconds", 1}, |
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.
The good news is that this test now fails for
- seconds because the unit is not supported and
- minutes because 10 minutes is too short.
I suggest you add those to the tests that throw.
You might also want to put in a test...
- at precisely 15 minutes (see it work) and another
- at 15 minutes minus one second (see it throw).
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.
Done
if (i < 8) | ||
{ | ||
if (i < 4) // 16 - 3 = 13 => 13/16 = 0.8125 => > 80% | ||
{ // 16 - 4 = 12 => 12/16 = 0.75 => < 80% |
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.
Nice comment.
Replace majority fraction with std::ratio Reference super majority threshold constant directly in AmendmentTable instead of passing majority fraction into constructor Rename fix3396 feature to fixAmendmentMajorityCalc Add test config test cases Refactor tests with/without amendment feature
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.
A couple of μNits to address, but overall looks good. Thanks.
Fix super majority threshold typo 205 -> 204
Weird unit test failures happening... |
I have been testing AmendmentTable only. It does fail on other tests. I'll take a look. |
The problem is that in some tests (for inst Feature_test::testWithMajorities()), we have one trusted validator and one vote. Consequently the threshold is max(1, 1*80/100) = 1. And because of the change for post 3396 fix the votes should be greater than the threshold, which is false and the test fails. Pre 3396 fix, the votes should be greater or equal to the threshold, which was true and the test succeeded. I'm investigating how to address this issue in the unit tests. |
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.
👍
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.
Veto: Unit test failures in CI. I have not investigated further
1485.6s, 185 suites, 1188 cases, 632056 tests total, 1 failure
@gregtatcam, please cherry-pick 2bc3898 to remove the "per-256" reporting in the |
Fix Windows compile Remove per-256 response in Feature rpc test
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.
👍 Looks good. I left two additional suggestions, but I leave them to your discretion.
@@ -97,25 +98,73 @@ struct AmendmentSet | |||
private: | |||
// How many yes votes each amendment received | |||
hash_map<uint256, int> votes_; | |||
Rules const& rules_; |
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 like the way that the decision making code has moved into AmendmentSet
. However I find it a bit disorienting to read AmendmentSet
in its current hybrid struct/class form. I suggest that you push AmendmentSet
all the way over to being a proper class.
- Declare it as a
class
, not astruct
. - Make member variables
mTrustedValidations
andmThreshold
private and move them up to the top of the class declaration next tovotes_
andrules_
. - Rename
mTrustedValidations
andmThreshold
totrustedValidations_
andthreshold_
respectively. - Provide
const
accessor methodstrustedValidations()
andthreshold()
. - Fix up the few places in this file that were directly referencing
mTrustedValidations
andmThreshold
to use the newly added accessor methods.
These changes don't affect functionality, they just make the class more self consistent and easier to comprehend IMO. So your call.
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.
Done
constexpr std::ratio<204, 256> amendmentSuperMajorityThresholdPre3396; | ||
|
||
constexpr std::ratio<80, 100> amendmentSuperMajorityThresholdPost3396; |
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.
Now that the fix
has been renamed these names don't make as much sense as they used to. There aren't any other mentions of 3396 in the source code. I don't have any great suggestions, but maybe...
preFixAmendmentMajorityCalcThreshold
andpostFixAmendmentMajorityCalcThreshold
orpreFixAmendmentMajorityCalcSuperMajority
andpostFixAmendmentMajorityCalcSuperMajority
would be a little easier to figure out. You may have better ideas than these.
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.
Done
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.
👍
Released in 1.6.0 https://xrpl.org/blog/2020/rippled-1.6.0.html Amendment activated in April 2021 |
Use base 10 for majority vote calculation
Add majority timer configuration
FIXES: #3396