diff --git a/Builds/VisualStudio2015/RippleD.vcxproj b/Builds/VisualStudio2015/RippleD.vcxproj index 75f46490a..6c57577cf 100644 --- a/Builds/VisualStudio2015/RippleD.vcxproj +++ b/Builds/VisualStudio2015/RippleD.vcxproj @@ -1061,6 +1061,10 @@ True True + + True + True + True True @@ -1075,6 +1079,8 @@ + + True True @@ -2297,12 +2303,6 @@ - - True - True - - - True True @@ -4199,6 +4199,10 @@ True True + + True + True + True True @@ -4715,10 +4719,6 @@ True True - - True - True - True True diff --git a/Builds/VisualStudio2015/RippleD.vcxproj.filters b/Builds/VisualStudio2015/RippleD.vcxproj.filters index 063b12fe2..2c99d99fb 100644 --- a/Builds/VisualStudio2015/RippleD.vcxproj.filters +++ b/Builds/VisualStudio2015/RippleD.vcxproj.filters @@ -1569,6 +1569,9 @@ ripple\app\misc\impl + + ripple\app\misc\impl + ripple\app\misc\impl @@ -1581,6 +1584,9 @@ ripple\app\misc + + ripple\app\misc + ripple\app\misc @@ -2880,12 +2886,6 @@ ripple\overlay\impl - - ripple\overlay\impl - - - ripple\overlay\impl - ripple\overlay\impl @@ -4983,6 +4983,9 @@ test\app + + test\app + test\app @@ -5427,9 +5430,6 @@ test\overlay - - test\overlay - test\overlay diff --git a/bin/python/ripple/util/ValidatorManifestTest.py b/bin/python/ripple/util/ValidatorManifestTest.py index 3907fb80f..1be45f755 100755 --- a/bin/python/ripple/util/ValidatorManifestTest.py +++ b/bin/python/ripple/util/ValidatorManifestTest.py @@ -104,9 +104,6 @@ n9L81uNCaPgtUJfaHh89gmdvXKAmSt5Gdsw2g1iPWaPkAHW5Nm4C RL3 n9KiYM9CgngLvtRCQHZwgC2gjpdaZcCcbt3VboxiNFcKuwFVujzS RL4 n9LdgEtkmGB9E2h3K4Vp7iGUaKuq23Zr32ehxiU8FWY7xoxbWTSA RL5 -[validation_quorum] -3 - [validation_seed] {validation_seed} #vaidation_public_key: {validation_public_key} diff --git a/bin/python/ripple/util/test_ConfigFile.py b/bin/python/ripple/util/test_ConfigFile.py index 8b6d04050..8aa1b22b4 100644 --- a/bin/python/ripple/util/test_ConfigFile.py +++ b/bin/python/ripple/util/test_ConfigFile.py @@ -45,7 +45,6 @@ RESULT = { 'websocket_public_port': '5206', 'peer_ip': '0.0.0.0', 'rpc_port': '5205', - 'validation_quorum': '3', 'websocket_ip': '127.0.0.1'} FULL = """ @@ -131,10 +130,6 @@ n9L81uNCaPgtUJfaHh89gmdvXKAmSt5Gdsw2g1iPWaPkAHW5Nm4C RL3 n9KiYM9CgngLvtRCQHZwgC2gjpdaZcCcbt3VboxiNFcKuwFVujzS RL4 n9LdgEtkmGB9E2h3K4Vp7iGUaKuq23Zr32ehxiU8FWY7xoxbWTSA RL5 -# Ditto. -[validation_quorum] -3 - [validation_seed] sh1T8T9yGuV7Jb6DPhqSzdU2s5LcV diff --git a/doc/rippled-example.cfg b/doc/rippled-example.cfg index 54c311e84..552daf1c4 100644 --- a/doc/rippled-example.cfg +++ b/doc/rippled-example.cfg @@ -546,8 +546,7 @@ # # These settings affect the behavior of the server instance with respect # to Ripple payment protocol level activities such as validating and -# closing ledgers, establishing a quorum, or adjusting fees in response -# to server overloads. +# closing ledgers or adjusting fees in response to server overloads. # # # @@ -607,11 +606,14 @@ # to always accept as validators as well as the minimum number of validators # needed to accept consensus. # -# The contents of the file should include a [validators] and a -# [validation_quorum] entry. [validators] should be followed by -# a list of validation public keys of nodes, one per line, optionally -# followed by a comment separated by whitespace. -# [validation_quorum] should be followed by a number. +# The contents of the file should include a [validators] and/or +# [validator_list_keys] entries. +# [validators] should be followed by a list of validation public keys of +# nodes, one per line. +# [validator_list_keys] should be followed by a list of keys belonging to +# trusted validator list publishers. Validator lists will only be +# considered if the list is accompanied by a valid signature from a trusted +# publisher key. # # Specify the file by its name or path. # Unless an absolute path is specified, it will be considered relative to @@ -623,14 +625,11 @@ # # Example content: # [validators] -# n949f75evCHwgyP4fPVgaHqNHxUVN15PsJEZ3B3HnXPcPjcZAoy7 RL1 -# n9MD5h24qrQqiyBC8aeqqCWvpiBiYQ3jxSr91uiDvmrkyHRdYLUj RL2 -# n9L81uNCaPgtUJfaHh89gmdvXKAmSt5Gdsw2g1iPWaPkAHW5Nm4C RL3 -# n9KiYM9CgngLvtRCQHZwgC2gjpdaZcCcbt3VboxiNFcKuwFVujzS RL4 -# n9LdgEtkmGB9E2h3K4Vp7iGUaKuq23Zr32ehxiU8FWY7xoxbWTSA RL5 -# -# [validation_quorum] -# 3 +# n949f75evCHwgyP4fPVgaHqNHxUVN15PsJEZ3B3HnXPcPjcZAoy7 +# n9MD5h24qrQqiyBC8aeqqCWvpiBiYQ3jxSr91uiDvmrkyHRdYLUj +# n9L81uNCaPgtUJfaHh89gmdvXKAmSt5Gdsw2g1iPWaPkAHW5Nm4C +# n9KiYM9CgngLvtRCQHZwgC2gjpdaZcCcbt3VboxiNFcKuwFVujzS +# n9LdgEtkmGB9E2h3K4Vp7iGUaKuq23Zr32ehxiU8FWY7xoxbWTSA # # # [path_search] @@ -1021,7 +1020,7 @@ pool.ntp.org [ips] r.ripple.com 51235 -# File containing validation quorum and trusted validator keys. +# File containing trusted validator keys or validator list publishers. # Unless an absolute path is specified, it will be considered relative to the # folder in which the rippled.cfg file is located. [validators_file] diff --git a/doc/validators-example.txt b/doc/validators-example.txt index 6d4941343..885b3a536 100644 --- a/doc/validators-example.txt +++ b/doc/validators-example.txt @@ -13,8 +13,6 @@ # [validators] # # List of the validation public keys of nodes to always accept as validators. -# A comment may, optionally, be associated with each entry, separated by -# whitespace from the validation public key. # # The latest list of recommended validators can be obtained from # https://ripple.com/ripple.txt @@ -23,26 +21,27 @@ # # Examples: # n9KorY8QtTdRx7TVDpwnG9NvyxsDwHUKUEeDLY3AkiGncVaSXZi5 -# n9MqiExBcoG19UXwoLjBJnhsxEhAZMuWwJDRdkyDz1EkEkwzQTNt John Doe +# n9MqiExBcoG19UXwoLjBJnhsxEhAZMuWwJDRdkyDz1EkEkwzQTNt # # # -# [validation_quorum] +# [validator_list_keys] # -# Sets the minimum number of trusted validations a ledger must have before -# the server considers it fully validated. Note that if you are validating, -# your validation counts. +# List of keys belonging to trusted validator list publishers. +# Validator lists will only be considered if the list is accompanied by a +# valid signature from a trusted publisher key. +# Validator list keys should be hex-encoded. +# +# Examples: +# ed499d732bded01504a7407c224412ef550cc1ade638a4de4eb88af7c36cb8b282 +# 0202d3f36a801349f3be534e3f64cfa77dede6e1b6310a0b48f40f20f955cec945 +# 02dd8b7075f64d77d9d2bdb88da364f29fcd975f9ea6f21894abcc7564efda8054 # # Public keys of the validators that this rippled instance trusts. [validators] -n949f75evCHwgyP4fPVgaHqNHxUVN15PsJEZ3B3HnXPcPjcZAoy7 RL1 -n9MD5h24qrQqiyBC8aeqqCWvpiBiYQ3jxSr91uiDvmrkyHRdYLUj RL2 -n9L81uNCaPgtUJfaHh89gmdvXKAmSt5Gdsw2g1iPWaPkAHW5Nm4C RL3 -n9KiYM9CgngLvtRCQHZwgC2gjpdaZcCcbt3VboxiNFcKuwFVujzS RL4 -n9LdgEtkmGB9E2h3K4Vp7iGUaKuq23Zr32ehxiU8FWY7xoxbWTSA RL5 - -# The number of validators rippled needs to accept a consensus. -# Don't change this unless you know what you're doing. -[validation_quorum] -3 +n949f75evCHwgyP4fPVgaHqNHxUVN15PsJEZ3B3HnXPcPjcZAoy7 +n9MD5h24qrQqiyBC8aeqqCWvpiBiYQ3jxSr91uiDvmrkyHRdYLUj +n9L81uNCaPgtUJfaHh89gmdvXKAmSt5Gdsw2g1iPWaPkAHW5Nm4C +n9KiYM9CgngLvtRCQHZwgC2gjpdaZcCcbt3VboxiNFcKuwFVujzS +n9LdgEtkmGB9E2h3K4Vp7iGUaKuq23Zr32ehxiU8FWY7xoxbWTSA diff --git a/src/ripple/app/ledger/LedgerMaster.h b/src/ripple/app/ledger/LedgerMaster.h index 3faf3137b..d3217073c 100644 --- a/src/ripple/app/ledger/LedgerMaster.h +++ b/src/ripple/app/ledger/LedgerMaster.h @@ -113,10 +113,6 @@ public: std::chrono::seconds getValidatedLedgerAge (); bool isCaughtUp(std::string& reason); - int getMinValidations (); - - void setMinValidations (int v, bool strict); - std::uint32_t getEarliestFetch (); bool storeLedger (std::shared_ptr ledger); @@ -257,7 +253,7 @@ private: void getFetchPack(LedgerHash missingHash, LedgerIndex missingIndex); boost::optional getLedgerHashForHistory(LedgerIndex index); - int getNeededValidations(); + std::size_t getNeededValidations(); void advanceThread(); // Try to publish ledgers, acquire missing ledgers void doAdvance(); @@ -313,8 +309,6 @@ private: std::unique_ptr mLedgerCleaner; - int mMinValidations; // The minimum validations to publish a ledger. - bool mStrictValCount; // Don't raise the minimum uint256 mLastValidateHash; std::uint32_t mLastValidateSeq; diff --git a/src/ripple/app/ledger/impl/LedgerConsensusImp.cpp b/src/ripple/app/ledger/impl/LedgerConsensusImp.cpp index ced613b7c..d4904dcdc 100644 --- a/src/ripple/app/ledger/impl/LedgerConsensusImp.cpp +++ b/src/ripple/app/ledger/impl/LedgerConsensusImp.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -1267,14 +1268,14 @@ LedgerConsensusImp::makeInitialPosition () -> app_.getValidations().getValidations ( previousLedger_->info().parentHash); - auto const count = std::count_if ( + std::size_t const count = std::count_if ( validations.begin(), validations.end(), [](auto const& v) { return v.second->isTrusted(); }); - if (count >= ledgerMaster_.getMinValidations()) + if (count >= app_.validators ().quorum ()) { feeVote_.doVoting ( previousLedger_, diff --git a/src/ripple/app/ledger/impl/LedgerMaster.cpp b/src/ripple/app/ledger/impl/LedgerMaster.cpp index 4873010cd..2b16d24eb 100644 --- a/src/ripple/app/ledger/impl/LedgerMaster.cpp +++ b/src/ripple/app/ledger/impl/LedgerMaster.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -53,9 +54,6 @@ namespace ripple { using namespace std::chrono_literals; -// 150/256ths of validations of previous ledger -#define MIN_VALIDATION_RATIO 150 - // Don't catch up more than 100 ledgers (cannot exceed 256) #define MAX_LEDGER_GAP 100 @@ -73,8 +71,6 @@ LedgerMaster::LedgerMaster (Application& app, Stopwatch& stopwatch, , mHeldTransactions (uint256 ()) , mLedgerCleaner (detail::make_LedgerCleaner ( app, *this, app_.journal("LedgerCleaner"))) - , mMinValidations (0) - , mStrictValCount (false) , mLastValidateSeq (0) , mAdvanceThread (false) , mAdvanceWork (false) @@ -115,13 +111,6 @@ LedgerMaster::isCompatible ( beast::Journal::Stream s, char const* reason) { - if (mStrictValCount) - { - // If we're only using validation count, then we can't - // reject a ledger even if it's incompatible - return true; - } - auto validLedger = getValidatedLedger(); if (validLedger && @@ -217,7 +206,7 @@ LedgerMaster::setValidLedger( NetClock::time_point signTime; - if (! times.empty () && times.size() >= mMinValidations) + if (! times.empty () && times.size() >= app_.validators ().quorum ()) { // Calculate the sample median std::sort (times.begin (), times.end ()); @@ -702,7 +691,7 @@ LedgerMaster::failedSave(std::uint32_t seq, uint256 const& hash) void LedgerMaster::checkAccept (uint256 const& hash, std::uint32_t seq) { - int valCount = 0; + std::size_t valCount = 0; if (seq != 0) { @@ -713,18 +702,11 @@ LedgerMaster::checkAccept (uint256 const& hash, std::uint32_t seq) valCount = app_.getValidations().getTrustedValidationCount (hash); - if (valCount >= mMinValidations) + if (valCount >= app_.validators ().quorum ()) { ScopedLockType ml (m_mutex); if (seq > mLastValidLedger.second) mLastValidLedger = std::make_pair (hash, seq); - - if (!mStrictValCount && (mMinValidations < (valCount/2 + 1))) - { - mMinValidations = (valCount/2 + 1); - JLOG (m_journal.info()) - << "Raising minimum validations to " << mMinValidations; - } } if (seq == mValidLedgerSeq) @@ -742,7 +724,7 @@ LedgerMaster::checkAccept (uint256 const& hash, std::uint32_t seq) if ((seq != 0) && (getValidLedgerIndex() == 0)) { // Set peers sane early if we can - if (valCount >= mMinValidations) + if (valCount >= app_.validators ().quorum ()) app_.overlay().checkSanity (seq); } @@ -757,30 +739,14 @@ LedgerMaster::checkAccept (uint256 const& hash, std::uint32_t seq) } /** - * Determines how many validations are needed to fully-validated a ledger + * Determines how many validations are needed to fully validate a ledger * * @return Number of validations needed */ -int +std::size_t LedgerMaster::getNeededValidations () { - if (standalone_) - return 0; - - int minVal = mMinValidations; - - if (mLastValidateHash.isNonZero ()) - { - int val = app_.getValidations ().getTrustedValidationCount ( - mLastValidateHash); - val *= MIN_VALIDATION_RATIO; - val /= 256; - - if (val > minVal) - minVal = val; - } - - return minVal; + return standalone_ ? 0 : app_.validators().quorum (); } void @@ -904,7 +870,7 @@ LedgerMaster::consensusBuilt( ledgerSeq_ = seq; } - int valCount_; + std::size_t valCount_; LedgerIndex ledgerSeq_; }; @@ -916,7 +882,7 @@ LedgerMaster::consensusBuilt( vs.mergeValidation (v->getFieldU32 (sfLedgerSequence)); } - auto neededValidations = getNeededValidations (); + auto const neededValidations = getNeededValidations (); auto maxSeq = mValidLedgerSeq.load(); auto maxLedger = ledger->info().hash; @@ -1313,21 +1279,6 @@ LedgerMaster::getPublishedLedger () return mPubLedger; } -int -LedgerMaster::getMinValidations () -{ - return mMinValidations; -} - -void -LedgerMaster::setMinValidations (int v, bool strict) -{ - JLOG (m_journal.info()) << "Validation quorum: " << v - << (strict ? " strict" : ""); - mMinValidations = v; - mStrictValCount = strict; -} - std::string LedgerMaster::getCompleteLedgers () { diff --git a/src/ripple/app/main/Application.cpp b/src/ripple/app/main/Application.cpp index 6976233a1..1ebd5d363 100644 --- a/src/ripple/app/main/Application.cpp +++ b/src/ripple/app/main/Application.cpp @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -347,6 +348,8 @@ public: TaggedCache m_acceptedLedgerCache; std::unique_ptr m_networkOPs; std::unique_ptr cluster_; + std::unique_ptr validatorManifests_; + std::unique_ptr publisherManifests_; std::unique_ptr validators_; std::unique_ptr serverHandler_; std::unique_ptr m_amendmentTable; @@ -473,8 +476,15 @@ public: , cluster_ (std::make_unique ( logs_->journal("Overlay"))) + , validatorManifests_ (std::make_unique ( + logs_->journal("ManifestCache"))) + + , publisherManifests_ (std::make_unique ( + logs_->journal("ManifestCache"))) + , validators_ (std::make_unique ( - logs_->journal("UniqueNodeList"))) + *validatorManifests_, *publisherManifests_, *timeKeeper_, + logs_->journal("ValidatorList"), config_->VALIDATION_QUORUM)) , serverHandler_ (make_ServerHandler (*this, *m_networkOPs, get_io_service (), *m_jobQueue, *m_networkOPs, *m_resourceManager, *m_collectorManager)) @@ -691,6 +701,16 @@ public: return *validators_; } + ManifestCache& validatorManifests() override + { + return *validatorManifests_; + } + + ManifestCache& publisherManifests() override + { + return *publisherManifests_; + } + Cluster& cluster () override { return *cluster_; @@ -846,7 +866,18 @@ public: mValidations->flush (); - m_overlay->saveValidatorKeyManifests (getWalletDB ()); + // TODO Store manifests in manifests.sqlite instead of wallet.db + validatorManifests_->save (getWalletDB (), "ValidatorManifests", + [this](PublicKey const& pubKey) + { + return validators().listed (pubKey); + }); + + publisherManifests_->save (getWalletDB (), "PublisherManifests", + [this](PublicKey const& pubKey) + { + return validators().trustedPublisher (pubKey); + }); stopped (); } @@ -1013,9 +1044,6 @@ bool ApplicationImp::setup() Pathfinder::initPathTable(); - m_ledgerMaster->setMinValidations ( - config_->VALIDATION_QUORUM, config_->LOCK_QUORUM); - auto const startUp = config_->START_UP; if (startUp == Config::FRESH) { @@ -1062,15 +1090,26 @@ bool ApplicationImp::setup() return false; } - if (!validators_->load (config().section (SECTION_VALIDATORS))) + if (!validatorManifests_->load ( + getWalletDB (), "ValidatorManifests", + config().section (SECTION_VALIDATION_MANIFEST).lines())) { - JLOG(m_journal.fatal()) << "Invalid entry in validator configuration."; + JLOG(m_journal.fatal()) << "Invalid configured validator manifest."; return false; } - if (validators_->size () == 0 && !config_->standalone()) + publisherManifests_->load ( + getWalletDB (), "PublisherManifests"); + + // Setup trusted validators + if (!validators_->load ( + config_->VALIDATION_PUB, + config().section (SECTION_VALIDATORS).values (), + config().section (SECTION_VALIDATOR_LIST_KEYS).values ())) { - JLOG(m_journal.warn()) << "No validators are configured."; + JLOG(m_journal.fatal()) << + "Invalid entry in validator configuration."; + return false; } m_nodeStore->tune (config_->getSize (siNodeCacheSize), config_->getSize (siNodeCacheAge)); @@ -1101,8 +1140,6 @@ bool ApplicationImp::setup() return false; } - m_overlay->setupValidatorKeyManifests (*config_, getWalletDB ()); - { auto setup = setup_ServerHandler( *config_, diff --git a/src/ripple/app/main/Application.h b/src/ripple/app/main/Application.h index 551457cf1..8f6d4ee5a 100644 --- a/src/ripple/app/main/Application.h +++ b/src/ripple/app/main/Application.h @@ -50,6 +50,7 @@ class InboundTransactions; class AcceptedLedger; class LedgerMaster; class LoadManager; +class ManifestCache; class NetworkOPs; class OpenLedger; class OrderBookDB; @@ -120,6 +121,8 @@ public: virtual Overlay& overlay () = 0; virtual TxQ& getTxQ() = 0; virtual ValidatorList& validators () = 0; + virtual ManifestCache& validatorManifests () = 0; + virtual ManifestCache& publisherManifests () = 0; virtual Cluster& cluster () = 0; virtual Validations& getValidations () = 0; virtual NodeStore::Database& getNodeStore () = 0; diff --git a/src/ripple/app/main/DBInit.cpp b/src/ripple/app/main/DBInit.cpp index faedc6a9d..eb8170c20 100644 --- a/src/ripple/app/main/DBInit.cpp +++ b/src/ripple/app/main/DBInit.cpp @@ -135,6 +135,10 @@ const char* WalletDBInit[] = RawData BLOB NOT NULL \ );", + "CREATE TABLE IF NOT EXISTS PublisherManifests ( \ + RawData BLOB NOT NULL \ + );", + // Old tables that were present in wallet.db and we // no longer need or use. "DROP INDEX IF EXISTS SeedNodeNext;", diff --git a/src/ripple/app/main/Main.cpp b/src/ripple/app/main/Main.cpp index 6ad534b40..46fa8aaa4 100644 --- a/src/ripple/app/main/Main.cpp +++ b/src/ripple/app/main/Main.cpp @@ -218,7 +218,7 @@ int run (int argc, char** argv) ("unittest-arg", po::value ()->implicit_value (""), "Supplies argument to unit tests.") ("parameters", po::value< vector > (), "Specify comma separated parameters.") ("quiet,q", "Reduce diagnotics.") - ("quorum", po::value (), "Set the validation quorum.") + ("quorum", po::value (), "Override the minimum validation quorum.") ("silent", "No output to the console after startup.") ("verbose,v", "Verbose logging.") ("load", "Load the current ledger from the local DB.") @@ -338,9 +338,6 @@ int run (int argc, char** argv) } config->START_UP = Config::NETWORK; - - if (config->VALIDATION_QUORUM < 2) - config->VALIDATION_QUORUM = 2; } // Override the RPC destination IP address. This must @@ -385,11 +382,7 @@ int run (int argc, char** argv) { try { - config->VALIDATION_QUORUM = vm["quorum"].as (); - config->LOCK_QUORUM = true; - - if (config->VALIDATION_QUORUM < 0) - Throw (""); + config->VALIDATION_QUORUM = vm["quorum"].as (); } catch(std::exception const&) { diff --git a/src/ripple/overlay/impl/Manifest.h b/src/ripple/app/misc/Manifest.h similarity index 50% rename from src/ripple/overlay/impl/Manifest.h rename to src/ripple/app/misc/Manifest.h index 3d9bce89e..8e2e122be 100644 --- a/src/ripple/overlay/impl/Manifest.h +++ b/src/ripple/app/misc/Manifest.h @@ -17,14 +17,11 @@ */ //============================================================================== -#ifndef RIPPLE_OVERLAY_MANIFEST_H_INCLUDED -#define RIPPLE_OVERLAY_MANIFEST_H_INCLUDED +#ifndef RIPPLE_APP_MISC_MANIFEST_H_INCLUDED +#define RIPPLE_APP_MISC_MANIFEST_H_INCLUDED -#include -#include #include #include -#include #include #include #include @@ -57,13 +54,10 @@ namespace ripple { There are two stores of information within rippled related to manifests. An instance of ManifestCache stores, for each trusted validator, (a) its master public key, and (b) the most senior of all valid manifests it has - seen for that validator, if any. On startup, the [validator_keys] config - entries are used to prime the manifest cache with the trusted master keys. - At this point, the manifest cache has all the entries it will ever have, - but none of them have manifests. The [validation_manifest] config entry - (which is the manifest for this validator) is then decoded and added to - the manifest cache. Other manifests are added as "gossip" is received - from rippled peers. + seen for that validator, if any. On startup, the [validation_manifest] + config entry (which is the manifest for this validator) is decoded and + added to the manifest cache. Other manifests are added as "gossip" is + received from rippled peers. The other data store (which does not involve manifests per se) contains the set of active ephemeral validator keys. Keys are added to the set @@ -97,127 +91,219 @@ struct Manifest Manifest(Manifest&& other) = default; Manifest& operator=(Manifest&& other) = default; + inline bool + operator==(Manifest const& rhs) const + { + return sequence == rhs.sequence && masterKey == rhs.masterKey && + signingKey == rhs.signingKey && serialized == rhs.serialized; + } + + inline bool + operator!=(Manifest const& rhs) const + { + return !(*this == rhs); + } + + /** Constructs Manifest from serialized string + + @param s Serialized manifest string + + @return `boost::none` if string is invalid + */ + static boost::optional make_Manifest(std::string s); + + /// Returns `true` if manifest signature is valid bool verify () const; + + /// Returns hash of serialized manifest data uint256 hash () const; + + /// Returns `true` if manifest revokes master key bool revoked () const; + + /// Returns manifest signature Blob getSignature () const; /// Returns manifest master key signature Blob getMasterSignature () const; }; -boost::optional make_Manifest(std::string s); - -inline bool operator==(Manifest const& lhs, Manifest const& rhs) -{ - return lhs.serialized == rhs.serialized && lhs.masterKey == rhs.masterKey && - lhs.signingKey == rhs.signingKey && lhs.sequence == rhs.sequence; -} - -inline bool operator!=(Manifest const& lhs, Manifest const& rhs) -{ - return !(lhs == rhs); -} - enum class ManifestDisposition { - accepted = 0, // everything checked out + /// Manifest is valid + accepted = 0, - untrusted, // manifest declares a master key we don't trust - stale, // trusted master key, but seq is too old - invalid, // trusted and timely, but invalid signature + /// Sequence is too old + stale, + + /// Timely, but invalid signature + invalid }; class DatabaseCon; + /** Remembers manifests with the highest sequence number. */ class ManifestCache { private: - struct MappedType - { - MappedType() = default; - MappedType(MappedType&&) = default; - MappedType& operator=(MappedType&&) = default; + beast::Journal mutable j_; + std::mutex apply_mutex_; + std::mutex mutable read_mutex_; - MappedType(std::string comment, - std::string serialized, - PublicKey pk, PublicKey spk, std::uint32_t seq) - :comment (std::move(comment)) - { - m.emplace (std::move(serialized), std::move(pk), std::move(spk), - seq); - } + /** Active manifests stored by master public key. */ + hash_map map_; - std::string comment; - boost::optional m; - }; - - using MapType = hash_map; - - mutable std::mutex mutex_; - MapType map_; - - ManifestDisposition - canApply (PublicKey const& pk, std::uint32_t seq, - beast::Journal journal) const; + /** Master public keys stored by current ephemeral public key. */ + hash_map signingToMasterKeys_; public: - ManifestCache() = default; - ManifestCache (ManifestCache const&) = delete; - ManifestCache& operator= (ManifestCache const&) = delete; - ~ManifestCache() = default; + explicit + ManifestCache (beast::Journal j = beast::Journal()) + : j_ (j) + { + }; - bool loadValidatorKeys( - Section const& keys, - beast::Journal journal); + /** Returns master key's current signing key. - void configManifest (Manifest m, ValidatorList& unl, beast::Journal journal); + @param pk Master public key - /** Determines whether a node is in the trusted master key list */ + @return pk if no known signing key from a manifest + + @par Thread Safety + + May be called concurrently + */ + PublicKey + getSigningKey (PublicKey const& pk) const; + + /** Returns ephemeral signing key's master public key. + + @param pk Ephemeral signing public key + + @return pk if signing key is not in a valid manifest + + @par Thread Safety + + May be called concurrently + */ + PublicKey + getMasterKey (PublicKey const& pk) const; + + /** Returns `true` if master key has been revoked in a manifest. + + @param pk Master public key + + @par Thread Safety + + May be called concurrently + */ bool - trusted ( - PublicKey const& identity) const; + revoked (PublicKey const& pk) const; - void addTrustedKey (PublicKey const& pk, std::string comment); + /** Add manifest to cache. - /** The number of installed trusted master keys */ - std::size_t - size () const; + @param m Manifest to add + @return `ManifestDisposition::accepted` if successful, or + `stale` or `invalid` otherwise + + @par Thread Safety + + May be called concurrently + */ ManifestDisposition applyManifest ( - Manifest m, ValidatorList& unl, beast::Journal journal); + Manifest m); + /** Populate manifest cache with manifests in database and config. + + @param dbCon Database connection with dbTable + + @param dbTable Database table + + @param configManifest Base64 encoded manifest for local node's + validator keys + + @par Thread Safety + + May be called concurrently + */ + bool load ( + DatabaseCon& dbCon, std::string const& dbTable, + std::vector const& configManifest); + + /** Populate manifest cache with manifests in database. + + @param dbCon Database connection with dbTable + + @param dbTable Database table + + @par Thread Safety + + May be called concurrently + */ void load ( - DatabaseCon& dbCon, ValidatorList& unl, beast::Journal journal); - void save (DatabaseCon& dbCon) const; + DatabaseCon& dbCon, std::string const& dbTable); - // A "for_each" for populated manifests only + /** Save cached manifests to database. + + @param dbCon Database connection with `ValidatorManifests` table + + @param isTrusted Function that returns true if manifest is trusted + + @par Thread Safety + + May be called concurrently + */ + void save ( + DatabaseCon& dbCon, std::string const& dbTable, + std::function isTrusted); + + /** Invokes the callback once for every populated manifest. + + @note Undefined behavior results when calling ManifestCache members from + within the callback + + @param f Function called for each manifest + + @par Thread Safety + + May be called concurrently + */ template void for_each_manifest(Function&& f) const { - std::lock_guard lock (mutex_); - for (auto const& e : map_) + std::lock_guard lock{read_mutex_}; + for (auto const& m : map_) { - if (auto const& m = e.second.m) - f(*m); + f(m.second); } } - // A "for_each" for populated manifests only - // The PreFun is called with the maximum number of - // times EachFun will be called (useful for memory allocations) + /** Invokes the callback once for every populated manifest. + + @note Undefined behavior results when calling ManifestCache members from + within the callback + + @param pf Pre-function called with the maximum number of times f will be + called (useful for memory allocations) + + @param f Function called for each manifest + + @par Thread Safety + + May be called concurrently + */ template void for_each_manifest(PreFun&& pf, EachFun&& f) const { - std::lock_guard lock (mutex_); + std::lock_guard lock{read_mutex_}; pf(map_.size ()); - for (auto const& e : map_) + for (auto const& m : map_) { - if (auto const& m = e.second.m) - f(*m); + f(m.second); } } }; diff --git a/src/ripple/app/misc/NetworkOPs.cpp b/src/ripple/app/misc/NetworkOPs.cpp index f597a7815..ca6c9bc46 100644 --- a/src/ripple/app/misc/NetworkOPs.cpp +++ b/src/ripple/app/misc/NetworkOPs.cpp @@ -1202,8 +1202,8 @@ public: if (nodesUsing > v.nodesUsing) return true; - if (nodesUsing < v.nodesUsing) return - false; + if (nodesUsing < v.nodesUsing) + return false; return highNodeUsing > v.highNodeUsing; } @@ -1485,6 +1485,9 @@ bool NetworkOPsImp::beginConsensus (uint256 const& networkClosed) assert (closingInfo.parentHash == m_ledgerMaster.getClosedLedger()->info().hash); + app_.validators().onConsensusStart ( + app_.getValidations().getCurrentPublicKeys ()); + mConsensus->startRound ( *mLedgerConsensus, networkClosed, @@ -2032,7 +2035,8 @@ Json::Value NetworkOPsImp::getServerInfo (bool human, bool admin) if (mNeedNetworkLedger) info[jss::network_ledger] = "waiting"; - info[jss::validation_quorum] = m_ledgerMaster.getMinValidations (); + info[jss::validation_quorum] = static_cast( + app_.validators ().quorum ()); info[jss::io_latency_ms] = static_cast ( app_.getIOLatency().count()); @@ -2050,7 +2054,7 @@ Json::Value NetworkOPsImp::getServerInfo (bool human, bool admin) s.reserve (Manifest::textLength); for (auto const& line : validation_manifest.lines()) s += beast::rfc2616::trim(line); - if (auto mo = make_Manifest (beast::detail::base64_decode(s))) + if (auto mo = Manifest::make_Manifest (beast::detail::base64_decode(s))) { Json::Value valManifest = Json::objectValue; valManifest [jss::master_key] = toBase58 ( diff --git a/src/ripple/app/misc/Validations.cpp b/src/ripple/app/misc/Validations.cpp index 2b477baac..5bb708be6 100644 --- a/src/ripple/app/misc/Validations.cpp +++ b/src/ripple/app/misc/Validations.cpp @@ -88,9 +88,12 @@ private: bool addValidation (STValidation::ref val, std::string const& source) override { auto signer = val->getSignerPublic (); + auto hash = val->getLedgerHash (); bool isCurrent = current (val); - if (!val->isTrusted() && app_.validators().trusted (signer)) + auto pubKey = app_.validators ().getTrustedKey (signer); + + if (!val->isTrusted() && pubKey) val->setTrusted(); if (!val->isTrusted ()) @@ -98,45 +101,71 @@ private: JLOG (j_.trace()) << "Node " << toBase58 (TokenType::TOKEN_NODE_PUBLIC, signer) << " not in UNL st=" << val->getSignTime().time_since_epoch().count() << - ", hash=" << val->getLedgerHash () << + ", hash=" << hash << ", shash=" << val->getSigningHash () << " src=" << source; } - auto hash = val->getLedgerHash (); - auto node = val->getNodeID (); + if (! pubKey) + pubKey = app_.validators ().getListedKey (signer); - if (val->isTrusted () && isCurrent) + if (isCurrent && + (val->isTrusted () || pubKey)) { ScopedLockType sl (mLock); - if (!findCreateSet (hash)->insert (std::make_pair (node, val)).second) + if (!findCreateSet (hash)->insert ( + std::make_pair (*pubKey, val)).second) return false; - auto it = mCurrentValidations.find (node); + auto it = mCurrentValidations.find (*pubKey); if (it == mCurrentValidations.end ()) { // No previous validation from this validator - mCurrentValidations.emplace (node, val); + mCurrentValidations.emplace (*pubKey, val); } else if (!it->second) { // Previous validation has expired it->second = val; } - else if (val->getSignTime () > it->second->getSignTime ()) - { - // This is a newer validation - val->setPreviousHash (it->second->getLedgerHash ()); - mStaleValidations.push_back (it->second); - it->second = val; - condWrite (); - } else { - // We already have a newer validation from this source - isCurrent = false; + auto const oldSeq = (*it->second)[~sfLedgerSequence]; + auto const newSeq = (*val)[~sfLedgerSequence]; + + if (oldSeq && newSeq && *oldSeq == *newSeq) + { + JLOG (j_.warn()) << + "Trusted node " << + toBase58 (TokenType::TOKEN_NODE_PUBLIC, *pubKey) << + " published multiple validations for ledger " << + *oldSeq; + + // Remove current validation for the revoked signing key + if (signer != it->second->getSignerPublic()) + { + auto set = findSet (it->second->getLedgerHash ()); + if (set) + set->erase (*pubKey); + } + } + + if (val->getSignTime () > it->second->getSignTime () || + signer != it->second->getSignerPublic()) + { + // This is either a newer validation or a new signing key + val->setPreviousHash (it->second->getLedgerHash ()); + mStaleValidations.push_back (it->second); + it->second = val; + condWrite (); + } + else + { + // We already have a newer validation from this source + isCurrent = false; + } } } @@ -185,9 +214,10 @@ private: (val->getSeenTime() < (now + VALIDATION_VALID_LOCAL))); } - int getTrustedValidationCount (uint256 const& ledger) override + std::size_t + getTrustedValidationCount (uint256 const& ledger) override { - int trusted = 0; + std::size_t trusted = 0; ScopedLockType sl (mLock); auto set = findSet (ledger); @@ -292,6 +322,37 @@ private: return ret; } + hash_set getCurrentPublicKeys () override + { + hash_set ret; + + ScopedLockType sl (mLock); + auto it = mCurrentValidations.begin (); + + while (it != mCurrentValidations.end ()) + { + if (!it->second) // contains no record + it = mCurrentValidations.erase (it); + else if (! current (it->second)) + { + // contains a stale record + mStaleValidations.push_back (it->second); + it->second.reset (); + condWrite (); + it = mCurrentValidations.erase (it); + } + else + { + // contains a live record + ret.insert (it->first); + + ++it; + } + } + + return ret; + } + LedgerToValidationCounter getCurrentValidations ( uint256 currentLedger, uint256 priorLedger, @@ -317,6 +378,8 @@ private: condWrite (); it = mCurrentValidations.erase (it); } + else if (! it->second->isTrusted()) + ++it; else if (! it->second->isFieldPresent (sfLedgerSequence) || (it->second->getFieldU32 (sfLedgerSequence) >= cutoffBefore)) { diff --git a/src/ripple/app/misc/Validations.h b/src/ripple/app/misc/Validations.h index 47af858b5..af6285d31 100644 --- a/src/ripple/app/misc/Validations.h +++ b/src/ripple/app/misc/Validations.h @@ -31,7 +31,7 @@ namespace ripple { // VFALCO TODO rename and move these type aliases into the Validations interface // nodes validating and highest node ID validating -using ValidationSet = hash_map; +using ValidationSet = hash_map; using ValidationCounter = std::pair; using LedgerToValidationCounter = hash_map; @@ -47,7 +47,7 @@ public: virtual ValidationSet getValidations (uint256 const& ledger) = 0; - virtual int getTrustedValidationCount (uint256 const& ledger) = 0; + virtual std::size_t getTrustedValidationCount (uint256 const& ledger) = 0; /** Returns fees reported by trusted validators in the given ledger. */ virtual @@ -57,6 +57,8 @@ public: virtual int getNodesAfter (uint256 const& ledger) = 0; virtual int getLoadRatio (bool overLoaded) = 0; + virtual hash_set getCurrentPublicKeys () = 0; + // VFALCO TODO make a type alias for this ugly return value! virtual LedgerToValidationCounter getCurrentValidations ( uint256 currentLedger, uint256 previousLedger, diff --git a/src/ripple/app/misc/ValidatorList.h b/src/ripple/app/misc/ValidatorList.h index e8eee389c..2ca4212e5 100644 --- a/src/ripple/app/misc/ValidatorList.h +++ b/src/ripple/app/misc/ValidatorList.h @@ -20,102 +20,437 @@ #ifndef RIPPLE_APP_MISC_VALIDATORLIST_H_INCLUDED #define RIPPLE_APP_MISC_VALIDATORLIST_H_INCLUDED -#include +#include #include #include +#include +#include #include -#include -#include -#include +#include +#include +#include +#include #include +#include namespace ripple { +enum class ListDisposition +{ + /// List is valid + accepted = 0, + + /// List version is not supported + unsupported_version, + + /// List signed by untrusted publisher key + untrusted, + + /// Trusted publisher key, but seq is too old + stale, + + /// Invalid format or signature + invalid, +}; + +/** + Trusted Validators List + ----------------------- + + Rippled accepts ledger proposals and validations from trusted validator + nodes. A ledger is considered fully-validated once the number of received + trusted validations for a ledger meets or exceeds a quorum value. + + This class manages the set of validation public keys the local rippled node + trusts. The list of trusted keys is populated using the keys listed in the + configuration file as well as lists signed by trusted publishers. The + trusted publisher public keys are specified in the config. + + New lists are expected to include the following data: + + @li @c "blob": Base64-encoded JSON string containing a @c "sequence", @c + "expiration", and @c "validators" field. @c "expiration" contains the + Ripple timestamp (seconds since January 1st, 2000 (00:00 UTC)) for when + the list expires. @c "validators" contains an array of objects with a + @c "validation_public_key" field. + @c "validation_public_key" should be the hex-encoded master public key. + + @li @c "manifest": Base64-encoded serialization of a manifest containing the + publisher's master and signing public keys. + + @li @c "signature": Hex-encoded signature of the blob using the publisher's + signing key. + + @li @c "version": 1 + + Individual validator lists are stored separately by publisher. The number of + lists on which a validator's public key appears is also tracked. + + The list of trusted validation public keys is reset at the start of each + consensus round to take into account the latest known lists as well as the + set of validators from whom validations are being received. Listed + validation public keys are shuffled and then sorted by the number of lists + they appear on. (The shuffling makes the order/rank of validators with the + same number of listings non-deterministic.) A quorum value is calculated for + the new trusted validator list. If there is only one list, all listed keys + are trusted. Otherwise, the trusted list size is set to 125% of the quorum. +*/ class ValidatorList { -private: - /** The non-ephemeral public keys from the configuration file. */ - hash_map permanent_; + struct PublisherList + { + bool available; + std::vector list; + std::size_t sequence; + std::size_t expiration; + }; - /** The ephemeral public keys from manifests. */ - hash_map ephemeral_; + ManifestCache& validatorManifests_; + ManifestCache& publisherManifests_; + TimeKeeper& timeKeeper_; + beast::Journal j_; + boost::shared_mutex mutable mutex_; - std::mutex mutable mutex_; - beast::Journal mutable j_; + std::atomic quorum_; + boost::optional minimumQuorum_; + + // Published lists stored by publisher master public key + hash_map publisherLists_; + + // Listed master public keys with the number of lists they appear on + hash_map keyListings_; + + // The current list of trusted master keys + hash_set trustedKeys_; + + PublicKey localPubKey_; public: - explicit - ValidatorList (beast::Journal j); + ValidatorList ( + ManifestCache& validatorManifests, + ManifestCache& publisherManifests, + TimeKeeper& timeKeeper, + beast::Journal j, + boost::optional minimumQuorum = boost::none); + ~ValidatorList (); - /** Determines whether a node is in the UNL - @return boost::none if the node isn't a member, - otherwise, the comment associated with the - node (which may be an empty string). + /** Load configured trusted keys. + + @param localSigningKey This node's validation public key + + @param configKeys List of trusted keys from config. Each entry consists + of a base58 encoded validation public key, optionally followed by a + comment. + + @param publisherKeys List of trusted publisher public keys. Each entry + contains a base58 encoded account public key. + + @par Thread Safety + + May be called concurrently + + @return `false` if an entry is invalid or unparsable */ - boost::optional - member ( - PublicKey const& identity) const; + bool + load ( + PublicKey const& localSigningKey, + std::vector const& configKeys, + std::vector const& publisherKeys); - /** Determines whether a node is in the UNL */ + /** Apply published list of public keys + + @param manifest base64-encoded publisher key manifest + + @param blob base64-encoded json containing published validator list + + @param signature Signature of the decoded blob + + @param version Version of published list format + + @return `ListDisposition::accepted` if list was successfully applied + + @par Thread Safety + + May be called concurrently + */ + ListDisposition + applyList ( + std::string const& manifest, + std::string const& blob, + std::string const& signature, + std::uint32_t version); + + /** Update trusted keys + + Reset the trusted keys based on latest manifests, received validations, + and lists. + + @param seenValidators Set of public keys used to sign recently + received validations + + @par Thread Safety + + May be called concurrently + */ + template + void + onConsensusStart ( + KeySet const& seenValidators); + + /** Get quorum value for current trusted key set + + The quorum is the minimum number of validations needed for a ledger to + be fully validated. It can change when the set of trusted validation + keys is updated (at the start of each consensus round) and primarily + depends on the number of trusted keys. + + @par Thread Safety + + May be called concurrently + + @return quorum value + */ + std::size_t + quorum () const + { + return quorum_; + }; + + /** Returns `true` if public key is trusted + + @param identity Validation public key + + @par Thread Safety + + May be called concurrently + */ bool trusted ( PublicKey const& identity) const; - /** Insert a short-term validator key published in a manifest. */ + /** Returns `true` if public key is included on any lists + + @param identity Validation public key + + @par Thread Safety + + May be called concurrently + */ bool - insertEphemeralKey ( - PublicKey const& identity, - std::string const& comment); + listed ( + PublicKey const& identity) const; - /** Remove a short-term validator revoked in a manifest. */ + /** Returns master public key if public key is trusted + + @param identity Validation public key + + @return `boost::none` if key is not trusted + + @par Thread Safety + + May be called concurrently + */ + boost::optional + getTrustedKey ( + PublicKey const& identity) const; + + /** Returns listed master public if public key is included on any lists + + @param identity Validation public key + + @return `boost::none` if key is not listed + + @par Thread Safety + + May be called concurrently + */ + boost::optional + getListedKey ( + PublicKey const& identity) const; + + /** Returns `true` if public key is a trusted publisher + + @param identity Publisher public key + + @par Thread Safety + + May be called concurrently + */ bool - removeEphemeralKey ( - PublicKey const& identity); + trustedPublisher ( + PublicKey const& identity) const; - /** Insert a long-term validator key. */ - bool - insertPermanentKey ( - PublicKey const& identity, - std::string const& comment); + /** Invokes the callback once for every listed validation public key. - /** Remove a long-term validator key. */ - bool - removePermanentKey ( - PublicKey const& identity); - - /** The number of installed permanent and ephemeral keys */ - std::size_t - size () const; - - /** Invokes the callback once for every node in the UNL. - @note You are not allowed to insert or remove any - nodes in the UNL from within the callback. + @note Undefined behavior results when calling ValidatorList members from + within the callback The arguments passed into the lambda are: - - The public key of the validator; - - A (possibly empty) comment. - - A boolean indicating whether this is a - permanent or ephemeral key; + + @li The validation public key + + @li A boolean indicating whether this is a trusted key + + @par Thread Safety + + May be called concurrently */ void - for_each ( - std::function func) const; + for_each_listed ( + std::function func) const; - /** Load the list of trusted validators. +private: + /** Check response for trusted valid published list - The section contains entries consisting of a base58 - encoded validator public key, optionally followed by - a comment. + @return `ListDisposition::accepted` if list can be applied - @return false if an entry could not be parsed or - contained an invalid validator public key, - true otherwise. + @par Thread Safety + + Calling public member function is expected to lock mutex + */ + ListDisposition + verify ( + Json::Value& list, + PublicKey& pubKey, + std::string const& manifest, + std::string const& blob, + std::string const& signature); + + /** Stop trusting publisher's list of keys. + + @param publisherKey Publisher public key + + @return `false` if key was not trusted + + @par Thread Safety + + Calling public member function is expected to lock mutex */ bool - load ( - Section const& validators); + removePublisherList (PublicKey const& publisherKey); + }; +//------------------------------------------------------------------------------ + +template +void +ValidatorList::onConsensusStart ( + KeySet const& seenValidators) +{ + boost::unique_lock lock{mutex_}; + + // Check that lists from all configured publishers are available + bool allListsAvailable = true; + + for (auto const& list : publisherLists_) + { + // Remove any expired published lists + if (list.second.expiration && + list.second.expiration <= + timeKeeper_.now().time_since_epoch().count()) + removePublisherList (list.first); + else if (! list.second.available) + allListsAvailable = false; + } + + std::multimap rankedKeys; + bool localKeyListed = false; + + // "Iterate" the listed keys in random order so that the rank of multiple + // keys with the same number of listings is not deterministic + std::vector indexes (keyListings_.size()); + std::iota (indexes.begin(), indexes.end(), 0); + std::shuffle (indexes.begin(), indexes.end(), crypto_prng()); + + for (auto const& index : indexes) + { + auto const& val = std::next (keyListings_.begin(), index); + + if (validatorManifests_.revoked (val->first)) + continue; + + if (val->first == localPubKey_) + { + localKeyListed = val->second > 1; + rankedKeys.insert ( + std::pair( + std::numeric_limits::max(), localPubKey_)); + } + // If no validations are being received, use all validators. + // Otherwise, do not use validators whose validations aren't being received + else if (seenValidators.empty() || + seenValidators.find (val->first) != seenValidators.end ()) + { + rankedKeys.insert ( + std::pair(val->second, val->first)); + } + } + + // This quorum guarantees sufficient overlap with the trusted sets of other + // nodes using the same set of published lists. + std::size_t quorum = keyListings_.size()/2 + 1; + + // Increment the quorum to prevent two unlisted validators using the same + // even number of listed validators from forking. + if (localPubKey_.size() && ! localKeyListed && + rankedKeys.size () > 1 && keyListings_.size () % 2 != 0) + ++quorum; + + JLOG (j_.debug()) << + rankedKeys.size() << " of " << keyListings_.size() << + " listed validators eligible for inclusion in the trusted set"; + + auto size = rankedKeys.size(); + + // Use all eligible keys if there is only one trusted list. + if (publisherLists_.size() == 1) + { + // Raise the quorum to 80% of the trusted set + std::size_t const targetQuorum = std::ceil (size * 0.8); + if (targetQuorum > quorum) + quorum = targetQuorum; + } + else + { + // reduce the trusted set size so that the quorum represents + // at least 80% + size = quorum * 1.25; + } + + if (minimumQuorum_ && (seenValidators.empty() || + rankedKeys.size() < quorum)) + quorum = *minimumQuorum_; + + // Do not use achievable quorum until lists from all configured + // publishers are available + else if (! allListsAvailable) + quorum = std::numeric_limits::max(); + + trustedKeys_.clear(); + quorum_ = quorum; + + for (auto const& val : boost::adaptors::reverse (rankedKeys)) + { + if (size <= trustedKeys_.size()) + break; + + trustedKeys_.insert (val.second); + } + + JLOG (j_.debug()) << + "Using quorum of " << quorum_ << " for new set of " << + trustedKeys_.size() << " trusted validators"; + + if (trustedKeys_.size() < quorum_) + { + JLOG (j_.warn()) << + "New quorum of " << quorum_ << + " exceeds the number of trusted validators (" << + trustedKeys_.size() << ")"; + } } +} // ripple + #endif diff --git a/src/ripple/app/misc/impl/Manifest.cpp b/src/ripple/app/misc/impl/Manifest.cpp new file mode 100644 index 000000000..61ce669e4 --- /dev/null +++ b/src/ripple/app/misc/impl/Manifest.cpp @@ -0,0 +1,373 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012, 2013 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 +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +boost::optional +Manifest::make_Manifest (std::string s) +{ + try + { + STObject st (sfGeneric); + SerialIter sit (s.data (), s.size ()); + st.set (sit); + auto const opt_pk = get(st, sfPublicKey); + auto const opt_spk = get(st, sfSigningPubKey); + auto const opt_seq = get (st, sfSequence); + auto const opt_sig = get (st, sfSignature); + auto const opt_msig = get (st, sfMasterSignature); + if (!opt_pk || !opt_spk || !opt_seq || !opt_sig || !opt_msig) + { + return boost::none; + } + return Manifest (std::move (s), *opt_pk, *opt_spk, *opt_seq); + } + catch (std::exception const&) + { + return boost::none; + } +} + +template +Stream& +logMftAct ( + Stream& s, + std::string const& action, + PublicKey const& pk, + std::uint32_t seq) +{ + s << "Manifest: " << action << + ";Pk: " << toBase58 (TokenType::TOKEN_NODE_PUBLIC, pk) << + ";Seq: " << seq << ";"; + return s; +} + +template +Stream& logMftAct ( + Stream& s, + std::string const& action, + PublicKey const& pk, + std::uint32_t seq, + std::uint32_t oldSeq) +{ + s << "Manifest: " << action << + ";Pk: " << toBase58 (TokenType::TOKEN_NODE_PUBLIC, pk) << + ";Seq: " << seq << + ";OldSeq: " << oldSeq << ";"; + return s; +} + +Manifest::Manifest (std::string s, + PublicKey pk, + PublicKey spk, + std::uint32_t seq) + : serialized (std::move (s)) + , masterKey (std::move (pk)) + , signingKey (std::move (spk)) + , sequence (seq) +{ +} + +bool Manifest::verify () const +{ + STObject st (sfGeneric); + SerialIter sit (serialized.data (), serialized.size ()); + st.set (sit); + if (! ripple::verify (st, HashPrefix::manifest, signingKey)) + return false; + + return ripple::verify ( + st, HashPrefix::manifest, masterKey, sfMasterSignature); +} + +uint256 Manifest::hash () const +{ + STObject st (sfGeneric); + SerialIter sit (serialized.data (), serialized.size ()); + st.set (sit); + return st.getHash (HashPrefix::manifest); +} + +bool Manifest::revoked () const +{ + /* + The maximum possible sequence number means that the master key + has been revoked. + */ + return sequence == std::numeric_limits::max (); +} + +Blob Manifest::getSignature () const +{ + STObject st (sfGeneric); + SerialIter sit (serialized.data (), serialized.size ()); + st.set (sit); + return st.getFieldVL (sfSignature); +} + +Blob Manifest::getMasterSignature () const +{ + STObject st (sfGeneric); + SerialIter sit (serialized.data (), serialized.size ()); + st.set (sit); + return st.getFieldVL (sfMasterSignature); +} + +PublicKey +ManifestCache::getSigningKey (PublicKey const& pk) const +{ + std::lock_guard lock{read_mutex_}; + auto const iter = map_.find (pk); + + if (iter != map_.end () && !iter->second.revoked ()) + return iter->second.signingKey; + + return pk; +} + +PublicKey +ManifestCache::getMasterKey (PublicKey const& pk) const +{ + std::lock_guard lock{read_mutex_}; + auto const iter = signingToMasterKeys_.find (pk); + + if (iter != signingToMasterKeys_.end ()) + return iter->second; + + return pk; +} + +bool +ManifestCache::revoked (PublicKey const& pk) const +{ + std::lock_guard lock{read_mutex_}; + auto const iter = map_.find (pk); + + if (iter != map_.end ()) + return iter->second.revoked (); + + return false; +} + +ManifestDisposition +ManifestCache::applyManifest (Manifest m) +{ + std::lock_guard applyLock{apply_mutex_}; + + /* + before we spend time checking the signature, make sure the + sequence number is newer than any we have. + */ + auto const iter = map_.find(m.masterKey); + + if (iter != map_.end() && + m.sequence <= iter->second.sequence) + { + /* + A manifest was received for a validator we're tracking, but + its sequence number is not higher than the one already stored. + This will happen normally when a peer without the latest gossip + connects. + */ + if (auto stream = j_.debug()) + logMftAct(stream, "Stale", m.masterKey, m.sequence, iter->second.sequence); + return ManifestDisposition::stale; // not a newer manifest, ignore + } + + if (! m.verify()) + { + /* + A manifest's signature is invalid. + This shouldn't happen normally. + */ + if (auto stream = j_.warn()) + logMftAct(stream, "Invalid", m.masterKey, m.sequence); + return ManifestDisposition::invalid; + } + + std::lock_guard readLock{read_mutex_}; + + bool const revoked = m.revoked(); + + if (iter == map_.end ()) + { + /* + This is the first received manifest for a trusted master key + (possibly our own). This only happens once per validator per + run. + */ + if (auto stream = j_.info()) + logMftAct(stream, "AcceptedNew", m.masterKey, m.sequence); + + if (! revoked) + signingToMasterKeys_[m.signingKey] = m.masterKey; + + map_.emplace (std::make_pair(m.masterKey, std::move (m))); + } + else + { + /* + An ephemeral key was revoked and superseded by a new key. + This is expected, but should happen infrequently. + */ + if (auto stream = j_.info()) + logMftAct(stream, "AcceptedUpdate", + m.masterKey, m.sequence, iter->second.sequence); + + signingToMasterKeys_.erase (iter->second.signingKey); + + if (! revoked) + signingToMasterKeys_[m.signingKey] = m.masterKey; + + iter->second = std::move (m); + } + + if (revoked) + { + /* + A validator master key has been compromised, so its manifests + are now untrustworthy. In order to prevent us from accepting + a forged manifest signed by the compromised master key, store + this manifest, which has the highest possible sequence number + and therefore can't be superseded by a forged one. + */ + if (auto stream = j_.warn()) + logMftAct(stream, "Revoked", m.masterKey, m.sequence); + } + + return ManifestDisposition::accepted; +} + +void +ManifestCache::load ( + DatabaseCon& dbCon, std::string const& dbTable) +{ + // Load manifests stored in database + std::string const sql = + "SELECT RawData FROM " + dbTable + ";"; + auto db = dbCon.checkoutDb (); + soci::blob sociRawData (*db); + soci::statement st = + (db->prepare << sql, + soci::into (sociRawData)); + st.execute (); + while (st.fetch ()) + { + std::string serialized; + convert (sociRawData, serialized); + if (auto mo = Manifest::make_Manifest (std::move (serialized))) + { + if (!mo->verify()) + { + JLOG(j_.warn()) + << "Unverifiable manifest in db"; + continue; + } + + applyManifest (std::move(*mo)); + } + else + { + JLOG(j_.warn()) + << "Malformed manifest in database"; + } + } +} + +bool +ManifestCache::load ( + DatabaseCon& dbCon, std::string const& dbTable, + std::vector const& configManifest) +{ + load (dbCon, dbTable); + + if (! configManifest.empty()) + { + std::string s; + s.reserve (Manifest::textLength); + for (auto const& line : configManifest) + s += beast::rfc2616::trim(line); + + auto mo = Manifest::make_Manifest (beast::detail::base64_decode(s)); + if (! mo) + { + JLOG (j_.error()) << "Malformed manifest in config"; + return false; + } + + if (mo->revoked()) + { + JLOG (j_.warn()) << + "Configured manifest revokes public key"; + } + + if (applyManifest (std::move(*mo)) == + ManifestDisposition::invalid) + { + JLOG (j_.error()) << "Manifest in config was rejected"; + return false; + } + } + + return true; +} + +void ManifestCache::save ( + DatabaseCon& dbCon, std::string const& dbTable, + std::function isTrusted) +{ + std::lock_guard lock{apply_mutex_}; + + auto db = dbCon.checkoutDb (); + + soci::transaction tr(*db); + *db << "DELETE FROM " << dbTable; + std::string const sql = + "INSERT INTO " + dbTable + " (RawData) VALUES (:rawData);"; + for (auto const& v : map_) + { + if (! isTrusted (v.second.masterKey)) + { + + JLOG(j_.info()) + << "Untrusted manifest in cache not saved to db"; + continue; + } + + // soci does not support bulk insertion of blob data + // Do not reuse blob because manifest ecdsa signatures vary in length + // but blob write length is expected to be >= the last write + soci::blob rawData(*db); + convert (v.second.serialized, rawData); + *db << sql, + soci::use (rawData); + } + tr.commit (); +} +} diff --git a/src/ripple/app/misc/impl/ValidatorList.cpp b/src/ripple/app/misc/impl/ValidatorList.cpp index 077c7aeaa..4b6fe7209 100644 --- a/src/ripple/app/misc/impl/ValidatorList.cpp +++ b/src/ripple/app/misc/impl/ValidatorList.cpp @@ -17,117 +17,39 @@ */ //============================================================================== -#include #include #include #include +#include +#include #include namespace ripple { -ValidatorList::ValidatorList (beast::Journal j) - : j_ (j) +ValidatorList::ValidatorList ( + ManifestCache& validatorManifests, + ManifestCache& publisherManifests, + TimeKeeper& timeKeeper, + beast::Journal j, + boost::optional minimumQuorum) + : validatorManifests_ (validatorManifests) + , publisherManifests_ (publisherManifests) + , timeKeeper_ (timeKeeper) + , j_ (j) + , quorum_ (minimumQuorum ? *minimumQuorum : 1) // Genesis ledger quorum + , minimumQuorum_ (minimumQuorum) { } -boost::optional -ValidatorList::member (PublicKey const& identity) const +ValidatorList::~ValidatorList() { - std::lock_guard sl (mutex_); - - auto ret = ephemeral_.find (identity); - - if (ret != ephemeral_.end()) - return ret->second; - - ret = permanent_.find (identity); - - if (ret != permanent_.end()) - return ret->second; - - return boost::none; -} - -bool -ValidatorList::trusted (PublicKey const& identity) const -{ - return static_cast (member(identity)); -} - -bool -ValidatorList::insertEphemeralKey ( - PublicKey const& identity, - std::string const& comment) -{ - std::lock_guard sl (mutex_); - - if (permanent_.find (identity) != permanent_.end()) - { - JLOG (j_.error()) << - toBase58 (TokenType::TOKEN_NODE_PUBLIC, identity) << - ": ephemeral key exists in permanent table!"; - return false; - } - - return ephemeral_.emplace (identity, comment).second; -} - -bool -ValidatorList::removeEphemeralKey ( - PublicKey const& identity) -{ - std::lock_guard sl (mutex_); - return ephemeral_.erase (identity); -} - -bool -ValidatorList::insertPermanentKey ( - PublicKey const& identity, - std::string const& comment) -{ - std::lock_guard sl (mutex_); - - if (ephemeral_.find (identity) != ephemeral_.end()) - { - JLOG (j_.error()) << - toBase58 (TokenType::TOKEN_NODE_PUBLIC, identity) << - ": permanent key exists in ephemeral table!"; - return false; - } - - return permanent_.emplace (identity, comment).second; -} - -bool -ValidatorList::removePermanentKey ( - PublicKey const& identity) -{ - std::lock_guard sl (mutex_); - return permanent_.erase (identity); -} - -std::size_t -ValidatorList::size () const -{ - std::lock_guard sl (mutex_); - return permanent_.size () + ephemeral_.size (); -} - -void -ValidatorList::for_each ( - std::function func) const -{ - std::lock_guard sl (mutex_); - - for (auto const& v : permanent_) - func (v.first, v.second, false); - for (auto const& v : ephemeral_) - func (v.first, v.second, true); } bool ValidatorList::load ( - Section const& validators) + PublicKey const& localSigningKey, + std::vector const& configKeys, + std::vector const& publisherKeys) { static boost::regex const re ( "[[:space:]]*" // skip leading whitespace @@ -141,12 +63,61 @@ ValidatorList::load ( ")?" // end optional comment block ); + boost::unique_lock read_lock{mutex_}; + JLOG (j_.debug()) << - "Loading configured validators"; + "Loading configured trusted validator list publisher keys"; std::size_t count = 0; + for (auto key : publisherKeys) + { + JLOG (j_.trace()) << + "Processing '" << key << "'"; - for (auto const& n : validators.values ()) + auto const ret = strUnHex (key); + + if (! ret.second || ! ret.first.size ()) + { + JLOG (j_.error()) << + "Invalid validator list publisher key: " << key; + return false; + } + + auto id = PublicKey(Slice{ ret.first.data (), ret.first.size() }); + + if (validatorManifests_.revoked (id)) + { + JLOG (j_.warn()) << + "Configured validator list publisher key is revoked: " << key; + continue; + } + + if (publisherLists_.count(id)) + { + JLOG (j_.warn()) << + "Duplicate validator list publisher key: " << key; + continue; + } + + publisherLists_[id].available = false; + ++count; + } + + JLOG (j_.debug()) << + "Loaded " << count << " keys"; + + localPubKey_ = validatorManifests_.getMasterKey (localSigningKey); + + // Treat local validator key as though it was listed in the config + if (localPubKey_.size()) + keyListings_.insert ({ localPubKey_, 1 }); + + JLOG (j_.debug()) << + "Loading configured validator keys"; + + count = 0; + PublicKey local; + for (auto const& n : configKeys) { JLOG (j_.trace()) << "Processing '" << n << "'"; @@ -165,20 +136,23 @@ ValidatorList::load ( if (!id) { - JLOG (j_.error()) << - "Invalid node identity: " << match[1]; + JLOG (j_.error()) << "Invalid node identity: " << match[1]; return false; } - if (trusted (*id)) + // Skip local key which was already added + if (*id == localPubKey_ || *id == localSigningKey) + continue; + + auto ret = keyListings_.insert ({*id, 1}); + if (! ret.second) { - JLOG (j_.warn()) << - "Duplicate node identity: " << match[1]; + JLOG (j_.warn()) << "Duplicate node identity: " << match[1]; continue; } - - if (insertPermanentKey(*id, trim_whitespace (match[2]))) - ++count; + publisherLists_[local].list.emplace_back (std::move(*id)); + publisherLists_[local].available = true; + ++count; } JLOG (j_.debug()) << @@ -187,4 +161,244 @@ ValidatorList::load ( return true; } + +ListDisposition +ValidatorList::applyList ( + std::string const& manifest, + std::string const& blob, + std::string const& signature, + std::uint32_t version) +{ + if (version != 1) + return ListDisposition::unsupported_version; + + boost::unique_lock lock{mutex_}; + + Json::Value list; + PublicKey pubKey; + auto const result = verify (list, pubKey, manifest, blob, signature); + if (result != ListDisposition::accepted) + return result; + + // Update publisher's list + Json::Value const& newList = list["validators"]; + publisherLists_[pubKey].available = true; + publisherLists_[pubKey].sequence = list["sequence"].asUInt (); + publisherLists_[pubKey].expiration = list["expiration"].asUInt (); + std::vector& publisherList = publisherLists_[pubKey].list; + + std::vector oldList = publisherList; + publisherList.clear (); + publisherList.reserve (newList.size ()); + for (auto const& val : newList) + { + if (val.isObject () && + val.isMember ("validation_public_key") && + val["validation_public_key"].isString ()) + { + std::pair ret (strUnHex ( + val["validation_public_key"].asString ())); + + if (! ret.second || ! ret.first.size ()) + { + JLOG (j_.error()) << + "Invalid node identity: " << + val["validation_public_key"].asString (); + } + else + { + publisherList.push_back ( + PublicKey(Slice{ ret.first.data (), ret.first.size() })); + } + } + } + + // Update keyListings_ for added and removed keys + std::sort ( + publisherList.begin (), + publisherList.end ()); + + auto iNew = publisherList.begin (); + auto iOld = oldList.begin (); + while (iNew != publisherList.end () || + iOld != oldList.end ()) + { + if (iOld == oldList.end () || + (iNew != publisherList.end () && + *iNew < *iOld)) + { + // Increment list count for added keys + ++keyListings_[*iNew]; + ++iNew; + } + else if (iNew == publisherList.end () || + (iOld != oldList.end () && *iOld < *iNew)) + { + // Decrement list count for removed keys + if (keyListings_[*iOld] == 1) + keyListings_.erase (*iOld); + else + --keyListings_[*iOld]; + ++iOld; + } + else + { + ++iNew; + ++iOld; + } + } + + if (publisherList.empty()) + { + JLOG (j_.warn()) << + "No validator keys included in valid list"; + } + + return ListDisposition::accepted; } + +ListDisposition +ValidatorList::verify ( + Json::Value& list, + PublicKey& pubKey, + std::string const& manifest, + std::string const& blob, + std::string const& signature) +{ + auto m = Manifest::make_Manifest (beast::detail::base64_decode(manifest)); + + if (! m || ! publisherLists_.count (m->masterKey)) + return ListDisposition::untrusted; + + pubKey = m->masterKey; + auto const revoked = m->revoked(); + + auto const result = publisherManifests_.applyManifest ( + std::move(*m)); + + if (revoked && result == ManifestDisposition::accepted) + { + removePublisherList (pubKey); + publisherLists_.erase (pubKey); + } + + if (revoked || result == ManifestDisposition::invalid) + return ListDisposition::untrusted; + + auto const sig = strUnHex(signature); + auto const data = beast::detail::base64_decode (blob); + if (! sig.second || + ! ripple::verify ( + publisherManifests_.getSigningKey(pubKey), + makeSlice(data), + makeSlice(sig.first))) + return ListDisposition::invalid; + + Json::Reader r; + if (! r.parse (data, list)) + return ListDisposition::invalid; + + if (list.isMember("sequence") && list["sequence"].isInt() && + list.isMember("expiration") && list["expiration"].isInt() && + list.isMember("validators") && list["validators"].isArray()) + { + auto const sequence = list["sequence"].asUInt (); + auto const expiration = list["expiration"].asUInt (); + if (sequence <= publisherLists_[pubKey].sequence || + expiration <= timeKeeper_.now().time_since_epoch().count()) + return ListDisposition::stale; + } + else + { + return ListDisposition::invalid; + } + + return ListDisposition::accepted; +} + +bool +ValidatorList::listed ( + PublicKey const& identity) const +{ + boost::shared_lock read_lock{mutex_}; + + auto const pubKey = validatorManifests_.getMasterKey (identity); + return keyListings_.find (pubKey) != keyListings_.end (); +} + +bool +ValidatorList::trusted (PublicKey const& identity) const +{ + boost::shared_lock read_lock{mutex_}; + + auto const pubKey = validatorManifests_.getMasterKey (identity); + return trustedKeys_.find (pubKey) != trustedKeys_.end(); +} + +boost::optional +ValidatorList::getListedKey ( + PublicKey const& identity) const +{ + boost::shared_lock read_lock{mutex_}; + + auto const pubKey = validatorManifests_.getMasterKey (identity); + if (keyListings_.find (pubKey) != keyListings_.end ()) + return pubKey; + return boost::none; +} + +boost::optional +ValidatorList::getTrustedKey (PublicKey const& identity) const +{ + boost::shared_lock read_lock{mutex_}; + + auto const pubKey = validatorManifests_.getMasterKey (identity); + if (trustedKeys_.find (pubKey) != trustedKeys_.end()) + return pubKey; + return boost::none; +} + +bool +ValidatorList::trustedPublisher (PublicKey const& identity) const +{ + boost::shared_lock read_lock{mutex_}; + return identity.size() && publisherLists_.count (identity); +} + +bool +ValidatorList::removePublisherList (PublicKey const& publisherKey) +{ + auto const iList = publisherLists_.find (publisherKey); + if (iList == publisherLists_.end ()) + return false; + + JLOG (j_.debug()) << + "Removing validator list for revoked publisher " << + toBase58(TokenType::TOKEN_NODE_PUBLIC, publisherKey); + + for (auto const& val : iList->second.list) + { + auto const& iVal = keyListings_.find (val); + if (iVal == keyListings_.end()) + continue; + + if (iVal->second <= 1) + keyListings_.erase (iVal); + else + --iVal->second; + } + + return true; +} + +void +ValidatorList::for_each_listed ( + std::function func) const +{ + boost::shared_lock read_lock{mutex_}; + + for (auto const& v : keyListings_) + func (v.first, trusted(v.first)); +} + +} // ripple diff --git a/src/ripple/core/Config.h b/src/ripple/core/Config.h index d55e8eab1..48754487b 100644 --- a/src/ripple/core/Config.h +++ b/src/ripple/core/Config.h @@ -138,8 +138,6 @@ public: // Note: The following parameters do not relate to the UNL or trust at all std::size_t NETWORK_QUORUM = 0; // Minimum number of nodes to consider the network present - int VALIDATION_QUORUM = 1; // Minimum validations to consider ledger authoritative - bool LOCK_QUORUM = false; // Do not raise the quorum // Peer networking parameters bool PEER_PRIVATE = false; // True to ask peers not to relay current IP. @@ -156,6 +154,7 @@ public: // Validation PublicKey VALIDATION_PUB; SecretKey VALIDATION_PRIV; + boost::optional VALIDATION_QUORUM; // Minimum validations to consider ledger authoritative // Node Identity std::string NODE_SEED; diff --git a/src/ripple/core/ConfigSections.h b/src/ripple/core/ConfigSections.h index ee2c219d6..95108ced7 100644 --- a/src/ripple/core/ConfigSections.h +++ b/src/ripple/core/ConfigSections.h @@ -60,11 +60,11 @@ struct ConfigSection #define SECTION_SSL_VERIFY_FILE "ssl_verify_file" #define SECTION_SSL_VERIFY_DIR "ssl_verify_dir" #define SECTION_VALIDATORS_FILE "validators_file" -#define SECTION_VALIDATION_QUORUM "validation_quorum" #define SECTION_VALIDATION_SEED "validation_seed" #define SECTION_WEBSOCKET_PING_FREQ "websocket_ping_frequency" -#define SECTION_VALIDATORS "validators" #define SECTION_VALIDATOR_KEYS "validator_keys" +#define SECTION_VALIDATOR_LIST_KEYS "validator_list_keys" +#define SECTION_VALIDATORS "validators" #define SECTION_VALIDATION_MANIFEST "validation_manifest" #define SECTION_VETO_AMENDMENTS "veto_amendments" diff --git a/src/ripple/core/Job.h b/src/ripple/core/Job.h index 937ab2cbf..e9ea70c70 100644 --- a/src/ripple/core/Job.h +++ b/src/ripple/core/Job.h @@ -48,7 +48,6 @@ enum JobType jtUPDATE_PF, // Update pathfinding requests jtTRANSACTION, // A transaction received from the network jtBATCH, // Apply batched transactions - jtUNL, // A Score or Fetch of the UNL (DEPRECATED) jtADVANCE, // Advance validated/acquired ledgers jtPUBLEDGER, // Publish a fully-accepted ledger jtTXN_DATA, // Fetch a proposed set diff --git a/src/ripple/core/JobTypes.h b/src/ripple/core/JobTypes.h index 97de34e8f..6943412e4 100644 --- a/src/ripple/core/JobTypes.h +++ b/src/ripple/core/JobTypes.h @@ -50,7 +50,6 @@ add( jtRPC, "RPC", maxLimit, false, 0, 0); add( jtUPDATE_PF, "updatePaths", maxLimit, false, 0, 0); add( jtTRANSACTION, "transaction", maxLimit, false, 250, 1000); add( jtBATCH, "batch", maxLimit, false, 250, 1000); -add( jtUNL, "unl", 1, false, 0, 0); add( jtADVANCE, "advanceLedger", maxLimit, false, 0, 0); add( jtPUBLEDGER, "publishNewLedger", maxLimit, false, 3000, 4500); add( jtTXN_DATA, "fetchTxnData", 1, false, 0, 0); diff --git a/src/ripple/core/impl/Config.cpp b/src/ripple/core/impl/Config.cpp index 491231c90..60ed2954f 100644 --- a/src/ripple/core/impl/Config.cpp +++ b/src/ripple/core/impl/Config.cpp @@ -318,7 +318,7 @@ void Config::loadFromString (std::string const& fileContents) std::string strTemp; if (getSingleSection (secConfig, SECTION_PEER_PRIVATE, strTemp, j_)) - PEER_PRIVATE = beast::lexicalCastThrow (strTemp); + PEER_PRIVATE = beast::lexicalCastThrow (strTemp); if (getSingleSection (secConfig, SECTION_PEERS_MAX, strTemp, j_)) PEERS_MAX = std::max (0, beast::lexicalCastThrow (strTemp)); @@ -378,9 +378,6 @@ void Config::loadFromString (std::string const& fileContents) if (getSingleSection (secConfig, SECTION_NETWORK_QUORUM, strTemp, j_)) NETWORK_QUORUM = beast::lexicalCastThrow (strTemp); - if (getSingleSection (secConfig, SECTION_VALIDATION_QUORUM, strTemp, j_)) - VALIDATION_QUORUM = std::max (0, beast::lexicalCastThrow (strTemp)); - if (getSingleSection (secConfig, SECTION_FEE_ACCOUNT_RESERVE, strTemp, j_)) FEE_ACCOUNT_RESERVE = beast::lexicalCastThrow (strTemp); @@ -425,107 +422,109 @@ void Config::loadFromString (std::string const& fileContents) if (getSingleSection (secConfig, SECTION_PATH_SEARCH_MAX, strTemp, j_)) PATH_SEARCH_MAX = beast::lexicalCastThrow (strTemp); - // If a file was explicitly specified, then throw if the - // path is malformed or if the file does not exist or is - // not a file. - // If the specified file is not an absolute path, then look - // for it in the same directory as the config file. - // If no path was specified, then look for validators.txt - // in the same directory as the config file, but don't complain - // if we can't find it. - boost::filesystem::path validatorsFile; - - if (getSingleSection (secConfig, SECTION_VALIDATORS_FILE, strTemp, j_)) - { - validatorsFile = strTemp; - - if (validatorsFile.empty ()) - Throw ( - "Invalid path specified in [" SECTION_VALIDATORS_FILE "]"); - - if (!validatorsFile.is_absolute() && !CONFIG_DIR.empty()) - validatorsFile = CONFIG_DIR / validatorsFile; - - if (!boost::filesystem::exists (validatorsFile)) - Throw ( - "The file specified in [" SECTION_VALIDATORS_FILE "] " - "does not exist: " + validatorsFile.string()); - - else if (!boost::filesystem::is_regular_file (validatorsFile) && - !boost::filesystem::is_symlink (validatorsFile)) - Throw ( - "Invalid file specified in [" SECTION_VALIDATORS_FILE "]: " + - validatorsFile.string()); - } - else if (!CONFIG_DIR.empty()) - { - validatorsFile = CONFIG_DIR / validatorsFileName; - - if (!validatorsFile.empty ()) - { - if(!boost::filesystem::exists (validatorsFile)) - validatorsFile.clear(); - else if (!boost::filesystem::is_regular_file (validatorsFile) && - !boost::filesystem::is_symlink (validatorsFile)) - validatorsFile.clear(); - } - } - - if (!validatorsFile.empty () && - boost::filesystem::exists (validatorsFile) && - (boost::filesystem::is_regular_file (validatorsFile) || - boost::filesystem::is_symlink (validatorsFile))) - { - std::ifstream ifsDefault (validatorsFile.native().c_str()); - - std::string data; - - data.assign ( - std::istreambuf_iterator(ifsDefault), - std::istreambuf_iterator()); - - auto iniFile = parseIniFile (data, true); - - auto entries = getIniFileSection ( - iniFile, - SECTION_VALIDATORS); - - if (entries) - section (SECTION_VALIDATORS).append (*entries); - - auto valKeyEntries = getIniFileSection( - iniFile, - SECTION_VALIDATOR_KEYS); - - if (valKeyEntries) - section (SECTION_VALIDATOR_KEYS).append (*valKeyEntries); - - if (!entries && !valKeyEntries) - Throw ( - "The file specified in [" SECTION_VALIDATORS_FILE "] " - "does not contain a [" SECTION_VALIDATORS "] or " - "[" SECTION_VALIDATOR_KEYS "] section: " + - validatorsFile.string()); - - // Look for [validation_quorum] in the validators file - // if it was not in the config - if (!getIniFileSection (secConfig, SECTION_VALIDATION_QUORUM)) - { - if (!getSingleSection ( - iniFile, SECTION_VALIDATION_QUORUM, strTemp, j_)) - Throw ( - "The file specified in [" SECTION_VALIDATORS_FILE "] " - "does not contain a [" SECTION_VALIDATION_QUORUM "] " - "section: " + validatorsFile.string()); - else - VALIDATION_QUORUM = std::max ( - 0, beast::lexicalCastThrow (strTemp)); - } - } - if (getSingleSection (secConfig, SECTION_DEBUG_LOGFILE, strTemp, j_)) DEBUG_LOGFILE = strTemp; + // Do not load trusted validator configuration for standalone mode + if (! RUN_STANDALONE) + { + // If a file was explicitly specified, then throw if the + // path is malformed or if the file does not exist or is + // not a file. + // If the specified file is not an absolute path, then look + // for it in the same directory as the config file. + // If no path was specified, then look for validators.txt + // in the same directory as the config file, but don't complain + // if we can't find it. + boost::filesystem::path validatorsFile; + + if (getSingleSection (secConfig, SECTION_VALIDATORS_FILE, strTemp, j_)) + { + validatorsFile = strTemp; + + if (validatorsFile.empty ()) + Throw ( + "Invalid path specified in [" SECTION_VALIDATORS_FILE "]"); + + if (!validatorsFile.is_absolute() && !CONFIG_DIR.empty()) + validatorsFile = CONFIG_DIR / validatorsFile; + + if (!boost::filesystem::exists (validatorsFile)) + Throw ( + "The file specified in [" SECTION_VALIDATORS_FILE "] " + "does not exist: " + validatorsFile.string()); + + else if (!boost::filesystem::is_regular_file (validatorsFile) && + !boost::filesystem::is_symlink (validatorsFile)) + Throw ( + "Invalid file specified in [" SECTION_VALIDATORS_FILE "]: " + + validatorsFile.string()); + } + else if (!CONFIG_DIR.empty()) + { + validatorsFile = CONFIG_DIR / validatorsFileName; + + if (!validatorsFile.empty ()) + { + if(!boost::filesystem::exists (validatorsFile)) + validatorsFile.clear(); + else if (!boost::filesystem::is_regular_file (validatorsFile) && + !boost::filesystem::is_symlink (validatorsFile)) + validatorsFile.clear(); + } + } + + if (!validatorsFile.empty () && + boost::filesystem::exists (validatorsFile) && + (boost::filesystem::is_regular_file (validatorsFile) || + boost::filesystem::is_symlink (validatorsFile))) + { + std::ifstream ifsDefault (validatorsFile.native().c_str()); + + std::string data; + + data.assign ( + std::istreambuf_iterator(ifsDefault), + std::istreambuf_iterator()); + + auto iniFile = parseIniFile (data, true); + + auto entries = getIniFileSection ( + iniFile, + SECTION_VALIDATORS); + + if (entries) + section (SECTION_VALIDATORS).append (*entries); + + auto valKeyEntries = getIniFileSection( + iniFile, + SECTION_VALIDATOR_KEYS); + + if (valKeyEntries) + section (SECTION_VALIDATOR_KEYS).append (*valKeyEntries); + + auto valListKeys = getIniFileSection( + iniFile, + SECTION_VALIDATOR_LIST_KEYS); + + if (valListKeys) + section (SECTION_VALIDATOR_LIST_KEYS).append (*valListKeys); + + if (!entries && !valKeyEntries && !valListKeys) + Throw ( + "The file specified in [" SECTION_VALIDATORS_FILE "] " + "does not contain a [" SECTION_VALIDATORS "], " + "[" SECTION_VALIDATOR_KEYS "] or " + "[" SECTION_VALIDATOR_LIST_KEYS "]" + " section: " + + validatorsFile.string()); + } + + // Consolidate [validator_keys] and [validators] + section (SECTION_VALIDATORS).append ( + section (SECTION_VALIDATOR_KEYS).lines ()); + } + { auto const part = section("features"); for(auto const& s : part.values()) diff --git a/src/ripple/net/InfoSub.h b/src/ripple/net/InfoSub.h index 93058341a..ee52f621e 100644 --- a/src/ripple/net/InfoSub.h +++ b/src/ripple/net/InfoSub.h @@ -22,7 +22,7 @@ #include #include -#include +#include #include #include #include diff --git a/src/ripple/overlay/Overlay.h b/src/ripple/overlay/Overlay.h index 0d0f4f1a2..103240bba 100644 --- a/src/ripple/overlay/Overlay.h +++ b/src/ripple/overlay/Overlay.h @@ -38,9 +38,6 @@ namespace boost { namespace asio { namespace ssl { class context; } } } namespace ripple { -class DatabaseCon; -class BasicConfig; - /** Manages the set of connected peers. */ class Overlay : public Stoppable @@ -164,15 +161,6 @@ public: relay (protocol::TMValidation& m, uint256 const& uid) = 0; - virtual - void - setupValidatorKeyManifests (BasicConfig const& config, - DatabaseCon& db) = 0; - - virtual - void - saveValidatorKeyManifests (DatabaseCon& db) const = 0; - /** Visit every active peer and return a value The functor must: - Be callable as: diff --git a/src/ripple/overlay/impl/Manifest.cpp b/src/ripple/overlay/impl/Manifest.cpp deleted file mode 100644 index ee2f8d584..000000000 --- a/src/ripple/overlay/impl/Manifest.cpp +++ /dev/null @@ -1,477 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2012, 2013 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 -#include -#include -#include -#include -#include -#include -#include -#include - -namespace ripple { - -boost::optional -make_Manifest (std::string s) -{ - try - { - STObject st (sfGeneric); - SerialIter sit (s.data (), s.size ()); - st.set (sit); - auto const opt_pk = get(st, sfPublicKey); - auto const opt_spk = get(st, sfSigningPubKey); - auto const opt_seq = get (st, sfSequence); - auto const opt_sig = get (st, sfSignature); - auto const opt_msig = get (st, sfMasterSignature); - if (!opt_pk || !opt_spk || !opt_seq || !opt_sig || !opt_msig) - { - return boost::none; - } - return Manifest (std::move (s), *opt_pk, *opt_spk, *opt_seq); - } - catch (std::exception const&) - { - return boost::none; - } -} - -template -Stream& -logMftAct ( - Stream& s, - std::string const& action, - PublicKey const& pk, - std::uint32_t seq) -{ - s << "Manifest: " << action << - ";Pk: " << toBase58 (TokenType::TOKEN_NODE_PUBLIC, pk) << - ";Seq: " << seq << ";"; - return s; -} - -template -Stream& logMftAct ( - Stream& s, - std::string const& action, - PublicKey const& pk, - std::uint32_t seq, - std::uint32_t oldSeq) -{ - s << "Manifest: " << action << - ";Pk: " << toBase58 (TokenType::TOKEN_NODE_PUBLIC, pk) << - ";Seq: " << seq << - ";OldSeq: " << oldSeq << ";"; - return s; -} - -Manifest::Manifest (std::string s, - PublicKey pk, - PublicKey spk, - std::uint32_t seq) - : serialized (std::move (s)) - , masterKey (std::move (pk)) - , signingKey (std::move (spk)) - , sequence (seq) -{ -} - -bool Manifest::verify () const -{ - STObject st (sfGeneric); - SerialIter sit (serialized.data (), serialized.size ()); - st.set (sit); - if (! ripple::verify (st, HashPrefix::manifest, signingKey)) - return false; - - return ripple::verify ( - st, HashPrefix::manifest, masterKey, sfMasterSignature); -} - -uint256 Manifest::hash () const -{ - STObject st (sfGeneric); - SerialIter sit (serialized.data (), serialized.size ()); - st.set (sit); - return st.getHash (HashPrefix::manifest); -} - -bool Manifest::revoked () const -{ - /* - The maximum possible sequence number means that the master key - has been revoked. - */ - return sequence == std::numeric_limits::max (); -} - -Blob Manifest::getSignature () const -{ - STObject st (sfGeneric); - SerialIter sit (serialized.data (), serialized.size ()); - st.set (sit); - return st.getFieldVL (sfSignature); -} - -Blob Manifest::getMasterSignature () const -{ - STObject st (sfGeneric); - SerialIter sit (serialized.data (), serialized.size ()); - st.set (sit); - return st.getFieldVL (sfMasterSignature); -} - -bool -ManifestCache::loadValidatorKeys( - Section const& keys, - beast::Journal journal) -{ - static boost::regex const re ( - "[[:space:]]*" // skip leading whitespace - "([[:alnum:]]+)" // node identity - "(?:" // begin optional comment block - "[[:space:]]+" // (skip all leading whitespace) - "(?:" // begin optional comment - "(.*[^[:space:]]+)" // the comment - "[[:space:]]*" // (skip all trailing whitespace) - ")?" // end optional comment - ")?" // end optional comment block - ); - - JLOG (journal.debug()) << - "Loading configured validator keys"; - - std::size_t count = 0; - - for (auto const& line : keys.lines()) - { - boost::smatch match; - - if (!boost::regex_match (line, match, re)) - { - JLOG (journal.error()) << - "Malformed entry: '" << line << "'"; - return false; - } - - auto const key = parseBase58( - TokenType::TOKEN_NODE_PUBLIC, match[1]); - - if (!key) - { - JLOG (journal.error()) << - "Error decoding validator key: " << match[1]; - return false; - } - - if (publicKeyType(*key) != KeyType::ed25519) - { - JLOG (journal.error()) << - "Validator key not using Ed25519: " << match[1]; - return false; - } - - JLOG (journal.debug()) << "Loaded key: " << match[1]; - - addTrustedKey (*key, match[2]); - ++count; - } - - JLOG (journal.debug()) << - "Loaded " << count << " entries"; - - return true; -} - -void -ManifestCache::configManifest ( - Manifest m, ValidatorList& unl, beast::Journal journal) -{ - if (! m.verify()) - { - Throw ("Unverifiable manifest in config"); - } - - // Trust our own master public key - if (!trusted(m.masterKey) && !unl.trusted (m.masterKey)) - { - addTrustedKey (m.masterKey, ""); - } - - auto const result = applyManifest (std::move(m), unl, journal); - - if (result != ManifestDisposition::accepted) - { - Throw ("Our own validation manifest was not accepted"); - } -} - -bool -ManifestCache::trusted (PublicKey const& identity) const -{ - return map_.count(identity); -} - -void -ManifestCache::addTrustedKey (PublicKey const& pk, std::string comment) -{ - std::lock_guard lock (mutex_); - - auto& value = map_[pk]; - - if (value.m) - { - Throw ( - "New trusted validator key already has a manifest"); - } - - value.comment = std::move(comment); -} - -std::size_t -ManifestCache::size () const -{ - std::lock_guard lock (mutex_); - return map_.size (); -} - -ManifestDisposition -ManifestCache::canApply (PublicKey const& pk, std::uint32_t seq, - beast::Journal journal) const -{ - auto const iter = map_.find(pk); - - if (iter == map_.end()) - { - /* - A manifest was received whose master key we don't trust. - Since rippled always sends all of its current manifests, - this will happen normally any time a peer connects. - */ - if (auto stream = journal.debug()) - logMftAct(stream, "Untrusted", pk, seq); - return ManifestDisposition::untrusted; - } - - auto& old = iter->second.m; - - if (old && seq <= old->sequence) - { - /* - A manifest was received for a validator we're tracking, but - its sequence number is no higher than the one already stored. - This will happen normally when a peer without the latest gossip - connects. - */ - if (auto stream = journal.debug()) - logMftAct(stream, "Stale", pk, seq, old->sequence); - return ManifestDisposition::stale; // not a newer manifest, ignore - } - - return ManifestDisposition::accepted; -} - - -ManifestDisposition -ManifestCache::applyManifest ( - Manifest m, ValidatorList& unl, beast::Journal journal) -{ - /* - Move master public key from permanent trusted key list - to manifest cache. - */ - if (auto unlComment = unl.member (m.masterKey)) - { - addTrustedKey (m.masterKey, *unlComment); - unl.removePermanentKey (m.masterKey); - } - - { - std::lock_guard lock (mutex_); - - /* - before we spend time checking the signature, make sure we trust the - master key and the sequence number is newer than any we have. - */ - auto const chk = canApply(m.masterKey, m.sequence, journal); - - if (chk != ManifestDisposition::accepted) - { - return chk; - } - } - - if (! m.verify()) - { - /* - A manifest's signature is invalid. - This shouldn't happen normally. - */ - if (auto stream = journal.warn()) - logMftAct(stream, "Invalid", m.masterKey, m.sequence); - return ManifestDisposition::invalid; - } - - std::lock_guard lock (mutex_); - - /* - We released the lock above, so we have to check again, in case - another thread accepted a newer manifest. - */ - auto const chk = canApply(m.masterKey, m.sequence, journal); - - if (chk != ManifestDisposition::accepted) - { - return chk; - } - - auto const iter = map_.find(m.masterKey); - - auto& old = iter->second.m; - - if (! old) - { - /* - This is the first received manifest for a trusted master key - (possibly our own). This only happens once per validator per - run (and possibly not at all, if there's an obsolete entry in - [validator_keys] for a validator that no longer exists). - */ - if (auto stream = journal.info()) - logMftAct(stream, "AcceptedNew", m.masterKey, m.sequence); - } - else - { - if (m.revoked ()) - { - /* - The MASTER key for this validator was revoked. This is - expected, but should happen at most *very* rarely. - */ - if (auto stream = journal.info()) - logMftAct(stream, "Revoked", - m.masterKey, m.sequence, old->sequence); - } - else - { - /* - An ephemeral key was revoked and superseded by a new key. - This is expected, but should happen infrequently. - */ - if (auto stream = journal.info()) - logMftAct(stream, "AcceptedUpdate", - m.masterKey, m.sequence, old->sequence); - } - - unl.removeEphemeralKey (old->signingKey); - } - - if (m.revoked ()) - { - // The master key is revoked -- don't insert the signing key - if (auto stream = journal.warn()) - logMftAct(stream, "Revoked", m.masterKey, m.sequence); - - /* - A validator master key has been compromised, so its manifests - are now untrustworthy. In order to prevent us from accepting - a forged manifest signed by the compromised master key, store - this manifest, which has the highest possible sequence number - and therefore can't be superseded by a forged one. - */ - } - else - { - unl.insertEphemeralKey (m.signingKey, iter->second.comment); - } - - old = std::move(m); - - return ManifestDisposition::accepted; -} - -void ManifestCache::load ( - DatabaseCon& dbCon, ValidatorList& unl, beast::Journal journal) -{ - static const char* const sql = - "SELECT RawData FROM ValidatorManifests;"; - auto db = dbCon.checkoutDb (); - soci::blob sociRawData (*db); - soci::statement st = - (db->prepare << sql, - soci::into (sociRawData)); - st.execute (); - while (st.fetch ()) - { - std::string serialized; - convert (sociRawData, serialized); - if (auto mo = make_Manifest (std::move (serialized))) - { - if (!mo->verify()) - { - JLOG(journal.warn()) - << "Unverifiable manifest in db"; - continue; - } - - if (trusted(mo->masterKey) || unl.trusted(mo->masterKey)) - { - applyManifest (std::move(*mo), unl, journal); - } - else - { - JLOG(journal.info()) - << "Manifest in db is no longer trusted"; - } - } - else - { - JLOG(journal.warn()) - << "Malformed manifest in database"; - } - } -} - -void ManifestCache::save (DatabaseCon& dbCon) const -{ - auto db = dbCon.checkoutDb (); - - soci::transaction tr(*db); - *db << "DELETE FROM ValidatorManifests"; - static const char* const sql = - "INSERT INTO ValidatorManifests (RawData) VALUES (:rawData);"; - for (auto const& v : map_) - { - if (!v.second.m) - continue; - - // soci does not support bulk insertion of blob data - // Do not reuse blob because manifest ecdsa signatures vary in length - // but blob write length is expected to be >= the last write - soci::blob rawData(*db); - convert (v.second.m->serialized, rawData); - *db << sql, - soci::use (rawData); - } - tr.commit (); -} -} diff --git a/src/ripple/overlay/impl/OverlayImpl.cpp b/src/ripple/overlay/impl/OverlayImpl.cpp index df347c17d..1c7ae3e75 100644 --- a/src/ripple/overlay/impl/OverlayImpl.cpp +++ b/src/ripple/overlay/impl/OverlayImpl.cpp @@ -20,7 +20,7 @@ #include #include #include -#include +#include #include #include #include @@ -479,57 +479,6 @@ OverlayImpl::checkStopped () stopped(); } -void -OverlayImpl::setupValidatorKeyManifests (BasicConfig const& config, - DatabaseCon& db) -{ - auto const loaded = manifestCache_.loadValidatorKeys ( - config.section (SECTION_VALIDATOR_KEYS), - journal_); - - if (!loaded) - Throw ( - "Unable to load keys from [" SECTION_VALIDATOR_KEYS "]"); - - auto const& validation_manifest = - config.section (SECTION_VALIDATION_MANIFEST); - - if (! validation_manifest.lines().empty()) - { - std::string s; - s.reserve (Manifest::textLength); - for (auto const& line : validation_manifest.lines()) - s += beast::rfc2616::trim(line); - if (auto mo = make_Manifest (beast::detail::base64_decode(s))) - { - manifestCache_.configManifest ( - std::move (*mo), - app_.validators(), - journal_); - } - else - { - Throw ("Malformed manifest in config"); - } - } - else - { - JLOG(journal_.debug()) << "No [" SECTION_VALIDATION_MANIFEST << - "] section in config"; - } - - manifestCache_.load ( - db, - app_.validators(), - journal_); -} - -void -OverlayImpl::saveValidatorKeyManifests (DatabaseCon& db) const -{ - manifestCache_.save (db); -} - void OverlayImpl::onPrepare() { @@ -713,21 +662,29 @@ OverlayImpl::onManifests ( { auto& s = m->list ().Get (i).stobject (); - if (auto mo = make_Manifest (s)) + if (auto mo = Manifest::make_Manifest (s)) { uint256 const hash = mo->hash (); if (!hashRouter.addSuppressionPeer (hash, from->id ())) continue; - auto const serialized = mo->serialized; - auto const result = manifestCache_.applyManifest ( - std::move(*mo), - app_.validators(), - journal); + if (! app_.validators().listed (mo->masterKey)) + { + JLOG(journal.info()) << "Untrusted manifest #" << i + 1; + app_.getOPs().pubManifest (*mo); + continue; + } - if (result == ManifestDisposition::accepted || - result == ManifestDisposition::untrusted) - app_.getOPs().pubManifest (*make_Manifest(serialized)); + auto const serialized = mo->serialized; + + auto const result = app_.validatorManifests ().applyManifest ( + std::move(*mo)); + + if (result == ManifestDisposition::accepted) + { + app_.getOPs().pubManifest ( + *Manifest::make_Manifest(serialized)); + } if (result == ManifestDisposition::accepted) { diff --git a/src/ripple/overlay/impl/OverlayImpl.h b/src/ripple/overlay/impl/OverlayImpl.h index 2f33873aa..bc1e327c2 100644 --- a/src/ripple/overlay/impl/OverlayImpl.h +++ b/src/ripple/overlay/impl/OverlayImpl.h @@ -23,7 +23,6 @@ #include #include #include -#include #include #include #include @@ -118,7 +117,6 @@ private: hash_map> ids_; Resolver& m_resolver; std::atomic next_id_; - ManifestCache manifestCache_; int timer_count_; //-------------------------------------------------------------------------- @@ -152,12 +150,6 @@ public: return serverHandler_; } - ManifestCache const& - manifestCache() const - { - return manifestCache_; - } - Setup const& setup() const { @@ -195,15 +187,6 @@ public: relay (protocol::TMValidation& m, uint256 const& uid) override; - virtual - void - setupValidatorKeyManifests (BasicConfig const& config, - DatabaseCon& db) override; - - virtual - void - saveValidatorKeyManifests (DatabaseCon& db) const override; - //-------------------------------------------------------------------------- // // OverlayImpl diff --git a/src/ripple/overlay/impl/PeerImp.cpp b/src/ripple/overlay/impl/PeerImp.cpp index f5d3459a0..fad75f8c8 100644 --- a/src/ripple/overlay/impl/PeerImp.cpp +++ b/src/ripple/overlay/impl/PeerImp.cpp @@ -703,8 +703,8 @@ PeerImp::doProtocolStart() protocol::TMManifests tm; - overlay_.manifestCache ().for_each_manifest ( - [&tm](size_t s){tm.mutable_list()->Reserve(s);}, + app_.validatorManifests ().for_each_manifest ( + [&tm](std::size_t s){tm.mutable_list()->Reserve(s);}, [&tm](Manifest const& manifest) { auto const& s = manifest.serialized; diff --git a/src/ripple/protocol/JsonFields.h b/src/ripple/protocol/JsonFields.h index 9a97b8801..86493b7db 100644 --- a/src/ripple/protocol/JsonFields.h +++ b/src/ripple/protocol/JsonFields.h @@ -116,10 +116,9 @@ JSS ( close_time_resolution ); // in: Application; out: LedgerToJson JSS ( closed ); // out: NetworkOPs, LedgerToJson, // handlers/Ledger JSS ( closed_ledger ); // out: NetworkOPs -JSS ( cluster ); // out: UniqueNodeList, PeerImp +JSS ( cluster ); // out: PeerImp JSS ( code ); // out: errors JSS ( command ); // in: RPCHandler -JSS ( comment ); // out: UnlList JSS ( complete ); // out: NetworkOPs, InboundLedger JSS ( complete_ledgers ); // out: NetworkOPs, PeerImp JSS ( consensus ); // out: NetworkOPs, LedgerConsensus @@ -156,7 +155,6 @@ JSS ( enabled ); // out: AmendmentTable JSS ( engine_result ); // out: NetworkOPs, TransactionSign, Submit JSS ( engine_result_code ); // out: NetworkOPs, TransactionSign, Submit JSS ( engine_result_message ); // out: NetworkOPs, TransactionSign, Submit -JSS ( ephemeral ); // out: UnlList JSS ( error ); // out: error JSS ( error_code ); // out: error JSS ( error_exception ); // out: Submit @@ -416,6 +414,7 @@ JSS ( transactions ); // out: LedgerToJson, JSS ( transitions ); // out: NetworkOPs JSS ( treenode_cache_size ); // out: GetCounts JSS ( treenode_track_size ); // out: GetCounts +JSS ( trusted ); // out: UnlList JSS ( tx ); // out: STTx, AccountTx* JSS ( tx_blob ); // in/out: Submit, // in: TransactionSign, AccountTx* diff --git a/src/ripple/rpc/handlers/UnlList.cpp b/src/ripple/rpc/handlers/UnlList.cpp index c2009bc4f..32a4a3f25 100644 --- a/src/ripple/rpc/handlers/UnlList.cpp +++ b/src/ripple/rpc/handlers/UnlList.cpp @@ -31,19 +31,16 @@ Json::Value doUnlList (RPC::Context& context) auto lock = make_lock(context.app.getMasterMutex()); Json::Value obj (Json::objectValue); - context.app.validators().for_each ( + context.app.validators().for_each_listed ( [&unl = obj[jss::unl]]( PublicKey const& publicKey, - std::string const& comment, - bool ephemeral) + bool trusted) { Json::Value node (Json::objectValue); node[jss::pubkey_validator] = toBase58( TokenType::TOKEN_NODE_PUBLIC, publicKey); - node[jss::ephemeral] = ephemeral; - if (!comment.empty()) - node[jss::comment] = comment; + node[jss::trusted] = trusted; unl.append (node); }); diff --git a/src/ripple/unity/app_misc.cpp b/src/ripple/unity/app_misc.cpp index 3fa5a8c83..33ad6ca28 100644 --- a/src/ripple/unity/app_misc.cpp +++ b/src/ripple/unity/app_misc.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include diff --git a/src/ripple/unity/overlay.cpp b/src/ripple/unity/overlay.cpp index 95a6b9e92..59026ed9c 100644 --- a/src/ripple/unity/overlay.cpp +++ b/src/ripple/unity/overlay.cpp @@ -21,7 +21,6 @@ #include #include -#include #include #include #include diff --git a/src/test/app/AmendmentTable_test.cpp b/src/test/app/AmendmentTable_test.cpp index fa15b207a..1ff93dec3 100644 --- a/src/test/app/AmendmentTable_test.cpp +++ b/src/test/app/AmendmentTable_test.cpp @@ -402,7 +402,7 @@ public: v->setFieldV256 (sfAmendments, field); v->setTrusted(); - validations [calcNodeID(val)] = v; + validations [val] = v; } ourVotes = table.doValidation (enabled); diff --git a/src/test/overlay/manifest_test.cpp b/src/test/app/Manifest_test.cpp similarity index 53% rename from src/test/overlay/manifest_test.cpp rename to src/test/app/Manifest_test.cpp index e6d121f30..42adebb46 100644 --- a/src/test/overlay/manifest_test.cpp +++ b/src/test/app/Manifest_test.cpp @@ -17,23 +17,25 @@ */ //============================================================================== -#include +#include +#include #include #include -#include -#include +#include #include #include #include #include #include +#include #include #include +#include namespace ripple { -namespace tests { +namespace test { -class manifest_test : public ripple::TestSuite +class Manifest_test : public beast::unit_test::suite { private: static PublicKey randomNode () @@ -78,8 +80,9 @@ private: { return boost::filesystem::current_path () / "manifest_test_databases"; } + public: - manifest_test () + Manifest_test () { try { @@ -89,7 +92,7 @@ public: { } } - ~manifest_test () + ~Manifest_test () { try { @@ -100,6 +103,30 @@ public: } } + std::string + makeManifestString ( + PublicKey const& pk, + SecretKey const& sk, + PublicKey const& spk, + SecretKey const& ssk, + int seq) + { + STObject st(sfGeneric); + st[sfSequence] = seq; + st[sfPublicKey] = pk; + st[sfSigningPubKey] = spk; + + sign(st, HashPrefix::manifest, *publicKeyType(spk), ssk); + sign(st, HashPrefix::manifest, *publicKeyType(pk), sk, + sfMasterSignature); + + Serializer s; + st.add(s); + + return beast::detail::base64_encode (std::string( + static_cast (s.data()), s.size())); + } + Manifest make_Manifest (SecretKey const& sk, KeyType type, SecretKey const& ssk, KeyType stype, @@ -129,10 +156,10 @@ public: st.add(s); std::string const m (static_cast (s.data()), s.size()); - if (auto r = ripple::make_Manifest (std::move (m))) + if (auto r = Manifest::make_Manifest (std::move (m))) return std::move (*r); Throw ("Could not create a manifest"); - return *ripple::make_Manifest(std::move(m)); // Silence compiler warning. + return *Manifest::make_Manifest(std::move(m)); // Silence compiler warning. } Manifest @@ -141,105 +168,7 @@ public: return Manifest (m.serialized, m.masterKey, m.signingKey, m.sequence); } - - void - testConfigLoad () - { - testcase ("Config Load"); - - ManifestCache cache; - beast::Journal journal; - - std::vector network; - network.reserve(8); - - while (network.size () != 8) - network.push_back (randomMasterKey()); - - auto format = []( - PublicKey const &publicKey, - char const* comment = nullptr) - { - auto ret = toBase58( - TokenType::TOKEN_NODE_PUBLIC, - publicKey); - - if (comment) - ret += comment; - - return ret; - }; - - Section s1; - - // Correct (empty) configuration - BEAST_EXPECT(cache.loadValidatorKeys (s1, journal)); - BEAST_EXPECT(cache.size() == 0); - - // Correct configuration - s1.append (format (network[0])); - s1.append (format (network[1], " Comment")); - s1.append (format (network[2], " Multi Word Comment")); - s1.append (format (network[3], " Leading Whitespace")); - s1.append (format (network[4], " Trailing Whitespace ")); - s1.append (format (network[5], " Leading & Trailing Whitespace ")); - s1.append (format (network[6], " Leading, Trailing & Internal Whitespace ")); - s1.append (format (network[7], " ")); - - BEAST_EXPECT(cache.loadValidatorKeys (s1, journal)); - - for (auto const& n : network) - BEAST_EXPECT(cache.trusted (n)); - - // Incorrect configurations: - Section s2; - s2.append ("NotAPublicKey"); - BEAST_EXPECT(!cache.loadValidatorKeys (s2, journal)); - - Section s3; - s3.append (format (network[0], "!")); - BEAST_EXPECT(!cache.loadValidatorKeys (s3, journal)); - - Section s4; - s4.append (format (network[0], "! Comment")); - BEAST_EXPECT(!cache.loadValidatorKeys (s4, journal)); - - // Check if we properly terminate when we encounter - // a malformed or unparseable entry: - auto const masterKey1 = randomMasterKey(); - auto const masterKey2 = randomMasterKey (); - - Section s5; - s5.append (format (masterKey1, "XXX")); - s5.append (format (masterKey2)); - BEAST_EXPECT(!cache.loadValidatorKeys (s5, journal)); - BEAST_EXPECT(!cache.trusted (masterKey1)); - BEAST_EXPECT(!cache.trusted (masterKey2)); - - // Reject secp256k1 permanent validator keys - auto const node1 = randomNode (); - auto const node2 = randomNode (); - - Section s6; - s6.append (format (node1)); - s6.append (format (node2, " Comment")); - BEAST_EXPECT(!cache.loadValidatorKeys (s6, journal)); - BEAST_EXPECT(!cache.trusted (node1)); - BEAST_EXPECT(!cache.trusted (node2)); - - // Trust our own master public key from configured manifest - auto unl = std::make_unique (journal); - - auto const sk = randomSecretKey(); - auto const kp = randomKeyPair(KeyType::secp256k1); - auto const m = make_Manifest ( - sk, KeyType::ed25519, kp.second, KeyType::secp256k1, 0); - - cache.configManifest (clone (m), *unl, journal); - BEAST_EXPECT(cache.trusted (m.masterKey)); - } - - void testLoadStore (ManifestCache const& m, ValidatorList& unl) + void testLoadStore (ManifestCache& m) { testcase ("load/store"); @@ -251,13 +180,6 @@ public: setup.dataDir = getDatabasePath (); DatabaseCon dbCon(setup, dbName, WalletDBInit, WalletDBCount); - if (!m.size ()) - fail (); - - m.save (dbCon); - - beast::Journal journal; - auto getPopulatedManifests = [](ManifestCache const& cache) -> std::vector { @@ -279,21 +201,45 @@ public: }; std::vector const inManifests ( sort (getPopulatedManifests (m))); + + beast::Journal journal; + jtx::Env env (*this); + auto unl = std::make_unique ( + m, m, env.timeKeeper(), journal); + { - // load should not load untrusted master keys from db + // save should not store untrusted master keys to db + m.save (dbCon, "ValidatorManifests", + [&unl](PublicKey const& pubKey) + { + return unl->listed (pubKey); + }); + ManifestCache loaded; - loaded.load (dbCon, unl, journal); - BEAST_EXPECT(loaded.size() == 0); + loaded.load (dbCon, "ValidatorManifests"); + for (auto const& man : inManifests) + BEAST_EXPECT( + loaded.getSigningKey (man->masterKey) == man->masterKey); } { - // load should load all trusted master keys from db - ManifestCache loaded; - + // save should store all trusted master keys to db + PublicKey emptyLocalKey; + std::vector s1; + std::vector keys; + std::vector cfgManifest; for (auto const& man : inManifests) - loaded.addTrustedKey (man->masterKey, ""); + s1.push_back (toBase58( + TokenType::TOKEN_NODE_PUBLIC, man->masterKey)); + unl->load (emptyLocalKey, s1, keys); - loaded.load (dbCon, unl, journal); + m.save (dbCon, "ValidatorManifests", + [&unl](PublicKey const& pubKey) + { + return unl->listed (pubKey); + }); + ManifestCache loaded; + loaded.load (dbCon, "ValidatorManifests"); std::vector const loadedManifests ( sort (getPopulatedManifests (loaded))); @@ -312,18 +258,23 @@ public: } } { - // load should remove master key from permanent key list - ManifestCache loaded; - auto const iMan = inManifests.begin(); + // load config manifest + std::vector const badManifest ({"bad manifest"}); - if (!*iMan) - fail (); - BEAST_EXPECT(m.trusted((*iMan)->masterKey)); - BEAST_EXPECT(unl.insertPermanentKey((*iMan)->masterKey, "trusted key")); - BEAST_EXPECT(unl.trusted((*iMan)->masterKey)); - loaded.load (dbCon, unl, journal); - BEAST_EXPECT(!unl.trusted((*iMan)->masterKey)); - BEAST_EXPECT(loaded.trusted((*iMan)->masterKey)); + ManifestCache loaded; + BEAST_EXPECT(! loaded.load ( + dbCon, "ValidatorManifests", badManifest)); + + auto const sk = randomSecretKey(); + auto const pk = derivePublicKey(KeyType::ed25519, sk); + auto const kp = randomKeyPair(KeyType::secp256k1); + + std::vector const cfgManifest ({ + makeManifestString (pk, sk, kp.first, kp.second, 0) + }); + + BEAST_EXPECT(loaded.load ( + dbCon, "ValidatorManifests", cfgManifest)); } } boost::filesystem::remove (getDatabasePath () / @@ -353,16 +304,76 @@ public: BEAST_EXPECT(strHex(masterSig) == strHex(m.getMasterSignature())); } + void testGetKeys() + { + testcase ("getKeys"); + + ManifestCache cache; + auto const sk = randomSecretKey(); + auto const pk = derivePublicKey(KeyType::ed25519, sk); + + // getSigningKey should return same key if there is no manifest + BEAST_EXPECT(cache.getSigningKey(pk) == pk); + + // getSigningKey should return the ephemeral public key + // for the listed validator master public key + // getMasterKey should return the listed validator master key + // for that ephemeral public key + auto const kp0 = randomKeyPair(KeyType::secp256k1); + auto const m0 = make_Manifest ( + sk, KeyType::ed25519, kp0.second, KeyType::secp256k1, 0); + BEAST_EXPECT(cache.applyManifest(clone (m0)) == + ManifestDisposition::accepted); + BEAST_EXPECT(cache.getSigningKey(pk) == kp0.first); + BEAST_EXPECT(cache.getMasterKey(kp0.first) == pk); + + // getSigningKey should return the latest ephemeral public key + // for the listed validator master public key + // getMasterKey should only return a master key for the latest + // ephemeral public key + auto const kp1 = randomKeyPair(KeyType::secp256k1); + auto const m1 = make_Manifest ( + sk, KeyType::ed25519, kp1.second, KeyType::secp256k1, 1); + BEAST_EXPECT(cache.applyManifest(clone (m1)) == + ManifestDisposition::accepted); + BEAST_EXPECT(cache.getSigningKey(pk) == kp1.first); + BEAST_EXPECT(cache.getMasterKey(kp1.first) == pk); + BEAST_EXPECT(cache.getMasterKey(kp0.first) == kp0.first); + + // getSigningKey and getMasterKey should return the same keys if + // a new manifest is applied with the same signing key but a higher + // sequence + auto const m2 = make_Manifest ( + sk, KeyType::ed25519, kp1.second, KeyType::secp256k1, 2); + BEAST_EXPECT(cache.applyManifest(clone (m2)) == + ManifestDisposition::accepted); + BEAST_EXPECT(cache.getSigningKey(pk) == kp1.first); + BEAST_EXPECT(cache.getMasterKey(kp1.first) == pk); + BEAST_EXPECT(cache.getMasterKey(kp0.first) == kp0.first); + + // getSigningKey should return boost::none for a + // revoked master public key + // getMasterKey should return boost::none for an ephemeral public key + // from a revoked master public key + auto const kpMax = randomKeyPair(KeyType::secp256k1); + auto const mMax = make_Manifest ( + sk, KeyType::ed25519, kpMax.second, KeyType::secp256k1, + std::numeric_limits::max ()); + BEAST_EXPECT(cache.applyManifest(clone (mMax)) == + ManifestDisposition::accepted); + BEAST_EXPECT(cache.revoked(pk)); + BEAST_EXPECT(cache.getSigningKey(pk) == pk); + BEAST_EXPECT(cache.getMasterKey(kpMax.first) == kpMax.first); + BEAST_EXPECT(cache.getMasterKey(kp1.first) == kp1.first); + } + void run() override { ManifestCache cache; - beast::Journal journal; - auto unl = std::make_unique (journal); { testcase ("apply"); auto const accepted = ManifestDisposition::accepted; - auto const untrusted = ManifestDisposition::untrusted; auto const stale = ManifestDisposition::stale; auto const invalid = ManifestDisposition::invalid; @@ -373,9 +384,11 @@ public: sk_a, KeyType::ed25519, kp_a.second, KeyType::secp256k1, 0); auto const s_a1 = make_Manifest ( sk_a, KeyType::ed25519, kp_a.second, KeyType::secp256k1, 1); + auto const s_aMax = make_Manifest ( + sk_a, KeyType::ed25519, kp_a.second, KeyType::secp256k1, + std::numeric_limits::max ()); auto const sk_b = randomSecretKey(); - auto const pk_b = derivePublicKey(KeyType::ed25519, sk_b); auto const kp_b = randomKeyPair(KeyType::secp256k1); auto const s_b0 = make_Manifest ( sk_b, KeyType::ed25519, kp_b.second, KeyType::secp256k1, 0); @@ -386,45 +399,39 @@ public: true); // broken auto const fake = s_b1.serialized + '\0'; - BEAST_EXPECT(cache.applyManifest (clone (s_a0), *unl, journal) == untrusted); + // applyManifest should accept new manifests with + // higher sequence numbers + BEAST_EXPECT(cache.applyManifest (clone (s_a0)) == accepted); + BEAST_EXPECT(cache.applyManifest (clone (s_a0)) == stale); - cache.addTrustedKey (pk_a, "a"); - cache.addTrustedKey (pk_b, "b"); + BEAST_EXPECT(cache.applyManifest (clone (s_a1)) == accepted); + BEAST_EXPECT(cache.applyManifest (clone (s_a1)) == stale); + BEAST_EXPECT(cache.applyManifest (clone (s_a0)) == stale); - BEAST_EXPECT(cache.applyManifest (clone (s_a0), *unl, journal) == accepted); - BEAST_EXPECT(cache.applyManifest (clone (s_a0), *unl, journal) == stale); + // applyManifest should accept manifests with max sequence numbers + // that revoke the master public key + BEAST_EXPECT(!cache.revoked (pk_a)); + BEAST_EXPECT(s_aMax.revoked ()); + BEAST_EXPECT(cache.applyManifest (clone (s_aMax)) == accepted); + BEAST_EXPECT(cache.applyManifest (clone (s_aMax)) == stale); + BEAST_EXPECT(cache.applyManifest (clone (s_a1)) == stale); + BEAST_EXPECT(cache.applyManifest (clone (s_a0)) == stale); + BEAST_EXPECT(cache.revoked (pk_a)); - BEAST_EXPECT(cache.applyManifest (clone (s_a1), *unl, journal) == accepted); - BEAST_EXPECT(cache.applyManifest (clone (s_a1), *unl, journal) == stale); - BEAST_EXPECT(cache.applyManifest (clone (s_a0), *unl, journal) == stale); + // applyManifest should reject manifests with invalid signatures + BEAST_EXPECT(cache.applyManifest (clone (s_b0)) == accepted); + BEAST_EXPECT(cache.applyManifest (clone (s_b0)) == stale); - BEAST_EXPECT(cache.applyManifest (clone (s_b0), *unl, journal) == accepted); - BEAST_EXPECT(cache.applyManifest (clone (s_b0), *unl, journal) == stale); - - BEAST_EXPECT(!ripple::make_Manifest(fake)); - BEAST_EXPECT(cache.applyManifest (clone (s_b2), *unl, journal) == invalid); - - // When trusted permanent key is found as manifest master key - // move to manifest cache - auto const sk_c = randomSecretKey(); - auto const pk_c = derivePublicKey(KeyType::ed25519, sk_c); - auto const kp_c = randomKeyPair(KeyType::secp256k1); - auto const s_c0 = make_Manifest ( - sk_c, KeyType::ed25519, kp_c.second, KeyType::secp256k1, 0); - BEAST_EXPECT(unl->insertPermanentKey(pk_c, "trusted key")); - BEAST_EXPECT(unl->trusted(pk_c)); - BEAST_EXPECT(!cache.trusted(pk_c)); - BEAST_EXPECT(cache.applyManifest(clone (s_c0), *unl, journal) == accepted); - BEAST_EXPECT(!unl->trusted(pk_c)); - BEAST_EXPECT(cache.trusted(pk_c)); + BEAST_EXPECT(!Manifest::make_Manifest(fake)); + BEAST_EXPECT(cache.applyManifest (clone (s_b2)) == invalid); } - testConfigLoad(); - testLoadStore (cache, *unl); + testLoadStore (cache); testGetSignature (); + testGetKeys (); } }; -BEAST_DEFINE_TESTSUITE(manifest,overlay,ripple); +BEAST_DEFINE_TESTSUITE(Manifest,app,ripple); -} // tests +} // test } // ripple diff --git a/src/test/app/ValidatorList_test.cpp b/src/test/app/ValidatorList_test.cpp index 777d6a7ee..d4fbbea09 100644 --- a/src/test/app/ValidatorList_test.cpp +++ b/src/test/app/ValidatorList_test.cpp @@ -17,48 +17,111 @@ */ //============================================================================== -#include +#include #include -#include +#include #include +#include +#include +#include +#include #include +#include + namespace ripple { -namespace tests { +namespace test { -class ValidatorList_test : public ripple::TestSuite +class ValidatorList_test : public beast::unit_test::suite { private: static PublicKey randomNode () { - return derivePublicKey ( - KeyType::secp256k1, - randomSecretKey()); + return derivePublicKey (KeyType::secp256k1, randomSecretKey()); } static PublicKey randomMasterKey () { - return derivePublicKey ( - KeyType::ed25519, - randomSecretKey()); + return derivePublicKey (KeyType::ed25519, randomSecretKey()); } - static - bool - isPresent ( - std::vector container, - PublicKey const& item) + std::string + makeManifestString ( + PublicKey const& pk, + SecretKey const& sk, + PublicKey const& spk, + SecretKey const& ssk, + int seq) { - auto found = std::find ( - std::begin (container), - std::end (container), - item); + STObject st(sfGeneric); + st[sfSequence] = seq; + st[sfPublicKey] = pk; + st[sfSigningPubKey] = spk; - return (found != std::end (container)); + sign(st, HashPrefix::manifest, *publicKeyType(spk), ssk); + sign(st, HashPrefix::manifest, *publicKeyType(pk), sk, + sfMasterSignature); + + Serializer s; + st.add(s); + + return std::string(static_cast (s.data()), s.size()); + } + + std::string + makeList ( + std::vector const& validators, + std::size_t sequence, + std::size_t expiration) + { + std::string data = + "{\"sequence\":" + std::to_string(sequence) + + ",\"expiration\":" + std::to_string(expiration) + + ",\"validators\":["; + + for (auto const& val : validators) + { + data += "{\"validation_public_key\":\"" + strHex(val) + "\"},"; + } + + data.pop_back(); + data += "]}"; + return beast::detail::base64_encode(data); + } + + std::string + signList ( + std::string const& blob, + std::pair const& keys) + { + auto const data = beast::detail::base64_decode (blob); + return strHex(sign( + keys.first, keys.second, makeSlice(data))); + } + + void + testGenesisQuorum () + { + testcase ("Genesis Quorum"); + + beast::Journal journal; + ManifestCache manifests; + jtx::Env env (*this); + { + auto trustedKeys = std::make_unique ( + manifests, manifests, env.timeKeeper(), journal); + BEAST_EXPECT(trustedKeys->quorum () == 1); + } + { + std::size_t minQuorum = 0; + auto trustedKeys = std::make_unique ( + manifests, manifests, env.timeKeeper(), journal, minQuorum); + BEAST_EXPECT(trustedKeys->quorum () == minQuorum); + } } void @@ -66,21 +129,28 @@ private: { testcase ("Config Load"); - auto validators = std::make_unique (beast::Journal ()); + beast::Journal journal; + jtx::Env env (*this); + PublicKey emptyLocalKey; + std::vector emptyCfgKeys; + std::vector emptyCfgPublishers; - std::vector network; - network.reserve(8); + auto const localSigningKeys = randomKeyPair(KeyType::secp256k1); + auto const localSigningPublic = localSigningKeys.first; + auto const localSigningSecret = localSigningKeys.second; + auto const localMasterSecret = randomSecretKey(); + auto const localMasterPublic = derivePublicKey( + KeyType::ed25519, localMasterSecret); - while (network.size () != 8) - network.push_back (randomNode()); + auto cfgManifest = makeManifestString ( + localMasterPublic, localMasterSecret, + localSigningPublic, localSigningSecret, 1); auto format = []( PublicKey const &publicKey, char const* comment = nullptr) { - auto ret = toBase58( - TokenType::TOKEN_NODE_PUBLIC, - publicKey); + auto ret = toBase58 (TokenType::TOKEN_NODE_PUBLIC, publicKey); if (comment) ret += comment; @@ -88,213 +158,592 @@ private: return ret; }; - Section s1; + std::vector configList; + configList.reserve(8); - // Correct (empty) configuration - BEAST_EXPECT(validators->load (s1)); - BEAST_EXPECT(validators->size() == 0); + while (configList.size () != 8) + configList.push_back (randomNode()); // Correct configuration - s1.append (format (network[0])); - s1.append (format (network[1], " Comment")); - s1.append (format (network[2], " Multi Word Comment")); - s1.append (format (network[3], " Leading Whitespace")); - s1.append (format (network[4], " Trailing Whitespace ")); - s1.append (format (network[5], " Leading & Trailing Whitespace ")); - s1.append (format (network[6], " Leading, Trailing & Internal Whitespace ")); - s1.append (format (network[7], " ")); - - BEAST_EXPECT(validators->load (s1)); - - for (auto const& n : network) - BEAST_EXPECT(validators->trusted (n)); - - // Incorrect configurations: - Section s2; - s2.append ("NotAPublicKey"); - BEAST_EXPECT(!validators->load (s2)); - - Section s3; - s3.append (format (network[0], "!")); - BEAST_EXPECT(!validators->load (s3)); - - Section s4; - s4.append (format (network[0], "! Comment")); - BEAST_EXPECT(!validators->load (s4)); - - // Check if we properly terminate when we encounter - // a malformed or unparseable entry: - auto const node1 = randomNode(); - auto const node2 = randomNode (); - - Section s5; - s5.append (format (node1, "XXX")); - s5.append (format (node2)); - BEAST_EXPECT(!validators->load (s5)); - BEAST_EXPECT(!validators->trusted (node1)); - BEAST_EXPECT(!validators->trusted (node2)); - - // Add Ed25519 master public keys to permanent validators list - auto const masterNode1 = randomMasterKey (); - auto const masterNode2 = randomMasterKey (); - - Section s6; - s6.append (format (masterNode1)); - s6.append (format (masterNode2, " Comment")); - BEAST_EXPECT(validators->load (s6)); - BEAST_EXPECT(validators->trusted (masterNode1)); - BEAST_EXPECT(validators->trusted (masterNode2)); - } - - void - testMembership () - { - // The servers on the permanentValidators - std::vector permanentValidators; - std::vector ephemeralValidators; - - while (permanentValidators.size () != 64) - permanentValidators.push_back (randomNode()); - - while (ephemeralValidators.size () != 64) - ephemeralValidators.push_back (randomNode()); + std::vector cfgKeys ({ + format (configList[0]), + format (configList[1], " Comment"), + format (configList[2], " Multi Word Comment"), + format (configList[3], " Leading Whitespace"), + format (configList[4], " Trailing Whitespace "), + format (configList[5], " Leading & Trailing Whitespace "), + format (configList[6], " Leading, Trailing & Internal Whitespace "), + format (configList[7], " ") + }); { - testcase ("Membership: No Validators"); + ManifestCache manifests; + auto trustedKeys = std::make_unique ( + manifests, manifests, env.timeKeeper(), journal); - auto vl = std::make_unique (beast::Journal ()); + // Correct (empty) configuration + BEAST_EXPECT(trustedKeys->load ( + emptyLocalKey, emptyCfgKeys, emptyCfgPublishers)); - for (auto const& v : permanentValidators) - BEAST_EXPECT(!vl->trusted (v)); + // load local validator key with or without manifest + BEAST_EXPECT(trustedKeys->load ( + localSigningPublic, emptyCfgKeys, emptyCfgPublishers)); + BEAST_EXPECT(trustedKeys->listed (localSigningPublic)); - for (auto const& v : ephemeralValidators) - BEAST_EXPECT(!vl->trusted (v)); + manifests.applyManifest (*Manifest::make_Manifest(cfgManifest)); + BEAST_EXPECT(trustedKeys->load ( + localSigningPublic, emptyCfgKeys, emptyCfgPublishers)); + + BEAST_EXPECT(trustedKeys->listed (localMasterPublic)); + BEAST_EXPECT(trustedKeys->listed (localSigningPublic)); } - { - testcase ("Membership: Non-Empty, Some Present, Some Not Present"); + // load should add validator keys from config + ManifestCache manifests; + auto trustedKeys = std::make_unique ( + manifests, manifests, env.timeKeeper(), journal); - std::vector p ( - permanentValidators.begin (), - permanentValidators.begin () + 16); + BEAST_EXPECT(trustedKeys->load ( + emptyLocalKey, cfgKeys, emptyCfgPublishers)); - while (p.size () != 32) - p.push_back (randomNode()); + for (auto const& n : configList) + BEAST_EXPECT(trustedKeys->listed (n)); - std::vector e ( - ephemeralValidators.begin (), - ephemeralValidators.begin () + 16); + // load should accept Ed25519 master public keys + auto const masterNode1 = randomMasterKey (); + auto const masterNode2 = randomMasterKey (); - while (e.size () != 32) - e.push_back (randomNode()); + std::vector cfgMasterKeys({ + format (masterNode1), + format (masterNode2, " Comment") + }); + BEAST_EXPECT(trustedKeys->load ( + emptyLocalKey, cfgMasterKeys, emptyCfgPublishers)); + BEAST_EXPECT(trustedKeys->listed (masterNode1)); + BEAST_EXPECT(trustedKeys->listed (masterNode2)); - auto vl = std::make_unique (beast::Journal ()); + // load should reject invalid config keys + std::vector badKeys({"NotAPublicKey"}); + BEAST_EXPECT(!trustedKeys->load ( + emptyLocalKey, badKeys, emptyCfgPublishers)); - for (auto const& v : p) - vl->insertPermanentKey (v, ""); + badKeys[0] = format (randomNode(), "!"); + BEAST_EXPECT(!trustedKeys->load ( + emptyLocalKey, badKeys, emptyCfgPublishers)); - for (auto const& v : e) - vl->insertEphemeralKey (v, ""); + badKeys[0] = format (randomNode(), "! Comment"); + BEAST_EXPECT(!trustedKeys->load ( + emptyLocalKey, badKeys, emptyCfgPublishers)); - for (auto const& v : p) - BEAST_EXPECT(vl->trusted (v)); + // load terminates when encountering an invalid entry + auto const goodKey = randomNode(); + badKeys.push_back (format (goodKey)); + BEAST_EXPECT(!trustedKeys->load ( + emptyLocalKey, badKeys, emptyCfgPublishers)); + BEAST_EXPECT(!trustedKeys->listed (goodKey)); + } + { + // local validator key on config list + ManifestCache manifests; + auto trustedKeys = std::make_unique ( + manifests, manifests, env.timeKeeper(), journal); - for (auto const& v : e) - BEAST_EXPECT(vl->trusted (v)); + auto const localSigningPublic = parseBase58 ( + TokenType::TOKEN_NODE_PUBLIC, cfgKeys.front()); - for (auto const& v : permanentValidators) - BEAST_EXPECT(static_cast(vl->trusted (v)) == isPresent (p, v)); + BEAST_EXPECT(trustedKeys->load ( + *localSigningPublic, cfgKeys, emptyCfgPublishers)); - for (auto const& v : ephemeralValidators) - BEAST_EXPECT(static_cast(vl->trusted (v)) == isPresent (e, v)); + BEAST_EXPECT(trustedKeys->listed (*localSigningPublic)); + for (auto const& n : configList) + BEAST_EXPECT(trustedKeys->listed (n)); + } + { + // local validator key not on config list + ManifestCache manifests; + auto trustedKeys = std::make_unique ( + manifests, manifests, env.timeKeeper(), journal); + + auto const localSigningPublic = randomNode(); + BEAST_EXPECT(trustedKeys->load ( + localSigningPublic, cfgKeys, emptyCfgPublishers)); + + BEAST_EXPECT(trustedKeys->listed (localSigningPublic)); + for (auto const& n : configList) + BEAST_EXPECT(trustedKeys->listed (n)); + } + { + // local validator key (with manifest) not on config list + ManifestCache manifests; + auto trustedKeys = std::make_unique ( + manifests, manifests, env.timeKeeper(), journal); + + manifests.applyManifest (*Manifest::make_Manifest(cfgManifest)); + + BEAST_EXPECT(trustedKeys->load ( + localSigningPublic, cfgKeys, emptyCfgPublishers)); + + BEAST_EXPECT(trustedKeys->listed (localSigningPublic)); + BEAST_EXPECT(trustedKeys->listed (localMasterPublic)); + for (auto const& n : configList) + BEAST_EXPECT(trustedKeys->listed (n)); + } + { + ManifestCache manifests; + auto trustedKeys = std::make_unique ( + manifests, manifests, env.timeKeeper(), journal); + + // load should reject invalid validator list signing keys + std::vector badPublishers( + {"NotASigningKey"}); + BEAST_EXPECT(!trustedKeys->load ( + emptyLocalKey, emptyCfgKeys, badPublishers)); + + // load should reject validator list signing keys with invalid encoding + std::vector keys ({ + randomMasterKey(), randomMasterKey(), randomMasterKey()}); + badPublishers.clear(); + for (auto const& key : keys) + badPublishers.push_back ( + toBase58 (TokenType::TOKEN_NODE_PUBLIC, key)); + + BEAST_EXPECT(! trustedKeys->load ( + emptyLocalKey, emptyCfgKeys, badPublishers)); + for (auto const& key : keys) + BEAST_EXPECT(!trustedKeys->trustedPublisher (key)); + + // load should accept valid validator list publisher keys + std::vector cfgPublishers; + for (auto const& key : keys) + cfgPublishers.push_back (strHex(key)); + + BEAST_EXPECT(trustedKeys->load ( + emptyLocalKey, emptyCfgKeys, cfgPublishers)); + for (auto const& key : keys) + BEAST_EXPECT(trustedKeys->trustedPublisher (key)); } } void - testModification () + testApplyList () { - testcase ("Insertion and Removal"); + testcase ("Apply list"); - auto vl = std::make_unique (beast::Journal ()); + beast::Journal journal; + ManifestCache manifests; + jtx::Env env (*this); + auto trustedKeys = std::make_unique ( + manifests, manifests, env.app().timeKeeper(), journal); - auto const v = randomNode (); + auto const publisherSecret = randomSecretKey(); + auto const publisherPublic = + derivePublicKey(KeyType::ed25519, publisherSecret); + auto const pubSigningKeys1 = randomKeyPair(KeyType::secp256k1); + auto const manifest1 = beast::detail::base64_encode(makeManifestString ( + publisherPublic, publisherSecret, + pubSigningKeys1.first, pubSigningKeys1.second, 1)); - // Inserting a new permanent key succeeds - BEAST_EXPECT(vl->insertPermanentKey (v, "Permanent")); - { - auto member = vl->member (v); - BEAST_EXPECT(static_cast(member)); - BEAST_EXPECT(member->compare("Permanent") == 0); - } - // Inserting the same permanent key fails: - BEAST_EXPECT(!vl->insertPermanentKey (v, "")); - { - auto member = vl->member (v); - BEAST_EXPECT(static_cast(member)); - BEAST_EXPECT(member->compare("Permanent") == 0); - } - // Inserting the same key as ephemeral fails: - BEAST_EXPECT(!vl->insertEphemeralKey (v, "Ephemeral")); - { - auto member = vl->member (v); - BEAST_EXPECT(static_cast(member)); - BEAST_EXPECT(member->compare("Permanent") == 0); - } - // Removing the key as ephemeral fails: - BEAST_EXPECT(!vl->removeEphemeralKey (v)); - { - auto member = vl->member (v); - BEAST_EXPECT(static_cast(member)); - BEAST_EXPECT(member->compare("Permanent") == 0); - } - // Deleting the key as permanent succeeds: - BEAST_EXPECT(vl->removePermanentKey (v)); - BEAST_EXPECT(!static_cast(vl->trusted (v))); + std::vector cfgKeys1({ + strHex(publisherPublic)}); + PublicKey emptyLocalKey; + std::vector emptyCfgKeys; + + BEAST_EXPECT(trustedKeys->load ( + emptyLocalKey, emptyCfgKeys, cfgKeys1)); + + auto constexpr listSize = 20; + std::vector list1; + list1.reserve (listSize); + while (list1.size () < listSize) + list1.push_back (randomNode()); + + std::vector list2; + list2.reserve (listSize); + while (list2.size () < listSize) + list2.push_back (randomNode()); + + // do not apply expired list + auto const version = 1; + auto const sequence = 1; + auto const expiredblob = makeList ( + list1, sequence, env.timeKeeper().now().time_since_epoch().count()); + auto const expiredSig = signList (expiredblob, pubSigningKeys1); + + BEAST_EXPECT(ListDisposition::stale == + trustedKeys->applyList ( + manifest1, expiredblob, expiredSig, version)); + + // apply single list + NetClock::time_point const expiration = + env.timeKeeper().now() + 3600s; + auto const blob1 = makeList ( + list1, sequence, expiration.time_since_epoch().count()); + auto const sig1 = signList (blob1, pubSigningKeys1); + + BEAST_EXPECT(ListDisposition::accepted == trustedKeys->applyList ( + manifest1, blob1, sig1, version)); + + for (auto const& val : list1) + BEAST_EXPECT(trustedKeys->listed (val)); + + // do not use list from untrusted publisher + auto const untrustedManifest = beast::detail::base64_encode( + makeManifestString ( + randomMasterKey(), publisherSecret, + pubSigningKeys1.first, pubSigningKeys1.second, 1)); + + BEAST_EXPECT(ListDisposition::untrusted == trustedKeys->applyList ( + untrustedManifest, blob1, sig1, version)); + + // do not use list with unhandled version + auto const badVersion = 666; + BEAST_EXPECT(ListDisposition::unsupported_version == + trustedKeys->applyList ( + manifest1, blob1, sig1, badVersion)); + + // apply list with highest sequence number + auto const sequence2 = 2; + auto const blob2 = makeList ( + list2, sequence2, expiration.time_since_epoch().count()); + auto const sig2 = signList (blob2, pubSigningKeys1); + + BEAST_EXPECT(ListDisposition::accepted == + trustedKeys->applyList ( + manifest1, blob2, sig2, version)); + + for (auto const& val : list1) + BEAST_EXPECT(! trustedKeys->listed (val)); + + for (auto const& val : list2) + BEAST_EXPECT(trustedKeys->listed (val)); + + // do not re-apply lists with past or current sequence numbers + BEAST_EXPECT(ListDisposition::stale == + trustedKeys->applyList ( + manifest1, blob1, sig1, version)); + + BEAST_EXPECT(ListDisposition::stale == + trustedKeys->applyList ( + manifest1, blob2, sig2, version)); + + // apply list with new publisher key updated by manifest + auto const pubSigningKeys2 = randomKeyPair(KeyType::secp256k1); + auto manifest2 = beast::detail::base64_encode(makeManifestString ( + publisherPublic, publisherSecret, + pubSigningKeys2.first, pubSigningKeys2.second, 2)); + + auto const sequence3 = 3; + auto const blob3 = makeList ( + list1, sequence3, expiration.time_since_epoch().count()); + auto const sig3 = signList (blob3, pubSigningKeys2); + + BEAST_EXPECT(ListDisposition::accepted == + trustedKeys->applyList ( + manifest2, blob3, sig3, version)); + + auto const sequence4 = 4; + auto const blob4 = makeList ( + list1, sequence4, expiration.time_since_epoch().count()); + auto const badSig = signList (blob4, pubSigningKeys1); + BEAST_EXPECT(ListDisposition::invalid == + trustedKeys->applyList ( + manifest1, blob4, badSig, version)); + + // do not apply list with revoked publisher key + // applied list is removed due to revoked publisher key + auto const signingKeysMax = randomKeyPair(KeyType::secp256k1); + auto maxManifest = beast::detail::base64_encode(makeManifestString ( + publisherPublic, publisherSecret, + pubSigningKeys2.first, pubSigningKeys2.second, + std::numeric_limits::max ())); + + auto const sequence5 = 5; + auto const blob5 = makeList ( + list1, sequence5, expiration.time_since_epoch().count()); + auto const sig5 = signList (blob5, signingKeysMax); + + BEAST_EXPECT(ListDisposition::untrusted == + trustedKeys->applyList ( + maxManifest, blob5, sig5, version)); + + BEAST_EXPECT(! trustedKeys->trustedPublisher(publisherPublic)); + for (auto const& val : list1) + BEAST_EXPECT(! trustedKeys->listed (val)); + } + + void + testUpdate () + { + testcase ("Update"); + + PublicKey emptyLocalKey; + ManifestCache manifests; + jtx::Env env (*this); + auto trustedKeys = std::make_unique ( + manifests, manifests, env.timeKeeper(), beast::Journal ()); + + std::vector cfgPublishers; + hash_set activeValidators; - // Insert an ephemeral validator key - BEAST_EXPECT(vl->insertEphemeralKey (v, "Ephemeral")); { - auto member = vl->member (v); - BEAST_EXPECT(static_cast(member)); - BEAST_EXPECT(member->compare("Ephemeral") == 0); + std::vector cfgKeys; + cfgKeys.reserve(20); + + while (cfgKeys.size () != 20) + { + auto const valKey = randomNode(); + cfgKeys.push_back (toBase58( + TokenType::TOKEN_NODE_PUBLIC, valKey)); + if (cfgKeys.size () <= 15) + activeValidators.emplace (valKey); + } + + BEAST_EXPECT(trustedKeys->load ( + emptyLocalKey, cfgKeys, cfgPublishers)); + + // onConsensusStart should make all available configured + // validators trusted + trustedKeys->onConsensusStart (activeValidators); + BEAST_EXPECT(trustedKeys->quorum () == 12); + std::size_t i = 0; + for (auto const& val : cfgKeys) + { + if (auto const valKey = parseBase58( + TokenType::TOKEN_NODE_PUBLIC, val)) + { + BEAST_EXPECT(trustedKeys->listed (*valKey)); + if (i++ < activeValidators.size ()) + BEAST_EXPECT(trustedKeys->trusted (*valKey)); + else + BEAST_EXPECT(!trustedKeys->trusted (*valKey)); + } + else + fail (); + } } - // Inserting the same ephemeral key fails - BEAST_EXPECT(!vl->insertEphemeralKey (v, "")); { - auto member = vl->member (v); - BEAST_EXPECT(static_cast(member)); - BEAST_EXPECT(member->compare("Ephemeral") == 0); + // update with manifests + auto const masterPrivate = randomSecretKey(); + auto const masterPublic = + derivePublicKey(KeyType::ed25519, masterPrivate); + + std::vector cfgKeys ({ + toBase58 (TokenType::TOKEN_NODE_PUBLIC, masterPublic)}); + + BEAST_EXPECT(trustedKeys->load ( + emptyLocalKey, cfgKeys, cfgPublishers)); + + auto const signingKeys1 = randomKeyPair(KeyType::secp256k1); + auto const signingPublic1 = signingKeys1.first; + activeValidators.emplace (masterPublic); + + // Should not trust ephemeral signing key if there is no manifest + trustedKeys->onConsensusStart (activeValidators); + BEAST_EXPECT(trustedKeys->listed (masterPublic)); + BEAST_EXPECT(trustedKeys->trusted (masterPublic)); + BEAST_EXPECT(!trustedKeys->listed (signingPublic1)); + BEAST_EXPECT(!trustedKeys->trusted (signingPublic1)); + + // Should trust the ephemeral signing key from the applied manifest + auto m1 = Manifest::make_Manifest (makeManifestString ( + masterPublic, masterPrivate, + signingPublic1, signingKeys1.second, 1)); + + BEAST_EXPECT( + manifests.applyManifest(std::move (*m1)) == + ManifestDisposition::accepted); + trustedKeys->onConsensusStart (activeValidators); + BEAST_EXPECT(trustedKeys->quorum () == 13); + BEAST_EXPECT(trustedKeys->listed (masterPublic)); + BEAST_EXPECT(trustedKeys->trusted (masterPublic)); + BEAST_EXPECT(trustedKeys->listed (signingPublic1)); + BEAST_EXPECT(trustedKeys->trusted (signingPublic1)); + + // Should only trust the ephemeral signing key + // from the newest applied manifest + auto const signingKeys2 = randomKeyPair(KeyType::secp256k1); + auto const signingPublic2 = signingKeys2.first; + auto m2 = Manifest::make_Manifest (makeManifestString ( + masterPublic, masterPrivate, + signingPublic2, signingKeys2.second, 2)); + + BEAST_EXPECT( + manifests.applyManifest(std::move (*m2)) == + ManifestDisposition::accepted); + trustedKeys->onConsensusStart (activeValidators); + BEAST_EXPECT(trustedKeys->quorum () == 13); + BEAST_EXPECT(trustedKeys->listed (masterPublic)); + BEAST_EXPECT(trustedKeys->trusted (masterPublic)); + BEAST_EXPECT(trustedKeys->listed (signingPublic2)); + BEAST_EXPECT(trustedKeys->trusted (signingPublic2)); + BEAST_EXPECT(!trustedKeys->listed (signingPublic1)); + BEAST_EXPECT(!trustedKeys->trusted (signingPublic1)); + + // Should not trust keys from revoked master public key + auto const signingKeysMax = randomKeyPair(KeyType::secp256k1); + auto const signingPublicMax = signingKeysMax.first; + activeValidators.emplace (signingPublicMax); + auto mMax = Manifest::make_Manifest (makeManifestString ( + masterPublic, masterPrivate, + signingPublicMax, signingKeysMax.second, + std::numeric_limits::max ())); + + BEAST_EXPECT(mMax->revoked ()); + BEAST_EXPECT( + manifests.applyManifest(std::move (*mMax)) == + ManifestDisposition::accepted); + BEAST_EXPECT(manifests.getSigningKey (masterPublic) == masterPublic); + BEAST_EXPECT(manifests.revoked (masterPublic)); + trustedKeys->onConsensusStart (activeValidators); + BEAST_EXPECT(trustedKeys->quorum () == 12); + BEAST_EXPECT(trustedKeys->listed (masterPublic)); + BEAST_EXPECT(!trustedKeys->trusted (masterPublic)); + BEAST_EXPECT(!trustedKeys->listed (signingPublicMax)); + BEAST_EXPECT(!trustedKeys->trusted (signingPublicMax)); + BEAST_EXPECT(!trustedKeys->listed (signingPublic2)); + BEAST_EXPECT(!trustedKeys->trusted (signingPublic2)); + BEAST_EXPECT(!trustedKeys->listed (signingPublic1)); + BEAST_EXPECT(!trustedKeys->trusted (signingPublic1)); } - // Inserting the same key as permanent fails: - BEAST_EXPECT(!vl->insertPermanentKey (v, "Permanent")); { - auto member = vl->member (v); - BEAST_EXPECT(static_cast(member)); - BEAST_EXPECT(member->compare("Ephemeral") == 0); + // Make quorum unattainable if lists from any publishers are unavailable + auto trustedKeys = std::make_unique ( + manifests, manifests, env.timeKeeper(), beast::Journal ()); + auto const publisherSecret = randomSecretKey(); + auto const publisherPublic = + derivePublicKey(KeyType::ed25519, publisherSecret); + + std::vector cfgPublishers({ + strHex(publisherPublic)}); + std::vector emptyCfgKeys; + + BEAST_EXPECT(trustedKeys->load ( + emptyLocalKey, emptyCfgKeys, cfgPublishers)); + + trustedKeys->onConsensusStart (activeValidators); + BEAST_EXPECT(trustedKeys->quorum () == + std::numeric_limits::max()); } - // Deleting the key as permanent fails: - BEAST_EXPECT(!vl->removePermanentKey (v)); { - auto member = vl->member (v); - BEAST_EXPECT(static_cast(member)); - BEAST_EXPECT(member->compare("Ephemeral") == 0); + // Trust all listed validators if none are active + auto trustedKeys = std::make_unique ( + manifests, manifests, env.timeKeeper(), beast::Journal ()); + + std::vector keys ({ randomNode (), randomNode () }); + hash_set activeValidators; + std::vector cfgKeys ({ + toBase58 (TokenType::TOKEN_NODE_PUBLIC, keys[0]), + toBase58 (TokenType::TOKEN_NODE_PUBLIC, keys[1])}); + + BEAST_EXPECT(trustedKeys->load ( + emptyLocalKey, cfgKeys, cfgPublishers)); + + trustedKeys->onConsensusStart (activeValidators); + + BEAST_EXPECT(trustedKeys->quorum () == 2); + for (auto const& key : keys) + BEAST_EXPECT(trustedKeys->trusted (key)); + } + { + // Should use custom minimum quorum + std::size_t const minQuorum = 0; + ManifestCache manifests; + auto trustedKeys = std::make_unique ( + manifests, manifests, env.timeKeeper(), beast::Journal (), minQuorum); + + auto const node = randomNode (); + std::vector cfgKeys ({ + toBase58 (TokenType::TOKEN_NODE_PUBLIC, node)}); + hash_set activeValidators; + + BEAST_EXPECT(trustedKeys->load ( + emptyLocalKey, cfgKeys, cfgPublishers)); + + trustedKeys->onConsensusStart (activeValidators); + BEAST_EXPECT(trustedKeys->quorum () == minQuorum); + + activeValidators.emplace (node); + trustedKeys->onConsensusStart (activeValidators); + BEAST_EXPECT(trustedKeys->quorum () == 1); + } + { + // Increase quorum when running as an unlisted validator + auto trustedKeys = std::make_unique ( + manifests, manifests, env.timeKeeper(), beast::Journal ()); + + std::vector keys ({ randomNode (), randomNode () }); + hash_set activeValidators ({ keys[0] }); + std::vector cfgKeys ({ + toBase58 (TokenType::TOKEN_NODE_PUBLIC, keys[0]), + toBase58 (TokenType::TOKEN_NODE_PUBLIC, keys[1])}); + + auto const localKey = randomNode (); + BEAST_EXPECT(trustedKeys->load ( + localKey, cfgKeys, cfgPublishers)); + + trustedKeys->onConsensusStart (activeValidators); + BEAST_EXPECT(trustedKeys->quorum () == 3); + + // local validator key is always trusted + BEAST_EXPECT(trustedKeys->trusted (localKey)); + } + { + // Remove expired published list + auto trustedKeys = std::make_unique ( + manifests, manifests, env.app().timeKeeper(), beast::Journal ()); + + PublicKey emptyLocalKey; + std::vector emptyCfgKeys; + auto const publisherKeys = randomKeyPair(KeyType::secp256k1); + auto const pubSigningKeys = randomKeyPair(KeyType::secp256k1); + auto const manifest = beast::detail::base64_encode ( + makeManifestString ( + publisherKeys.first, publisherKeys.second, + pubSigningKeys.first, pubSigningKeys.second, 1)); + + std::vector cfgKeys ({ + strHex(publisherKeys.first)}); + + BEAST_EXPECT(trustedKeys->load ( + emptyLocalKey, emptyCfgKeys, cfgKeys)); + + std::vector list ({randomNode()}); + hash_set activeValidators ({ list[0] }); + + // do not apply expired list + auto const version = 1; + auto const sequence = 1; + NetClock::time_point const expiration = + env.timeKeeper().now() + 60s; + auto const blob = makeList ( + list, sequence, expiration.time_since_epoch().count()); + auto const sig = signList (blob, pubSigningKeys); + + BEAST_EXPECT(ListDisposition::accepted == + trustedKeys->applyList ( + manifest, blob, sig, version)); + + trustedKeys->onConsensusStart (activeValidators); + BEAST_EXPECT(trustedKeys->trusted (list[0])); + + env.timeKeeper().set(expiration); + trustedKeys->onConsensusStart (activeValidators); + BEAST_EXPECT(! trustedKeys->trusted (list[0])); } - // Deleting the key as ephemeral succeeds: - BEAST_EXPECT(vl->removeEphemeralKey (v)); - BEAST_EXPECT(!vl->trusted(v)); } public: void run() override { - testConfigLoad(); - testMembership (); - testModification (); + testGenesisQuorum (); + testConfigLoad (); + testApplyList (); + testUpdate (); } }; BEAST_DEFINE_TESTSUITE(ValidatorList, app, ripple); -} // tests +} // test } // ripple diff --git a/src/test/core/Config_test.cpp b/src/test/core/Config_test.cpp index 5fc422028..f852d52f3 100644 --- a/src/test/core/Config_test.cpp +++ b/src/test/core/Config_test.cpp @@ -285,9 +285,9 @@ public: } }; -std::string valFileContents (boost::optional const& quorum) +std::string valFileContents () { - static boost::format configContentsTemplate (R"rippleConfig( + std::string configContents (R"rippleConfig( [validators] n949f75evCHwgyP4fPVgaHqNHxUVN15PsJEZ3B3HnXPcPjcZAoy7 n9MD5h24qrQqiyBC8aeqqCWvpiBiYQ3jxSr91uiDvmrkyHRdYLUj @@ -300,14 +300,11 @@ nHUhG1PgAG8H8myUENypM35JgfqXAKNQvRVVAFDRzJrny5eZN8d5 nHBu9PTL9dn2GuZtdW4U2WzBwffyX9qsQCd9CNU4Z5YG3PQfViM8 nHUPDdcdb2Y5DZAJne4c2iabFuAP3F34xZUgYQT2NH7qfkdapgnz -%1% - +[validator_list_keys] +03E74EE14CB525AFBB9F1B7D86CD58ECC4B91452294B42AB4E78F260BD905C091D +030775A669685BD6ABCEBD80385921C7851783D991A8055FD21D2F3966C96F1B56 )rippleConfig"); - - std::string quorumSection = - quorum ? "[validation_quorum]\n" + to_string(*quorum) : ""; - return boost::str ( - configContentsTemplate % quorumSection); + return configContents; } /** @@ -321,7 +318,7 @@ private: public: ValidatorsTxtGuard (beast::unit_test::suite& test, path subDir, path const& validatorsFileName, - boost::optional const& quorum, bool useCounter = true) + bool useCounter = true) : ConfigGuard (test, std::move (subDir), useCounter) { using namespace boost::filesystem; @@ -332,7 +329,7 @@ public: if (!exists (validatorsFile_)) { std::ofstream o (validatorsFile_.string ()); - o << valFileContents (quorum); + o << valFileContents (); } else { @@ -392,9 +389,6 @@ port_wss_admin [ssl_verify] 0 - -[validation_quorum] -3 )rippleConfig"); c.loadFromString (toLoad); @@ -499,9 +493,8 @@ port_wss_admin } { // load should throw for invalid [validators_file] - int const quorum = 3; detail::ValidatorsTxtGuard const vtg ( - *this, "test_cfg", "validators.cfg", quorum); + *this, "test_cfg", "validators.cfg"); path const invalidFile = current_path () / vtg.subdir (); boost::format cc ("[validators_file]\n%1%\n"); std::string error; @@ -517,7 +510,7 @@ port_wss_admin BEAST_EXPECT(error == expectedError); } { - // load validators and quorum from config + // load validators from config into single section Config c; std::string toLoad(R"rippleConfig( [validators] @@ -528,53 +521,59 @@ n9L81uNCaPgtUJfaHh89gmdvXKAmSt5Gdsw2g1iPWaPkAHW5Nm4C [validator_keys] nHUhG1PgAG8H8myUENypM35JgfqXAKNQvRVVAFDRzJrny5eZN8d5 nHBu9PTL9dn2GuZtdW4U2WzBwffyX9qsQCd9CNU4Z5YG3PQfViM8 - -[validation_quorum] -4 )rippleConfig"); c.loadFromString (toLoad); BEAST_EXPECT(c.legacy ("validators_file").empty ()); - BEAST_EXPECT(c.section (SECTION_VALIDATORS).values ().size () == 3); - BEAST_EXPECT(c.section (SECTION_VALIDATOR_KEYS).values ().size () == 2); - BEAST_EXPECT(c.VALIDATION_QUORUM == 4); + BEAST_EXPECT(c.section (SECTION_VALIDATORS).values ().size () == 5); + } + { + // load validator list keys from config + Config c; + std::string toLoad(R"rippleConfig( +[validator_list_keys] +021A99A537FDEBC34E4FCA03B39BEADD04299BB19E85097EC92B15A3518801E566 +)rippleConfig"); + c.loadFromString (toLoad); + BEAST_EXPECT( + c.section (SECTION_VALIDATOR_LIST_KEYS).values ().size () == 1); + BEAST_EXPECT( + c.section (SECTION_VALIDATOR_LIST_KEYS).values ()[0] == + "021A99A537FDEBC34E4FCA03B39BEADD04299BB19E85097EC92B15A3518801E566"); } { // load from specified [validators_file] absolute path - int const quorum = 3; detail::ValidatorsTxtGuard const vtg ( - *this, "test_cfg", "validators.cfg", quorum); + *this, "test_cfg", "validators.cfg"); BEAST_EXPECT(vtg.validatorsFileExists ()); Config c; boost::format cc ("[validators_file]\n%1%\n"); c.loadFromString (boost::str (cc % vtg.validatorsFile ())); BEAST_EXPECT(c.legacy ("validators_file") == vtg.validatorsFile ()); - BEAST_EXPECT(c.section (SECTION_VALIDATORS).values ().size () == 5); - BEAST_EXPECT(c.section (SECTION_VALIDATOR_KEYS).values ().size () == 3); - BEAST_EXPECT(c.VALIDATION_QUORUM == quorum); + BEAST_EXPECT(c.section (SECTION_VALIDATORS).values ().size () == 8); + BEAST_EXPECT( + c.section (SECTION_VALIDATOR_LIST_KEYS).values ().size () == 2); } { // load from specified [validators_file] file name // in config directory - int const quorum = 3; std::string const valFileName = "validators.txt"; detail::ValidatorsTxtGuard const vtg ( - *this, "test_cfg", valFileName, quorum); + *this, "test_cfg", valFileName); detail::RippledCfgGuard const rcg ( *this, vtg.subdir (), "", valFileName, false); BEAST_EXPECT(vtg.validatorsFileExists ()); BEAST_EXPECT(rcg.configFileExists ()); auto const& c (rcg.config ()); BEAST_EXPECT(c.legacy ("validators_file") == valFileName); - BEAST_EXPECT(c.section (SECTION_VALIDATORS).values ().size () == 5); - BEAST_EXPECT(c.section (SECTION_VALIDATOR_KEYS).values ().size () == 3); - BEAST_EXPECT(c.VALIDATION_QUORUM == quorum); + BEAST_EXPECT(c.section (SECTION_VALIDATORS).values ().size () == 8); + BEAST_EXPECT( + c.section (SECTION_VALIDATOR_LIST_KEYS).values ().size () == 2); } { // load from specified [validators_file] relative path // to config directory - int const quorum = 3; detail::ValidatorsTxtGuard const vtg ( - *this, "test_cfg", "validators.txt", quorum); + *this, "test_cfg", "validators.txt"); auto const valFilePath = ".." / vtg.subdir() / "validators.txt"; detail::RippledCfgGuard const rcg ( *this, vtg.subdir (), "", valFilePath, false); @@ -582,63 +581,41 @@ nHBu9PTL9dn2GuZtdW4U2WzBwffyX9qsQCd9CNU4Z5YG3PQfViM8 BEAST_EXPECT(rcg.configFileExists ()); auto const& c (rcg.config ()); BEAST_EXPECT(c.legacy ("validators_file") == valFilePath); - BEAST_EXPECT(c.section (SECTION_VALIDATORS).values ().size () == 5); - BEAST_EXPECT(c.section (SECTION_VALIDATOR_KEYS).values ().size () == 3); - BEAST_EXPECT(c.VALIDATION_QUORUM == quorum); + BEAST_EXPECT(c.section (SECTION_VALIDATORS).values ().size () == 8); + BEAST_EXPECT( + c.section (SECTION_VALIDATOR_LIST_KEYS).values ().size () == 2); } { // load from validators file in default location - int const quorum = 3; detail::ValidatorsTxtGuard const vtg ( - *this, "test_cfg", "validators.txt", quorum); + *this, "test_cfg", "validators.txt"); detail::RippledCfgGuard const rcg (*this, vtg.subdir (), "", "", false); BEAST_EXPECT(vtg.validatorsFileExists ()); BEAST_EXPECT(rcg.configFileExists ()); auto const& c (rcg.config ()); BEAST_EXPECT(c.legacy ("validators_file").empty ()); - BEAST_EXPECT(c.section (SECTION_VALIDATORS).values ().size () == 5); - BEAST_EXPECT(c.section (SECTION_VALIDATOR_KEYS).values ().size () == 3); - BEAST_EXPECT(c.VALIDATION_QUORUM == quorum); + BEAST_EXPECT(c.section (SECTION_VALIDATORS).values ().size () == 8); + BEAST_EXPECT( + c.section (SECTION_VALIDATOR_LIST_KEYS).values ().size () == 2); } { // load from specified [validators_file] instead // of default location - int const quorum = 3; detail::ValidatorsTxtGuard const vtg ( - *this, "test_cfg", "validators.cfg", quorum); + *this, "test_cfg", "validators.cfg"); BEAST_EXPECT(vtg.validatorsFileExists ()); detail::ValidatorsTxtGuard const vtgDefault ( - *this, vtg.subdir (), "validators.txt", 4, false); + *this, vtg.subdir (), "validators.txt", false); BEAST_EXPECT(vtgDefault.validatorsFileExists ()); detail::RippledCfgGuard const rcg ( *this, vtg.subdir (), "", vtg.validatorsFile (), false); BEAST_EXPECT(rcg.configFileExists ()); auto const& c (rcg.config ()); BEAST_EXPECT(c.legacy ("validators_file") == vtg.validatorsFile ()); - BEAST_EXPECT(c.section (SECTION_VALIDATORS).values ().size () == 5); - BEAST_EXPECT(c.section (SECTION_VALIDATOR_KEYS).values ().size () == 3); - BEAST_EXPECT(c.VALIDATION_QUORUM == quorum); - } - { - // do not load quorum from validators file if in config - boost::format cc (R"rippleConfig( -[validators_file] -%1% - -[validation_quorum] -4 -)rippleConfig"); - int const quorum = 3; - detail::ValidatorsTxtGuard const vtg ( - *this, "test_cfg", "validators.cfg", quorum); - BEAST_EXPECT(vtg.validatorsFileExists ()); - Config c; - c.loadFromString (boost::str (cc % vtg.validatorsFile ())); - BEAST_EXPECT(c.legacy ("validators_file") == vtg.validatorsFile ()); - BEAST_EXPECT(c.section (SECTION_VALIDATORS).values ().size () == 5); - BEAST_EXPECT(c.section (SECTION_VALIDATOR_KEYS).values ().size () == 3); - BEAST_EXPECT(c.VALIDATION_QUORUM == 4); + BEAST_EXPECT(c.section (SECTION_VALIDATORS).values ().size () == 8); + BEAST_EXPECT( + c.section (SECTION_VALIDATOR_LIST_KEYS).values ().size () == 2); } { @@ -658,51 +635,34 @@ n9LdgEtkmGB9E2h3K4Vp7iGUaKuq23Zr32ehxiU8FWY7xoxbWTSA nHB1X37qrniVugfQcuBTAjswphC1drx7QjFFojJPZwKHHnt8kU7v nHUkAWDR4cB8AgPg7VXMX6et8xRTQb2KJfgv1aBEXozwrawRKgMB +[validator_list_keys] +021A99A537FDEBC34E4FCA03B39BEADD04299BB19E85097EC92B15A3518801E566 )rippleConfig"); - int const quorum = 3; detail::ValidatorsTxtGuard const vtg ( - *this, "test_cfg", "validators.cfg", quorum); + *this, "test_cfg", "validators.cfg"); BEAST_EXPECT(vtg.validatorsFileExists ()); Config c; c.loadFromString (boost::str (cc % vtg.validatorsFile ())); BEAST_EXPECT(c.legacy ("validators_file") == vtg.validatorsFile ()); - BEAST_EXPECT(c.section (SECTION_VALIDATORS).values ().size () == 10); - BEAST_EXPECT(c.section (SECTION_VALIDATOR_KEYS).values ().size () == 5); - BEAST_EXPECT(c.VALIDATION_QUORUM == quorum); + BEAST_EXPECT(c.section (SECTION_VALIDATORS).values ().size () == 15); + BEAST_EXPECT( + c.section (SECTION_VALIDATOR_LIST_KEYS).values ().size () == 3); } { - // load should throw if [validators] and [validator_keys] are - // missing from rippled cfg and validators file + // load should throw if [validators], [validator_keys] and + // [validator_list_keys] are missing from rippled cfg and + // validators file + Config c; boost::format cc ("[validators_file]\n%1%\n"); std::string error; detail::ValidatorsTxtGuard const vtg ( - *this, "test_cfg", "validators.cfg", boost::none); + *this, "test_cfg", "validators.cfg"); BEAST_EXPECT(vtg.validatorsFileExists ()); auto const expectedError = "The file specified in [validators_file] does not contain a " - "[validators] or [validator_keys] section: " + + "[validators], [validator_keys] or [validator_list_keys] section: " + vtg.validatorsFile (); std::ofstream o (vtg.validatorsFile ()); - o << "[validation_quorum]\n3\n"; - try { - Config c; - c.loadFromString (boost::str (cc % vtg.validatorsFile ())); - } catch (std::runtime_error& e) { - error = e.what(); - } - BEAST_EXPECT(error == expectedError); - } - { - // load should throw if [validation_quorum] is - // missing from rippled cfg and validators file - boost::format cc ("[validators_file]\n%1%\n"); - std::string error; - detail::ValidatorsTxtGuard const vtg ( - *this, "test_cfg", "validators.cfg", boost::none); - BEAST_EXPECT(vtg.validatorsFileExists ()); - auto const expectedError = - "The file specified in [validators_file] does not contain a " - "[validation_quorum] section: " + vtg.validatorsFile (); try { Config c; c.loadFromString (boost::str (cc % vtg.validatorsFile ())); diff --git a/src/test/rpc/ServerInfo_test.cpp b/src/test/rpc/ServerInfo_test.cpp index fc750290d..da2cd8c48 100644 --- a/src/test/rpc/ServerInfo_test.cpp +++ b/src/test/rpc/ServerInfo_test.cpp @@ -57,10 +57,13 @@ public: [validation_seed] %2% + +[validators] +%3% )rippleConfig"); p->loadFromString (boost::str ( - toLoad % validator::manifest % validator::seed)); + toLoad % validator::manifest % validator::seed % validator::master_key)); setupConfigForUnitTests(*p); diff --git a/src/test/unity/app_test_unity.cpp b/src/test/unity/app_test_unity.cpp index 314218f68..e43b800ff 100644 --- a/src/test/unity/app_test_unity.cpp +++ b/src/test/unity/app_test_unity.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/unity/overlay_test_unity.cpp b/src/test/unity/overlay_test_unity.cpp index 6c20024f2..7b9cd76ac 100644 --- a/src/test/unity/overlay_test_unity.cpp +++ b/src/test/unity/overlay_test_unity.cpp @@ -19,6 +19,5 @@ //============================================================================== #include -#include #include #include \ No newline at end of file