Skip to content
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

Limit amendment flapping (fixes #4350) #4364

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 35 additions & 17 deletions src/ripple/app/misc/impl/AmendmentTable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,17 @@ class AmendmentSet
private:
// How many yes votes each amendment received
hash_map<uint256, int> votes_;

Rules const& rules_;

// number of trusted validations
int trustedValidations_ = 0;
// number of votes needed
int threshold_ = 0;

// minimum number of votes needed to begin activation countdown
int activationThreshold_ = 0;

// minimum number of votes needed to continue activate countdown
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// minimum number of votes needed to continue activate countdown
// minimum number of votes needed to continue activation countdown

int deactivationThreshold_ = 0;

public:
AmendmentSet(
Expand All @@ -128,23 +134,25 @@ class AmendmentSet
}
}

threshold_ = !rules_.enabled(fixAmendmentMajorityCalc)
activationThreshold_ = !rules_.enabled(fixAmendmentMajorityCalc)
? std::max(
1L,
static_cast<long>(
1,
static_cast<int>(
(trustedValidations_ *
preFixAmendmentMajorityCalcThreshold.num) /
preFixAmendmentMajorityCalcThreshold.den))
: std::max(
1L,
static_cast<long>(
1,
static_cast<int>(
(trustedValidations_ *
postFixAmendmentMajorityCalcThreshold.num) /
postFixAmendmentMajorityCalcThreshold.den));

deactivationThreshold_ = activationThreshold_;
}

bool
passes(uint256 const& amendment) const
passes(uint256 const& amendment, int threshold) const
{
auto const& it = votes_.find(amendment);

Expand All @@ -158,9 +166,9 @@ class AmendmentSet
// to gain majority.
if (!rules_.enabled(fixAmendmentMajorityCalc) ||
trustedValidations_ == 1)
return it->second >= threshold_;
return it->second >= threshold;

return it->second > threshold_;
return it->second > threshold;
}

int
Expand All @@ -181,9 +189,15 @@ class AmendmentSet
}

int
threshold() const
activationThreshold() const
{
return threshold_;
return activationThreshold_;
}

int
deactivationThreshold() const
{
return deactivationThreshold_;
}
};

Expand Down Expand Up @@ -619,8 +633,9 @@ AmendmentTableImpl::doVoting(
auto vote = std::make_unique<AmendmentSet>(rules, valSet);

JLOG(j_.debug()) << "Received " << vote->trustedValidations()
<< " trusted validations, threshold is: "
<< vote->threshold();
<< " trusted validations. Thresholds are: "
<< vote->activationThreshold() << " and "
<< vote->deactivationThreshold();

// Map of amendments to the action to be taken for each one. The action is
// the value of the flags in the pseudo-transaction
Expand All @@ -633,7 +648,8 @@ AmendmentTableImpl::doVoting(
{
NetClock::time_point majorityTime = {};

bool const hasValMajority = vote->passes(entry.first);
bool const hasValMajority =
vote->passes(entry.first, vote->activationThreshold());

{
auto const it = majorityAmendments.find(entry.first);
Expand All @@ -653,7 +669,9 @@ AmendmentTableImpl::doVoting(
JLOG(j_.debug()) << entry.first << ": amendment got majority";
actions[entry.first] = tfGotMajority;
}
else if (!hasValMajority && (majorityTime != NetClock::time_point{}))
else if (
!hasValMajority && (majorityTime != NetClock::time_point{}) &&
!vote->passes(entry.first, vote->deactivationThreshold()))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is right and should have the same behavior as the existing code, but someone else should triple-check the logic when the amendment being checked is unknown. Here's the logic: hasValMajority will be false; before this commit, we'd always enter this block. Now vote->passes will return false, and the ! will turn it to true putting us in this block, just like before.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the amendment is unknown, then how would majorityTime be non-zero? And if it is zero for unknown amendments, then how would this branch have been entered before the change?

{
// Ledger says majority, validators say no
JLOG(j_.debug()) << entry.first << ": amendment lost majority";
Expand Down Expand Up @@ -740,7 +758,7 @@ AmendmentTableImpl::injectJson(
if (!fs.enabled && lastVote_)
{
auto const votesTotal = lastVote_->trustedValidations();
auto const votesNeeded = lastVote_->threshold();
auto const votesNeeded = lastVote_->activationThreshold();
auto const votesFor = lastVote_->votes(id);

v[jss::count] = votesFor;
Expand Down
78 changes: 52 additions & 26 deletions src/ripple/app/tx/impl/Change.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ Change::activateTrustLinesToSelfFix()
TER
Change::applyAmendment()
{
uint256 amendment(ctx_.tx.getFieldH256(sfAmendment));
uint256 const amendment(ctx_.tx.getFieldH256(sfAmendment));

auto const k = keylet::amendments();

Expand All @@ -233,41 +233,67 @@ Change::applyAmendment()
if (gotMajority && lostMajority)
return temINVALID_FLAG;

STArray newMajorities(sfMajorities);
STArray majorities = amendmentObject->getFieldArray(sfMajorities);

bool found = false;
if (amendmentObject->isFieldPresent(sfMajorities))
auto it = std::find_if(
majorities.begin(), majorities.end(), [&amendment](STObject const& o) {
return o[sfAmendment] == amendment;
});

if (it == majorities.end() && lostMajority)
return tefALREADY;

if (it != majorities.end())
{
const STArray& oldMajorities =
amendmentObject->getFieldArray(sfMajorities);
for (auto const& majority : oldMajorities)
if (gotMajority)
return tefALREADY;

majorities.erase(it);
}
Comment on lines +238 to +252
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first time an amendment loses majority, the program will reach line 251, remove the amendment from the sfMajorities array, and continue, presumably to add it to the sfFlappingAmendments array. The second time an amendment loses majority, the program will reach line 244 and return, never continuing to fix the sfFlappingAmendments array. Line 251 needs to be conditional on whether the amendment is in the sfFlappingAmendments array (which itself is conditional on the amendment).

There does not seem to be a new test for this new behavior. Have you added one?


if (view().rules().enabled(fixAmendmentFlapping))
{
STVector256 flappingAmendments =
amendmentObject->getFieldV256(sfFlappingAmendments);

auto it = std::find(
flappingAmendments.begin(), flappingAmendments.end(), amendment);

// When amendment loses a majority and is not already on the list we
// add it and give it second chance.
if (lostMajority && it == flappingAmendments.end())
{
if (majority.getFieldH256(sfAmendment) == amendment)
{
if (gotMajority)
return tefALREADY;
found = true;
}
flappingAmendments.push_back(amendment);
amendmentObject->setFieldV256(
sfFlappingAmendments, flappingAmendments);
view().update(amendmentObject);
return tesSUCCESS;
}

// Remove the amendment now because the tracking is
// not needed any more.
if (it != flappingAmendments.end())
{
flappingAmendments.erase(it);

if (flappingAmendments.empty())
amendmentObject->makeFieldAbsent(sfFlappingAmendments);
else
{
// pass through
newMajorities.push_back(majority);
}
amendmentObject->setFieldV256(
sfFlappingAmendments, flappingAmendments);
}
}

if (!found && lostMajority)
return tefALREADY;

if (gotMajority)
{
// This amendment now has a majority
newMajorities.push_back(STObject(sfMajority));
auto& entry = newMajorities.back();
entry.emplace_back(STUInt256(sfAmendment, amendment));
entry.emplace_back(STUInt32(
STObject maj(sfMajority);
maj.emplace_back(STUInt256(sfAmendment, amendment));
maj.emplace_back(STUInt32(
sfCloseTime, view().parentCloseTime().time_since_epoch().count()));

majorities.push_back(std::move(maj));

if (!ctx_.app.getAmendmentTable().isSupported(amendment))
{
JLOG(j_.warn()) << "Unsupported amendment " << amendment
Expand All @@ -293,10 +319,10 @@ Change::applyAmendment()
}
}

if (newMajorities.empty())
if (majorities.empty())
amendmentObject->makeFieldAbsent(sfMajorities);
else
amendmentObject->setFieldArray(sfMajorities, newMajorities);
amendmentObject->setFieldArray(sfMajorities, majorities);

view().update(amendmentObject);

Expand Down
3 changes: 2 additions & 1 deletion src/ripple/protocol/Feature.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ namespace detail {
// Feature.cpp. Because it's only used to reserve storage, and determine how
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
// the actual number of amendments. A LogicError on startup will verify this.
static constexpr std::size_t numFeatures = 54;
static constexpr std::size_t numFeatures = 55;

/** Amendments that this server supports and the default voting behavior.
Whether they are enabled depends on the Rules defined in the validated
Expand Down Expand Up @@ -341,6 +341,7 @@ extern uint256 const fixTrustLinesToSelf;
extern uint256 const fixRemoveNFTokenAutoTrustLine;
extern uint256 const featureImmediateOfferKilled;
extern uint256 const featureDisallowIncoming;
extern uint256 const fixAmendmentFlapping;

} // namespace ripple

Expand Down
1 change: 1 addition & 0 deletions src/ripple/protocol/SField.h
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ extern SF_VECTOR256 const sfIndexes;
extern SF_VECTOR256 const sfHashes;
extern SF_VECTOR256 const sfAmendments;
extern SF_VECTOR256 const sfNFTokenOffers;
extern SF_VECTOR256 const sfFlappingAmendments;

// inner object
// OBJECT/1 is reserved for end of object
Expand Down
1 change: 1 addition & 0 deletions src/ripple/protocol/impl/Feature.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,7 @@ REGISTER_FIX (fixTrustLinesToSelf, Supported::yes, DefaultVote::no)
REGISTER_FIX (fixRemoveNFTokenAutoTrustLine, Supported::yes, DefaultVote::yes);
REGISTER_FEATURE(ImmediateOfferKilled, Supported::yes, DefaultVote::no);
REGISTER_FEATURE(DisallowIncoming, Supported::yes, DefaultVote::no);
REGISTER_FIX (fixAmendmentFlapping, Supported::yes, DefaultVote::yes);

// The following amendments have been active for at least two years. Their
// pre-amendment code has been removed and the identifiers are deprecated.
Expand Down
1 change: 1 addition & 0 deletions src/ripple/protocol/impl/LedgerFormats.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ LedgerFormats::LedgerFormats()
{
{sfAmendments, soeOPTIONAL}, // Enabled
{sfMajorities, soeOPTIONAL},
{sfFlappingAmendments, soeOPTIONAL},
},
commonFields);

Expand Down
1 change: 1 addition & 0 deletions src/ripple/protocol/impl/SField.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ CONSTRUCT_TYPED_SFIELD(sfIndexes, "Indexes", VECTOR25
CONSTRUCT_TYPED_SFIELD(sfHashes, "Hashes", VECTOR256, 2);
CONSTRUCT_TYPED_SFIELD(sfAmendments, "Amendments", VECTOR256, 3);
CONSTRUCT_TYPED_SFIELD(sfNFTokenOffers, "NFTokenOffers", VECTOR256, 4);
CONSTRUCT_TYPED_SFIELD(sfFlappingAmendments, "FlappingAmendments", VECTOR256, 5);

// path set
CONSTRUCT_UNTYPED_SFIELD(sfPaths, "Paths", PATHSET, 1);
Expand Down
Loading