diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index 4bd48d723..8c45ae620 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -150,6 +150,7 @@ install ( src/ripple/basics/IOUAmount.h src/ripple/basics/LocalValue.h src/ripple/basics/Log.h + src/ripple/basics/MathUtilities.h src/ripple/basics/safe_cast.h src/ripple/basics/Slice.h src/ripple/basics/StringUtilities.h @@ -870,6 +871,7 @@ target_sources (rippled PRIVATE test sources: subdir: protocol #]===============================] + src/test/protocol/BuildInfo_test.cpp src/test/protocol/InnerObjectFormats_test.cpp src/test/protocol/Issue_test.cpp src/test/protocol/PublicKey_test.cpp diff --git a/src/ripple/app/ledger/LedgerMaster.h b/src/ripple/app/ledger/LedgerMaster.h index 7ce8d1627..8c3f81254 100644 --- a/src/ripple/app/ledger/LedgerMaster.h +++ b/src/ripple/app/ledger/LedgerMaster.h @@ -407,6 +407,9 @@ private: // without first wiping the database. LedgerIndex const max_ledger_difference_{1000000}; + // Time that the previous upgrade warning was issued. + TimeKeeper::time_point upgradeWarningPrevTime_{}; + private: struct Stats { diff --git a/src/ripple/app/ledger/impl/LedgerMaster.cpp b/src/ripple/app/ledger/impl/LedgerMaster.cpp index f7db824e5..7de7872af 100644 --- a/src/ripple/app/ledger/impl/LedgerMaster.cpp +++ b/src/ripple/app/ledger/impl/LedgerMaster.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -43,6 +44,7 @@ #include #include #include +#include #include #include #include @@ -1049,6 +1051,78 @@ LedgerMaster::checkAccept(std::shared_ptr const& ledger) app_.getFeeTrack().setRemoteFee(fee); tryAdvance(); + + if (ledger->seq() % 256 == 0) + { + // Check if the majority of validators run a higher version rippled + // software. If so print a warning. + // + // Once the HardenedValidations amendment is enabled, validators include + // their rippled software version in the validation messages of every + // (flag - 1) ledger. We wait for one ledger time before checking the + // version information to accumulate more validation messages. + + auto currentTime = app_.timeKeeper().now(); + bool needPrint = false; + + // The variable upgradeWarningPrevTime_ will be set when and only when + // the warning is printed. + if (upgradeWarningPrevTime_ == TimeKeeper::time_point()) + { + // Have not printed the warning before, check if need to print. + auto const vals = app_.getValidations().getTrustedForLedger( + ledger->info().parentHash); + std::size_t higherVersionCount = 0; + std::size_t rippledCount = 0; + for (auto const& v : vals) + { + if (v->isFieldPresent(sfServerVersion)) + { + auto version = v->getFieldU64(sfServerVersion); + higherVersionCount += + BuildInfo::isNewerVersion(version) ? 1 : 0; + rippledCount += + BuildInfo::isRippledVersion(version) ? 1 : 0; + } + } + // We report only if (1) we have accumulated validation messages + // from 90% validators from the UNL, (2) 60% of validators + // running the rippled implementation have higher version numbers, + // and (3) the calculation won't cause divide-by-zero. + if (higherVersionCount > 0 && rippledCount > 0) + { + constexpr std::size_t reportingPercent = 90; + constexpr std::size_t cutoffPercent = 60; + auto const unlSize{ + app_.validators().getQuorumKeys().second.size()}; + needPrint = unlSize > 0 && + calculatePercent(vals.size(), unlSize) >= + reportingPercent && + calculatePercent(higherVersionCount, rippledCount) >= + cutoffPercent; + } + } + // To throttle the warning messages, instead of printing a warning + // every flag ledger, we print every week. + else if (currentTime - upgradeWarningPrevTime_ >= weeks{1}) + { + // Printed the warning before, and assuming most validators + // do not downgrade, we keep printing the warning + // until the local server is restarted. + needPrint = true; + } + + if (needPrint) + { + upgradeWarningPrevTime_ = currentTime; + auto const upgradeMsg = + "Check for upgrade: " + "A majority of trusted validators are " + "running a newer version."; + std::cerr << upgradeMsg << std::endl; + JLOG(m_journal.error()) << upgradeMsg; + } + } } /** Report that the consensus process built a particular ledger */ diff --git a/src/ripple/basics/MathUtilities.h b/src/ripple/basics/MathUtilities.h new file mode 100644 index 000000000..f9dbcbbbc --- /dev/null +++ b/src/ripple/basics/MathUtilities.h @@ -0,0 +1,68 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2020 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_BASICS_MATHUTILITIES_H_INCLUDED +#define RIPPLE_BASICS_MATHUTILITIES_H_INCLUDED + +#include +#include +#include + +namespace ripple { + +/** Calculate one number divided by another number in percentage. + * The result is rounded up to the next integer, and capped in the range [0,100] + * E.g. calculatePercent(1, 100) = 1 because 1/100 = 0.010000 + * calculatePercent(1, 99) = 2 because 1/99 = 0.010101 + * calculatePercent(0, 100) = 0 + * calculatePercent(100, 100) = 100 + * calculatePercent(200, 100) = 100 because the result is capped to 100 + * + * @param count dividend + * @param total divisor + * @return the percentage, in [0, 100] + * + * @note total cannot be zero. + * */ +constexpr std::size_t +calculatePercent(std::size_t count, std::size_t total) +{ + assert(total != 0); + return ((std::min(count, total) * 100) + total - 1) / total; +} + +// unit tests +static_assert(calculatePercent(1, 2) == 50); +static_assert(calculatePercent(0, 100) == 0); +static_assert(calculatePercent(100, 100) == 100); +static_assert(calculatePercent(200, 100) == 100); +static_assert(calculatePercent(1, 100) == 1); +static_assert(calculatePercent(1, 99) == 2); +static_assert(calculatePercent(6, 14) == 43); +static_assert(calculatePercent(29, 33) == 88); +static_assert(calculatePercent(1, 64) == 2); +static_assert(calculatePercent(0, 100'000'000) == 0); +static_assert(calculatePercent(1, 100'000'000) == 1); +static_assert(calculatePercent(50'000'000, 100'000'000) == 50); +static_assert(calculatePercent(50'000'001, 100'000'000) == 51); +static_assert(calculatePercent(99'999'999, 100'000'000) == 100); + +} // namespace ripple + +#endif diff --git a/src/ripple/protocol/BuildInfo.h b/src/ripple/protocol/BuildInfo.h index 84a833134..cfe35d338 100644 --- a/src/ripple/protocol/BuildInfo.h +++ b/src/ripple/protocol/BuildInfo.h @@ -43,7 +43,7 @@ getVersionString(); std::string const& getFullVersionString(); -/** Returns the server version packed in a 64-bit integer. +/** Encode an arbitrary server software version in a 64-bit integer. The general format is: @@ -64,10 +64,38 @@ getFullVersionString(); 10 if an RC 01 if a beta N: 6-bit rc/beta number (1-63) + + @param the version string + @return the encoded version in a 64-bit integer */ std::uint64_t +encodeSoftwareVersion(char const* const versionStr); + +/** Returns this server's version packed in a 64-bit integer. */ +std::uint64_t getEncodedVersion(); +/** Check if the encoded software version is a rippled software version. + + @param version another node's encoded software version + @return true if the version is a rippled software version, false otherwise +*/ +bool +isRippledVersion(std::uint64_t version); + +/** Check if the version is newer than the local node's rippled software + version. + + @param version another node's encoded software version + @return true if the version is newer than the local node's rippled software + version, false otherwise. + + @note This function only understands version numbers that are generated by + rippled. Please see the encodeSoftwareVersion() function for detail. +*/ +bool +isNewerVersion(std::uint64_t version); + } // namespace BuildInfo } // namespace ripple diff --git a/src/ripple/protocol/impl/BuildInfo.cpp b/src/ripple/protocol/impl/BuildInfo.cpp index 9cf30d294..75bfdbfbb 100644 --- a/src/ripple/protocol/impl/BuildInfo.cpp +++ b/src/ripple/protocol/impl/BuildInfo.cpp @@ -77,77 +77,98 @@ getFullVersionString() return value; } +static constexpr std::uint64_t implementationVersionIdentifier = + 0x183B'0000'0000'0000LLU; +static constexpr std::uint64_t implementationVersionIdentifierMask = + 0xFFFF'0000'0000'0000LLU; + std::uint64_t -getEncodedVersion() +encodeSoftwareVersion(char const* const versionStr) { - static std::uint64_t const cookie = []() { - std::uint64_t c = 0x183B000000000000; + std::uint64_t c = implementationVersionIdentifier; - beast::SemanticVersion v; + beast::SemanticVersion v; - if (v.parse(versionString)) + if (v.parse(std::string(versionStr))) + { + if (v.majorVersion >= 0 && v.majorVersion <= 255) + c |= static_cast(v.majorVersion) << 40; + + if (v.minorVersion >= 0 && v.minorVersion <= 255) + c |= static_cast(v.minorVersion) << 32; + + if (v.patchVersion >= 0 && v.patchVersion <= 255) + c |= static_cast(v.patchVersion) << 24; + + if (!v.isPreRelease()) + c |= static_cast(0xC00000); + + if (v.isPreRelease()) { - if (v.majorVersion >= 0 && v.majorVersion <= 255) - c |= static_cast(v.majorVersion) << 40; + std::uint8_t x = 0; - if (v.minorVersion >= 0 && v.minorVersion <= 255) - c |= static_cast(v.minorVersion) << 32; - - if (v.patchVersion >= 0 && v.patchVersion <= 255) - c |= static_cast(v.patchVersion) << 24; - - if (!v.isPreRelease()) - c |= static_cast(0xC00000); - - if (v.isPreRelease()) + for (auto id : v.preReleaseIdentifiers) { - std::uint8_t x = 0; + auto parsePreRelease = [](std::string_view identifier, + std::string_view prefix, + std::uint8_t key, + std::uint8_t lok, + std::uint8_t hik) -> std::uint8_t { + std::uint8_t ret = 0; - for (auto id : v.preReleaseIdentifiers) + if (prefix != identifier.substr(0, prefix.length())) + return 0; + + if (!beast::lexicalCastChecked( + ret, + std::string(identifier.substr(prefix.length())))) + return 0; + + if (std::clamp(ret, lok, hik) != ret) + return 0; + + return ret + key; + }; + + x = parsePreRelease(id, "rc", 0x80, 0, 63); + + if (x == 0) + x = parsePreRelease(id, "b", 0x40, 0, 63); + + if (x & 0xC0) { - auto parsePreRelease = - [](std::string_view identifier, - std::string_view prefix, - std::uint8_t key, - std::uint8_t lok, - std::uint8_t hik) -> std::uint8_t { - std::uint8_t ret = 0; - - if (prefix != identifier.substr(0, prefix.length())) - return 0; - - if (!beast::lexicalCastChecked( - ret, - std::string( - identifier.substr(prefix.length())))) - return 0; - - if (std::clamp(ret, lok, hik) != ret) - return 0; - - return ret + key; - }; - - x = parsePreRelease(id, "rc", 0x80, 0, 63); - - if (x == 0) - x = parsePreRelease(id, "b", 0x40, 0, 63); - - if (x & 0xC0) - { - c |= static_cast(x) << 16; - break; - } + c |= static_cast(x) << 16; + break; } } } + } - return c; - }(); + return c; +} +std::uint64_t +getEncodedVersion() +{ + static std::uint64_t const cookie = {encodeSoftwareVersion(versionString)}; return cookie; } +bool +isRippledVersion(std::uint64_t version) +{ + return (version & implementationVersionIdentifierMask) == + implementationVersionIdentifier; +} + +bool +isNewerVersion(std::uint64_t version) +{ + if (isRippledVersion(version)) + return version > getEncodedVersion(); + return false; +} + } // namespace BuildInfo } // namespace ripple diff --git a/src/test/protocol/BuildInfo_test.cpp b/src/test/protocol/BuildInfo_test.cpp new file mode 100644 index 000000000..82ad4d679 --- /dev/null +++ b/src/test/protocol/BuildInfo_test.cpp @@ -0,0 +1,118 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2020 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include + +namespace ripple { + +class BuildInfo_test : public beast::unit_test::suite +{ +public: + void + testEncodeSoftwareVersion() + { + testcase("EncodeSoftwareVersion"); + + auto encodedVersion = BuildInfo::encodeSoftwareVersion("1.2.3-b7"); + + // the first two bytes identify the particular implementation, 0x183B + BEAST_EXPECT( + (encodedVersion & 0xFFFF'0000'0000'0000LLU) == + 0x183B'0000'0000'0000LLU); + + // the next three bytes: major version, minor version, patch version, + // 0x010203 + BEAST_EXPECT( + (encodedVersion & 0x0000'FFFF'FF00'0000LLU) == + 0x0000'0102'0300'0000LLU); + + // the next two bits: + { + // 01 if a beta + BEAST_EXPECT( + (encodedVersion & 0x0000'0000'00C0'0000LLU) >> 22 == 0b01); + // 10 if an RC + encodedVersion = BuildInfo::encodeSoftwareVersion("1.2.4-rc7"); + BEAST_EXPECT( + (encodedVersion & 0x0000'0000'00C0'0000LLU) >> 22 == 0b10); + // 11 if neither an RC nor a beta + encodedVersion = BuildInfo::encodeSoftwareVersion("1.2.5"); + BEAST_EXPECT( + (encodedVersion & 0x0000'0000'00C0'0000LLU) >> 22 == 0b11); + } + + // the next six bits: rc/beta number (1-63) + encodedVersion = BuildInfo::encodeSoftwareVersion("1.2.6-b63"); + BEAST_EXPECT((encodedVersion & 0x0000'0000'003F'0000LLU) >> 16 == 63); + + // the last two bytes are zeros + BEAST_EXPECT((encodedVersion & 0x0000'0000'0000'FFFFLLU) == 0); + + // Test some version strings with wrong formats: + // no rc/beta number + encodedVersion = BuildInfo::encodeSoftwareVersion("1.2.3-b"); + BEAST_EXPECT((encodedVersion & 0x0000'0000'00FF'0000LLU) == 0); + // rc/beta number out of range + encodedVersion = BuildInfo::encodeSoftwareVersion("1.2.3-b64"); + BEAST_EXPECT((encodedVersion & 0x0000'0000'00FF'0000LLU) == 0); + + // Check that the rc/beta number of a release is 0: + encodedVersion = BuildInfo::encodeSoftwareVersion("1.2.6"); + BEAST_EXPECT((encodedVersion & 0x0000'0000'003F'0000LLU) == 0); + } + + void + testIsRippledVersion() + { + testcase("IsRippledVersion"); + auto vFF = 0xFFFF'FFFF'FFFF'FFFFLLU; + BEAST_EXPECT(!BuildInfo::isRippledVersion(vFF)); + auto vRippled = 0x183B'0000'0000'0000LLU; + BEAST_EXPECT(BuildInfo::isRippledVersion(vRippled)); + } + + void + testIsNewerVersion() + { + testcase("IsNewerVersion"); + auto vFF = 0xFFFF'FFFF'FFFF'FFFFLLU; + BEAST_EXPECT(!BuildInfo::isNewerVersion(vFF)); + + auto v159 = BuildInfo::encodeSoftwareVersion("1.5.9"); + BEAST_EXPECT(!BuildInfo::isNewerVersion(v159)); + + auto vCurrent = BuildInfo::getEncodedVersion(); + BEAST_EXPECT(!BuildInfo::isNewerVersion(vCurrent)); + + auto vMax = BuildInfo::encodeSoftwareVersion("255.255.255"); + BEAST_EXPECT(BuildInfo::isNewerVersion(vMax)); + } + + void + run() override + { + testEncodeSoftwareVersion(); + testIsRippledVersion(); + testIsNewerVersion(); + } +}; + +BEAST_DEFINE_TESTSUITE(BuildInfo, protocol, ripple); +} // namespace ripple