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

Refactor Feature name management and creation: (Improve amendment management and default votes) #3877

Closed
wants to merge 1 commit into from

Conversation

ximinez
Copy link
Collaborator

@ximinez ximinez commented Jul 1, 2021

High Level Overview of Change

This is a follow-up / second attempt to #3458 at changing default voting amendment behavior. It's more comprehensive, and simplifies the process quite a bit, but it is not intended to be the definitive answer to the question of how to manage amendment voting on the network.

It is organized into three commits. Each of the commits should build, but I don't guarantee that they'll pass unit tests. I think it makes more sense to review the changes as a single unit, but they're split up in case there are questions of how something evolved, or if the reviewers absolutely hate some subset of the changes.

  • The first contains the changes from the older PR (accounting for code drift). Adds yet another list of amendments that should not be voted for by default.
  • The second refactors "feature" creation code to centralize the naming, specifying whether it's supported, and providing the default voting behavior in one place.
  • The third reorganizes the feature access functions so that supported amendments are always provided with their default votes.

As stated above, the scope of this change is intentionally relatively small. It does not address some of the advanced and controversial questions of amendment voting behavior including, but not limited to:

  • Vote proxies
  • Abstaining from all voting
  • Requiring all amendments to be voted on before starting
  • Reducing the voting period
  • Adding a "no" voting option. (Current options are "yes" and "abstain".)

Context of Change

There are two significant problems related to amendment management.

  1. All validators will vote for any amendment that is supported (added to the supportedAmendments list) by default. This makes it impossible to have multiple versions of the software support a feature without voting for it, without manual intervention from validator operators.
  2. It's annoying and error-prone to create a new amendment. The name currently has to be copied in at least three different places.

These changes resolve those issues (and only those issues) by

  1. Moving the creation of amendments to one place which specifies the name, whether it's supported, and whether rippled will vote for it by default.
  2. Improving instructions and adding runtime validity checks. Developers not familiar with the process should have a much easier time creating and managing the settings for amendments.
  3. Allowing an amendment to be supported without being voted for by default by all upgraded validators. This will allow completed features to be released and supported for one or more release cycles before they are voted on by default. Operators will still have full control over voting for or against amendments on their own nodes. If UNL validators wait for more than one release before voting for an amendment (either explicitly or by default), then the "amendment blocked" gating can be significantly reduced.

Type of Change

  • [X ] New feature (non-breaking change which adds functionality)
  • [X ] Refactor (non-breaking change that only restructures code)
  • [X ] Tests (You added tests for code that already exists, or your new feature included in this PR)

Before / After

Before

To create an amendment, the name has to be specified (as a string) in two places:

  • In Feature.h, the featureNames list
  • In Feature.cpp a call to getRegisteredFeature to define and initialize a unit256 variable.

Additionally, a variable need to be declared (at least if you want to use it anywhere else in the code):

  • In Feature.h an extern uint256 const variable declaration

Later, when a feature is development complete, and ready to support, the name also needs to be added (as a string) to

  • In Feature.cpp, the supportedAmendments() list

There is no way to add an amendment to supportedAmendments() and not vote for it without operator intervention.

After

To create an amendment, the name has to be specified in one place:

  • In Feature.cpp a call to registerFeature to define and initialize a unit256 variable.

Additionally, the numFeatures counter must be incremented. However, if this step is skipped, an assert on startup will catch it and remind the developer (reducing potential errors).

The variable still needs to be declared:

  • In Feature.h an extern uint256 const variable declaration

Later, when a feature is development complete, and ready to support, the existing call to registerFeature only needs to have one of it's parameters modified. Another parameter to registerFeature specifies the default voting behavior for supported amendments: yes or abstain.

Test Plan

The "CryptoConditionsSuite" is listed as supported, and has not yet been enabled on the Mainnet, but is "not ready for prime time". However, it cannot be removed, because it could be enabled at any time, and removing it would cause all future versions to be amendment blocked, and cause other problems. Any current node that has not explicitly vetoed it will vote for it. These votes can be captured and verified on any network (main or test) that hasn't enabled it yet.

The updated registration for this amendment specifies it as Supported::yes, DefaultVote::abstain, which means it's still supported, but will not be voted for without explicit operator action. Votes captured from updated nodes on any network where it's not enabled will show that amendment not being voted for.

Copy link
Collaborator

@gregtatcam gregtatcam left a comment

Choose a reason for hiding this comment

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

There is a compile warning:
src/ripple/protocol/impl/Feature.cpp:232:21: warning: unused variable ‘feature’ [-Wunused-variable] 232 | auto const& feature = features.emplace_back(name, f);
and a compile error:
src/ripple/protocol/impl/Feature.cpp:129:53: required from here boost_1_75_0/boost/container_hash/extensions.hpp:305:30: error: no matching function for call to ‘hash_value(const ripple::base_uint<256>&)’ 305 | return hash_value(val);

@ximinez
Copy link
Collaborator Author

ximinez commented Jul 20, 2021

There is a compile warning:
src/ripple/protocol/impl/Feature.cpp:232:21: warning: unused variable ‘feature’ [-Wunused-variable] 232 | auto const& feature = features.emplace_back(name, f);
and a compile error:
src/ripple/protocol/impl/Feature.cpp:129:53: required from here boost_1_75_0/boost/container_hash/extensions.hpp:305:30: error: no matching function for call to ‘hash_value(const ripple::base_uint<256>&)’ 305 | return hash_value(val);

Fixed. Thanks for catching these. Ironically, I actually ran across the same error in VS, and fixed it there, which is why I only had to move code. I didn't follow up with the other builds.

Copy link
Collaborator

@gregtatcam gregtatcam left a comment

Choose a reason for hiding this comment

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

👍 Nice improvements!

@scottschurr
Copy link
Collaborator

I'm still in the middle of this review. However I have some significant changes to AmendmentTable I'd like to suggest. I thought I'd go ahead and put these out there so you can think about them.

The biggest change has to do with the way the Wallet db handles vetoed. Firstly, despite what our API says, there are no vetoes. There are only up-votes and down-votes. No one gets a veto.

Secondly, vetoed is stored as 1 and not-vetoed as 0. This means every time I run across this I have to stand on my head. Am I voting in favor of this or not?

We can't change what is stored in the database, but we can change how we talk about what is stored in the database.

So I suggest we add an AmendmentVote enum to RelationalDBInterface_global.h. We can replace the integer values that are going in and out of the database with this enum, at least at the interface. I tried this and I felt like it really helped the readability of the AmendmentTable code. Of course one person's repast is another person's poison. You may not like it. But I thought I'd bring it up for discussion.

A smaller point is the getName and getName2 generic lambdas in AmendmentTableImpl::AmendmentTableImpl. I found the pair of generic lambdas really hard to figure out. I had to do some experimenting with the code to understand the (simple) thing that is going on. I suggest reducing that to a single non-generic lambda. That was a significant readability improvement for me.

I tried out both of these changes. If you'd like to look at the results, here's what I got: scottschurr@bd282fd I'm not insisting that these are good changes. But I hope you'll look them over and see what you think. Thanks.

Copy link
Collaborator

@scottschurr scottschurr left a comment

Choose a reason for hiding this comment

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

Very nice changes! The unit test coverage looks pretty good. And I like that we're getting the first use of boost::multi_index into the code base.

I made a few suggestions for changes along the way. All of these suggestions are open for discussion. But I'm going to hold off approving this pull request until we've had a chance to talk through the suggestions.

supportedAmendments.append(saHashes);
auto const supportedAmendments = []() {
auto const& amendments = detail::supportedAmendments();
std::vector<AmendmentTable::FeatureInfo> hashes;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Calling this vector hashes is now misleading. FeatureInfo has more than just hashes. Perhaps supported would be a better name?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Cleaned up the names in this entire block

src/ripple/app/main/Application.cpp Outdated Show resolved Hide resolved
src/ripple/app/misc/impl/AmendmentTable.cpp Show resolved Hide resolved
src/ripple/app/misc/impl/AmendmentTable.cpp Show resolved Hide resolved
Comment on lines 335 to 342
auto getName2 = [](auto const& amendment, auto const& name) {
if (name.empty())
return featureToName(amendment);
return name;
};
auto getName = [&getName2](auto const& amd) {
return getName2(amd.first, amd.second);
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

While this two-generic-lambda approach works, I find the result very difficult to read using just a text view (no IDE). I think you can get nearly the same functionality with a lot more readability

  1. The generic lambdas do not need to be generic. They are always called with the same arguments. Using auto for the arguments undoubtably makes them easier to type. But, for my eyes, the auto hides the types that the lambda expects. This hinders readability and makes it more difficult to modify the code in the future.
  2. I feel like the getName lambda does not really provide a lot of functionality. It's just taking a pair<some,thing> and pulling it apart. That can be done almost as easily at the call site.

So I suggest having one lambda that looks like this:

    auto getName = [](uint256 const& amendment,
                       std::string const& name) -> std::string {
        if (name.empty())
            return featureToName(amendment);
        return name;
    };

Then call it with the appropriate arguments. One example might look like this:

            persistVote(a.first, getName(a.first, a.second), false);  // upvote

I'm not gonna insist on this change. But I was unable to "just read" the code as it stands. I had to poke at it and compile a few changes in; I had to experiment to uncover the intent.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed by pulling in your experimental commit.

Feature const* feature = getByName(name);
if (feature)
return feature->feature;
return std::nullopt;
Copy link
Collaborator

Choose a reason for hiding this comment

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

This line is currently not exercised by the unit tests. I wonder if there's an easy way to exercise it...

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, easily.

@@ -36,6 +37,19 @@ namespace ripple {
class AmendmentTable
{
public:
struct FeatureInfo
{
FeatureInfo() = delete;
Copy link
Collaborator

Choose a reason for hiding this comment

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

FWIW, deleting the default constructor is a noop. C++ won't automatically make a default constructor for you if you provide any kind of a non-default constructor (see https://en.cppreference.com/w/cpp/language/default_constructor). But I'm fine if you leave the line in the code since it documents your intention.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, I wanted to be explicit.

src/test/jtx/Env_test.cpp Show resolved Hide resolved
Comment on lines 351 to 359
BEAST_EXPECTS(
table->isEnabled(supportedID) ==
(allEnabled.find(supportedID) != allEnabled.end()));
(allEnabled.find(supportedID) != allEnabled.end()),
a +
(table->isEnabled(supportedID) ? " enabled "
: " disabled ") +
(allEnabled.find(supportedID) != allEnabled.end()
? " found"
: " not found"));
Copy link
Collaborator

Choose a reason for hiding this comment

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

I found reading this chunk of source code to be a bit of a trial, particularly the part where text formatting is happening. I'm sure adding the string was a really good thing for understanding failing cases. And I know we're working within the constraints of clang-format. Still, is it possible to make this code easier to read? Thanks.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah. This cleaned up nicely.

#include <cstring>

namespace ripple {

//------------------------------------------------------------------------------
enum class Supported : bool { no = false, yes };
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm a little concerned about declaring Supported at namespace ripple scope. There are a huge number of things in the code base that are supported. In this case we're only talking about whether an Amendment or Feature is supported. Yeah, this declaration is only visible to humans in this file. But the compiler leaks the name ripple::Supported out to the entire application.

Consider renaming this to AmendmentSupported, which I'll admit is a little unwieldy. Another option would be to declare it as a member of the Feature class and use the class to limit the scope. Or maybe you have a yet better idea.

I won't insist, I'm just a bit concerned.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Wouldn't it only leak the name to parts of the application in the same TU?
I put it in the detail namespace. I can't say I'm entirely happy with the way the variable initialization looks now, but it works.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm pretty sure the name leaks into the entire executable through the linker.

What you could do is put the declaration in an unnamed namespace like this:

namespace
{
    enum class Supported : bool { no = false, yes };
}

According to cppreference, for an unnamed name space...

Its members have potential scope from their point of declaration to the end of the translation unit, and have internal linkage.

https://en.cppreference.com/w/cpp/language/namespace

I think an unnamed namespace would provide the smallest possible scope.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done.

@ximinez ximinez force-pushed the internalveto2 branch 2 times, most recently from 96daa54 to bfaf343 Compare July 29, 2021 01:54
@ximinez
Copy link
Collaborator Author

ximinez commented Jul 29, 2021

Rebased on to 1.8.0-b4. Added new amendments, but did not make any other code changes.

Copy link
Collaborator

@intelliot intelliot left a comment

Choose a reason for hiding this comment

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

LGTM

@scottschurr
Copy link
Collaborator

@ximinez, I left some comments over a month ago. Do you have time to go through those comments? Once that's done then maybe we can mark this as passed...

@manojsdoshi manojsdoshi added the Passed Passed code review & PR owner thinks it's ready to merge. Perf sign-off may still be required. label Sep 8, 2021
@ximinez
Copy link
Collaborator Author

ximinez commented Sep 8, 2021

@scottschurr Soon:tm:... I haven't forgotten, but I've had other things going on that pushed this to the backburner.

@ximinez ximinez force-pushed the internalveto2 branch 2 times, most recently from 76aecff to b35da48 Compare September 24, 2021 19:25
@ximinez
Copy link
Collaborator Author

ximinez commented Sep 27, 2021

@scottschurr I think that all your questions and issues have now been resolved. CC: @intelliot @gregtatcam

Copy link
Collaborator

@scottschurr scottschurr left a comment

Choose a reason for hiding this comment

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

Looks great! 👍 Thanks for your patience with my review.

* Only require adding the new feature names in one place. (Also need to
  increment a counter, but a check on startup will catch that.)
* Allows rippled to have the code to support a given amendment, but
  not vote for it by default. This allows the amendment to be enabled in
  a future version without necessarily amendment blocking these older
  versions.
* The default vote is carried with the amendment name in the list of
  supported amendments.
* The amendment table is constructed with the amendment and default
  vote.
@manojsdoshi
Copy link
Contributor

"Merged as part of #3948"

@manojsdoshi manojsdoshi closed this Oct 7, 2021
@intelliot
Copy link
Collaborator

Note: Released in version 1.8.0

intelliot referenced this pull request Sep 25, 2024
* Only require adding the new feature names in one place. (Also need to
  increment a counter, but a check on startup will catch that.)
* Allows rippled to have the code to support a given amendment, but
  not vote for it by default. This allows the amendment to be enabled in
  a future version without necessarily amendment blocking these older
  versions.
* The default vote is carried with the amendment name in the list of
  supported amendments.
* The amendment table is constructed with the amendment and default
  vote.
@intelliot intelliot changed the title Improve amendment management and default votes Refactor Feature name management and creation: (Improve amendment management and default votes) Sep 25, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Amendment Passed Passed code review & PR owner thinks it's ready to merge. Perf sign-off may still be required. Tech Debt Non-urgent improvements
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants