From d6ca06e8556886f703a366953cd59de8ec3f6638 Mon Sep 17 00:00:00 2001 From: JCW Date: Tue, 2 Jun 2026 16:08:39 +0100 Subject: [PATCH] Migrate the amendment table test --- .../ledger}/AmendmentTable.cpp | 0 src/test/app/AmendmentTable_test.cpp | 1228 ----------------- src/tests/libxrpl/CMakeLists.txt | 4 + .../libxrpl/helpers/TestServiceRegistry.h | 18 +- src/tests/libxrpl/ledger/AmendmentTable.cpp | 976 +++++++++++++ src/tests/libxrpl/ledger/main.cpp | 8 + src/xrpld/app/misc/AmendmentTableImpl.h | 18 - 7 files changed, 1005 insertions(+), 1247 deletions(-) rename src/{xrpld/app/misc/detail => libxrpl/ledger}/AmendmentTable.cpp (100%) delete mode 100644 src/test/app/AmendmentTable_test.cpp create mode 100644 src/tests/libxrpl/ledger/AmendmentTable.cpp create mode 100644 src/tests/libxrpl/ledger/main.cpp delete mode 100644 src/xrpld/app/misc/AmendmentTableImpl.h diff --git a/src/xrpld/app/misc/detail/AmendmentTable.cpp b/src/libxrpl/ledger/AmendmentTable.cpp similarity index 100% rename from src/xrpld/app/misc/detail/AmendmentTable.cpp rename to src/libxrpl/ledger/AmendmentTable.cpp diff --git a/src/test/app/AmendmentTable_test.cpp b/src/test/app/AmendmentTable_test.cpp deleted file mode 100644 index 219aaabdda..0000000000 --- a/src/test/app/AmendmentTable_test.cpp +++ /dev/null @@ -1,1228 +0,0 @@ -#include -#include -#include - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace xrpl { - -class AmendmentTable_test final : public beast::unit_test::Suite -{ -private: - static uint256 - amendmentId(std::string in) - { - sha256_hasher h; - using beast::hash_append; - hash_append(h, in); - auto const d = static_cast(h); - uint256 result; - std::memcpy(result.data(), d.data(), d.size()); - return result; - } - - static Section - makeSection(std::string const& name, std::vector const& amendments) - { - Section section(name); - for (auto const& a : amendments) - section.append(to_string(amendmentId(a)) + " " + a); - return section; - } - - static Section - makeSection(std::vector const& amendments) - { - return makeSection("Test", amendments); - } - - static Section - makeSection(uint256 const& amendment) - { - Section section("Test"); - section.append(to_string(amendment) + " " + to_string(amendment)); - return section; - } - - std::unique_ptr - makeConfig() - { - auto cfg = test::jtx::envconfig(); - cfg->section(SECTION_AMENDMENTS) = makeSection(SECTION_AMENDMENTS, enabled_); - cfg->section(SECTION_VETO_AMENDMENTS) = makeSection(SECTION_VETO_AMENDMENTS, vetoed_); - return cfg; - } - - static std::vector - makeFeatureInfo(std::vector const& amendments, VoteBehavior voteBehavior) - { - std::vector result; - result.reserve(amendments.size()); - for (auto const& a : amendments) - { - result.emplace_back(a, amendmentId(a), voteBehavior); - } - return result; - } - - static std::vector - makeDefaultYes(std::vector const& amendments) - { - return makeFeatureInfo(amendments, VoteBehavior::DefaultYes); - } - - static std::vector - makeDefaultYes(uint256 const amendment) - { - std::vector result{ - {to_string(amendment), amendment, VoteBehavior::DefaultYes}}; - return result; - } - - static std::vector - makeDefaultNo(std::vector const& amendments) - { - return makeFeatureInfo(amendments, VoteBehavior::DefaultNo); - } - - static std::vector - makeObsolete(std::vector const& amendments) - { - return makeFeatureInfo(amendments, VoteBehavior::Obsolete); - } - - template - static size_t - totalsize(std::vector const& src, Args const&... args) - { - if constexpr (sizeof...(args) > 0) - return src.size() + totalsize(args...); - return src.size(); - } - - template - static void - combineArg(std::vector& dest, std::vector const& src, Args const&... args) - { - assert(dest.capacity() >= dest.size() + src.size()); - std::copy(src.begin(), src.end(), std::back_inserter(dest)); - if constexpr (sizeof...(args) > 0) - combineArg(dest, args...); - } - - template - static std::vector - combine( - // Pass "left" by value. The values will need to be copied one way or - // another, so just reuse it. - std::vector left, - std::vector const& right, - Args const&... args) - { - left.reserve(totalsize(left, right, args...)); - - combineArg(left, right, args...); - - return left; - } - - // All useful amendments are supported amendments. - // Enabled amendments are typically a subset of supported amendments. - // Vetoed amendments should be supported but not enabled. - // Unsupported amendments may be added to the AmendmentTable. - std::vector const yes_{"g", "i", "k", "m", "o", "q", "r", "s", "t", "u"}; - std::vector const enabled_{"b", "d", "f", "h", "j", "l", "n", "p"}; - std::vector const vetoed_{"a", "c", "e"}; - std::vector const obsolete_{"0", "1", "2"}; - std::vector const allSupported_{combine(yes_, enabled_, vetoed_, obsolete_)}; - std::vector const unsupported_{"v", "w", "x"}; - std::vector const unsupportedMajority_{"y", "z"}; - - Section const emptySection_; - std::vector const emptyYes_; - - test::SuiteJournal journal_; - -public: - AmendmentTable_test() : journal_("AmendmentTable_test", *this) - { - } - - std::unique_ptr - makeTable( - Application& app, - std::chrono::seconds majorityTime, - std::vector const& supported, - Section const& enabled, - Section const& vetoed) - { - return makeAmendmentTable(app, majorityTime, supported, enabled, vetoed, journal_); - } - - std::unique_ptr - makeTable( - test::jtx::Env& env, - std::chrono::seconds majorityTime, - std::vector const& supported, - Section const& enabled, - Section const& vetoed) - { - return makeTable(env.app(), majorityTime, supported, enabled, vetoed); - } - - std::unique_ptr - makeTable(test::jtx::Env& env, std::chrono::seconds majorityTime) - { - static std::vector const kSupported = combine( - makeDefaultYes(yes_), - // Use non-intuitive default votes for "enabled_" and "vetoed_" - // so that when the tests later explicitly enable or veto them, - // we can be certain that they are not simply going by their - // default vote setting. - makeDefaultNo(enabled_), - makeDefaultYes(vetoed_), - makeObsolete(obsolete_)); - return makeTable( - env.app(), majorityTime, kSupported, makeSection(enabled_), makeSection(vetoed_)); - } - - void - testConstruct() - { - testcase("Construction"); - test::jtx::Env env{*this, makeConfig()}; - auto table = makeTable(env, weeks(1)); - - for (auto const& a : allSupported_) - BEAST_EXPECT(table->isSupported(amendmentId(a))); - - for (auto const& a : yes_) - BEAST_EXPECT(table->isSupported(amendmentId(a))); - - for (auto const& a : enabled_) - BEAST_EXPECT(table->isSupported(amendmentId(a))); - - for (auto const& a : vetoed_) - { - BEAST_EXPECT(table->isSupported(amendmentId(a))); - BEAST_EXPECT(!table->isEnabled(amendmentId(a))); - } - - for (auto const& a : obsolete_) - { - BEAST_EXPECT(table->isSupported(amendmentId(a))); - BEAST_EXPECT(!table->isEnabled(amendmentId(a))); - } - } - - void - testGet() - { - testcase("Name to ID mapping"); - - test::jtx::Env env{*this, makeConfig()}; - auto table = makeTable(env, weeks(1)); - - for (auto const& a : yes_) - BEAST_EXPECT(table->find(a) == amendmentId(a)); - for (auto const& a : enabled_) - BEAST_EXPECT(table->find(a) == amendmentId(a)); - for (auto const& a : vetoed_) - BEAST_EXPECT(table->find(a) == amendmentId(a)); - for (auto const& a : obsolete_) - BEAST_EXPECT(table->find(a) == amendmentId(a)); - for (auto const& a : unsupported_) - BEAST_EXPECT(!table->find(a)); - for (auto const& a : unsupportedMajority_) - BEAST_EXPECT(!table->find(a)); - - // Vetoing an unsupported amendment should add the amendment to table. - // Verify that unsupportedID is not in table. - uint256 const unsupportedID = amendmentId(unsupported_[0]); - { - json::Value const unsupp = - table->getJson(unsupportedID, true)[to_string(unsupportedID)]; - BEAST_EXPECT(unsupp.size() == 0); - } - - // After vetoing unsupportedID verify that it is in table. - table->veto(unsupportedID); - { - json::Value const unsupp = - table->getJson(unsupportedID, true)[to_string(unsupportedID)]; - BEAST_EXPECT(unsupp[jss::vetoed].asBool()); - } - } - - void - testBadConfig() - { - auto const yesVotes = makeDefaultYes(yes_); - auto const section = makeSection(vetoed_); - auto const id = to_string(amendmentId(enabled_[0])); - - testcase("Bad Config"); - - { // Two arguments are required - we pass one - Section test = section; - test.append(id); - - try - { - test::jtx::Env env{*this, makeConfig()}; - if (makeTable(env, weeks(2), yesVotes, test, emptySection_)) - fail("Accepted only amendment ID"); - } - catch (std::exception const& e) - { - BEAST_EXPECT(e.what() == "Invalid entry '" + id + "' in [Test]"); - } - } - - { // Two arguments are required - we pass three - Section test = section; - test.append(id + " Test Name"); - - try - { - test::jtx::Env env{*this, makeConfig()}; - if (makeTable(env, weeks(2), yesVotes, test, emptySection_)) - fail("Accepted extra arguments"); - } - catch (std::exception const& e) - { - BEAST_EXPECT(e.what() == "Invalid entry '" + id + " Test Name' in [Test]"); - } - } - - { - auto sid = id; - sid.resize(sid.length() - 1); - - Section test = section; - test.append(sid + " Name"); - - try - { - test::jtx::Env env{*this, makeConfig()}; - if (makeTable(env, weeks(2), yesVotes, test, emptySection_)) - fail("Accepted short amendment ID"); - } - catch (std::exception const& e) - { - BEAST_EXPECT(e.what() == "Invalid entry '" + sid + " Name' in [Test]"); - } - } - - { - auto sid = id; - sid.resize(sid.length() + 1, '0'); - - Section test = section; - test.append(sid + " Name"); - - try - { - test::jtx::Env env{*this, makeConfig()}; - if (makeTable(env, weeks(2), yesVotes, test, emptySection_)) - fail("Accepted long amendment ID"); - } - catch (std::exception const& e) - { - BEAST_EXPECT(e.what() == "Invalid entry '" + sid + " Name' in [Test]"); - } - } - - { - auto sid = id; - sid.resize(sid.length() - 1); - sid.push_back('Q'); - - Section test = section; - test.append(sid + " Name"); - - try - { - test::jtx::Env env{*this, makeConfig()}; - if (makeTable(env, weeks(2), yesVotes, test, emptySection_)) - fail("Accepted non-hex amendment ID"); - } - catch (std::exception const& e) - { - BEAST_EXPECT(e.what() == "Invalid entry '" + sid + " Name' in [Test]"); - } - } - } - - void - testEnableVeto() - { - testcase("enable and veto"); - - test::jtx::Env env{*this, makeConfig()}; - std::unique_ptr table = makeTable(env, weeks(2)); - - // Note which entries are enabled (convert the amendment names to IDs) - std::set allEnabled; - for (auto const& a : enabled_) - allEnabled.insert(amendmentId(a)); - - for (uint256 const& a : allEnabled) - BEAST_EXPECT(table->enable(a)); - - // So far all enabled amendments are supported. - BEAST_EXPECT(!table->hasUnsupportedEnabled()); - - // Verify all enables are enabled and nothing else. - for (std::string const& a : yes_) - { - uint256 const supportedID = amendmentId(a); - bool const enabled = table->isEnabled(supportedID); - bool const found = allEnabled.contains(supportedID); - BEAST_EXPECTS( - enabled == found, - a + (enabled ? " enabled " : " disabled ") + (found ? " found" : " not found")); - } - - // All supported and unVetoed amendments should be returned as desired. - { - std::set vetoed; - for (std::string const& a : vetoed_) - vetoed.insert(amendmentId(a)); - - std::vector const desired = table->getDesired(); - for (uint256 const& a : desired) - BEAST_EXPECT(not vetoed.contains(a)); - - // Unveto an amendment that is already not vetoed. Shouldn't - // hurt anything, but the values returned by getDesired() - // shouldn't change. - BEAST_EXPECT(!table->unVeto(amendmentId(yes_[1]))); - BEAST_EXPECT(desired == table->getDesired()); - } - - // UnVeto one of the vetoed amendments. It should now be desired. - { - uint256 const unvetoedID = amendmentId(vetoed_[0]); - BEAST_EXPECT(table->unVeto(unvetoedID)); - - std::vector const desired = table->getDesired(); - BEAST_EXPECT(std::ranges::find(desired, unvetoedID) != desired.end()); - } - - // Veto all supported amendments. Now desired should be empty. - for (std::string const& a : allSupported_) - { - table->veto(amendmentId(a)); - } - BEAST_EXPECT(table->getDesired().empty()); - - // Enable an unsupported amendment. - { - BEAST_EXPECT(!table->hasUnsupportedEnabled()); - table->enable(amendmentId(unsupported_[0])); - BEAST_EXPECT(table->hasUnsupportedEnabled()); - } - } - - // Make a list of trusted validators. - // Register the validators with AmendmentTable and return the list. - static std::vector> - makeValidators(int num, std::unique_ptr const& table) - { - std::vector> ret; - ret.reserve(num); - hash_set trustedValidators; - trustedValidators.reserve(num); - for (int i = 0; i < num; ++i) - { - auto const& back = ret.emplace_back(randomKeyPair(KeyType::Secp256k1)); - trustedValidators.insert(back.first); - } - table->trustChanged(trustedValidators); - return ret; - } - - static NetClock::time_point - hourTime(std::chrono::hours h) - { - return NetClock::time_point{h}; - } - - // Execute a pretend consensus round for a flag ledger - static void - doRound( - Rules const& rules, - AmendmentTable& table, - std::chrono::hours hour, - std::vector> const& validators, - std::vector> const& votes, - std::vector& ourVotes, - std::set& enabled, - majorityAmendments_t& majority) - { - // Do a round at the specified time - // Returns the amendments we voted for - - // Parameters: - // table: Our table of known and vetoed amendments - // validators: The addresses of validators we trust - // votes: Amendments and the number of validators who vote for them - // ourVotes: The amendments we vote for in our validation - // enabled: In/out enabled amendments - // majority: In/our majority amendments (and when they got a majority) - - auto const roundTime = hourTime(hour); - - // Build validations - std::vector> validations; - validations.reserve(validators.size()); - - int i = 0; - for (auto const& [pub, sec] : validators) - { - ++i; - std::vector field; - - for (auto const& [hash, nVotes] : votes) - { - if (nVotes >= i) - { - // We vote yes on this amendment - field.push_back(hash); - } - } - - auto v = std::make_shared( - xrpl::NetClock::time_point{}, pub, sec, calcNodeID(pub), [&field](STValidation& v) { - if (!field.empty()) - v.setFieldV256(sfAmendments, STVector256(sfAmendments, field)); - v.setFieldU32(sfLedgerSequence, 6180339); - }); - - validations.emplace_back(v); - } - - ourVotes = table.doValidation(enabled); - - auto actions = table.doVoting(rules, roundTime, enabled, majority, validations); - for (auto const& [hash, action] : actions) - { - // This code assumes other validators do as we do - - switch (action) - { - case 0: - // amendment goes from majority to enabled - if (enabled.contains(hash)) - Throw("enabling already enabled"); - if (!majority.contains(hash)) - Throw("enabling without majority"); - enabled.insert(hash); - majority.erase(hash); - break; - - case tfGotMajority: - if (majority.contains(hash)) - Throw("got majority while having majority"); - majority[hash] = roundTime; - break; - - case tfLostMajority: - if (!majority.contains(hash)) - Throw("lost majority without majority"); - majority.erase(hash); - break; - - default: - Throw("unknown action"); - } - } - } - - // No vote on unknown amendment - void - testNoOnUnknown(FeatureBitset const& feat) - { - testcase("Vote NO on unknown"); - - auto const testAmendment = amendmentId("TestAmendment"); - - test::jtx::Env env{*this, feat}; - auto table = makeTable(env, weeks(2), emptyYes_, emptySection_, emptySection_); - - auto const validators = makeValidators(10, table); - - std::vector> votes; - std::vector ourVotes; - std::set enabled; - majorityAmendments_t majority; - - doRound( - env.current()->rules(), - *table, - weeks{1}, - validators, - votes, - ourVotes, - enabled, - majority); - BEAST_EXPECT(ourVotes.empty()); - BEAST_EXPECT(enabled.empty()); - BEAST_EXPECT(majority.empty()); - - uint256 const unsupportedID = amendmentId(unsupported_[0]); - { - json::Value const unsupp = - table->getJson(unsupportedID, false)[to_string(unsupportedID)]; - BEAST_EXPECT(unsupp.size() == 0); - } - - table->veto(unsupportedID); - { - json::Value const unsupp = - table->getJson(unsupportedID, false)[to_string(unsupportedID)]; - BEAST_EXPECT(!unsupp[jss::vetoed].asBool()); - } - - votes.emplace_back(testAmendment, validators.size()); - - votes.emplace_back(testAmendment, validators.size()); - - doRound( - env.current()->rules(), - *table, - weeks{2}, - validators, - votes, - ourVotes, - enabled, - majority); - BEAST_EXPECT(ourVotes.empty()); - BEAST_EXPECT(enabled.empty()); - - majority[testAmendment] = hourTime(weeks{1}); - - // Note that the simulation code assumes others behave as we do, - // so the amendment won't get enabled - doRound( - env.current()->rules(), - *table, - weeks{5}, - validators, - votes, - ourVotes, - enabled, - majority); - BEAST_EXPECT(ourVotes.empty()); - BEAST_EXPECT(enabled.empty()); - } - - // No vote on vetoed amendment - void - testNoOnVetoed(FeatureBitset const& feat) - { - testcase("Vote NO on vetoed"); - - auto const testAmendment = amendmentId("vetoedAmendment"); - - test::jtx::Env env{*this, feat}; - auto table = makeTable(env, weeks(2), emptyYes_, emptySection_, makeSection(testAmendment)); - - auto const validators = makeValidators(10, table); - - std::vector> votes; - std::vector ourVotes; - std::set enabled; - majorityAmendments_t majority; - - doRound( - env.current()->rules(), - *table, - weeks{1}, - validators, - votes, - ourVotes, - enabled, - majority); - BEAST_EXPECT(ourVotes.empty()); - BEAST_EXPECT(enabled.empty()); - BEAST_EXPECT(majority.empty()); - - votes.emplace_back(testAmendment, validators.size()); - - doRound( - env.current()->rules(), - *table, - weeks{2}, - validators, - votes, - ourVotes, - enabled, - majority); - BEAST_EXPECT(ourVotes.empty()); - BEAST_EXPECT(enabled.empty()); - - majority[testAmendment] = hourTime(weeks{1}); - - doRound( - env.current()->rules(), - *table, - weeks{5}, - validators, - votes, - ourVotes, - enabled, - majority); - BEAST_EXPECT(ourVotes.empty()); - BEAST_EXPECT(enabled.empty()); - } - - // Vote on and enable known, not-enabled amendment - void - testVoteEnable(FeatureBitset const& feat) - { - testcase("voteEnable"); - - test::jtx::Env env{*this, feat}; - auto table = makeTable(env, weeks(2), makeDefaultYes(yes_), emptySection_, emptySection_); - - auto const validators = makeValidators(10, table); - - std::vector> votes; - std::vector ourVotes; - std::set enabled; - majorityAmendments_t majority; - - // Week 1: We should vote for all known amendments not enabled - doRound( - env.current()->rules(), - *table, - weeks{1}, - validators, - votes, - ourVotes, - enabled, - majority); - BEAST_EXPECT(ourVotes.size() == yes_.size()); - BEAST_EXPECT(enabled.empty()); - for (auto const& i : yes_) - BEAST_EXPECT(not majority.contains(amendmentId(i))); - - // Now, everyone votes for this feature - for (auto const& i : yes_) - votes.emplace_back(amendmentId(i), validators.size()); - - // Week 2: We should recognize a majority - doRound( - env.current()->rules(), - *table, - weeks{2}, - validators, - votes, - ourVotes, - enabled, - majority); - BEAST_EXPECT(ourVotes.size() == yes_.size()); - BEAST_EXPECT(enabled.empty()); - - for (auto const& i : yes_) - BEAST_EXPECT(majority[amendmentId(i)] == hourTime(weeks{2})); - - // Week 5: We should enable the amendment - doRound( - env.current()->rules(), - *table, - weeks{5}, - validators, - votes, - ourVotes, - enabled, - majority); - BEAST_EXPECT(enabled.size() == yes_.size()); - - // Week 6: We should remove it from our votes and from having a majority - doRound( - env.current()->rules(), - *table, - weeks{6}, - validators, - votes, - ourVotes, - enabled, - majority); - BEAST_EXPECT(enabled.size() == yes_.size()); - BEAST_EXPECT(ourVotes.empty()); - for (auto const& i : yes_) - BEAST_EXPECT(not majority.contains(amendmentId(i))); - } - - // Detect majority at 80%, enable later - void - testDetectMajority(FeatureBitset const& feat) - { - testcase("detectMajority"); - - auto const testAmendment = amendmentId("detectMajority"); - test::jtx::Env env{*this, feat}; - auto table = - makeTable(env, weeks(2), makeDefaultYes(testAmendment), emptySection_, emptySection_); - - auto const validators = makeValidators(16, table); - - std::set enabled; - majorityAmendments_t majority; - - for (int i = 0; i <= 17; ++i) - { - std::vector> votes; - std::vector ourVotes; - - if ((i > 0) && (i < 17)) - votes.emplace_back(testAmendment, i); - - doRound( - env.current()->rules(), - *table, - weeks{i}, - validators, - votes, - ourVotes, - enabled, - majority); - - if (i < 13) // 13 => 13/16 = 0.8125 => > 80% - { - // We are voting yes, not enabled, no majority - BEAST_EXPECT(!ourVotes.empty()); - BEAST_EXPECT(enabled.empty()); - BEAST_EXPECT(majority.empty()); - } - else if (i < 15) - { - // We have a majority, not enabled, keep voting - BEAST_EXPECT(!ourVotes.empty()); - BEAST_EXPECT(!majority.empty()); - BEAST_EXPECT(enabled.empty()); - } - else if (i == 15) - { - // enable, keep voting, remove from majority - BEAST_EXPECT(!ourVotes.empty()); - BEAST_EXPECT(majority.empty()); - BEAST_EXPECT(!enabled.empty()); - } - else - { - // Done, we should be enabled and not voting - BEAST_EXPECT(ourVotes.empty()); - BEAST_EXPECT(majority.empty()); - BEAST_EXPECT(!enabled.empty()); - } - } - } - - // Detect loss of majority - void - testLostMajority(FeatureBitset const& feat) - { - testcase("lostMajority"); - - auto const testAmendment = amendmentId("lostMajority"); - - test::jtx::Env env{*this, feat}; - auto table = - makeTable(env, weeks(8), makeDefaultYes(testAmendment), emptySection_, emptySection_); - - auto const validators = makeValidators(16, table); - - std::set enabled; - majorityAmendments_t majority; - - { - // establish majority - std::vector> votes; - std::vector ourVotes; - - votes.emplace_back(testAmendment, validators.size()); - - doRound( - env.current()->rules(), - *table, - weeks{1}, - validators, - votes, - ourVotes, - enabled, - majority); - - BEAST_EXPECT(enabled.empty()); - BEAST_EXPECT(!majority.empty()); - } - - for (int i = 1; i < 8; ++i) - { - std::vector> votes; - std::vector ourVotes; - - // Gradually reduce support - votes.emplace_back(testAmendment, validators.size() - i); - - doRound( - env.current()->rules(), - *table, - weeks{i + 1}, - validators, - votes, - ourVotes, - enabled, - majority); - - if (i < 4) // 16 - 3 = 13 => 13/16 = 0.8125 => > 80% - { // 16 - 4 = 12 => 12/16 = 0.75 => < 80% - // We are voting yes, not enabled, majority - BEAST_EXPECT(!ourVotes.empty()); - BEAST_EXPECT(enabled.empty()); - BEAST_EXPECT(!majority.empty()); - } - else - { - // No majority, not enabled, keep voting - BEAST_EXPECT(!ourVotes.empty()); - BEAST_EXPECT(majority.empty()); - BEAST_EXPECT(enabled.empty()); - } - } - } - - // Exercise the UNL changing while voting is in progress. - void - testChangedUNL(FeatureBitset const& feat) - { - testcase("changedUNL"); - - auto const testAmendment = amendmentId("changedUNL"); - test::jtx::Env env{*this, feat}; - auto table = - makeTable(env, weeks(8), makeDefaultYes(testAmendment), emptySection_, emptySection_); - - std::vector> validators = makeValidators(10, table); - - std::set enabled; - majorityAmendments_t majority; - - { - // 10 validators with 2 voting against won't get majority. - std::vector> votes; - std::vector ourVotes; - - votes.emplace_back(testAmendment, validators.size() - 2); - - doRound( - env.current()->rules(), - *table, - weeks{1}, - validators, - votes, - ourVotes, - enabled, - majority); - - BEAST_EXPECT(enabled.empty()); - BEAST_EXPECT(majority.empty()); - } - - // Add one new validator to the UNL. - validators.emplace_back(randomKeyPair(KeyType::Secp256k1)); - - // A lambda that updates the AmendmentTable with the latest - // trusted validators. - auto callTrustChanged = [](std::vector> const& validators, - std::unique_ptr const& table) { - // We need a hash_set to pass to trustChanged. - hash_set trustedValidators; - trustedValidators.reserve(validators.size()); - std::ranges::for_each(validators, [&trustedValidators](auto const& val) { - trustedValidators.insert(val.first); - }); - - // Tell the AmendmentTable that the UNL changed. - table->trustChanged(trustedValidators); - }; - - // Tell the table that there's been a change in trusted validators. - callTrustChanged(validators, table); - - { - // 11 validators with 2 voting against gains majority. - std::vector> votes; - std::vector ourVotes; - - votes.emplace_back(testAmendment, validators.size() - 2); - - doRound( - env.current()->rules(), - *table, - weeks{2}, - validators, - votes, - ourVotes, - enabled, - majority); - - BEAST_EXPECT(enabled.empty()); - BEAST_EXPECT(!majority.empty()); - } - { - // One of the validators goes flaky and doesn't send validations - // (without the UNL changing) so the amendment loses majority. - std::pair const savedValidator = validators.front(); - validators.erase(validators.begin()); - - std::vector> votes; - std::vector ourVotes; - - votes.emplace_back(testAmendment, validators.size() - 2); - - doRound( - env.current()->rules(), - *table, - weeks{3}, - validators, - votes, - ourVotes, - enabled, - majority); - - BEAST_EXPECT(enabled.empty()); - BEAST_EXPECT(majority.empty()); - - // Simulate the validator re-syncing to the network by adding it - // back to the validators vector - validators.insert(validators.begin(), savedValidator); - - votes.front().second = validators.size() - 2; - - doRound( - env.current()->rules(), - *table, - weeks{4}, - validators, - votes, - ourVotes, - enabled, - majority); - - BEAST_EXPECT(enabled.empty()); - BEAST_EXPECT(!majority.empty()); - - // Finally, remove one validator from the UNL and see that majority - // is lost. - validators.erase(validators.begin()); - - // Tell the table that there's been a change in trusted validators. - callTrustChanged(validators, table); - - votes.front().second = validators.size() - 2; - - doRound( - env.current()->rules(), - *table, - weeks{5}, - validators, - votes, - ourVotes, - enabled, - majority); - - BEAST_EXPECT(enabled.empty()); - BEAST_EXPECT(majority.empty()); - } - } - - // Exercise a validator losing connectivity and then regaining it after - // extended delays. Depending on how long that delay is an amendment - // either will or will not go live. - void - testValidatorFlapping(FeatureBitset const& feat) - { - testcase("validatorFlapping"); - - // We run a test where a validator flaps on and off every 23 hours - // and another one one where it flaps on and off every 25 hours. - // - // Since the local validator vote record expires after 24 hours, - // with 23 hour flapping the amendment will go live. But with 25 - // hour flapping the amendment will not go live. - for (int const flapRateHours : {23, 25}) - { - test::jtx::Env env{*this, feat}; - auto const testAmendment = amendmentId("validatorFlapping"); - auto table = makeTable( - env, weeks(1), makeDefaultYes(testAmendment), emptySection_, emptySection_); - - // Make two lists of validators, one with a missing validator, to - // make it easy to simulate validator flapping. - auto const allValidators = makeValidators(11, table); - decltype(allValidators) - const mostValidators(allValidators.begin() + 1, allValidators.end()); - BEAST_EXPECT(allValidators.size() == mostValidators.size() + 1); - - std::set enabled; - majorityAmendments_t majority; - - std::vector> votes; - std::vector ourVotes; - - votes.emplace_back(testAmendment, allValidators.size() - 2); - - int delay = flapRateHours; - // Loop for 1 week plus a day. - for (int hour = 1; hour < (24 * 8); ++hour) - { - decltype(allValidators) const& thisHoursValidators = - (delay < flapRateHours) ? mostValidators : allValidators; - delay = delay == flapRateHours ? 0 : delay + 1; - - votes.front().second = thisHoursValidators.size() - 2; - - using namespace std::chrono; - doRound( - env.current()->rules(), - *table, - hours(hour), - thisHoursValidators, - votes, - ourVotes, - enabled, - majority); - - if (hour <= (24 * 7) || flapRateHours > 24) - { - // The amendment should not be enabled under any - // circumstance until one week has elapsed. - BEAST_EXPECT(enabled.empty()); - - // If flapping is less than 24 hours, there should be - // no flapping. Otherwise we should only have majority - // if allValidators vote -- which means there are no - // missing validators. - bool const expectMajority = - (delay <= 24) ? true : &thisHoursValidators == &allValidators; - BEAST_EXPECT(majority.empty() != expectMajority); - } - else - { - // We're... - // o Past one week, and - // o AmendmentFlapping was less than 24 hours. - // The amendment should be enabled. - BEAST_EXPECT(!enabled.empty()); - BEAST_EXPECT(majority.empty()); - } - } - } - } - - void - testHasUnsupported() - { - testcase("hasUnsupportedEnabled"); - - using namespace std::chrono_literals; - constexpr weeks kW(1); - test::jtx::Env env{*this, makeConfig()}; - auto table = makeTable(env, kW); - BEAST_EXPECT(!table->hasUnsupportedEnabled()); - BEAST_EXPECT(!table->firstUnsupportedExpected()); - BEAST_EXPECT(table->needValidatedLedger(1)); - - std::set enabled; - std::ranges::for_each( - unsupported_, [&enabled](auto const& s) { enabled.insert(amendmentId(s)); }); - - majorityAmendments_t majority; - table->doValidatedLedger(1, enabled, majority); - BEAST_EXPECT(table->hasUnsupportedEnabled()); - BEAST_EXPECT(!table->firstUnsupportedExpected()); - - NetClock::duration t{1000s}; - std::ranges::for_each(unsupportedMajority_, [&majority, &t](auto const& s) { - majority[amendmentId(s)] = NetClock::time_point{--t}; - }); - - table->doValidatedLedger(1, enabled, majority); - BEAST_EXPECT(table->hasUnsupportedEnabled()); - BEAST_EXPECT( - table->firstUnsupportedExpected() && - *table->firstUnsupportedExpected() == NetClock::time_point{t} + kW); - - // Make sure the table knows when it needs an update. - BEAST_EXPECT(!table->needValidatedLedger(256)); - BEAST_EXPECT(table->needValidatedLedger(257)); - } - - void - testFeature(FeatureBitset const& feat) - { - testNoOnUnknown(feat); - testNoOnVetoed(feat); - testVoteEnable(feat); - testDetectMajority(feat); - testLostMajority(feat); - testChangedUNL(feat); - testValidatorFlapping(feat); - } - - void - run() override - { - FeatureBitset const all{test::jtx::testableAmendments()}; - - testConstruct(); - testGet(); - testBadConfig(); - testEnableVeto(); - testHasUnsupported(); - testFeature(all); - } -}; - -BEAST_DEFINE_TESTSUITE(AmendmentTable, app, xrpl); - -} // namespace xrpl diff --git a/src/tests/libxrpl/CMakeLists.txt b/src/tests/libxrpl/CMakeLists.txt index ee07698519..531b679f9d 100644 --- a/src/tests/libxrpl/CMakeLists.txt +++ b/src/tests/libxrpl/CMakeLists.txt @@ -35,6 +35,10 @@ xrpl_add_test(json) target_link_libraries(xrpl.test.json PRIVATE xrpl.imports.test) add_dependencies(xrpl.tests xrpl.test.json) +xrpl_add_test(ledger) +target_link_libraries(xrpl.test.ledger PRIVATE xrpl.imports.test) +add_dependencies(xrpl.tests xrpl.test.ledger) + xrpl_add_test(tx) target_link_libraries(xrpl.test.tx PRIVATE xrpl.imports.test) add_dependencies(xrpl.tests xrpl.test.tx) diff --git a/src/tests/libxrpl/helpers/TestServiceRegistry.h b/src/tests/libxrpl/helpers/TestServiceRegistry.h index 0536176344..cc1c54c021 100644 --- a/src/tests/libxrpl/helpers/TestServiceRegistry.h +++ b/src/tests/libxrpl/helpers/TestServiceRegistry.h @@ -5,14 +5,19 @@ #include #include #include +#include #include +#include #include +#include #include #include #include +#include +#include #include #include @@ -81,6 +86,8 @@ class TestServiceRegistry : public ServiceRegistry logs_.journal("TaggedCache")}; PendingSaves pendingSaves_; std::optional trapTxID_; + mutable std::mutex walletDBMutex_; + mutable std::unique_ptr walletDB_; public: TestServiceRegistry() = default; @@ -358,10 +365,19 @@ public: return trapTxID_; } + /** Returns a lazily-created in-memory wallet DB suitable for tests. */ DatabaseCon& getWalletDB() override { - throw std::logic_error("TestServiceRegistry::getWalletDB() not implemented"); + std::scoped_lock lock(walletDBMutex_); + if (!walletDB_) + { + DatabaseCon::Setup setup; + setup.standAlone = true; + setup.startUp = StartUpType::Normal; + walletDB_ = makeWalletDB(setup, logs_.journal("WalletDB")); + } + return *walletDB_; } // Temporary: Get the underlying Application diff --git a/src/tests/libxrpl/ledger/AmendmentTable.cpp b/src/tests/libxrpl/ledger/AmendmentTable.cpp new file mode 100644 index 0000000000..d3a067f30a --- /dev/null +++ b/src/tests/libxrpl/ledger/AmendmentTable.cpp @@ -0,0 +1,976 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl::test { + +/** + * @brief Test fixture for the AmendmentTable. + * + * Provides a TestServiceRegistry (with an in-memory wallet DB) and a collection + * of helpers for building amendment sections, feature lists and validators. + */ +class AmendmentTableTest : public ::testing::Test +{ +protected: + static uint256 + amendmentId(std::string in) + { + sha256_hasher h; + using beast::hash_append; + hash_append(h, in); + auto const d = static_cast(h); + uint256 result; + std::memcpy(result.data(), d.data(), d.size()); + return result; + } + + static Section + makeSection(std::string const& name, std::vector const& amendments) + { + Section section(name); + for (auto const& a : amendments) + section.append(to_string(amendmentId(a)) + " " + a); + return section; + } + + static Section + makeSection(std::vector const& amendments) + { + return makeSection("Test", amendments); + } + + static Section + makeSection(uint256 const& amendment) + { + Section section("Test"); + section.append(to_string(amendment) + " " + to_string(amendment)); + return section; + } + + static std::vector + makeFeatureInfo(std::vector const& amendments, VoteBehavior voteBehavior) + { + std::vector result; + result.reserve(amendments.size()); + for (auto const& a : amendments) + { + result.emplace_back(a, amendmentId(a), voteBehavior); + } + return result; + } + + static std::vector + makeDefaultYes(std::vector const& amendments) + { + return makeFeatureInfo(amendments, VoteBehavior::DefaultYes); + } + + static std::vector + makeDefaultYes(uint256 const amendment) + { + std::vector result{ + {to_string(amendment), amendment, VoteBehavior::DefaultYes}}; + return result; + } + + static std::vector + makeDefaultNo(std::vector const& amendments) + { + return makeFeatureInfo(amendments, VoteBehavior::DefaultNo); + } + + static std::vector + makeObsolete(std::vector const& amendments) + { + return makeFeatureInfo(amendments, VoteBehavior::Obsolete); + } + + template + static std::size_t + totalsize(std::vector const& src, Args const&... args) + { + if constexpr (sizeof...(args) > 0) + return src.size() + totalsize(args...); + return src.size(); + } + + template + static void + combineArg(std::vector& dest, std::vector const& src, Args const&... args) + { + std::copy(src.begin(), src.end(), std::back_inserter(dest)); + if constexpr (sizeof...(args) > 0) + combineArg(dest, args...); + } + + template + static std::vector + combine(std::vector left, std::vector const& right, Args const&... args) + { + left.reserve(totalsize(left, right, args...)); + combineArg(left, right, args...); + return left; + } + + // All useful amendments are supported amendments. + // Enabled amendments are typically a subset of supported amendments. + // Vetoed amendments should be supported but not enabled. + // Unsupported amendments may be added to the AmendmentTable. + std::vector const yes_{"g", "i", "k", "m", "o", "q", "r", "s", "t", "u"}; + std::vector const enabled_{"b", "d", "f", "h", "j", "l", "n", "p"}; + std::vector const vetoed_{"a", "c", "e"}; + std::vector const obsolete_{"0", "1", "2"}; + std::vector const allSupported_{combine(yes_, enabled_, vetoed_, obsolete_)}; + std::vector const unsupported_{"v", "w", "x"}; + std::vector const unsupportedMajority_{"y", "z"}; + + Section const emptySection_; + std::vector const emptyYes_; + + TestServiceRegistry registry_; + beast::Journal journal_{registry_.getJournal("AmendmentTableTest")}; + + std::unique_ptr + makeTable( + std::chrono::seconds majorityTime, + std::vector const& supported, + Section const& enabled, + Section const& vetoed) + { + return makeAmendmentTable(registry_, majorityTime, supported, enabled, vetoed, journal_); + } + + std::unique_ptr + makeTable(std::chrono::seconds majorityTime) + { + static std::vector const kSupported = combine( + makeDefaultYes(yes_), + // Use non-intuitive default votes for "enabled_" and "vetoed_" + // so that when the tests later explicitly enable or veto them, + // we can be certain that they are not simply going by their + // default vote setting. + makeDefaultNo(enabled_), + makeDefaultYes(vetoed_), + makeObsolete(obsolete_)); + return makeTable(majorityTime, kSupported, makeSection(enabled_), makeSection(vetoed_)); + } + + // Build a Rules object that has all testable amendments enabled. + static Rules const& + allRules() + { + static Rules const kRules = [] { + std::unordered_set> featureSet; + foreachFeature(allFeatures(), [&](uint256 const& f) { featureSet.insert(f); }); + return Rules{featureSet}; + }(); + return kRules; + } + + // Make a list of trusted validators. + // Register the validators with AmendmentTable and return the list. + static std::vector> + makeValidators(int num, std::unique_ptr const& table) + { + std::vector> ret; + ret.reserve(num); + hash_set trustedValidators; + trustedValidators.reserve(num); + for (int i = 0; i < num; ++i) + { + auto const& back = ret.emplace_back(randomKeyPair(KeyType::Secp256k1)); + trustedValidators.insert(back.first); + } + table->trustChanged(trustedValidators); + return ret; + } + + static NetClock::time_point + hourTime(std::chrono::hours h) + { + return NetClock::time_point{h}; + } + + // Execute a pretend consensus round for a flag ledger + static void + doRound( + Rules const& rules, + AmendmentTable& table, + std::chrono::hours hour, + std::vector> const& validators, + std::vector> const& votes, + std::vector& ourVotes, + std::set& enabled, + majorityAmendments_t& majority) + { + // Do a round at the specified time + // Returns the amendments we voted for + + // Parameters: + // table: Our table of known and vetoed amendments + // validators: The addresses of validators we trust + // votes: Amendments and the number of validators who vote for them + // ourVotes: The amendments we vote for in our validation + // enabled: In/out enabled amendments + // majority: In/our majority amendments (and when they got a majority) + + auto const roundTime = hourTime(hour); + + // Build validations + std::vector> validations; + validations.reserve(validators.size()); + + int i = 0; + for (auto const& [pub, sec] : validators) + { + ++i; + std::vector field; + + for (auto const& [hash, nVotes] : votes) + { + if (nVotes >= i) + { + // We vote yes on this amendment + field.push_back(hash); + } + } + + auto v = std::make_shared( + xrpl::NetClock::time_point{}, pub, sec, calcNodeID(pub), [&field](STValidation& v) { + if (!field.empty()) + v.setFieldV256(sfAmendments, STVector256(sfAmendments, field)); + v.setFieldU32(sfLedgerSequence, 6180339); + }); + + validations.emplace_back(v); + } + + ourVotes = table.doValidation(enabled); + + auto actions = table.doVoting(rules, roundTime, enabled, majority, validations); + for (auto const& [hash, action] : actions) + { + // This code assumes other validators do as we do + + switch (action) + { + case 0: + // amendment goes from majority to enabled + if (enabled.contains(hash)) + Throw("enabling already enabled"); + if (!majority.contains(hash)) + Throw("enabling without majority"); + enabled.insert(hash); + majority.erase(hash); + break; + + case tfGotMajority: + if (majority.contains(hash)) + Throw("got majority while having majority"); + majority[hash] = roundTime; + break; + + case tfLostMajority: + if (!majority.contains(hash)) + Throw("lost majority without majority"); + majority.erase(hash); + break; + + default: + Throw("unknown action"); + } + } + } +}; + +TEST_F(AmendmentTableTest, construction) +{ + auto table = makeTable(weeks(1)); + + for (auto const& a : allSupported_) + EXPECT_TRUE(table->isSupported(amendmentId(a))); + + for (auto const& a : yes_) + EXPECT_TRUE(table->isSupported(amendmentId(a))); + + for (auto const& a : enabled_) + EXPECT_TRUE(table->isSupported(amendmentId(a))); + + for (auto const& a : vetoed_) + { + EXPECT_TRUE(table->isSupported(amendmentId(a))); + EXPECT_FALSE(table->isEnabled(amendmentId(a))); + } + + for (auto const& a : obsolete_) + { + EXPECT_TRUE(table->isSupported(amendmentId(a))); + EXPECT_FALSE(table->isEnabled(amendmentId(a))); + } +} + +TEST_F(AmendmentTableTest, name_to_id_mapping) +{ + auto table = makeTable(weeks(1)); + + for (auto const& a : yes_) + EXPECT_EQ(table->find(a), amendmentId(a)); + for (auto const& a : enabled_) + EXPECT_EQ(table->find(a), amendmentId(a)); + for (auto const& a : vetoed_) + EXPECT_EQ(table->find(a), amendmentId(a)); + for (auto const& a : obsolete_) + EXPECT_EQ(table->find(a), amendmentId(a)); + for (auto const& a : unsupported_) + EXPECT_FALSE(table->find(a)); + for (auto const& a : unsupportedMajority_) + EXPECT_FALSE(table->find(a)); + + // Vetoing an unsupported amendment should add the amendment to table. + // Verify that unsupportedID is not in table. + uint256 const unsupportedID = amendmentId(unsupported_[0]); + { + json::Value const unsupp = table->getJson(unsupportedID, true)[to_string(unsupportedID)]; + EXPECT_EQ(unsupp.size(), 0u); + } + + // After vetoing unsupportedID verify that it is in table. + table->veto(unsupportedID); + { + json::Value const unsupp = table->getJson(unsupportedID, true)[to_string(unsupportedID)]; + EXPECT_TRUE(unsupp[jss::vetoed].asBool()); + } +} + +TEST_F(AmendmentTableTest, bad_config) +{ + auto const yesVotes = makeDefaultYes(yes_); + auto const section = makeSection(vetoed_); + auto const id = to_string(amendmentId(enabled_[0])); + + { // Two arguments are required - we pass one + Section test = section; + test.append(id); + + try + { + if (makeTable(weeks(2), yesVotes, test, emptySection_)) + ADD_FAILURE() << "Accepted only amendment ID"; + } + catch (std::exception const& e) + { + EXPECT_EQ(e.what(), "Invalid entry '" + id + "' in [Test]"); + } + } + + { // Two arguments are required - we pass three + Section test = section; + test.append(id + " Test Name"); + + try + { + if (makeTable(weeks(2), yesVotes, test, emptySection_)) + ADD_FAILURE() << "Accepted extra arguments"; + } + catch (std::exception const& e) + { + EXPECT_EQ(e.what(), "Invalid entry '" + id + " Test Name' in [Test]"); + } + } + + { + auto sid = id; + sid.resize(sid.length() - 1); + + Section test = section; + test.append(sid + " Name"); + + try + { + if (makeTable(weeks(2), yesVotes, test, emptySection_)) + ADD_FAILURE() << "Accepted short amendment ID"; + } + catch (std::exception const& e) + { + EXPECT_EQ(e.what(), "Invalid entry '" + sid + " Name' in [Test]"); + } + } + + { + auto sid = id; + sid.resize(sid.length() + 1, '0'); + + Section test = section; + test.append(sid + " Name"); + + try + { + if (makeTable(weeks(2), yesVotes, test, emptySection_)) + ADD_FAILURE() << "Accepted long amendment ID"; + } + catch (std::exception const& e) + { + EXPECT_EQ(e.what(), "Invalid entry '" + sid + " Name' in [Test]"); + } + } + + { + auto sid = id; + sid.resize(sid.length() - 1); + sid.push_back('Q'); + + Section test = section; + test.append(sid + " Name"); + + try + { + if (makeTable(weeks(2), yesVotes, test, emptySection_)) + ADD_FAILURE() << "Accepted non-hex amendment ID"; + } + catch (std::exception const& e) + { + EXPECT_EQ(e.what(), "Invalid entry '" + sid + " Name' in [Test]"); + } + } +} + +TEST_F(AmendmentTableTest, enable_veto) +{ + std::unique_ptr table = makeTable(weeks(2)); + + // Note which entries are enabled (convert the amendment names to IDs) + std::set allEnabled; + for (auto const& a : enabled_) + allEnabled.insert(amendmentId(a)); + + for (uint256 const& a : allEnabled) + EXPECT_TRUE(table->enable(a)); + + // So far all enabled amendments are supported. + EXPECT_FALSE(table->hasUnsupportedEnabled()); + + // Verify all enables are enabled and nothing else. + for (std::string const& a : yes_) + { + uint256 const supportedID = amendmentId(a); + bool const enabled = table->isEnabled(supportedID); + bool const found = allEnabled.contains(supportedID); + EXPECT_EQ(enabled, found) << a << (enabled ? " enabled " : " disabled ") + << (found ? " found" : " not found"); + } + + // All supported and unVetoed amendments should be returned as desired. + { + std::set vetoed; + for (std::string const& a : vetoed_) + vetoed.insert(amendmentId(a)); + + std::vector const desired = table->getDesired(); + for (uint256 const& a : desired) + EXPECT_TRUE(not vetoed.contains(a)); + + // Unveto an amendment that is already not vetoed. Shouldn't + // hurt anything, but the values returned by getDesired() + // shouldn't change. + EXPECT_FALSE(table->unVeto(amendmentId(yes_[1]))); + EXPECT_EQ(desired, table->getDesired()); + } + + // UnVeto one of the vetoed amendments. It should now be desired. + { + uint256 const unvetoedID = amendmentId(vetoed_[0]); + EXPECT_TRUE(table->unVeto(unvetoedID)); + + std::vector const desired = table->getDesired(); + EXPECT_TRUE(std::ranges::find(desired, unvetoedID) != desired.end()); + } + + // Veto all supported amendments. Now desired should be empty. + for (std::string const& a : allSupported_) + { + table->veto(amendmentId(a)); + } + EXPECT_TRUE(table->getDesired().empty()); + + // Enable an unsupported amendment. + { + EXPECT_FALSE(table->hasUnsupportedEnabled()); + table->enable(amendmentId(unsupported_[0])); + EXPECT_TRUE(table->hasUnsupportedEnabled()); + } +} + +TEST_F(AmendmentTableTest, has_unsupported_enabled) +{ + using namespace std::chrono_literals; + constexpr weeks kW(1); + auto table = makeTable(kW); + EXPECT_FALSE(table->hasUnsupportedEnabled()); + EXPECT_FALSE(table->firstUnsupportedExpected()); + EXPECT_TRUE(table->needValidatedLedger(1)); + + std::set enabled; + std::ranges::for_each( + unsupported_, [&enabled](auto const& s) { enabled.insert(amendmentId(s)); }); + + majorityAmendments_t majority; + table->doValidatedLedger(1, enabled, majority); + EXPECT_TRUE(table->hasUnsupportedEnabled()); + EXPECT_FALSE(table->firstUnsupportedExpected()); + + NetClock::duration t{1000s}; + std::ranges::for_each(unsupportedMajority_, [&majority, &t](auto const& s) { + majority[amendmentId(s)] = NetClock::time_point{--t}; + }); + + table->doValidatedLedger(1, enabled, majority); + EXPECT_TRUE(table->hasUnsupportedEnabled()); + EXPECT_TRUE(table->firstUnsupportedExpected()); + EXPECT_EQ(*table->firstUnsupportedExpected(), NetClock::time_point{t} + kW); + + // Make sure the table knows when it needs an update. + EXPECT_FALSE(table->needValidatedLedger(256)); + EXPECT_TRUE(table->needValidatedLedger(257)); +} + +// No vote on unknown amendment +TEST_F(AmendmentTableTest, no_on_unknown) +{ + auto const testAmendment = amendmentId("TestAmendment"); + + auto table = makeTable(weeks(2), emptyYes_, emptySection_, emptySection_); + + auto const validators = makeValidators(10, table); + + std::vector> votes; + std::vector ourVotes; + std::set enabled; + majorityAmendments_t majority; + + doRound(allRules(), *table, weeks{1}, validators, votes, ourVotes, enabled, majority); + EXPECT_TRUE(ourVotes.empty()); + EXPECT_TRUE(enabled.empty()); + EXPECT_TRUE(majority.empty()); + + uint256 const unsupportedID = amendmentId(unsupported_[0]); + { + json::Value const unsupp = table->getJson(unsupportedID, false)[to_string(unsupportedID)]; + EXPECT_EQ(unsupp.size(), 0u); + } + + table->veto(unsupportedID); + { + json::Value const unsupp = table->getJson(unsupportedID, false)[to_string(unsupportedID)]; + EXPECT_FALSE(unsupp[jss::vetoed].asBool()); + } + + votes.emplace_back(testAmendment, validators.size()); + + votes.emplace_back(testAmendment, validators.size()); + + doRound(allRules(), *table, weeks{2}, validators, votes, ourVotes, enabled, majority); + EXPECT_TRUE(ourVotes.empty()); + EXPECT_TRUE(enabled.empty()); + + majority[testAmendment] = hourTime(weeks{1}); + + // Note that the simulation code assumes others behave as we do, + // so the amendment won't get enabled + doRound(allRules(), *table, weeks{5}, validators, votes, ourVotes, enabled, majority); + EXPECT_TRUE(ourVotes.empty()); + EXPECT_TRUE(enabled.empty()); +} + +// No vote on vetoed amendment +TEST_F(AmendmentTableTest, no_on_vetoed) +{ + auto const testAmendment = amendmentId("vetoedAmendment"); + + auto table = makeTable(weeks(2), emptyYes_, emptySection_, makeSection(testAmendment)); + + auto const validators = makeValidators(10, table); + + std::vector> votes; + std::vector ourVotes; + std::set enabled; + majorityAmendments_t majority; + + doRound(allRules(), *table, weeks{1}, validators, votes, ourVotes, enabled, majority); + EXPECT_TRUE(ourVotes.empty()); + EXPECT_TRUE(enabled.empty()); + EXPECT_TRUE(majority.empty()); + + votes.emplace_back(testAmendment, validators.size()); + + doRound(allRules(), *table, weeks{2}, validators, votes, ourVotes, enabled, majority); + EXPECT_TRUE(ourVotes.empty()); + EXPECT_TRUE(enabled.empty()); + + majority[testAmendment] = hourTime(weeks{1}); + + doRound(allRules(), *table, weeks{5}, validators, votes, ourVotes, enabled, majority); + EXPECT_TRUE(ourVotes.empty()); + EXPECT_TRUE(enabled.empty()); +} + +// Vote on and enable known, not-enabled amendment +TEST_F(AmendmentTableTest, vote_enable) +{ + auto table = makeTable(weeks(2), makeDefaultYes(yes_), emptySection_, emptySection_); + + auto const validators = makeValidators(10, table); + + std::vector> votes; + std::vector ourVotes; + std::set enabled; + majorityAmendments_t majority; + + // Week 1: We should vote for all known amendments not enabled + doRound(allRules(), *table, weeks{1}, validators, votes, ourVotes, enabled, majority); + EXPECT_EQ(ourVotes.size(), yes_.size()); + EXPECT_TRUE(enabled.empty()); + for (auto const& i : yes_) + EXPECT_TRUE(not majority.contains(amendmentId(i))); + + // Now, everyone votes for this feature + for (auto const& i : yes_) + votes.emplace_back(amendmentId(i), validators.size()); + + // Week 2: We should recognize a majority + doRound(allRules(), *table, weeks{2}, validators, votes, ourVotes, enabled, majority); + EXPECT_EQ(ourVotes.size(), yes_.size()); + EXPECT_TRUE(enabled.empty()); + + for (auto const& i : yes_) + EXPECT_EQ(majority[amendmentId(i)], hourTime(weeks{2})); + + // Week 5: We should enable the amendment + doRound(allRules(), *table, weeks{5}, validators, votes, ourVotes, enabled, majority); + EXPECT_EQ(enabled.size(), yes_.size()); + + // Week 6: We should remove it from our votes and from having a majority + doRound(allRules(), *table, weeks{6}, validators, votes, ourVotes, enabled, majority); + EXPECT_EQ(enabled.size(), yes_.size()); + EXPECT_TRUE(ourVotes.empty()); + for (auto const& i : yes_) + EXPECT_TRUE(not majority.contains(amendmentId(i))); +} + +// Detect majority at 80%, enable later +TEST_F(AmendmentTableTest, detect_majority) +{ + auto const testAmendment = amendmentId("detectMajority"); + auto table = makeTable(weeks(2), makeDefaultYes(testAmendment), emptySection_, emptySection_); + + auto const validators = makeValidators(16, table); + + std::set enabled; + majorityAmendments_t majority; + + for (int i = 0; i <= 17; ++i) + { + std::vector> votes; + std::vector ourVotes; + + if ((i > 0) && (i < 17)) + votes.emplace_back(testAmendment, i); + + doRound(allRules(), *table, weeks{i}, validators, votes, ourVotes, enabled, majority); + + if (i < 13) // 13 => 13/16 = 0.8125 => > 80% + { + // We are voting yes, not enabled, no majority + EXPECT_FALSE(ourVotes.empty()); + EXPECT_TRUE(enabled.empty()); + EXPECT_TRUE(majority.empty()); + } + else if (i < 15) + { + // We have a majority, not enabled, keep voting + EXPECT_FALSE(ourVotes.empty()); + EXPECT_FALSE(majority.empty()); + EXPECT_TRUE(enabled.empty()); + } + else if (i == 15) + { + // enable, keep voting, remove from majority + EXPECT_FALSE(ourVotes.empty()); + EXPECT_TRUE(majority.empty()); + EXPECT_FALSE(enabled.empty()); + } + else + { + // Done, we should be enabled and not voting + EXPECT_TRUE(ourVotes.empty()); + EXPECT_TRUE(majority.empty()); + EXPECT_FALSE(enabled.empty()); + } + } +} + +// Detect loss of majority +TEST_F(AmendmentTableTest, lost_majority) +{ + auto const testAmendment = amendmentId("lostMajority"); + + auto table = makeTable(weeks(8), makeDefaultYes(testAmendment), emptySection_, emptySection_); + + auto const validators = makeValidators(16, table); + + std::set enabled; + majorityAmendments_t majority; + + { + // establish majority + std::vector> votes; + std::vector ourVotes; + + votes.emplace_back(testAmendment, validators.size()); + + doRound(allRules(), *table, weeks{1}, validators, votes, ourVotes, enabled, majority); + + EXPECT_TRUE(enabled.empty()); + EXPECT_FALSE(majority.empty()); + } + + for (int i = 1; i < 8; ++i) + { + std::vector> votes; + std::vector ourVotes; + + // Gradually reduce support + votes.emplace_back(testAmendment, validators.size() - i); + + doRound(allRules(), *table, weeks{i + 1}, validators, votes, ourVotes, enabled, majority); + + if (i < 4) // 16 - 3 = 13 => 13/16 = 0.8125 => > 80% + { // 16 - 4 = 12 => 12/16 = 0.75 => < 80% + // We are voting yes, not enabled, majority + EXPECT_FALSE(ourVotes.empty()); + EXPECT_TRUE(enabled.empty()); + EXPECT_FALSE(majority.empty()); + } + else + { + // No majority, not enabled, keep voting + EXPECT_FALSE(ourVotes.empty()); + EXPECT_TRUE(majority.empty()); + EXPECT_TRUE(enabled.empty()); + } + } +} + +// Exercise the UNL changing while voting is in progress. +TEST_F(AmendmentTableTest, changed_unl) +{ + auto const testAmendment = amendmentId("changedUNL"); + auto table = makeTable(weeks(8), makeDefaultYes(testAmendment), emptySection_, emptySection_); + + std::vector> validators = makeValidators(10, table); + + std::set enabled; + majorityAmendments_t majority; + + { + // 10 validators with 2 voting against won't get majority. + std::vector> votes; + std::vector ourVotes; + + votes.emplace_back(testAmendment, validators.size() - 2); + + doRound(allRules(), *table, weeks{1}, validators, votes, ourVotes, enabled, majority); + + EXPECT_TRUE(enabled.empty()); + EXPECT_TRUE(majority.empty()); + } + + // Add one new validator to the UNL. + validators.emplace_back(randomKeyPair(KeyType::Secp256k1)); + + // A lambda that updates the AmendmentTable with the latest + // trusted validators. + auto callTrustChanged = [](std::vector> const& validators, + std::unique_ptr const& table) { + // We need a hash_set to pass to trustChanged. + hash_set trustedValidators; + trustedValidators.reserve(validators.size()); + std::ranges::for_each(validators, [&trustedValidators](auto const& val) { + trustedValidators.insert(val.first); + }); + + // Tell the AmendmentTable that the UNL changed. + table->trustChanged(trustedValidators); + }; + + // Tell the table that there's been a change in trusted validators. + callTrustChanged(validators, table); + + { + // 11 validators with 2 voting against gains majority. + std::vector> votes; + std::vector ourVotes; + + votes.emplace_back(testAmendment, validators.size() - 2); + + doRound(allRules(), *table, weeks{2}, validators, votes, ourVotes, enabled, majority); + + EXPECT_TRUE(enabled.empty()); + EXPECT_FALSE(majority.empty()); + } + { + // One of the validators goes flaky and doesn't send validations + // (without the UNL changing) so the amendment loses majority. + std::pair const savedValidator = validators.front(); + validators.erase(validators.begin()); + + std::vector> votes; + std::vector ourVotes; + + votes.emplace_back(testAmendment, validators.size() - 2); + + doRound(allRules(), *table, weeks{3}, validators, votes, ourVotes, enabled, majority); + + EXPECT_TRUE(enabled.empty()); + EXPECT_TRUE(majority.empty()); + + // Simulate the validator re-syncing to the network by adding it + // back to the validators vector + validators.insert(validators.begin(), savedValidator); + + votes.front().second = validators.size() - 2; + + doRound(allRules(), *table, weeks{4}, validators, votes, ourVotes, enabled, majority); + + EXPECT_TRUE(enabled.empty()); + EXPECT_FALSE(majority.empty()); + + // Finally, remove one validator from the UNL and see that majority + // is lost. + validators.erase(validators.begin()); + + // Tell the table that there's been a change in trusted validators. + callTrustChanged(validators, table); + + votes.front().second = validators.size() - 2; + + doRound(allRules(), *table, weeks{5}, validators, votes, ourVotes, enabled, majority); + + EXPECT_TRUE(enabled.empty()); + EXPECT_TRUE(majority.empty()); + } +} + +// Exercise a validator losing connectivity and then regaining it after +// extended delays. Depending on how long that delay is an amendment +// either will or will not go live. +TEST_F(AmendmentTableTest, validator_flapping) +{ + // We run a test where a validator flaps on and off every 23 hours + // and another one one where it flaps on and off every 25 hours. + // + // Since the local validator vote record expires after 24 hours, + // with 23 hour flapping the amendment will go live. But with 25 + // hour flapping the amendment will not go live. + for (int const flapRateHours : {23, 25}) + { + auto const testAmendment = amendmentId("validatorFlapping"); + auto table = + makeTable(weeks(1), makeDefaultYes(testAmendment), emptySection_, emptySection_); + + // Make two lists of validators, one with a missing validator, to + // make it easy to simulate validator flapping. + auto const allValidators = makeValidators(11, table); + decltype(allValidators) + const mostValidators(allValidators.begin() + 1, allValidators.end()); + EXPECT_EQ(allValidators.size(), mostValidators.size() + 1); + + std::set enabled; + majorityAmendments_t majority; + + std::vector> votes; + std::vector ourVotes; + + votes.emplace_back(testAmendment, allValidators.size() - 2); + + int delay = flapRateHours; + // Loop for 1 week plus a day. + for (int hour = 1; hour < (24 * 8); ++hour) + { + decltype(allValidators) const& thisHoursValidators = + (delay < flapRateHours) ? mostValidators : allValidators; + delay = delay == flapRateHours ? 0 : delay + 1; + + votes.front().second = thisHoursValidators.size() - 2; + + using namespace std::chrono; + doRound( + allRules(), + *table, + hours(hour), + thisHoursValidators, + votes, + ourVotes, + enabled, + majority); + + if (hour <= (24 * 7) || flapRateHours > 24) + { + // The amendment should not be enabled under any + // circumstance until one week has elapsed. + EXPECT_TRUE(enabled.empty()); + + // If flapping is less than 24 hours, there should be + // no flapping. Otherwise we should only have majority + // if allValidators vote -- which means there are no + // missing validators. + bool const expectMajority = + (delay <= 24) ? true : &thisHoursValidators == &allValidators; + EXPECT_NE(majority.empty(), expectMajority); + } + else + { + // We're... + // o Past one week, and + // o AmendmentFlapping was less than 24 hours. + // The amendment should be enabled. + EXPECT_FALSE(enabled.empty()); + EXPECT_TRUE(majority.empty()); + } + } + } +} + +} // namespace xrpl::test diff --git a/src/tests/libxrpl/ledger/main.cpp b/src/tests/libxrpl/ledger/main.cpp new file mode 100644 index 0000000000..5142bbe08a --- /dev/null +++ b/src/tests/libxrpl/ledger/main.cpp @@ -0,0 +1,8 @@ +#include + +int +main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/src/xrpld/app/misc/AmendmentTableImpl.h b/src/xrpld/app/misc/AmendmentTableImpl.h deleted file mode 100644 index fe7c067d5a..0000000000 --- a/src/xrpld/app/misc/AmendmentTableImpl.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include - -#include - -namespace xrpl { - -std::unique_ptr -make_AmendmentTable( - ServiceRegistry& registry, - std::chrono::seconds majorityTime, - std::vector const& supported, - Section const& enabled, - Section const& vetoed, - beast::Journal journal); - -} // namespace xrpl