From 4ed6cbdd5bd56a570aedd3fa3a92d47336b20097 Mon Sep 17 00:00:00 2001 From: wilsonianb Date: Tue, 10 May 2016 16:46:11 -0700 Subject: [PATCH] Get quorum and trusted master validator keys from validators.txt: * Load specified [validators_file] relative to config dir * Add default [validators_file] to rippled-example.cfg * Remove [validators] and [validation_quorum] from rippled-example.cfg * Add [validation_quorum] to validators-example.txt * Allow validators.txt to be a symlink * Throw for invalid [validators_file] instead of logging * Trust own master public key from configured manifest * Do not load untrusted manifests from database Trusted validators are loaded from [validators] and [validator_keys] sections from both rippled.cfg and validators.txt Quorum is loaded from [validation_quorum] section in validators.txt only if it is not configured in rippled.cfg --- doc/rippled-example.cfg | 62 +-- doc/validators-example.txt | 35 +- src/ripple/core/ConfigSections.h | 2 + src/ripple/core/impl/Config.cpp | 90 ++-- src/ripple/core/tests/Config.test.cpp | 461 ++++++++++++++++++--- src/ripple/overlay/impl/Manifest.cpp | 24 +- src/ripple/overlay/impl/OverlayImpl.cpp | 10 +- src/ripple/overlay/tests/manifest_test.cpp | 92 ++-- 8 files changed, 585 insertions(+), 191 deletions(-) diff --git a/doc/rippled-example.cfg b/doc/rippled-example.cfg index 92f6c87774..9420668e74 100644 --- a/doc/rippled-example.cfg +++ b/doc/rippled-example.cfg @@ -513,14 +513,6 @@ # # # -# [validation_quorum] -# -# 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. -# -# -# # [ledger_history] # # The number of past ledgers to acquire on server startup and the minimum to @@ -562,29 +554,21 @@ # # # -# [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. -# -# Examples: -# n9KorY8QtTdRx7TVDpwnG9NvyxsDwHUKUEeDLY3AkiGncVaSXZi5 -# n9MqiExBcoG19UXwoLjBJnhsxEhAZMuWwJDRdkyDz1EkEkwzQTNt John Doe -# -# -# # [validators_file] # -# Path to a file that contains 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. +# Path or name of a file that contains the validation public keys of nodes +# 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] entry, followed by -# a list of validation public keys of nodes to always accept as validators, -# one per line, optionally followed by a comment separated by whitespace: +# 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. # -# Specify the file by specifying its full path. +# Specify the file by its name or path. +# Unless an absolute path is specified, it will be considered relative to +# the folder in which the rippled.cfg file is located. # # Examples: # /home/ripple/validators.txt @@ -598,6 +582,9 @@ # n9KiYM9CgngLvtRCQHZwgC2gjpdaZcCcbt3VboxiNFcKuwFVujzS RL4 # n9LdgEtkmGB9E2h3K4Vp7iGUaKuq23Zr32ehxiU8FWY7xoxbWTSA RL5 # +# [validation_quorum] +# 3 +# # # [path_search] # When searching for paths, the default search aggressiveness. This can take @@ -987,22 +974,11 @@ pool.ntp.org [ips] r.ripple.com 51235 -# Public keys of the validators that this rippled instance trusts. The latest -# list of validators can be obtained from https://ripple.com/ripple.txt -# -# See also https://wiki.ripple.com/Ripple.txt -# -[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 +# File containing validation quorum and trusted validator keys. +# Unless an absolute path is specified, it will be considered relative to the +# folder in which the rippled.cfg file is located. +[validators_file] +validators.txt # Turn down default logging to save disk space in the long run. # Valid values here are trace, debug, info, warning, error, and fatal diff --git a/doc/validators-example.txt b/doc/validators-example.txt index 1ed83bbe5f..6d4941343f 100644 --- a/doc/validators-example.txt +++ b/doc/validators-example.txt @@ -8,20 +8,41 @@ # Blank lines and lines starting with a '#' are ignored. # All other lines should be hankos or domain names. # -# [validators]: -# List of nodes to accept as validators specified by public key or domain. # -# For domains, rippled will probe for https web servers at the specified -# domain in the following order: ripple.DOMAIN, www.DOMAIN, DOMAIN # -# Examples: redstem.com -# n9KorY8QtTdRx7TVDpwnG9NvyxsDwHUKUEeDLY3AkiGncVaSXZi5 -# n9MqiExBcoG19UXwoLjBJnhsxEhAZMuWwJDRdkyDz1EkEkwzQTNt John Doe +# [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 +# +# See also https://wiki.ripple.com/Ripple.txt +# +# Examples: +# n9KorY8QtTdRx7TVDpwnG9NvyxsDwHUKUEeDLY3AkiGncVaSXZi5 +# n9MqiExBcoG19UXwoLjBJnhsxEhAZMuWwJDRdkyDz1EkEkwzQTNt John Doe +# +# +# +# [validation_quorum] +# +# 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. # +# 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 diff --git a/src/ripple/core/ConfigSections.h b/src/ripple/core/ConfigSections.h index 0207972da4..ee2c219d62 100644 --- a/src/ripple/core/ConfigSections.h +++ b/src/ripple/core/ConfigSections.h @@ -64,6 +64,8 @@ struct ConfigSection #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_VALIDATION_MANIFEST "validation_manifest" #define SECTION_VETO_AMENDMENTS "veto_amendments" } // ripple diff --git a/src/ripple/core/impl/Config.cpp b/src/ripple/core/impl/Config.cpp index 80dada5b7f..ac758d6cbc 100644 --- a/src/ripple/core/impl/Config.cpp +++ b/src/ripple/core/impl/Config.cpp @@ -426,11 +426,13 @@ 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 warn if the + // 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 path as the config file but don't complain + // in the same directory as the config file, but don't complain // if we can't find it. boost::filesystem::path validatorsFile; @@ -439,29 +441,22 @@ void Config::loadFromString (std::string const& fileContents) validatorsFile = strTemp; if (validatorsFile.empty ()) - { - JLOG (j_.error()) << - "[" SECTION_VALIDATORS_FILE "]" << - ": " << strTemp << - " is not a valid path"; - validatorsFile.clear (); - } - else if (!boost::filesystem::exists (validatorsFile)) - { - JLOG (j_.error()) << - "[" SECTION_VALIDATORS_FILE "]" << - ": the file " << validatorsFile << - " does not exist"; - validatorsFile.clear (); - } - else if (!boost::filesystem::is_regular_file (validatorsFile)) - { - JLOG (j_.error()) << - "[" SECTION_VALIDATORS_FILE "]" << - ": the file " << validatorsFile << - " is not a regular file"; - validatorsFile.clear (); - } + Throw ( + "Invalid path specified in [" SECTION_VALIDATORS_FILE "]"); + + if (!validatorsFile.is_absolute()) + 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 { @@ -471,14 +466,16 @@ void Config::loadFromString (std::string const& fileContents) { if(!boost::filesystem::exists (validatorsFile)) validatorsFile.clear(); - else if (!boost::filesystem::is_regular_file (validatorsFile)) + 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_regular_file (validatorsFile) || + boost::filesystem::is_symlink (validatorsFile))) { std::ifstream ifsDefault (validatorsFile.native().c_str()); @@ -494,17 +491,36 @@ void Config::loadFromString (std::string const& fileContents) iniFile, SECTION_VALIDATORS); - if (!entries) - { - JLOG (j_.error()) << - "[" SECTION_VALIDATORS_FILE "]" << - ": the file " << validatorsFile << - " does not contain a [" SECTION_VALIDATORS << - "] section"; - } - else - { + 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)); } } diff --git a/src/ripple/core/tests/Config.test.cpp b/src/ripple/core/tests/Config.test.cpp index b21fdca2a4..b04ce6a215 100644 --- a/src/ripple/core/tests/Config.test.cpp +++ b/src/ripple/core/tests/Config.test.cpp @@ -30,7 +30,8 @@ namespace ripple { namespace detail { -std::string configContents (std::string const& dbPath) +std::string configContents (std::string const& dbPath, + std::string const& validatorsFile) { static boost::format configContentsTemplate (R"rippleConfig( [server] @@ -80,6 +81,7 @@ file_size_mb=8 file_size_mult=2 %1% + %2% # This needs to be an absolute directory reference, not a relative one. @@ -98,20 +100,6 @@ pool.ntp.org [ips] r.ripple.com 51235 -# The latest validators can be obtained from -# https://ripple.com/ripple.txt -# -[validators] -n949f75evCHwgyP4fPVgaHqNHxUVN15PsJEZ3B3HnXPcPjcZAoy7 RL1 -n9MD5h24qrQqiyBC8aeqqCWvpiBiYQ3jxSr91uiDvmrkyHRdYLUj RL2 -n9L81uNCaPgtUJfaHh89gmdvXKAmSt5Gdsw2g1iPWaPkAHW5Nm4C RL3 -n9KiYM9CgngLvtRCQHZwgC2gjpdaZcCcbt3VboxiNFcKuwFVujzS RL4 -n9LdgEtkmGB9E2h3K4Vp7iGUaKuq23Zr32ehxiU8FWY7xoxbWTSA RL5 - -# Ditto. -[validation_quorum] -3 - # Turn down default logging to save disk space in the long run. # Valid values here are trace, debug, info, warning, error, and fatal [rpc_startup] @@ -126,10 +114,12 @@ n9LdgEtkmGB9E2h3K4Vp7iGUaKuq23Zr32ehxiU8FWY7xoxbWTSA RL5 backend=sqlite )rippleConfig"); - if (!dbPath.empty ()) - return boost::str (configContentsTemplate % "[database_path]" % dbPath); - else - return boost::str (configContentsTemplate % "" % ""); + std::string dbPathSection = + dbPath.empty () ? "" : "[database_path]\n" + dbPath; + std::string valFileSection = + validatorsFile.empty () ? "" : "[validators_file]\n" + validatorsFile; + return boost::str ( + configContentsTemplate % dbPathSection % valFileSection); } /** @@ -138,28 +128,28 @@ backend=sqlite class ConfigGuard { private: + bool rmSubDir_{false}; + +protected: using path = boost::filesystem::path; path subDir_; - path configFile_; - path dataDir_; + beast::unit_test::suite& test_; - bool rmSubDir_{false}; - bool rmDataDir_{false}; - - Config config_; + auto rmDir (path const& toRm) + { + if (is_directory (toRm) && is_empty (toRm)) + remove (toRm); + else + test_.log << "Expected " << toRm.string () + << " to be an empty existing directory."; + }; public: - ConfigGuard (std::string subDir, std::string const& dbPath) - : subDir_ (std::move (subDir)), dataDir_ (dbPath) + ConfigGuard (beast::unit_test::suite& test, std::string subDir) + : subDir_ (std::move (subDir)) + , test_ (test) { using namespace boost::filesystem; - - if (dbPath.empty ()) - { - dataDir_ = subDir_ / path (Config::databaseDirName); - } - - configFile_ = subDir_ / path (Config::configFileName); { if (!exists (subDir_)) { @@ -170,17 +160,60 @@ public: rmSubDir_ = false; else { - // Cannot run the test someone created a file where we want to - // put out directory + // Cannot run the test. Someone created a file where we want to + // put our directory Throw ( "Cannot create directory: " + subDir_.string ()); } } + } + ~ConfigGuard () + { + try + { + using namespace boost::filesystem; + + if (rmSubDir_) + rmDir (subDir_); + else + test_.log << "Skipping rm dir: " << subDir_.string (); + } + catch (std::exception& e) + { + // if we throw here, just let it die. + test_.log << "Error in ~ConfigGuard: " << e.what (); + }; + } +}; + +/** + Write a rippled config file and remove when done. + */ +class RippledCfgGuard : ConfigGuard +{ +private: + path configFile_; + path dataDir_; + + bool rmDataDir_{false}; + + Config config_; + +public: + RippledCfgGuard (beast::unit_test::suite& test, + std::string subDir, std::string const& dbPath, + std::string const& validatorsFile) + : ConfigGuard (test, std::move (subDir)), dataDir_ (dbPath) + { + if (dbPath.empty ()) + dataDir_ = subDir_ / path (Config::databaseDirName); + + configFile_ = subDir_ / path (Config::configFileName); if (!exists (configFile_)) { std::ofstream o (configFile_.string ()); - o << configContents (dbPath); + o << configContents (dbPath, validatorsFile); } else { @@ -202,42 +235,111 @@ public: } bool configFileExists () const { - return boost::filesystem::is_regular_file (configFile_); + return boost::filesystem::exists (configFile_); } - ~ConfigGuard () + ~RippledCfgGuard () { try { using namespace boost::filesystem; - if (!is_regular_file (configFile_)) - std::cerr << "Expected " << configFile_.string () - << " to be an existing file.\n"; + if (!boost::filesystem::exists (configFile_)) + test_.log << "Expected " << configFile_.string () + << " to be an existing file."; else remove (configFile_.string ()); - auto rmDir = [](path const& toRm) - { - if (is_directory (toRm) && is_empty (toRm)) - remove (toRm); - else - std::cerr << "Expected " << toRm.string () - << " to be an empty existing directory.\n"; - }; - if (rmDataDir_) rmDir (dataDir_); else - std::cerr << "Skipping rm dir: " << dataDir_.string () << "\n"; - - if (rmSubDir_) - rmDir (subDir_); - else - std::cerr << "Skipping rm dir: " << subDir_.string () << "\n"; + test_.log << "Skipping rm dir: " << dataDir_.string (); } catch (std::exception& e) { // if we throw here, just let it die. - std::cerr << "Error in CreateConfigGuard: " << e.what () << "\n"; + test_.log << "Error in ~RippledCfgGuard: " << e.what (); + }; + } +}; + +std::string valFileContents (boost::optional const& quorum) +{ + static boost::format configContentsTemplate (R"rippleConfig( +[validators] +n949f75evCHwgyP4fPVgaHqNHxUVN15PsJEZ3B3HnXPcPjcZAoy7 +n9MD5h24qrQqiyBC8aeqqCWvpiBiYQ3jxSr91uiDvmrkyHRdYLUj +n9L81uNCaPgtUJfaHh89gmdvXKAmSt5Gdsw2g1iPWaPkAHW5Nm4C +n9KiYM9CgngLvtRCQHZwgC2gjpdaZcCcbt3VboxiNFcKuwFVujzS +n9LdgEtkmGB9E2h3K4Vp7iGUaKuq23Zr32ehxiU8FWY7xoxbWTSA + +[validator_keys] +nHUhG1PgAG8H8myUENypM35JgfqXAKNQvRVVAFDRzJrny5eZN8d5 +nHBu9PTL9dn2GuZtdW4U2WzBwffyX9qsQCd9CNU4Z5YG3PQfViM8 +nHUPDdcdb2Y5DZAJne4c2iabFuAP3F34xZUgYQT2NH7qfkdapgnz + +%1% + +)rippleConfig"); + + std::string quorumSection = + quorum ? "[validation_quorum]\n" + to_string(*quorum) : ""; + return boost::str ( + configContentsTemplate % quorumSection); +} + +/** + Write a validators.txt file and remove when done. + */ +class ValidatorsTxtGuard : ConfigGuard +{ +private: + path validatorsFile_; + +public: + ValidatorsTxtGuard (beast::unit_test::suite& test, + std::string subDir, std::string const& validatorsFileName, + boost::optional const& quorum) + : ConfigGuard (test, std::move (subDir)) + { + using namespace boost::filesystem; + validatorsFile_ = current_path () / subDir_ / path ( + validatorsFileName.empty () ? Config::validatorsFileName : + validatorsFileName); + + if (!exists (validatorsFile_)) + { + std::ofstream o (validatorsFile_.string ()); + o << valFileContents (quorum); + } + else + { + Throw ( + "Refusing to overwrite existing config file: " + + validatorsFile_.string ()); + } + } + bool validatorsFileExists () const + { + return boost::filesystem::exists (validatorsFile_); + } + std::string validatorsFile () const + { + return validatorsFile_.string (); + } + ~ValidatorsTxtGuard () + { + try + { + using namespace boost::filesystem; + if (!boost::filesystem::exists (validatorsFile_)) + test_.log << "Expected " << validatorsFile_.string () + << " to be an existing file."; + else + remove (validatorsFile_.string ()); + } + catch (std::exception& e) + { + // if we throw here, just let it die. + test_.log << "Error in ~ValidatorsTxtGuard: " << e.what (); }; } }; @@ -301,7 +403,7 @@ port_wss_admin expect (c.legacy ("database_path") == dataDirAbs.string ()); } { - // No db sectcion. + // No db section. // N.B. Config::setup will give database_path a default, // load will not. Config c; @@ -314,7 +416,7 @@ port_wss_admin auto const cwd = current_path (); path const dataDirRel ("test_data_dir"); path const dataDirAbs (cwd / path ("test_db") / dataDirRel); - detail::ConfigGuard g ("test_db", dataDirAbs.string ()); + detail::RippledCfgGuard g (*this, "test_db", dataDirAbs.string (), ""); auto& c (g.config ()); expect (g.dataDirExists ()); expect (g.configFileExists ()); @@ -324,7 +426,7 @@ port_wss_admin { // read from file relative path std::string const dbPath ("my_db"); - detail::ConfigGuard g ("test_db", dbPath); + detail::RippledCfgGuard g (*this, "test_db", dbPath, ""); auto& c (g.config ()); std::string const nativeDbPath = absolute (path (dbPath)).string (); expect (g.dataDirExists ()); @@ -334,7 +436,7 @@ port_wss_admin } { // read from file no path - detail::ConfigGuard g ("test_db", ""); + detail::RippledCfgGuard g (*this, "test_db", "", ""); auto& c (g.config ()); std::string const nativeDbPath = absolute (path ("test_db") / @@ -346,10 +448,245 @@ port_wss_admin "dbPath No Path"); } } + void testValidatorsFile () + { + testcase ("validators_file"); + + using namespace boost::filesystem; + { + // load should throw for missing specified validators file + Config c; + boost::format cc ("[validators_file]\n%1%\n"); + std::string error; + std::string const missingPath = "/no/way/this/path/exists"; + auto const expectedError = + "The file specified in [validators_file] does not exist: " + + missingPath; + try { + c.loadFromString (boost::str (cc % missingPath)); + } catch (std::runtime_error& e) { + error = e.what(); + } + expect (error == expectedError); + } + { + // load should throw for invalid [validators_file] + int const quorum = 3; + detail::ValidatorsTxtGuard vtg ( + *this, "test_cfg", "validators.cfg", quorum); + Config c; + path const invalidFile = current_path () / "test_cfg"; + boost::format cc ("[validators_file]\n%1%\n"); + std::string error; + auto const expectedError = + "Invalid file specified in [validators_file]: " + + invalidFile.string (); + try { + c.loadFromString (boost::str (cc % invalidFile.string ())); + } catch (std::runtime_error& e) { + error = e.what(); + } + expect (error == expectedError); + } + { + // load validators and quorum from config + Config c; + std::string toLoad(R"rippleConfig( +[validators] +n949f75evCHwgyP4fPVgaHqNHxUVN15PsJEZ3B3HnXPcPjcZAoy7 +n9MD5h24qrQqiyBC8aeqqCWvpiBiYQ3jxSr91uiDvmrkyHRdYLUj +n9L81uNCaPgtUJfaHh89gmdvXKAmSt5Gdsw2g1iPWaPkAHW5Nm4C + +[validator_keys] +nHUhG1PgAG8H8myUENypM35JgfqXAKNQvRVVAFDRzJrny5eZN8d5 +nHBu9PTL9dn2GuZtdW4U2WzBwffyX9qsQCd9CNU4Z5YG3PQfViM8 + +[validation_quorum] +4 +)rippleConfig"); + c.loadFromString (toLoad); + expect (c.legacy ("validators_file").empty ()); + expect (c.section (SECTION_VALIDATORS).values ().size () == 3); + expect (c.section (SECTION_VALIDATOR_KEYS).values ().size () == 2); + expect (c.VALIDATION_QUORUM == 4); + } + { + // load from specified [validators_file] absolute path + int const quorum = 3; + detail::ValidatorsTxtGuard vtg ( + *this, "test_cfg", "validators.cfg", quorum); + expect (vtg.validatorsFileExists ()); + Config c; + boost::format cc ("[validators_file]\n%1%\n"); + c.loadFromString (boost::str (cc % vtg.validatorsFile ())); + expect (c.legacy ("validators_file") == vtg.validatorsFile ()); + expect (c.section (SECTION_VALIDATORS).values ().size () == 5); + expect (c.section (SECTION_VALIDATOR_KEYS).values ().size () == 3); + expect (c.VALIDATION_QUORUM == quorum); + } + { + // load from specified [validators_file] file name + // in config directory + int const quorum = 3; + std::string const valFileName = "validators.txt"; + detail::ValidatorsTxtGuard vtg ( + *this, "test_cfg", valFileName, quorum); + detail::RippledCfgGuard rcg ( + *this, "test_cfg", "", valFileName); + expect (vtg.validatorsFileExists ()); + expect (rcg.configFileExists ()); + auto& c (rcg.config ()); + expect (c.legacy ("validators_file") == valFileName); + expect (c.section (SECTION_VALIDATORS).values ().size () == 5); + expect (c.section (SECTION_VALIDATOR_KEYS).values ().size () == 3); + expect (c.VALIDATION_QUORUM == quorum); + } + { + // load from specified [validators_file] relative path + // to config directory + int const quorum = 3; + std::string const valFilePath = "../test_cfg/validators.txt"; + detail::ValidatorsTxtGuard vtg ( + *this, "test_cfg", "validators.txt", quorum); + detail::RippledCfgGuard rcg ( + *this, "test_cfg", "", valFilePath); + expect (vtg.validatorsFileExists ()); + expect (rcg.configFileExists ()); + auto& c (rcg.config ()); + expect (c.legacy ("validators_file") == valFilePath); + expect (c.section (SECTION_VALIDATORS).values ().size () == 5); + expect (c.section (SECTION_VALIDATOR_KEYS).values ().size () == 3); + expect (c.VALIDATION_QUORUM == quorum); + } + { + // load from validators file in default location + int const quorum = 3; + detail::ValidatorsTxtGuard vtg ( + *this, "test_cfg", "validators.txt", quorum); + detail::RippledCfgGuard rcg (*this, "test_cfg", "", ""); + expect (vtg.validatorsFileExists ()); + expect (rcg.configFileExists ()); + auto& c (rcg.config ()); + expect (c.legacy ("validators_file").empty ()); + expect (c.section (SECTION_VALIDATORS).values ().size () == 5); + expect (c.section (SECTION_VALIDATOR_KEYS).values ().size () == 3); + expect (c.VALIDATION_QUORUM == quorum); + } + { + // load from specified [validators_file] instead + // of default location + int const quorum = 3; + detail::ValidatorsTxtGuard vtg ( + *this, "test_cfg", "validators.cfg", quorum); + expect (vtg.validatorsFileExists ()); + detail::ValidatorsTxtGuard vtgDefault ( + *this, "test_cfg", "validators.txt", 4); + expect (vtgDefault.validatorsFileExists ()); + detail::RippledCfgGuard rcg ( + *this, "test_cfg", "", vtg.validatorsFile ()); + expect (rcg.configFileExists ()); + auto& c (rcg.config ()); + expect (c.legacy ("validators_file") == vtg.validatorsFile ()); + expect (c.section (SECTION_VALIDATORS).values ().size () == 5); + expect (c.section (SECTION_VALIDATOR_KEYS).values ().size () == 3); + 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 vtg ( + *this, "test_cfg", "validators.cfg", quorum); + expect (vtg.validatorsFileExists ()); + Config c; + c.loadFromString (boost::str (cc % vtg.validatorsFile ())); + expect (c.legacy ("validators_file") == vtg.validatorsFile ()); + expect (c.section (SECTION_VALIDATORS).values ().size () == 5); + expect (c.section (SECTION_VALIDATOR_KEYS).values ().size () == 3); + expect (c.VALIDATION_QUORUM == 4); + } + { + // load validators from both config and validators file + boost::format cc (R"rippleConfig( +[validators_file] +%1% + +[validators] +n949f75evCHwgyP4fPVgaHqNHxUVN15PsJEZ3B3HnXPcPjcZAoy7 +n9MD5h24qrQqiyBC8aeqqCWvpiBiYQ3jxSr91uiDvmrkyHRdYLUj +n9L81uNCaPgtUJfaHh89gmdvXKAmSt5Gdsw2g1iPWaPkAHW5Nm4C +n9KiYM9CgngLvtRCQHZwgC2gjpdaZcCcbt3VboxiNFcKuwFVujzS +n9LdgEtkmGB9E2h3K4Vp7iGUaKuq23Zr32ehxiU8FWY7xoxbWTSA + +[validator_keys] +nHB1X37qrniVugfQcuBTAjswphC1drx7QjFFojJPZwKHHnt8kU7v +nHUkAWDR4cB8AgPg7VXMX6et8xRTQb2KJfgv1aBEXozwrawRKgMB + +)rippleConfig"); + int const quorum = 3; + detail::ValidatorsTxtGuard vtg ( + *this, "test_cfg", "validators.cfg", quorum); + expect (vtg.validatorsFileExists ()); + Config c; + c.loadFromString (boost::str (cc % vtg.validatorsFile ())); + expect (c.legacy ("validators_file") == vtg.validatorsFile ()); + expect (c.section (SECTION_VALIDATORS).values ().size () == 10); + expect (c.section (SECTION_VALIDATOR_KEYS).values ().size () == 5); + expect (c.VALIDATION_QUORUM == quorum); + } + { + // load should throw if [validators] and [validator_keys] are + // missing from rippled cfg and validators file + Config c; + boost::format cc ("[validators_file]\n%1%\n"); + std::string error; + detail::ValidatorsTxtGuard vtg ( + *this, "test_cfg", "validators.cfg", boost::none); + expect (vtg.validatorsFileExists ()); + auto const expectedError = + "The file specified in [validators_file] does not contain a " + "[validators] or [validator_keys] section: " + + vtg.validatorsFile (); + std::ofstream o (vtg.validatorsFile ()); + o << "[validation_quorum]\n3\n"; + try { + c.loadFromString (boost::str (cc % vtg.validatorsFile ())); + } catch (std::runtime_error& e) { + error = e.what(); + } + expect (error == expectedError); + } + { + // load should throw if [validation_quorum] is + // missing from rippled cfg and validators file + Config c; + boost::format cc ("[validators_file]\n%1%\n"); + std::string error; + detail::ValidatorsTxtGuard vtg ( + *this, "test_cfg", "validators.cfg", boost::none); + expect (vtg.validatorsFileExists ()); + auto const expectedError = + "The file specified in [validators_file] does not contain a " + "[validation_quorum] section: " + vtg.validatorsFile (); + try { + c.loadFromString (boost::str (cc % vtg.validatorsFile ())); + } catch (std::runtime_error& e) { + error = e.what(); + } + expect (error == expectedError); + } + } void run () { testLegacy (); testDbPath (); + testValidatorsFile (); } }; diff --git a/src/ripple/overlay/impl/Manifest.cpp b/src/ripple/overlay/impl/Manifest.cpp index 4ebd669780..e01b94647e 100644 --- a/src/ripple/overlay/impl/Manifest.cpp +++ b/src/ripple/overlay/impl/Manifest.cpp @@ -198,6 +198,12 @@ ManifestCache::configManifest ( 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) @@ -412,15 +418,15 @@ void ManifestCache::load ( Throw ("Unverifiable manifest in db"); } - // Remove master public key from permanent trusted key list - if (unl.trusted(mo->masterKey)) - unl.removePermanentKey (mo->masterKey); - - // add trusted key - map_[mo->masterKey]; - - // OK if not accepted (may have been loaded from the config file) - applyManifest (std::move(*mo), unl, journal); + 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 { diff --git a/src/ripple/overlay/impl/OverlayImpl.cpp b/src/ripple/overlay/impl/OverlayImpl.cpp index 966824c417..cdf3aca897 100644 --- a/src/ripple/overlay/impl/OverlayImpl.cpp +++ b/src/ripple/overlay/impl/OverlayImpl.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -483,15 +484,15 @@ OverlayImpl::setupValidatorKeyManifests (BasicConfig const& config, DatabaseCon& db) { auto const loaded = manifestCache_.loadValidatorKeys ( - config.section ("validator_keys"), + config.section (SECTION_VALIDATOR_KEYS), journal_); if (!loaded) Throw ( - "Unable to load keys from [validator_keys]"); + "Unable to load keys from [" SECTION_VALIDATOR_KEYS "]"); auto const validation_manifest = - config.section ("validation_manifest"); + config.section (SECTION_VALIDATION_MANIFEST); if (! validation_manifest.lines().empty()) { @@ -513,7 +514,8 @@ OverlayImpl::setupValidatorKeyManifests (BasicConfig const& config, } else { - JLOG(journal_.debug()) << "No [validation_manifest] section in config"; + JLOG(journal_.debug()) << "No [" SECTION_VALIDATION_MANIFEST << + "] section in config"; } manifestCache_.load ( diff --git a/src/ripple/overlay/tests/manifest_test.cpp b/src/ripple/overlay/tests/manifest_test.cpp index 0696c4e5d2..c914dcbba3 100644 --- a/src/ripple/overlay/tests/manifest_test.cpp +++ b/src/ripple/overlay/tests/manifest_test.cpp @@ -221,10 +221,19 @@ public: expect (!cache.loadValidatorKeys (s6, journal)); expect (!cache.trusted (node1)); 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 (KeyType::ed25519, sk, kp.first, 0); + + cache.configManifest (clone (m), *unl, journal); + expect (cache.trusted (m.masterKey)); } - void testLoadStore (ManifestCache const& m, ValidatorList& unl, - PublicKey& pk) + void testLoadStore (ManifestCache const& m, ValidatorList& unl) { testcase ("load/store"); @@ -236,19 +245,13 @@ public: setup.dataDir = getDatabasePath (); DatabaseCon dbCon(setup, dbName, WalletDBInit, WalletDBCount); + if (!m.size ()) + fail (); + m.save (dbCon); - ManifestCache loaded; beast::Journal journal; - // load should remove master key from permanent key list - expect (m.trusted(pk)); - expect (unl.insertPermanentKey(pk, "trusted key")); - expect (unl.trusted(pk)); - loaded.load (dbCon, unl, journal); - expect (!unl.trusted(pk)); - expect (loaded.trusted(pk)); - auto getPopulatedManifests = [](ManifestCache const& cache) -> std::vector { @@ -270,19 +273,51 @@ public: }; std::vector const inManifests ( sort (getPopulatedManifests (m))); - std::vector const loadedManifests ( - sort (getPopulatedManifests (loaded))); - if (inManifests.size () == loadedManifests.size ()) { - expect (std::equal - (inManifests.begin (), inManifests.end (), - loadedManifests.begin (), - [](Manifest const* lhs, Manifest const* rhs) - {return *lhs == *rhs;})); + // load should not load untrusted master keys from db + ManifestCache loaded; + + loaded.load (dbCon, unl, journal); + expect (loaded.size() == 0); } - else { - fail (); + // load should load all trusted master keys from db + ManifestCache loaded; + + for (auto const& man : inManifests) + loaded.addTrustedKey (man->masterKey, ""); + + loaded.load (dbCon, unl, journal); + + std::vector const loadedManifests ( + sort (getPopulatedManifests (loaded))); + + if (inManifests.size () == loadedManifests.size ()) + { + expect (std::equal + (inManifests.begin (), inManifests.end (), + loadedManifests.begin (), + [](Manifest const* lhs, Manifest const* rhs) + {return *lhs == *rhs;})); + } + else + { + fail (); + } + } + { + // load should remove master key from permanent key list + ManifestCache loaded; + auto const iMan = inManifests.begin(); + + if (!*iMan) + fail (); + expect (m.trusted((*iMan)->masterKey)); + expect (unl.insertPermanentKey((*iMan)->masterKey, "trusted key")); + expect (unl.trusted((*iMan)->masterKey)); + loaded.load (dbCon, unl, journal); + expect (!unl.trusted((*iMan)->masterKey)); + expect (loaded.trusted((*iMan)->masterKey)); } } boost::filesystem::remove (getDatabasePath () / @@ -315,7 +350,6 @@ public: ManifestCache cache; beast::Journal journal; auto unl = std::make_unique (journal); - PublicKey pk; { testcase ("apply"); auto const accepted = ManifestDisposition::accepted; @@ -360,18 +394,18 @@ public: // When trusted permanent key is found as manifest master key // move to manifest cache auto const sk_c = randomSecretKey(); - pk = derivePublicKey(KeyType::ed25519, sk_c); + auto const pk_c = derivePublicKey(KeyType::ed25519, sk_c); auto const kp_c = randomKeyPair(KeyType::secp256k1); auto const s_c0 = make_Manifest (KeyType::ed25519, sk_c, kp_c.first, 0); - expect (unl->insertPermanentKey(pk, "trusted key")); - expect (unl->trusted(pk)); - expect (!cache.trusted(pk)); + expect (unl->insertPermanentKey(pk_c, "trusted key")); + expect (unl->trusted(pk_c)); + expect (!cache.trusted(pk_c)); expect (cache.applyManifest(clone (s_c0), *unl, journal) == accepted); - expect (!unl->trusted(pk)); - expect (cache.trusted(pk)); + expect (!unl->trusted(pk_c)); + expect (cache.trusted(pk_c)); } testConfigLoad(); - testLoadStore (cache, *unl, pk); + testLoadStore (cache, *unl); testGetSignature (); } };