diff --git a/src/ripple/core/Config.h b/src/ripple/core/Config.h index f66bf3b8bd..632320e955 100644 --- a/src/ripple/core/Config.h +++ b/src/ripple/core/Config.h @@ -143,7 +143,14 @@ public: // True to ask peers not to relay current IP. bool PEER_PRIVATE = false; + // peers_max is a legacy configuration, which is going to be replaced + // with individual inbound peers peers_in_max and outbound peers + // peers_out_max configuration. for now we support both the legacy and + // the new configuration. if peers_max is configured then peers_in_max and + // peers_out_max are ignored. std::size_t PEERS_MAX = 0; + std::size_t PEERS_OUT_MAX = 0; + std::size_t PEERS_IN_MAX = 0; std::chrono::seconds WEBSOCKET_PING_FREQ = std::chrono::minutes{5}; diff --git a/src/ripple/core/ConfigSections.h b/src/ripple/core/ConfigSections.h index e3b864d858..8612396dad 100644 --- a/src/ripple/core/ConfigSections.h +++ b/src/ripple/core/ConfigSections.h @@ -72,6 +72,8 @@ struct ConfigSection #define SECTION_PATH_SEARCH_MAX "path_search_max" #define SECTION_PEER_PRIVATE "peer_private" #define SECTION_PEERS_MAX "peers_max" +#define SECTION_PEERS_IN_MAX "peers_in_max" +#define SECTION_PEERS_OUT_MAX "peers_out_max" #define SECTION_REDUCE_RELAY "reduce_relay" #define SECTION_RELAY_PROPOSALS "relay_proposals" #define SECTION_RELAY_VALIDATIONS "relay_validations" diff --git a/src/ripple/core/impl/Config.cpp b/src/ripple/core/impl/Config.cpp index fe0da432ac..ada2774e33 100644 --- a/src/ripple/core/impl/Config.cpp +++ b/src/ripple/core/impl/Config.cpp @@ -366,7 +366,45 @@ Config::loadFromString(std::string const& fileContents) PEER_PRIVATE = beast::lexicalCastThrow(strTemp); if (getSingleSection(secConfig, SECTION_PEERS_MAX, strTemp, j_)) + { PEERS_MAX = beast::lexicalCastThrow(strTemp); + } + else + { + std::optional peers_in_max{}; + if (getSingleSection(secConfig, SECTION_PEERS_IN_MAX, strTemp, j_)) + { + peers_in_max = beast::lexicalCastThrow(strTemp); + if (*peers_in_max > 1000) + Throw( + "Invalid value specified in [" SECTION_PEERS_IN_MAX + "] section; the value must be less or equal than 1000"); + } + + std::optional peers_out_max{}; + if (getSingleSection(secConfig, SECTION_PEERS_OUT_MAX, strTemp, j_)) + { + peers_out_max = beast::lexicalCastThrow(strTemp); + if (*peers_out_max < 10 || *peers_out_max > 1000) + Throw( + "Invalid value specified in [" SECTION_PEERS_OUT_MAX + "] section; the value must be in range 10-1000"); + } + + // if one section is configured then the other must be configured too + if ((peers_in_max && !peers_out_max) || + (peers_out_max && !peers_in_max)) + Throw("Both sections [" SECTION_PEERS_IN_MAX + "]" + "and [" SECTION_PEERS_OUT_MAX + "] must be configured"); + + if (peers_in_max && peers_out_max) + { + PEERS_IN_MAX = *peers_in_max; + PEERS_OUT_MAX = *peers_out_max; + } + } if (getSingleSection(secConfig, SECTION_NODE_SIZE, strTemp, j_)) { diff --git a/src/ripple/overlay/impl/OverlayImpl.cpp b/src/ripple/overlay/impl/OverlayImpl.cpp index af8c90711e..7441a05914 100644 --- a/src/ripple/overlay/impl/OverlayImpl.cpp +++ b/src/ripple/overlay/impl/OverlayImpl.cpp @@ -499,37 +499,11 @@ OverlayImpl::checkStopped() void OverlayImpl::onPrepare() { - PeerFinder::Config config; - - if (app_.config().PEERS_MAX != 0) - config.maxPeers = app_.config().PEERS_MAX; - - config.outPeers = config.calcOutPeers(); - - auto const port = serverHandler_.setup().overlay.port; - - config.peerPrivate = app_.config().PEER_PRIVATE; - - // Servers with peer privacy don't want to allow incoming connections - config.wantIncoming = (!config.peerPrivate) && (port != 0); - - // This will cause servers configured as validators to request that - // peers they connect to never report their IP address. We set this - // after we set the 'wantIncoming' because we want a "soft" version - // of peer privacy unless the operator explicitly asks for it. - if (!app_.getValidationPublicKey().empty()) - config.peerPrivate = true; - - // if it's a private peer or we are running as standalone - // automatic connections would defeat the purpose. - config.autoConnect = - !app_.config().standalone() && !app_.config().PEER_PRIVATE; - config.listeningPort = port; - config.features = ""; - config.ipLimit = setup_.ipLimit; - - // Enforce business rules - config.applyTuning(); + PeerFinder::Config config = PeerFinder::Config::makeConfig( + app_.config(), + serverHandler_.setup().overlay.port, + !app_.getValidationPublicKey().empty(), + setup_.ipLimit); m_peerFinder->setConfig(config); diff --git a/src/ripple/peerfinder/PeerfinderManager.h b/src/ripple/peerfinder/PeerfinderManager.h index 9c175cc976..bcca3c919c 100644 --- a/src/ripple/peerfinder/PeerfinderManager.h +++ b/src/ripple/peerfinder/PeerfinderManager.h @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -43,19 +44,19 @@ struct Config This includes both inbound and outbound, but does not include fixed peers. */ - int maxPeers; + std::size_t maxPeers; /** The number of automatic outbound connections to maintain. Outbound connections are only maintained if autoConnect - is `true`. The value can be fractional; The decision to round up - or down will be made using a per-process pseudorandom number and - a probability proportional to the fractional part. - Example: - If outPeers is 9.3, then 30% of nodes will maintain 9 outbound - connections, while 70% of nodes will maintain 10 outbound - connections. + is `true`. */ - double outPeers; + std::size_t outPeers; + + /** The number of automatic inbound connections to maintain. + Inbound connections are only maintained if wantIncoming + is `true`. + */ + std::size_t inPeers; /** `true` if we want our IP address kept private. */ bool peerPrivate = true; @@ -81,7 +82,7 @@ struct Config Config(); /** Returns a suitable value for outPeers according to the rules. */ - double + std::size_t calcOutPeers() const; /** Adjusts the values so they follow the business rules. */ @@ -91,6 +92,20 @@ struct Config /** Write the configuration into a property stream */ void onWrite(beast::PropertyStream::Map& map); + + /** Make PeerFinder::Config from configuration parameters + * @param config server's configuration + * @param port server's listening port + * @param validationPublicKey true if validation public key is not empty + * @param ipLimit limit of incoming connections per IP + * @return PeerFinder::Config + */ + static Config + makeConfig( + ripple::Config const& config, + std::uint16_t port, + bool validationPublicKey, + int ipLimit); }; //------------------------------------------------------------------------------ diff --git a/src/ripple/peerfinder/impl/Counts.h b/src/ripple/peerfinder/impl/Counts.h index 7053c5789c..5d9e318594 100644 --- a/src/ripple/peerfinder/impl/Counts.h +++ b/src/ripple/peerfinder/impl/Counts.h @@ -48,8 +48,6 @@ public: , m_acceptCount(0) , m_closingCount(0) { - m_roundingThreshold = - std::generate_canonical(default_prng()); } //-------------------------------------------------------------------------- @@ -136,28 +134,9 @@ public: void onConfig(Config const& config) { - // Calculate the number of outbound peers we want. If we dont want or - // can't accept incoming, this will simply be equal to maxPeers. - // Otherwise we calculate a fractional amount based on percentages and - // pseudo-randomly round up or down. - // + m_out_max = config.outPeers; if (config.wantIncoming) - { - // Round outPeers upwards using a Bernoulli distribution - m_out_max = std::floor(config.outPeers); - if (m_roundingThreshold < (config.outPeers - m_out_max)) - ++m_out_max; - } - else - { - m_out_max = config.maxPeers; - } - - // Calculate the largest number of inbound connections we could take. - if (config.maxPeers >= m_out_max) - m_in_max = config.maxPeers - m_out_max; - else - m_in_max = 0; + m_in_max = config.inPeers; } /** Returns the number of accepted connections that haven't handshaked. */ @@ -350,13 +329,6 @@ private: // Number of connections that are gracefully closing. int m_closingCount; - - /** Fractional threshold below which we round down. - This is used to round the value of Config::outPeers up or down in - such a way that the network-wide average number of outgoing - connections approximates the recommended, fractional value. - */ - double m_roundingThreshold; }; } // namespace PeerFinder diff --git a/src/ripple/peerfinder/impl/PeerfinderConfig.cpp b/src/ripple/peerfinder/impl/PeerfinderConfig.cpp index 3cd41e24f2..8b90bc9718 100644 --- a/src/ripple/peerfinder/impl/PeerfinderConfig.cpp +++ b/src/ripple/peerfinder/impl/PeerfinderConfig.cpp @@ -26,6 +26,7 @@ namespace PeerFinder { Config::Config() : maxPeers(Tuning::defaultMaxPeers) , outPeers(calcOutPeers()) + , inPeers(0) , wantIncoming(true) , autoConnect(true) , listeningPort(0) @@ -33,22 +34,17 @@ Config::Config() { } -double +std::size_t Config::calcOutPeers() const { return std::max( - maxPeers * Tuning::outPercent * 0.01, double(Tuning::minOutCount)); + (maxPeers * Tuning::outPercent + 50) / 100, + std::size_t(Tuning::minOutCount)); } void Config::applyTuning() { - if (maxPeers < Tuning::minOutCount) - maxPeers = Tuning::minOutCount; - outPeers = calcOutPeers(); - - auto const inPeers = maxPeers - outPeers; - if (ipLimit == 0) { // Unless a limit is explicitly set, we allow between @@ -78,5 +74,67 @@ Config::onWrite(beast::PropertyStream::Map& map) map["ip_limit"] = ipLimit; } +Config +Config::makeConfig( + ripple::Config const& cfg, + std::uint16_t port, + bool validationPublicKey, + int ipLimit) +{ + PeerFinder::Config config; + + config.peerPrivate = cfg.PEER_PRIVATE; + + // Servers with peer privacy don't want to allow incoming connections + config.wantIncoming = (!config.peerPrivate) && (port != 0); + + if (!cfg.PEERS_OUT_MAX && !cfg.PEERS_IN_MAX) + { + if (cfg.PEERS_MAX != 0) + config.maxPeers = cfg.PEERS_MAX; + + if (config.maxPeers < Tuning::minOutCount) + config.maxPeers = Tuning::minOutCount; + config.outPeers = config.calcOutPeers(); + + // Calculate the number of outbound peers we want. If we dont want + // or can't accept incoming, this will simply be equal to maxPeers. + if (!config.wantIncoming) + config.outPeers = config.maxPeers; + + // Calculate the largest number of inbound connections we could + // take. + if (config.maxPeers >= config.outPeers) + config.inPeers = config.maxPeers - config.outPeers; + else + config.inPeers = 0; + } + else + { + config.outPeers = cfg.PEERS_OUT_MAX; + config.inPeers = cfg.PEERS_IN_MAX; + config.maxPeers = 0; + } + + // This will cause servers configured as validators to request that + // peers they connect to never report their IP address. We set this + // after we set the 'wantIncoming' because we want a "soft" version + // of peer privacy unless the operator explicitly asks for it. + if (validationPublicKey) + config.peerPrivate = true; + + // if it's a private peer or we are running as standalone + // automatic connections would defeat the purpose. + config.autoConnect = !cfg.standalone() && !cfg.PEER_PRIVATE; + config.listeningPort = port; + config.features = ""; + config.ipLimit = ipLimit; + + // Enforce business rules + config.applyTuning(); + + return config; +} + } // namespace PeerFinder } // namespace ripple diff --git a/src/test/peerfinder/PeerFinder_test.cpp b/src/test/peerfinder/PeerFinder_test.cpp index b57911d28b..b0750e689f 100644 --- a/src/test/peerfinder/PeerFinder_test.cpp +++ b/src/test/peerfinder/PeerFinder_test.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -156,11 +157,130 @@ public: BEAST_EXPECT(n <= (seconds + 59) / 60); } + void + test_config() + { + // if peers_max is configured then peers_in_max and peers_out_max are + // ignored + auto run = [&](std::string const& test, + std::optional maxPeers, + std::optional maxIn, + std::optional maxOut, + std::uint16_t port, + std::uint16_t expectOut, + std::uint16_t expectIn, + std::uint16_t expectIpLimit) { + ripple::Config c; + + testcase(test); + + std::string toLoad = ""; + int max = 0; + if (maxPeers) + { + max = maxPeers.value(); + toLoad += "[peers_max]\n" + std::to_string(max) + "\n" + + "[peers_in_max]\n" + std::to_string(maxIn.value_or(0)) + + "\n" + "[peers_out_max]\n" + + std::to_string(maxOut.value_or(0)) + "\n"; + } + else if (maxIn && maxOut) + { + toLoad += "[peers_in_max]\n" + std::to_string(*maxIn) + "\n" + + "[peers_out_max]\n" + std::to_string(*maxOut) + "\n"; + } + + c.loadFromString(toLoad); + BEAST_EXPECT( + (c.PEERS_MAX == max && c.PEERS_IN_MAX == 0 && + c.PEERS_OUT_MAX == 0) || + (c.PEERS_IN_MAX == *maxIn && c.PEERS_OUT_MAX == *maxOut)); + + Config config = Config::makeConfig(c, port, false, 0); + + Counts counts; + counts.onConfig(config); + BEAST_EXPECT( + counts.out_max() == expectOut && + counts.inboundSlots() == expectIn && + config.ipLimit == expectIpLimit); + }; + + // if max_peers == 0 => maxPeers = 21, + // else if max_peers < 10 => maxPeers = 10 else maxPeers = max_peers + // expectOut => if legacy => max(0.15 * maxPeers, 10), + // if legacy && !wantIncoming => maxPeers else max_out_peers + // expectIn => if legacy && wantIncoming => maxPeers - outPeers + // else if !wantIncoming => 0 else max_in_peers + // ipLimit => if expectIn <= 21 => 2 else 2 + min(5, expectIn/21) + // ipLimit = max(1, min(ipLimit, expectIn/2)) + + // legacy test with max_peers + run("legacy no config", {}, {}, {}, 4000, 10, 11, 2); + run("legacy max_peers 0", 0, 100, 10, 4000, 10, 11, 2); + run("legacy max_peers 5", 5, 100, 10, 4000, 10, 0, 1); + run("legacy max_peers 20", 20, 100, 10, 4000, 10, 10, 2); + run("legacy max_peers 100", 100, 100, 10, 4000, 15, 85, 6); + run("legacy max_peers 20, private", 20, 100, 10, 0, 20, 0, 1); + + // test with max_in_peers and max_out_peers + run("new in 100/out 10", {}, 100, 10, 4000, 10, 100, 6); + run("new in 0/out 10", {}, 0, 10, 4000, 10, 0, 1); + run("new in 100/out 10, private", {}, 100, 10, 0, 10, 0, 6); + } + + void + test_invalid_config() + { + testcase("invalid config"); + + auto run = [&](std::string const& toLoad) { + ripple::Config c; + try + { + c.loadFromString(toLoad); + fail(); + } + catch (...) + { + pass(); + } + }; + run(R"rippleConfig( +[peers_in_max] +100 +)rippleConfig"); + run(R"rippleConfig( +[peers_out_max] +100 +)rippleConfig"); + run(R"rippleConfig( +[peers_in_max] +100 +[peers_out_max] +5 +)rippleConfig"); + run(R"rippleConfig( +[peers_in_max] +1001 +[peers_out_max] +10 +)rippleConfig"); + run(R"rippleConfig( +[peers_in_max] +10 +[peers_out_max] +1001 +)rippleConfig"); + } + void run() override { test_backoff1(); test_backoff2(); + test_config(); + test_invalid_config(); } };