diff --git a/.github/scripts/levelization/results/loops.txt b/.github/scripts/levelization/results/loops.txt index cf70468e32..ea7b8a372a 100644 --- a/.github/scripts/levelization/results/loops.txt +++ b/.github/scripts/levelization/results/loops.txt @@ -1,9 +1,6 @@ Loop: xrpld.app xrpld.overlay xrpld.app > xrpld.overlay -Loop: xrpld.app xrpld.peerfinder - xrpld.peerfinder ~= xrpld.app - Loop: xrpld.app xrpld.rpc xrpld.rpc > xrpld.app diff --git a/.github/scripts/levelization/results/ordering.txt b/.github/scripts/levelization/results/ordering.txt index 3c9c514516..fdd134dc8a 100644 --- a/.github/scripts/levelization/results/ordering.txt +++ b/.github/scripts/levelization/results/ordering.txt @@ -25,6 +25,9 @@ libxrpl.nodestore > xrpl.config libxrpl.nodestore > xrpl.json libxrpl.nodestore > xrpl.nodestore libxrpl.nodestore > xrpl.protocol +libxrpl.peerfinder > xrpl.basics +libxrpl.peerfinder > xrpl.peerfinder +libxrpl.peerfinder > xrpl.protocol libxrpl.protocol > xrpl.basics libxrpl.protocol > xrpl.json libxrpl.protocol > xrpl.protocol @@ -146,19 +149,13 @@ test.overlay > xrpl.config test.overlay > xrpld.app test.overlay > xrpld.core test.overlay > xrpld.overlay -test.overlay > xrpld.peerfinder test.overlay > xrpl.json test.overlay > xrpl.nodestore +test.overlay > xrpl.peerfinder test.overlay > xrpl.protocol test.overlay > xrpl.resource test.overlay > xrpl.server test.overlay > xrpl.shamap -test.peerfinder > test.beast -test.peerfinder > test.unit_test -test.peerfinder > xrpl.basics -test.peerfinder > xrpld.core -test.peerfinder > xrpld.peerfinder -test.peerfinder > xrpl.protocol test.protocol > test.jtx test.protocol > test.unit_test test.protocol > xrpl.basics @@ -197,6 +194,7 @@ tests.libxrpl > xrpl.json tests.libxrpl > xrpl.ledger tests.libxrpl > xrpl.net tests.libxrpl > xrpl.nodestore +tests.libxrpl > xrpl.peerfinder tests.libxrpl > xrpl.protocol tests.libxrpl > xrpl.protocol_autogen tests.libxrpl > xrpl.resource @@ -220,6 +218,8 @@ xrpl.nodestore > xrpl.basics xrpl.nodestore > xrpl.config xrpl.nodestore > xrpl.json xrpl.nodestore > xrpl.protocol +xrpl.peerfinder > xrpl.basics +xrpl.peerfinder > xrpl.protocol xrpl.protocol > xrpl.basics xrpl.protocol > xrpl.json xrpl.protocol_autogen > xrpl.json @@ -253,6 +253,7 @@ xrpld.app > xrpl.json xrpld.app > xrpl.ledger xrpld.app > xrpl.net xrpld.app > xrpl.nodestore +xrpld.app > xrpl.peerfinder xrpld.app > xrpl.protocol xrpld.app > xrpl.rdb xrpld.app > xrpl.resource @@ -277,15 +278,16 @@ xrpld.overlay > xrpld.core xrpld.overlay > xrpld.peerfinder xrpld.overlay > xrpl.json xrpld.overlay > xrpl.ledger +xrpld.overlay > xrpl.peerfinder xrpld.overlay > xrpl.protocol xrpld.overlay > xrpl.resource xrpld.overlay > xrpl.server xrpld.overlay > xrpl.shamap xrpld.overlay > xrpl.tx xrpld.peerfinder > xrpl.basics -xrpld.peerfinder > xrpl.config +xrpld.peerfinder > xrpld.app xrpld.peerfinder > xrpld.core -xrpld.peerfinder > xrpl.protocol +xrpld.peerfinder > xrpl.peerfinder xrpld.peerfinder > xrpl.rdb xrpld.perflog > xrpl.basics xrpld.perflog > xrpl.config diff --git a/cmake/XrplCore.cmake b/cmake/XrplCore.cmake index 3e49267715..62a8fe143b 100644 --- a/cmake/XrplCore.cmake +++ b/cmake/XrplCore.cmake @@ -133,6 +133,12 @@ target_link_libraries( add_module(xrpl resource) target_link_libraries(xrpl.libxrpl.resource PUBLIC xrpl.libxrpl.protocol) +add_module(xrpl peerfinder) +target_link_libraries( + xrpl.libxrpl.peerfinder + PUBLIC xrpl.libxrpl.basics xrpl.libxrpl.protocol +) + # Level 08 add_module(xrpl net) target_link_libraries( @@ -227,6 +233,7 @@ target_link_modules( ledger net nodestore + peerfinder protocol protocol_autogen rdb diff --git a/include/xrpl/beast/container/detail/aged_unordered_container.h b/include/xrpl/beast/container/detail/aged_unordered_container.h index db10e8cc23..c4287b1ca1 100644 --- a/include/xrpl/beast/container/detail/aged_unordered_container.h +++ b/include/xrpl/beast/container/detail/aged_unordered_container.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include diff --git a/include/xrpl/peerfinder/Config.h b/include/xrpl/peerfinder/Config.h new file mode 100644 index 0000000000..9ff0d342c3 --- /dev/null +++ b/include/xrpl/peerfinder/Config.h @@ -0,0 +1,163 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +namespace xrpl::PeerFinder { + +struct PeerLimitConfig +{ + std::optional maxPeers; + std::optional inPeers; + std::optional outPeers; +}; + +/** + * PeerFinder configuration settings. + */ +struct Config +{ + /** + * The largest number of public peer slots to allow. + * This includes both inbound and outbound, but does not include + * fixed peers. + */ + std::size_t maxPeers{Tuning::kDefaultMaxPeers}; + + /** + * The number of automatic outbound connections to maintain. + * Outbound connections are only maintained if autoConnect + * is `true`. + */ + std::size_t outPeers = calcOutPeers(); // Note: relies on `maxPeers` being initialized + + /** + * The number of automatic inbound connections to maintain. + * Inbound connections are only maintained if wantIncoming + * is `true`. + */ + std::size_t inPeers{0}; + + /** + * `true` if we want our IP address kept private. + */ + bool peerPrivate = true; + + /** + * `true` if we want to accept incoming connections. + */ + bool wantIncoming{true}; + + /** + * `true` if we want to establish connections automatically + */ + bool autoConnect{true}; + + /** + * The listening port number. + */ + std::uint16_t listeningPort{0}; + + /** + * The set of features we advertise. + */ + std::string features; + + /** + * Limit how many incoming connections we allow per IP + */ + int ipLimit{0}; + + /** + * `true` if we want to verify endpoints in TMEndpoints messages + */ + bool verifyEndpoints = true; + + //-------------------------------------------------------------------------- + + /** + * Returns a suitable value for outPeers according to the rules. + */ + [[nodiscard]] std::size_t + calcOutPeers() const; + + /** + * Adjusts the values so they follow the business rules. + */ + void + applyTuning(); + + /** + * Write the configuration into a property stream + */ + void + onWrite(beast::PropertyStream::Map& map) const; + + /** + * Make PeerFinder::Config from peer limit and server mode parameters. + */ + static Config + makeConfig( + bool peerPrivate, + bool standalone, + PeerLimitConfig const& limits, + std::uint16_t port, + bool validationPublicKey, + int ipLimit, + bool verifyEndpoints); + + /** + * Compares two configurations for equality field by field. + */ + friend bool + operator==(Config const& lhs, Config const& rhs) = default; +}; + +//------------------------------------------------------------------------------ + +/** + * Possible results from activating a slot. + */ +enum class Result { InboundDisabled, DuplicatePeer, IpLimitExceeded, Full, Success }; + +/** + * @brief Converts a `Result` enum value to its string representation. + * + * This function provides a human-readable string for a given `Result` enum, + * which is useful for logging, debugging, or displaying status messages. + * + * @param result The `Result` enum value to convert. + * @return A `std::string_view` representing the enum value. Returns "unknown" + * if the enum value is not explicitly handled. + * + * @note This function returns a `std::string_view` for performance. + * A `std::string` would need to allocate memory on the heap and copy the + * string literal into it every time the function is called. + */ +inline std::string_view +to_string(Result result) noexcept +{ + switch (result) + { + case Result::InboundDisabled: + return "inbound disabled"; + case Result::DuplicatePeer: + return "peer already connected"; + case Result::IpLimitExceeded: + return "ip limit exceeded"; + case Result::Full: + return "slots full"; + case Result::Success: + return "success"; + } + + return "unknown"; +} + +} // namespace xrpl::PeerFinder diff --git a/include/xrpl/peerfinder/PeerfinderManager.h b/include/xrpl/peerfinder/PeerfinderManager.h new file mode 100644 index 0000000000..ed683520c1 --- /dev/null +++ b/include/xrpl/peerfinder/PeerfinderManager.h @@ -0,0 +1,179 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace xrpl::PeerFinder { + +/** + * Maintains a set of IP addresses used for getting into the network. + */ +class Manager : public beast::PropertyStream::Source +{ +protected: + Manager() noexcept; + +public: + /** + * Destroy the object. + * Any pending source fetch operations are aborted. + * There may be some listener calls made before the + * destructor returns. + */ + ~Manager() override = default; + + /** + * Set the configuration for the manager. + * The new settings will be applied asynchronously. + * Thread safety: + * Can be called from any threads at any time. + */ + virtual void + setConfig(Config const& config) = 0; + + /** + * Transition to the started state, synchronously. + */ + virtual void + start() = 0; + + /** + * Transition to the stopped state, synchronously. + */ + virtual void + stop() = 0; + + /** + * Returns the configuration for the manager. + */ + virtual Config + config() = 0; + + /** + * Add a peer that should always be connected. + * This is useful for maintaining a private cluster of peers. + * The string is the name as specified in the configuration + * file, along with the set of corresponding IP addresses. + */ + virtual void + addFixedPeer(std::string_view name, std::vector const& addresses) = 0; + + /** + * Add a set of strings as fallback IP::Endpoint sources. + * @param name A label used for diagnostics. + */ + virtual void + addFallbackStrings(std::string const& name, std::vector const& strings) = 0; + + /** + * Add a URL as a fallback location to obtain IP::Endpoint sources. + * @param name A label used for diagnostics. + */ + /* VFALCO NOTE Unimplemented + virtual void addFallbackURL (std::string const& name, + std::string const& url) = 0; + */ + + //-------------------------------------------------------------------------- + + /** + * Create a new inbound slot with the specified remote endpoint. + * If nullptr is returned, then the slot could not be assigned. + * Usually this is because of a detected self-connection. + */ + virtual std::pair, Result> + newInboundSlot( + beast::IP::Endpoint const& localEndpoint, + beast::IP::Endpoint const& remoteEndpoint) = 0; + + /** + * Create a new outbound slot with the specified remote endpoint. + * If nullptr is returned, then the slot could not be assigned. + * Usually this is because of a duplicate connection. + */ + virtual std::pair, Result> + newOutboundSlot(beast::IP::Endpoint const& remoteEndpoint) = 0; + + /** + * Called when mtENDPOINTS is received. + */ + virtual void + onEndpoints(std::shared_ptr const& slot, Endpoints const& endpoints) = 0; + + /** + * Called when the slot is closed. + * This always happens when the socket is closed, unless the socket + * was canceled. + */ + virtual void + onClosed(std::shared_ptr const& slot) = 0; + + /** + * Called when an outbound connection is deemed to have failed + */ + virtual void + onFailure(std::shared_ptr const& slot) = 0; + + /** + * Called when we received redirect IPs from a busy peer. + */ + virtual void + onRedirects( + boost::asio::ip::tcp::endpoint const& remoteAddress, + std::vector const& eps) = 0; + + //-------------------------------------------------------------------------- + + /** + * Called when an outbound connection attempt succeeds. + * The local endpoint must be valid. If the caller receives an error + * when retrieving the local endpoint from the socket, it should + * proceed as if the connection attempt failed by calling on_closed + * instead of on_connected. + * @return `true` if the connection should be kept + */ + virtual bool + onConnected(std::shared_ptr const& slot, beast::IP::Endpoint const& localEndpoint) = 0; + + /** + * Request an active slot type. + */ + virtual Result + activate(std::shared_ptr const& slot, PublicKey const& key, bool reserved) = 0; + + /** + * Returns a set of endpoints suitable for redirection. + */ + virtual std::vector + redirect(std::shared_ptr const& slot) = 0; + + /** + * Return a set of addresses we should connect to. + */ + virtual std::vector + autoconnect() = 0; + + virtual std::vector, std::vector>> + buildEndpointsForPeers() = 0; + + /** + * Perform periodic activity. + * This should be called once per second. + */ + virtual void + oncePerSecond() = 0; +}; + +} // namespace xrpl::PeerFinder diff --git a/src/xrpld/peerfinder/Slot.h b/include/xrpl/peerfinder/Slot.h similarity index 100% rename from src/xrpld/peerfinder/Slot.h rename to include/xrpl/peerfinder/Slot.h diff --git a/include/xrpl/peerfinder/Types.h b/include/xrpl/peerfinder/Types.h new file mode 100644 index 0000000000..9e82d9d65c --- /dev/null +++ b/include/xrpl/peerfinder/Types.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include + +namespace xrpl::PeerFinder { + +using clock_type = beast::AbstractClock; + +/** + * Represents a set of addresses. + */ +using IPAddresses = std::vector; + +//------------------------------------------------------------------------------ + +/** + * Describes a connectable peer address along with some metadata. + */ +struct Endpoint +{ + Endpoint() = default; + + Endpoint(beast::IP::Endpoint ep, std::uint32_t hops); + + std::uint32_t hops = 0; + beast::IP::Endpoint address; +}; + +inline bool +operator<(Endpoint const& lhs, Endpoint const& rhs) +{ + return lhs.address < rhs.address; +} + +/** + * A set of Endpoint used for connecting. + */ +using Endpoints = std::vector; + +} // namespace xrpl::PeerFinder diff --git a/src/xrpld/peerfinder/detail/Bootcache.h b/include/xrpl/peerfinder/detail/Bootcache.h similarity index 97% rename from src/xrpld/peerfinder/detail/Bootcache.h rename to include/xrpl/peerfinder/detail/Bootcache.h index c84fed42c7..2141a374fa 100644 --- a/src/xrpld/peerfinder/detail/Bootcache.h +++ b/include/xrpl/peerfinder/detail/Bootcache.h @@ -1,11 +1,11 @@ #pragma once -#include -#include - #include #include #include +#include +#include +#include #include #include diff --git a/src/xrpld/peerfinder/detail/Checker.h b/include/xrpl/peerfinder/detail/Checker.h similarity index 100% rename from src/xrpld/peerfinder/detail/Checker.h rename to include/xrpl/peerfinder/detail/Checker.h diff --git a/src/xrpld/peerfinder/detail/Counts.h b/include/xrpl/peerfinder/detail/Counts.h similarity index 98% rename from src/xrpld/peerfinder/detail/Counts.h rename to include/xrpl/peerfinder/detail/Counts.h index c90598c1a1..ce78eadce4 100644 --- a/src/xrpld/peerfinder/detail/Counts.h +++ b/include/xrpl/peerfinder/detail/Counts.h @@ -1,11 +1,10 @@ #pragma once -#include -#include -#include - #include #include +#include +#include +#include #include #include diff --git a/src/xrpld/peerfinder/detail/Fixed.h b/include/xrpl/peerfinder/detail/Fixed.h similarity index 92% rename from src/xrpld/peerfinder/detail/Fixed.h rename to include/xrpl/peerfinder/detail/Fixed.h index 24d54775ef..6754ec6dbd 100644 --- a/src/xrpld/peerfinder/detail/Fixed.h +++ b/include/xrpl/peerfinder/detail/Fixed.h @@ -1,7 +1,7 @@ #pragma once -#include -#include +#include +#include #include #include diff --git a/src/xrpld/peerfinder/detail/Handouts.h b/include/xrpl/peerfinder/detail/Handouts.h similarity index 98% rename from src/xrpld/peerfinder/detail/Handouts.h rename to include/xrpl/peerfinder/detail/Handouts.h index 757f1a8e1b..cb5fd7f850 100644 --- a/src/xrpld/peerfinder/detail/Handouts.h +++ b/include/xrpl/peerfinder/detail/Handouts.h @@ -1,12 +1,11 @@ #pragma once -#include -#include -#include - #include #include #include +#include +#include +#include #include #include diff --git a/src/xrpld/peerfinder/detail/Livecache.h b/include/xrpl/peerfinder/detail/Livecache.h similarity index 95% rename from src/xrpld/peerfinder/detail/Livecache.h rename to include/xrpl/peerfinder/detail/Livecache.h index 2015098847..cac284d1cc 100644 --- a/src/xrpld/peerfinder/detail/Livecache.h +++ b/include/xrpl/peerfinder/detail/Livecache.h @@ -1,9 +1,5 @@ #pragma once -#include -#include -#include - #include #include #include @@ -12,6 +8,8 @@ #include #include #include +#include +#include #include #include @@ -22,6 +20,8 @@ #include #include #include +#include +#include #include #include #include @@ -411,7 +411,7 @@ Livecache::expire() } if (n > 0) { - JLOG(journal_.debug()) << beast::Leftw(18) << "Livecache expired " << n + JLOG(journal_.debug()) << std::left << std::setw(18) << "Livecache expired " << n << ((n > 1) ? " entries" : " entry"); } } @@ -434,7 +434,7 @@ Livecache::insert(Endpoint const& ep) if (result.second) { hops.insert(e); - JLOG(journal_.debug()) << beast::Leftw(18) << "Livecache insert " << ep.address + JLOG(journal_.debug()) << std::left << std::setw(18) << "Livecache insert " << ep.address << " at hops " << ep.hops; return; } @@ -442,7 +442,7 @@ Livecache::insert(Endpoint const& ep) { // Drop duplicates at higher hops std::size_t const excess(ep.hops - e.endpoint.hops); - JLOG(journal_.trace()) << beast::Leftw(18) << "Livecache drop " << ep.address + JLOG(journal_.trace()) << std::left << std::setw(18) << "Livecache drop " << ep.address << " at hops +" << excess; return; } @@ -453,12 +453,12 @@ Livecache::insert(Endpoint const& ep) if (ep.hops < e.endpoint.hops) { hops.reinsert(e, ep.hops); - JLOG(journal_.debug()) << beast::Leftw(18) << "Livecache update " << ep.address + JLOG(journal_.debug()) << std::left << std::setw(18) << "Livecache update " << ep.address << " at hops " << ep.hops; } else { - JLOG(journal_.trace()) << beast::Leftw(18) << "Livecache refresh " << ep.address + JLOG(journal_.trace()) << std::left << std::setw(18) << "Livecache refresh " << ep.address << " at hops " << ep.hops; } } diff --git a/src/xrpld/peerfinder/detail/Logic.h b/include/xrpl/peerfinder/detail/Logic.h similarity index 91% rename from src/xrpld/peerfinder/detail/Logic.h rename to include/xrpl/peerfinder/detail/Logic.h index a7dbbf850d..c623263884 100644 --- a/src/xrpld/peerfinder/detail/Logic.h +++ b/include/xrpl/peerfinder/detail/Logic.h @@ -1,17 +1,5 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - #include #include #include @@ -22,18 +10,34 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include + #include #include #include #include +#include +#include #include #include #include #include #include #include +#include #include #include #include @@ -197,8 +201,8 @@ public: if (result.second) { - JLOG(journal.debug()) - << beast::Leftw(18) << "Logic add fixed '" << name << "' at " << remoteAddress; + JLOG(journal.debug()) << std::left << std::setw(18) << "Logic add fixed '" << name + << "' at " << remoteAddress; return; } } @@ -221,7 +225,7 @@ public: if (iter == slots.end()) { // The slot disconnected before we finished the check - JLOG(journal.debug()) << beast::Leftw(18) << "Logic tested " << checkedAddress + JLOG(journal.debug()) << std::left << std::setw(18) << "Logic tested " << checkedAddress << " but the connection was closed"; return; } @@ -255,7 +259,7 @@ public: beast::IP::Endpoint const& localEndpoint, beast::IP::Endpoint const& remoteEndpoint) { - JLOG(journal.debug()) << beast::Leftw(18) << "Logic accept" << remoteEndpoint + JLOG(journal.debug()) << std::left << std::setw(18) << "Logic accept" << remoteEndpoint << " on local " << localEndpoint; std::scoped_lock const _(lock); @@ -266,7 +270,7 @@ public: auto const count = connectedAddresses.count(remoteEndpoint.address()); if (count + 1 > config_.ipLimit) { - JLOG(journal.debug()) << beast::Leftw(18) << "Logic dropping inbound " + JLOG(journal.debug()) << std::left << std::setw(18) << "Logic dropping inbound " << remoteEndpoint << " because of ip limits."; return {SlotImp::ptr(), Result::IpLimitExceeded}; } @@ -275,8 +279,8 @@ public: // Check for duplicate connection if (slots.contains(remoteEndpoint)) { - JLOG(journal.debug()) << beast::Leftw(18) << "Logic dropping " << remoteEndpoint - << " as duplicate incoming"; + JLOG(journal.debug()) << std::left << std::setw(18) << "Logic dropping " + << remoteEndpoint << " as duplicate incoming"; return {SlotImp::ptr(), Result::DuplicatePeer}; } @@ -304,15 +308,15 @@ public: std::pair newOutboundSlot(beast::IP::Endpoint const& remoteEndpoint) { - JLOG(journal.debug()) << beast::Leftw(18) << "Logic connect " << remoteEndpoint; + JLOG(journal.debug()) << std::left << std::setw(18) << "Logic connect " << remoteEndpoint; std::scoped_lock const _(lock); // Check for duplicate connection if (slots.contains(remoteEndpoint)) { - JLOG(journal.debug()) << beast::Leftw(18) << "Logic dropping " << remoteEndpoint - << " as duplicate connect"; + JLOG(journal.debug()) << std::left << std::setw(18) << "Logic dropping " + << remoteEndpoint << " as duplicate connect"; return {SlotImp::ptr(), Result::DuplicatePeer}; } @@ -506,15 +510,15 @@ public: if (!h.list().empty()) { - JLOG(journal.debug()) - << beast::Leftw(18) << "Logic connect " << h.list().size() << " fixed"; + JLOG(journal.debug()) << std::left << std::setw(18) << "Logic connect " + << h.list().size() << " fixed"; return h.list(); } if (counts_.attempts() > 0) { - JLOG(journal.debug()) - << beast::Leftw(18) << "Logic waiting on " << counts_.attempts() << " attempts"; + JLOG(journal.debug()) << std::left << std::setw(18) << "Logic waiting on " + << counts_.attempts() << " attempts"; return none; } } @@ -535,14 +539,14 @@ public: if (!h.list().empty()) { JLOG(journal.debug()) - << beast::Leftw(18) << "Logic connect " << h.list().size() << " live " + << std::left << std::setw(18) << "Logic connect " << h.list().size() << " live " << ((h.list().size() > 1) ? "endpoints" : "endpoint"); return h.list(); } if (counts_.attempts() > 0) { - JLOG(journal.debug()) - << beast::Leftw(18) << "Logic waiting on " << counts_.attempts() << " attempts"; + JLOG(journal.debug()) << std::left << std::setw(18) << "Logic waiting on " + << counts_.attempts() << " attempts"; return none; } } @@ -568,8 +572,9 @@ public: if (!h.list().empty()) { - JLOG(journal.debug()) << beast::Leftw(18) << "Logic connect " << h.list().size() - << " boot " << ((h.list().size() > 1) ? "addresses" : "address"); + JLOG(journal.debug()) << std::left << std::setw(18) << "Logic connect " + << h.list().size() << " boot " + << ((h.list().size() > 1) ? "addresses" : "address"); return h.list(); } @@ -689,8 +694,8 @@ public: // Enforce hop limit if (ep.hops > Tuning::kMaxHops) { - JLOG(journal.debug()) << beast::Leftw(18) << "Endpoints drop " << ep.address - << " for excess hops " << ep.hops; + JLOG(journal.debug()) << std::left << std::setw(18) << "Endpoints drop " + << ep.address << " for excess hops " << ep.hops; iter = list.erase(iter); continue; } @@ -706,18 +711,18 @@ public: } else { - JLOG(journal.debug()) - << beast::Leftw(18) << "Endpoints drop " << ep.address << " for extra self"; + JLOG(journal.debug()) << std::left << std::setw(18) << "Endpoints drop " + << ep.address << " for extra self"; iter = list.erase(iter); continue; } } // Discard invalid addresses - if (config_.verifyEndpoints && !isValidAddress(ep.address)) + if (!isValidAddress(ep.address)) { - JLOG(journal.debug()) - << beast::Leftw(18) << "Endpoints drop " << ep.address << " as invalid"; + JLOG(journal.debug()) << std::left << std::setw(18) << "Endpoints drop " + << ep.address << " as invalid"; iter = list.erase(iter); continue; } @@ -727,8 +732,8 @@ public: return ep.address == other.address; })) { - JLOG(journal.debug()) - << beast::Leftw(18) << "Endpoints drop " << ep.address << " as duplicate"; + JLOG(journal.debug()) << std::left << std::setw(18) << "Endpoints drop " + << ep.address << " as duplicate"; iter = list.erase(iter); continue; } @@ -1074,13 +1079,13 @@ public: if (!results.error) { int const count(addBootcacheAddresses(results.addresses)); - JLOG(journal.info()) << beast::Leftw(18) << "Logic added " << count << " new " + JLOG(journal.info()) << std::left << std::setw(18) << "Logic added " << count << " new " << ((count == 1) ? "address" : "addresses") << " from " << source->name(); } else { - JLOG(journal.error()) << beast::Leftw(18) << "Logic failed " + JLOG(journal.error()) << std::left << std::setw(18) << "Logic failed " << "'" << source->name() << "' fetch, " << results.error.message(); } @@ -1098,8 +1103,6 @@ public: { if (isUnspecified(address)) return false; - if (isLoopback(address)) - return false; if (!isPublic(address)) return false; if (address.port() == 0) @@ -1221,8 +1224,8 @@ Logic::onRedirects( bootcache.insert(beast::IPAddressConversion::fromAsio(*first)); if (n > 0) { - JLOG(journal.trace()) << beast::Leftw(18) << "Logic add " << n << " redirect IPs from " - << remoteAddress; + JLOG(journal.trace()) << std::left << std::setw(18) << "Logic add " << n + << " redirect IPs from " << remoteAddress; } } diff --git a/src/xrpld/peerfinder/detail/SlotImp.h b/include/xrpl/peerfinder/detail/SlotImp.h similarity index 96% rename from src/xrpld/peerfinder/detail/SlotImp.h rename to include/xrpl/peerfinder/detail/SlotImp.h index 898941b157..35c61b13cf 100644 --- a/src/xrpld/peerfinder/detail/SlotImp.h +++ b/include/xrpl/peerfinder/detail/SlotImp.h @@ -1,10 +1,9 @@ #pragma once -#include -#include - #include #include +#include +#include #include #include @@ -172,7 +171,7 @@ private: std::optional localEndpoint_; std::optional publicKey_; - static constexpr std::int32_t kUnknownPort = -1; + static std::int32_t constexpr kUnknownPort = -1; std::atomic listeningPort_; public: diff --git a/src/xrpld/peerfinder/detail/Source.h b/include/xrpl/peerfinder/detail/Source.h similarity index 95% rename from src/xrpld/peerfinder/detail/Source.h rename to include/xrpl/peerfinder/detail/Source.h index b205dc8dfb..5cdb535bdd 100644 --- a/src/xrpld/peerfinder/detail/Source.h +++ b/include/xrpl/peerfinder/detail/Source.h @@ -1,8 +1,7 @@ #pragma once -#include - #include +#include #include diff --git a/src/xrpld/peerfinder/detail/SourceStrings.h b/include/xrpl/peerfinder/detail/SourceStrings.h similarity index 90% rename from src/xrpld/peerfinder/detail/SourceStrings.h rename to include/xrpl/peerfinder/detail/SourceStrings.h index b79cf0df03..325a024764 100644 --- a/src/xrpld/peerfinder/detail/SourceStrings.h +++ b/include/xrpl/peerfinder/detail/SourceStrings.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include diff --git a/src/xrpld/peerfinder/detail/Store.h b/include/xrpl/peerfinder/detail/Store.h similarity index 100% rename from src/xrpld/peerfinder/detail/Store.h rename to include/xrpl/peerfinder/detail/Store.h diff --git a/src/xrpld/peerfinder/detail/Tuning.h b/include/xrpl/peerfinder/detail/Tuning.h similarity index 100% rename from src/xrpld/peerfinder/detail/Tuning.h rename to include/xrpl/peerfinder/detail/Tuning.h diff --git a/include/xrpl/peerfinder/make_Manager.h b/include/xrpl/peerfinder/make_Manager.h new file mode 100644 index 0000000000..5da372e588 --- /dev/null +++ b/include/xrpl/peerfinder/make_Manager.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +#include + +namespace xrpl::PeerFinder { + +/** + * @brief Create a new Manager. + * + * @param ioContext The io_context used to schedule asynchronous work. + * @param clock The clock used for timekeeping. + * @param journal The journal used for logging. + * @param store The persistence backend for the bootstrap cache. The caller + * retains ownership and must keep it alive (and opened) for the lifetime of + * the returned Manager. This lets consumers supply their own Store + * implementation (e.g. the SQLite-backed StoreSqdb in xrpld). + * @param collector The collector used to report metrics. + * @return The newly created Manager. + */ +std::unique_ptr +makeManager( + boost::asio::io_context& ioContext, + clock_type& clock, + beast::Journal journal, + Store& store, + beast::insight::Collector::ptr const& collector); + +} // namespace xrpl::PeerFinder diff --git a/src/xrpld/peerfinder/detail/Bootcache.cpp b/src/libxrpl/peerfinder/Bootcache.cpp similarity index 80% rename from src/xrpld/peerfinder/detail/Bootcache.cpp rename to src/libxrpl/peerfinder/Bootcache.cpp index a0a753530b..a2a56b4d01 100644 --- a/src/xrpld/peerfinder/detail/Bootcache.cpp +++ b/src/libxrpl/peerfinder/Bootcache.cpp @@ -1,19 +1,19 @@ -#include - -#include -#include -#include -#include +#include #include #include #include #include #include +#include +#include +#include #include #include #include +#include +#include #include namespace xrpl::PeerFinder { @@ -82,13 +82,14 @@ Bootcache::load() auto const result(this->map_.insert(value_type(endpoint, valence))); if (!result.second) { - JLOG(this->journal_.error()) << beast::Leftw(18) << "Bootcache discard " << endpoint; + JLOG(this->journal_.error()) + << std::left << std::setw(18) << "Bootcache discard " << endpoint; } })); if (n > 0) { - JLOG(journal_.info()) << beast::Leftw(18) << "Bootcache loaded " << n + JLOG(journal_.info()) << std::left << std::setw(18) << "Bootcache loaded " << n << ((n > 1) ? " addresses" : " address"); prune(); } @@ -100,7 +101,7 @@ Bootcache::insert(beast::IP::Endpoint const& endpoint) auto const result(map_.insert(value_type(endpoint, 0))); if (result.second) { - JLOG(journal_.trace()) << beast::Leftw(18) << "Bootcache insert " << endpoint; + JLOG(journal_.trace()) << std::left << std::setw(18) << "Bootcache insert " << endpoint; prune(); flagForUpdate(); } @@ -121,7 +122,7 @@ Bootcache::insertStatic(beast::IP::Endpoint const& endpoint) if (result.second) { - JLOG(journal_.trace()) << beast::Leftw(18) << "Bootcache insert " << endpoint; + JLOG(journal_.trace()) << std::left << std::setw(18) << "Bootcache insert " << endpoint; prune(); flagForUpdate(); } @@ -146,8 +147,9 @@ Bootcache::onSuccess(beast::IP::Endpoint const& endpoint) XRPL_ASSERT(result.second, "xrpl::PeerFinder::Bootcache::onSuccess : endpoint inserted"); } Entry const& entry(result.first->right); - JLOG(journal_.info()) << beast::Leftw(18) << "Bootcache connect " << endpoint << " with " - << entry.valence() << ((entry.valence() > 1) ? " successes" : " success"); + JLOG(journal_.info()) << std::left << std::setw(18) << "Bootcache connect " << endpoint + << " with " << entry.valence() + << ((entry.valence() > 1) ? " successes" : " success"); flagForUpdate(); } @@ -170,8 +172,8 @@ Bootcache::onFailure(beast::IP::Endpoint const& endpoint) } Entry const& entry(result.first->right); auto const n(std::abs(entry.valence())); - JLOG(journal_.debug()) << beast::Leftw(18) << "Bootcache failed " << endpoint << " with " << n - << ((n > 1) ? " attempts" : " attempt"); + JLOG(journal_.debug()) << std::left << std::setw(18) << "Bootcache failed " << endpoint + << " with " << n << ((n > 1) ? " attempts" : " attempt"); flagForUpdate(); } @@ -209,17 +211,19 @@ Bootcache::prune() // Work backwards because bimap doesn't handle // erasing using a reverse iterator very well. // - for (auto iter(map_.right.end()); count-- > 0 && iter != map_.right.begin(); ++pruned) + for (auto iter(map_.right.end()); count > 0 && iter != map_.right.begin(); ++pruned) { + --count; --iter; beast::IP::Endpoint const& endpoint(iter->get_left()); Entry const& entry(iter->get_right()); - JLOG(journal_.trace()) << beast::Leftw(18) << "Bootcache pruned" << endpoint + JLOG(journal_.trace()) << std::left << std::setw(18) << "Bootcache pruned" << endpoint << " at valence " << entry.valence(); iter = map_.right.erase(iter); } - JLOG(journal_.debug()) << beast::Leftw(18) << "Bootcache pruned " << pruned << " entries total"; + JLOG(journal_.debug()) << std::left << std::setw(18) << "Bootcache pruned " << pruned + << " entries total"; } // Updates the Store with the current set of entries if needed. diff --git a/src/libxrpl/peerfinder/Config.cpp b/src/libxrpl/peerfinder/Config.cpp new file mode 100644 index 0000000000..3e2f74f42b --- /dev/null +++ b/src/libxrpl/peerfinder/Config.cpp @@ -0,0 +1,135 @@ +#include + +#include + +#include +#include +#include +#include + +namespace xrpl::PeerFinder { + +std::size_t +Config::calcOutPeers() const +{ + return std::max( + ((maxPeers * Tuning::kOutPercent) + 50) / 100, std::size_t(Tuning::kMinOutCount)); +} + +void +Config::applyTuning() +{ + if (ipLimit == 0) + { + // Unless a limit is explicitly set, we allow between + // 2 and 5 connections from non RFC-1918 "private" + // IP addresses. + ipLimit = 2; + + if (inPeers > Tuning::kDefaultMaxPeers) + ipLimit += std::min(5, static_cast(inPeers / Tuning::kDefaultMaxPeers)); + } + + // We don't allow a single IP to consume all incoming slots, + // unless we only have one incoming slot available. + ipLimit = std::max(1, std::min(ipLimit, static_cast(inPeers / 2))); +} + +void +Config::onWrite(beast::PropertyStream::Map& map) const +{ + map["max_peers"] = maxPeers; + map["out_peers"] = outPeers; + map["want_incoming"] = wantIncoming; + map["auto_connect"] = autoConnect; + map["port"] = listeningPort; + map["features"] = features; + map["ip_limit"] = ipLimit; + map["verify_endpoints"] = verifyEndpoints; +} + +Config +Config::makeConfig( + bool peerPrivate, + bool standalone, + PeerLimitConfig const& limits, + std::uint16_t port, + bool validationPublicKey, + int ipLimit, + bool verifyEndpoints) +{ + PeerFinder::Config config; + + if (!limits.maxPeers) + { + if (limits.inPeers && !limits.outPeers) + throw std::runtime_error("Both inbound and outbound peer limits must be configured"); + + if (limits.outPeers && !limits.inPeers) + throw std::runtime_error("Both inbound and outbound peer limits must be configured"); + + if (limits.inPeers && *limits.inPeers > 1000) + throw std::runtime_error("Inbound peer limit must be less than or equal to 1000"); + + if (limits.outPeers && (*limits.outPeers < 10 || *limits.outPeers > 1000)) + throw std::runtime_error("Outbound peer limit must be in the range 10-1000"); + } + + config.peerPrivate = peerPrivate; + + // Servers with peer privacy don't want to allow incoming connections + config.wantIncoming = (!config.peerPrivate) && (port != 0); + + if (limits.maxPeers || (!limits.inPeers && !limits.outPeers)) + { + if (limits.maxPeers && *limits.maxPeers != 0) + config.maxPeers = *limits.maxPeers; + + config.maxPeers = std::max(config.maxPeers, Tuning::kMinOutCount); + 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 = *limits.outPeers; + config.inPeers = *limits.inPeers; + 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 = !standalone && !peerPrivate; + config.listeningPort = port; + config.features = ""; + config.ipLimit = ipLimit; + config.verifyEndpoints = verifyEndpoints; + + // Enforce business rules + config.applyTuning(); + + return config; +} + +} // namespace xrpl::PeerFinder diff --git a/src/xrpld/peerfinder/detail/Endpoint.cpp b/src/libxrpl/peerfinder/Endpoint.cpp similarity index 76% rename from src/xrpld/peerfinder/detail/Endpoint.cpp rename to src/libxrpl/peerfinder/Endpoint.cpp index 15de5cd153..12f2725ea5 100644 --- a/src/xrpld/peerfinder/detail/Endpoint.cpp +++ b/src/libxrpl/peerfinder/Endpoint.cpp @@ -1,7 +1,5 @@ -#include -#include - #include +#include #include #include diff --git a/src/xrpld/peerfinder/detail/PeerfinderManager.cpp b/src/libxrpl/peerfinder/PeerfinderManager.cpp similarity index 92% rename from src/xrpld/peerfinder/detail/PeerfinderManager.cpp rename to src/libxrpl/peerfinder/PeerfinderManager.cpp index 2727a03013..2219627f09 100644 --- a/src/xrpld/peerfinder/detail/PeerfinderManager.cpp +++ b/src/libxrpl/peerfinder/PeerfinderManager.cpp @@ -1,11 +1,4 @@ -#include - -#include -#include -#include -#include -#include -#include +#include #include #include @@ -13,7 +6,15 @@ #include #include #include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include @@ -38,10 +39,9 @@ public: std::optional> work_; clock_type& clock_; beast::Journal journal_; - StoreSqdb store_; + Store& store_; Checker checker_; Logic logic_; - BasicConfig const& config_; // NOLINTEND(readability-identifier-naming) //-------------------------------------------------------------------------- @@ -50,16 +50,15 @@ public: boost::asio::io_context& ioContext, clock_type& clock, beast::Journal journal, - BasicConfig const& config, + Store& store, beast::insight::Collector::ptr const& collector) : io_context_(ioContext) , work_(std::in_place, boost::asio::make_work_guard(io_context_)) , clock_(clock) , journal_(journal) - , store_(journal) + , store_(store) , checker_(io_context_) , logic_(clock, store_, checker_, journal) - , config_(config) , stats_([this] { collectMetrics(); }, collector) { } @@ -206,7 +205,6 @@ public: void start() override { - store_.open(config_); logic_.load(); } @@ -261,10 +259,10 @@ makeManager( boost::asio::io_context& ioContext, clock_type& clock, beast::Journal journal, - BasicConfig const& config, + Store& store, beast::insight::Collector::ptr const& collector) { - return std::make_unique(ioContext, clock, journal, config, collector); + return std::make_unique(ioContext, clock, journal, store, collector); } } // namespace xrpl::PeerFinder diff --git a/src/xrpld/peerfinder/detail/SlotImp.cpp b/src/libxrpl/peerfinder/SlotImp.cpp similarity index 93% rename from src/xrpld/peerfinder/detail/SlotImp.cpp rename to src/libxrpl/peerfinder/SlotImp.cpp index a54ddb56e7..0a4f32fd62 100644 --- a/src/xrpld/peerfinder/detail/SlotImp.cpp +++ b/src/libxrpl/peerfinder/SlotImp.cpp @@ -1,12 +1,10 @@ -#include +#include -#include -#include -#include - -#include #include #include +#include +#include +#include #include #include diff --git a/src/xrpld/peerfinder/detail/SourceStrings.cpp b/src/libxrpl/peerfinder/SourceStrings.cpp similarity index 93% rename from src/xrpld/peerfinder/detail/SourceStrings.cpp rename to src/libxrpl/peerfinder/SourceStrings.cpp index 7b28db4306..f47e0cd51d 100644 --- a/src/xrpld/peerfinder/detail/SourceStrings.cpp +++ b/src/libxrpl/peerfinder/SourceStrings.cpp @@ -1,9 +1,8 @@ -#include - -#include +#include #include #include +#include #include #include diff --git a/src/test/overlay/TMGetObjectByHash_test.cpp b/src/test/overlay/TMGetObjectByHash_test.cpp index e579989181..f52220a90c 100644 --- a/src/test/overlay/TMGetObjectByHash_test.cpp +++ b/src/test/overlay/TMGetObjectByHash_test.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include #include @@ -16,6 +15,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/overlay/tx_reduce_relay_test.cpp b/src/test/overlay/tx_reduce_relay_test.cpp index a0d91d3aed..43f6ef2506 100644 --- a/src/test/overlay/tx_reduce_relay_test.cpp +++ b/src/test/overlay/tx_reduce_relay_test.cpp @@ -9,12 +9,12 @@ #include #include #include -#include #include #include #include #include +#include #include #include #include diff --git a/src/test/peerfinder/Livecache_test.cpp b/src/test/peerfinder/Livecache_test.cpp deleted file mode 100644 index 4f2d6e97e1..0000000000 --- a/src/test/peerfinder/Livecache_test.cpp +++ /dev/null @@ -1,212 +0,0 @@ -#include -#include - -#include -#include -#include - -#include -#include -#include -#include - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -namespace xrpl::PeerFinder { - -bool -operator==(Endpoint const& a, Endpoint const& b) -{ - return (a.hops == b.hops && a.address == b.address); -} - -class Livecache_test : public beast::unit_test::Suite -{ - TestStopwatch clock_; - test::SuiteJournal journal_; - -public: - Livecache_test() : journal_("Livecache_test", *this) - { - } - - // Add the address as an endpoint - template - void - add(beast::IP::Endpoint ep, C& c, std::uint32_t hops = 0) - { - Endpoint const cep{ep, hops}; - c.insert(cep); - } - - void - testBasicInsert() - { - testcase("Basic Insert"); - Livecache<> c(clock_, journal_); - BEAST_EXPECT(c.empty()); - - for (auto i = 0; i < 10; ++i) - add(beast::IP::randomEP(true), c); - - BEAST_EXPECT(!c.empty()); - BEAST_EXPECT(c.size() == 10); - - for (auto i = 0; i < 10; ++i) - add(beast::IP::randomEP(false), c); - - BEAST_EXPECT(!c.empty()); - BEAST_EXPECT(c.size() == 20); - } - - void - testInsertUpdate() - { - testcase("Insert/Update"); - Livecache<> c(clock_, journal_); - - auto ep1 = Endpoint{beast::IP::randomEP(), 2}; - c.insert(ep1); - BEAST_EXPECT(c.size() == 1); - // third position list will contain the entry - BEAST_EXPECT((c.hops.begin() + 2)->begin()->hops == 2); - - auto ep2 = Endpoint{ep1.address, 4}; - // this will not change the entry has higher hops - c.insert(ep2); - BEAST_EXPECT(c.size() == 1); - // still in third position list - BEAST_EXPECT((c.hops.begin() + 2)->begin()->hops == 2); - - auto ep3 = Endpoint{ep1.address, 2}; - // this will not change the entry has the same hops as existing - c.insert(ep3); - BEAST_EXPECT(c.size() == 1); - // still in third position list - BEAST_EXPECT((c.hops.begin() + 2)->begin()->hops == 2); - - auto ep4 = Endpoint{ep1.address, 1}; - c.insert(ep4); - BEAST_EXPECT(c.size() == 1); - // now at second position list - BEAST_EXPECT((c.hops.begin() + 1)->begin()->hops == 1); - } - - void - testExpire() - { - testcase("Expire"); - using namespace std::chrono_literals; - Livecache<> c(clock_, journal_); - - auto ep1 = Endpoint{beast::IP::randomEP(), 1}; - c.insert(ep1); - BEAST_EXPECT(c.size() == 1); - c.expire(); - BEAST_EXPECT(c.size() == 1); - // verify that advancing to 1 sec before expiration - // leaves our entry intact - clock_.advance(Tuning::kLiveCacheSecondsToLive - 1s); - c.expire(); - BEAST_EXPECT(c.size() == 1); - // now advance to the point of expiration - clock_.advance(1s); - c.expire(); - BEAST_EXPECT(c.empty()); - } - - void - testHistogram() - { - testcase("Histogram"); - static constexpr auto kNumEps = 40; - Livecache<> c(clock_, journal_); - for (auto i = 0; i < kNumEps; ++i) - add(beast::IP::randomEP(true), c, xrpl::randInt()); - auto h = c.hops.histogram(); - if (!BEAST_EXPECT(!h.empty())) - return; - std::vector v; - boost::split(v, h, boost::algorithm::is_any_of(",")); - auto sum = 0; - for (auto const& n : v) - { - auto val = boost::lexical_cast(boost::trim_copy(n)); - sum += val; - BEAST_EXPECT(val >= 0); - } - BEAST_EXPECT(sum == kNumEps); - } - - void - testShuffle() - { - testcase("Shuffle"); - Livecache<> c(clock_, journal_); - for (auto i = 0; i < 100; ++i) - add(beast::IP::randomEP(true), c, xrpl::randInt(Tuning::kMaxHops + 1)); - - using at_hop = std::vector; - using all_hops = std::array; - - auto cmpEp = [](Endpoint const& a, Endpoint const& b) { - return (b.hops < a.hops || (b.hops == a.hops && b.address < a.address)); - }; - all_hops before; - all_hops beforeSorted; - for (auto i = std::make_pair(0, c.hops.begin()); i.second != c.hops.end(); - ++i.first, ++i.second) - { - std::ranges::copy(*i.second, std::back_inserter(before[i.first])); - std::ranges::copy(*i.second, std::back_inserter(beforeSorted[i.first])); - std::ranges::sort(beforeSorted[i.first], cmpEp); - } - - c.hops.shuffle(); - - all_hops after; - all_hops afterSorted; - for (auto i = std::make_pair(0, c.hops.begin()); i.second != c.hops.end(); - ++i.first, ++i.second) - { - std::ranges::copy(*i.second, std::back_inserter(after[i.first])); - std::ranges::copy(*i.second, std::back_inserter(afterSorted[i.first])); - std::ranges::sort(afterSorted[i.first], cmpEp); - } - - // each hop bucket should contain the same items - // before and after sort, albeit in different order - bool allMatch = true; - for (auto i = 0; i < before.size(); ++i) - { - BEAST_EXPECT(before[i].size() == after[i].size()); - allMatch = allMatch && (before[i] == after[i]); - BEAST_EXPECT(beforeSorted[i] == afterSorted[i]); - } - BEAST_EXPECT(!allMatch); - } - - void - run() override - { - testBasicInsert(); - testInsertUpdate(); - testExpire(); - testHistogram(); - testShuffle(); - } -}; - -BEAST_DEFINE_TESTSUITE(Livecache, peerfinder, xrpl); - -} // namespace xrpl::PeerFinder diff --git a/src/test/peerfinder/PeerFinder_test.cpp b/src/test/peerfinder/PeerFinder_test.cpp deleted file mode 100644 index cf91800951..0000000000 --- a/src/test/peerfinder/PeerFinder_test.cpp +++ /dev/null @@ -1,789 +0,0 @@ -#include - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace xrpl::PeerFinder { - -class PeerFinder_test : public beast::unit_test::Suite -{ - test::SuiteJournal journal_; - -public: - PeerFinder_test() : journal_("PeerFinder_test", *this) - { - } - - struct TestStore : Store - { - std::size_t - load(load_callback const& cb) override - { - return 0; - } - - void - save(std::vector const&) override - { - } - }; - - struct TestChecker - { - void - stop() - { - } - - void - wait() - { - } - - template - void - asyncConnect(beast::IP::Endpoint const& ep, Handler&& handler) - { - // NOLINTNEXTLINE(misc-const-correctness) - boost::system::error_code ec; - handler(ec); - } - }; - - void - testBackoff1() - { - auto const seconds = 10000; - testcase("backoff 1"); - TestStore store; - TestChecker checker; - TestStopwatch clock; - Logic logic(clock, store, checker, journal_); - logic.addFixedPeer("test", beast::IP::Endpoint::fromString("65.0.0.1:5")); - { - Config c; - c.autoConnect = false; - c.listeningPort = 1024; - logic.config(c); - } - std::size_t n = 0; - for (std::size_t i = 0; i < seconds; ++i) - { - auto const list = logic.autoconnect(); - if (!list.empty()) - { - BEAST_EXPECT(list.size() == 1); - auto const [slot, _] = logic.newOutboundSlot(list.front()); - BEAST_EXPECT( - logic.onConnected(slot, beast::IP::Endpoint::fromString("65.0.0.2:5"))); - logic.onClosed(slot); - ++n; - } - clock.advance(std::chrono::seconds(1)); - logic.oncePerSecond(); - } - // Less than 20 attempts - BEAST_EXPECT(n < 20); - } - - // with activate - void - testBackoff2() - { - auto const seconds = 10000; - testcase("backoff 2"); - TestStore store; - TestChecker checker; - TestStopwatch clock; - Logic logic(clock, store, checker, journal_); - logic.addFixedPeer("test", beast::IP::Endpoint::fromString("65.0.0.1:5")); - { - Config c; - c.autoConnect = false; - c.listeningPort = 1024; - logic.config(c); - } - - PublicKey const pk(randomKeyPair(KeyType::Secp256k1).first); - std::size_t n = 0; - - for (std::size_t i = 0; i < seconds; ++i) - { - auto const list = logic.autoconnect(); - if (!list.empty()) - { - BEAST_EXPECT(list.size() == 1); - auto const [slot, _] = logic.newOutboundSlot(list.front()); - if (!BEAST_EXPECT( - logic.onConnected(slot, beast::IP::Endpoint::fromString("65.0.0.2:5")))) - return; - if (!BEAST_EXPECT(logic.activate(slot, pk, false) == PeerFinder::Result::Success)) - return; - logic.onClosed(slot); - ++n; - } - clock.advance(std::chrono::seconds(1)); - logic.oncePerSecond(); - } - // No more often than once per minute - BEAST_EXPECT(n <= (seconds + 59) / 60); - } - - // test accepting an incoming slot for an already existing outgoing slot - void - testDuplicateOutIn() - { - testcase("duplicate out/in"); - TestStore store; - TestChecker checker; - TestStopwatch clock; - Logic logic(clock, store, checker, journal_); - { - Config c; - c.autoConnect = false; - c.listeningPort = 1024; - c.ipLimit = 2; - logic.config(c); - } - - auto const remote = beast::IP::Endpoint::fromString("65.0.0.1:5"); - auto const [slot1, r] = logic.newOutboundSlot(remote); - BEAST_EXPECT(slot1 != nullptr); - BEAST_EXPECT(r == Result::Success); - BEAST_EXPECT(logic.connectedAddresses.count(remote.address()) == 1); - - auto const local = beast::IP::Endpoint::fromString("65.0.0.2:1024"); - auto const [slot2, r2] = logic.newInboundSlot(local, remote); - BEAST_EXPECT(logic.connectedAddresses.count(remote.address()) == 1); - BEAST_EXPECT(r2 == Result::DuplicatePeer); - - if (!BEAST_EXPECT(slot2 == nullptr)) - logic.onClosed(slot2); - - logic.onClosed(slot1); - } - - // test establishing outgoing slot for an already existing incoming slot - void - testDuplicateInOut() - { - testcase("duplicate in/out"); - TestStore store; - TestChecker checker; - TestStopwatch clock; - Logic logic(clock, store, checker, journal_); - { - Config c; - c.autoConnect = false; - c.listeningPort = 1024; - c.ipLimit = 2; - logic.config(c); - } - - auto const remote = beast::IP::Endpoint::fromString("65.0.0.1:5"); - auto const local = beast::IP::Endpoint::fromString("65.0.0.2:1024"); - - auto const [slot1, r] = logic.newInboundSlot(local, remote); - BEAST_EXPECT(slot1 != nullptr); - BEAST_EXPECT(r == Result::Success); - BEAST_EXPECT(logic.connectedAddresses.count(remote.address()) == 1); - - auto const [slot2, r2] = logic.newOutboundSlot(remote); - BEAST_EXPECT(r2 == Result::DuplicatePeer); - BEAST_EXPECT(logic.connectedAddresses.count(remote.address()) == 1); - if (!BEAST_EXPECT(slot2 == nullptr)) - logic.onClosed(slot2); - logic.onClosed(slot1); - } - - void - testPeerLimitExceeded() - { - testcase("peer limit exceeded"); - TestStore store; - TestChecker checker; - TestStopwatch clock; - Logic logic(clock, store, checker, journal_); - { - Config c; - c.autoConnect = false; - c.listeningPort = 1024; - c.ipLimit = 2; - logic.config(c); - } - - auto const local = beast::IP::Endpoint::fromString("65.0.0.2:1024"); - auto const [slot, r] = - logic.newInboundSlot(local, beast::IP::Endpoint::fromString("55.104.0.2:1025")); - BEAST_EXPECT(slot != nullptr); - BEAST_EXPECT(r == Result::Success); - - auto const [slot1, r1] = - logic.newInboundSlot(local, beast::IP::Endpoint::fromString("55.104.0.2:1026")); - BEAST_EXPECT(slot1 != nullptr); - BEAST_EXPECT(r1 == Result::Success); - - auto const [slot2, r2] = - logic.newInboundSlot(local, beast::IP::Endpoint::fromString("55.104.0.2:1027")); - BEAST_EXPECT(r2 == Result::IpLimitExceeded); - - if (!BEAST_EXPECT(slot2 == nullptr)) - logic.onClosed(slot2); - logic.onClosed(slot1); - logic.onClosed(slot); - } - - void - testActivateDuplicatePeer() - { - testcase("test activate duplicate peer"); - TestStore store; - TestChecker checker; - TestStopwatch clock; - Logic logic(clock, store, checker, journal_); - { - Config c; - c.autoConnect = false; - c.listeningPort = 1024; - c.ipLimit = 2; - logic.config(c); - } - - auto const local = beast::IP::Endpoint::fromString("65.0.0.2:1024"); - - PublicKey const pk1(randomKeyPair(KeyType::Secp256k1).first); - - auto const [slot, rSlot] = - logic.newOutboundSlot(beast::IP::Endpoint::fromString("55.104.0.2:1025")); - BEAST_EXPECT(slot != nullptr); - BEAST_EXPECT(rSlot == Result::Success); - - auto const [slot2, r2Slot] = - logic.newOutboundSlot(beast::IP::Endpoint::fromString("55.104.0.2:1026")); - BEAST_EXPECT(slot2 != nullptr); - BEAST_EXPECT(r2Slot == Result::Success); - - BEAST_EXPECT(logic.onConnected(slot, local)); - BEAST_EXPECT(logic.onConnected(slot2, local)); - - BEAST_EXPECT(logic.activate(slot, pk1, false) == Result::Success); - - // activating a different slot with the same node ID (pk) must fail - BEAST_EXPECT(logic.activate(slot2, pk1, false) == Result::DuplicatePeer); - - logic.onClosed(slot); - - // accept the same key for a new slot after removing the old slot - BEAST_EXPECT(logic.activate(slot2, pk1, false) == Result::Success); - logic.onClosed(slot2); - } - - void - testActivateInboundDisabled() - { - testcase("test activate inbound disabled"); - TestStore store; - TestChecker checker; - TestStopwatch clock; - Logic logic(clock, store, checker, journal_); - { - Config c; - c.autoConnect = false; - c.listeningPort = 1024; - c.ipLimit = 2; - logic.config(c); - } - - PublicKey const pk1(randomKeyPair(KeyType::Secp256k1).first); - auto const local = beast::IP::Endpoint::fromString("65.0.0.2:1024"); - - auto const [slot, rSlot] = - logic.newInboundSlot(local, beast::IP::Endpoint::fromString("55.104.0.2:1025")); - BEAST_EXPECT(slot != nullptr); - BEAST_EXPECT(rSlot == Result::Success); - - BEAST_EXPECT(logic.activate(slot, pk1, false) == Result::InboundDisabled); - - { - Config c; - c.autoConnect = false; - c.listeningPort = 1024; - c.ipLimit = 2; - c.inPeers = 1; - logic.config(c); - } - // new inbound slot must succeed when inbound connections are enabled - BEAST_EXPECT(logic.activate(slot, pk1, false) == Result::Success); - - // creating a new inbound slot must succeed as IP Limit is not exceeded - auto const [slot2, r2Slot] = - logic.newInboundSlot(local, beast::IP::Endpoint::fromString("55.104.0.2:1026")); - BEAST_EXPECT(slot2 != nullptr); - BEAST_EXPECT(r2Slot == Result::Success); - - PublicKey const pk2(randomKeyPair(KeyType::Secp256k1).first); - - // an inbound slot exceeding inPeers limit must fail - BEAST_EXPECT(logic.activate(slot2, pk2, false) == Result::Full); - - logic.onClosed(slot2); - logic.onClosed(slot); - } - - void - testAddFixedPeerNoPort() - { - testcase("test addFixedPeer no port"); - TestStore store; - TestChecker checker; - TestStopwatch clock; - Logic logic(clock, store, checker, journal_); - try - { - logic.addFixedPeer("test", beast::IP::Endpoint::fromString("65.0.0.2")); - fail("invalid endpoint successfully added"); - } - catch (std::runtime_error const& e) - { - pass(); - } - } - - void - testIsValidAddress() - { - testcase("is_valid_address"); - TestStore store; - TestChecker checker; - TestStopwatch clock; - Logic logic(clock, store, checker, journal_); - - auto const pass = [&](std::string const& s) { - BEAST_EXPECT(logic.isValidAddress(beast::IP::Endpoint::fromString(s))); - }; - auto const fail = [&](std::string const& s) { - BEAST_EXPECT(!logic.isValidAddress(beast::IP::Endpoint::fromString(s))); - }; - - // Invalid: port 0 - fail("65.0.0.1:0"); - - // --- IPv4 ranges --- - // For each range: 1 before (pass), first (fail), last (fail), - // 1 after (pass) - - // 0.0.0.0/8 - "This network" - // No "before" - nothing before 0.0.0.0 - fail("0.0.0.0:8080"); - fail("0.255.255.255:8080"); - pass("1.0.0.0:8080"); - - // 10.0.0.0/8 - Private (RFC 1918) - pass("9.255.255.255:8080"); - fail("10.0.0.0:8080"); - fail("10.255.255.255:8080"); - pass("11.0.0.0:8080"); - - // 100.64.0.0/10 - Shared Address Space / CGNAT (RFC 6598) - pass("100.63.255.255:8080"); - fail("100.64.0.0:8080"); - fail("100.127.255.255:8080"); - pass("100.128.0.0:8080"); - - // 127.0.0.0/8 - Loopback - pass("126.255.255.255:8080"); - fail("127.0.0.0:8080"); - fail("127.255.255.255:8080"); - pass("128.0.0.0:8080"); - - // 169.254.0.0/16 - Link-local - pass("169.253.255.255:8080"); - fail("169.254.0.0:8080"); - fail("169.254.255.255:8080"); - pass("169.255.0.0:8080"); - - // 172.16.0.0/12 - Private (RFC 1918) - pass("172.15.255.255:8080"); - fail("172.16.0.0:8080"); - fail("172.31.255.255:8080"); - pass("172.32.0.0:8080"); - - // 192.0.0.0/24 - IETF Protocol Assignments (RFC 6890) - pass("191.255.255.255:8080"); - fail("192.0.0.0:8080"); - fail("192.0.0.255:8080"); - pass("192.0.1.0:8080"); - - // 192.0.2.0/24 - TEST-NET-1 (RFC 5737) - pass("192.0.1.255:8080"); - fail("192.0.2.0:8080"); - fail("192.0.2.255:8080"); - pass("192.0.3.0:8080"); - - // 192.88.99.0/24 - 6to4 Relay Anycast (RFC 7526) - pass("192.88.98.255:8080"); - fail("192.88.99.0:8080"); - fail("192.88.99.255:8080"); - pass("192.88.100.0:8080"); - - // 192.168.0.0/16 - Private (RFC 1918) - pass("192.167.255.255:8080"); - fail("192.168.0.0:8080"); - fail("192.168.255.255:8080"); - pass("192.169.0.0:8080"); - - // 198.18.0.0/15 - Benchmarking (RFC 2544) - pass("198.17.255.255:8080"); - fail("198.18.0.0:8080"); - fail("198.19.255.255:8080"); - pass("198.20.0.0:8080"); - - // 198.51.100.0/24 - TEST-NET-2 (RFC 5737) - pass("198.51.99.255:8080"); - fail("198.51.100.0:8080"); - fail("198.51.100.255:8080"); - pass("198.51.101.0:8080"); - - // 203.0.113.0/24 - TEST-NET-3 (RFC 5737) - pass("203.0.112.255:8080"); - fail("203.0.113.0:8080"); - fail("203.0.113.255:8080"); - pass("203.0.114.0:8080"); - - // 224.0.0.0/4 - Multicast - pass("223.255.255.255:8080"); - fail("224.0.0.0:8080"); - fail("239.255.255.255:8080"); - // 240.0.0.0 (after multicast) is also blocked (reserved) - - // 240.0.0.0/4 - Reserved (RFC 1112) - // 239.255.255.255 (before reserved) is also blocked (multicast) - fail("240.0.0.0:8080"); - fail("255.255.255.255:8080"); - - // --- IPv6 ranges --- - - // ::1 - Loopback (single address) - fail("[::1]:8080"); - - // :: - Unspecified (single address) - fail("[::]:8080"); - - // fc00::/7 - Unique Local Address (ULA) - pass("[fb00::1]:8080"); - fail("[fc00::1]:8080"); - fail("[fdff::1]:8080"); - pass("[fe00::1]:8080"); - - // fe80::/10 - Link-local - pass("[fe7f::1]:8080"); - fail("[fe80::1]:8080"); - fail("[febf::1]:8080"); - pass("[fec0::1]:8080"); - - // ff00::/8 - Multicast - pass("[feff::1]:8080"); - fail("[ff00::1]:8080"); - fail("[ffff::1]:8080"); - // No "after" - ffff:... is the highest IPv6 range - - // 100::/64 - Discard prefix (RFC 6666) - pass("[ff::1]:8080"); - fail("[100::]:8080"); - fail("[100::ffff:ffff:ffff:ffff]:8080"); - pass("[100:0:0:1::1]:8080"); - - // 2001::/32 - IETF Protocol Assignments / Teredo (RFC 4380) - pass("[2000:ffff::1]:8080"); - fail("[2001::]:8080"); - fail("[2001:0:ffff::1]:8080"); - pass("[2001:1::1]:8080"); - - // 2001:20::/28 - ORCHIDv2 (RFC 7343) - pass("[2001:1f::1]:8080"); - fail("[2001:20::1]:8080"); - fail("[2001:2f::1]:8080"); - pass("[2001:30::1]:8080"); - - // 2001:db8::/32 - Documentation (RFC 3849) - pass("[2001:db7::1]:8080"); - fail("[2001:db8::1]:8080"); - fail("[2001:db8:ffff::1]:8080"); - pass("[2001:db9::1]:8080"); - - // 2002::/16 - 6to4 (RFC 3056, deprecated) - pass("[2001:ffff::1]:8080"); - fail("[2002::1]:8080"); - fail("[2002:ffff::1]:8080"); - pass("[2003::1]:8080"); - - // --- IPv6 v4-mapped (delegates to IPv4 checks) --- - fail("[::ffff:10.0.0.1]:8080"); - fail("[::ffff:100.64.0.1]:8080"); - fail("[::ffff:169.254.1.1]:8080"); - fail("[::ffff:192.0.2.1]:8080"); - fail("[::ffff:198.18.0.1]:8080"); - fail("[::ffff:224.0.0.1]:8080"); - fail("[::ffff:240.0.0.1]:8080"); - - // --- Valid public addresses --- - pass("8.8.8.8:443"); - pass("65.0.0.1:8080"); - pass("[2001:4860:4860::8888]:8080"); - pass("[2606:4700:4700::1111]:8080"); - } - - void - testVerifyEndpoints() - { - // Helper that sets up a Logic instance, creates and activates a slot, - // then calls on_endpoints with the given list and returns the - // livecache size afterwards. - auto run = [&](bool verifyEndpoints, Endpoints eps) -> std::size_t { - TestStore store; - TestChecker checker; - TestStopwatch clock; - Logic logic(clock, store, checker, journal_); - { - Config c; - c.autoConnect = false; - c.listeningPort = 1024; - c.ipLimit = 2; - c.verifyEndpoints = verifyEndpoints; - logic.config(c); - } - - auto const remote = beast::IP::Endpoint::fromString("65.0.0.1:5"); - auto const local = beast::IP::Endpoint::fromString("65.0.0.2:1024"); - - auto const [slot, r] = logic.newOutboundSlot(remote); - BEAST_EXPECT(slot != nullptr); - BEAST_EXPECT(r == Result::Success); - BEAST_EXPECT(logic.onConnected(slot, local)); - - PublicKey const pk(randomKeyPair(KeyType::Secp256k1).first); - BEAST_EXPECT(logic.activate(slot, pk, false) == Result::Success); - - logic.onEndpoints(slot, std::move(eps)); - - auto const size = logic.livecache.size(); - logic.onClosed(slot); - return size; - }; - - { - testcase("verify_endpoints enabled"); - - // Valid public addresses - Endpoints eps; - eps.emplace_back(beast::IP::Endpoint::fromString("44.0.0.1:5"), 1); - eps.emplace_back(beast::IP::Endpoint::fromString("44.0.0.2:6"), 1); - // Invalid: private address - eps.emplace_back(beast::IP::Endpoint::fromString("10.0.0.1:5"), 1); - // Invalid: port 0 - eps.emplace_back(beast::IP::Endpoint::fromString("44.0.0.3:0"), 1); - - // With verification enabled, only the 2 valid endpoints survive - BEAST_EXPECT(run(true, eps) == 2); - } - { - testcase("verify_endpoints disabled"); - - Endpoints eps; - eps.emplace_back(beast::IP::Endpoint::fromString("44.0.0.1:5"), 1); - eps.emplace_back(beast::IP::Endpoint::fromString("44.0.0.2:6"), 1); - // Private address — kept when verification is off - eps.emplace_back(beast::IP::Endpoint::fromString("10.0.0.1:5"), 1); - // Port 0 — kept when verification is off - eps.emplace_back(beast::IP::Endpoint::fromString("44.0.0.3:0"), 1); - - // Without verification, all 4 endpoints survive - BEAST_EXPECT(run(false, eps) == 4); - } - } - - void - testOnConnectedSelfConnection() - { - testcase("test onConnected self connection"); - TestStore store; - TestChecker checker; - TestStopwatch clock; - Logic logic(clock, store, checker, journal_); - - auto const local = beast::IP::Endpoint::fromString("65.0.0.2:1234"); - auto const [slot, r] = logic.newOutboundSlot(local); - BEAST_EXPECT(slot != nullptr); - BEAST_EXPECT(r == Result::Success); - - // Must fail when a slot is to our own IP address - BEAST_EXPECT(!logic.onConnected(slot, local)); - logic.onClosed(slot); - } - - void - testConfig() - { - // 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) { - xrpl::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.peersMax == max && c.peersInMax == 0 && c.peersOutMax == 0) || - (c.peersInMax == *maxIn && c.peersOutMax == *maxOut)); - - Config const config = Config::makeConfig(c, port, false, 0, true); - - Counts counts; - counts.onConfig(config); - BEAST_EXPECT( - counts.outMax() == expectOut && counts.inMax() == expectIn && - config.ipLimit == expectIpLimit); - - TestStore store; - TestChecker checker; - TestStopwatch clock; - Logic logic(clock, store, checker, journal_); - logic.config(config); - - BEAST_EXPECT(logic.config() == config); - }; - - // 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 - testInvalidConfig() - { - testcase("invalid config"); - - auto run = [&](std::string const& toLoad) { - xrpl::Config c; - try - { - c.loadFromString(toLoad); - fail(); - } - catch (...) - { - pass(); - } - }; - run(R"xrpldConfig( -[peers_in_max] -100 -)xrpldConfig"); - run(R"xrpldConfig( -[peers_out_max] -100 -)xrpldConfig"); - run(R"xrpldConfig( -[peers_in_max] -100 -[peers_out_max] -5 -)xrpldConfig"); - run(R"xrpldConfig( -[peers_in_max] -1001 -[peers_out_max] -10 -)xrpldConfig"); - run(R"xrpldConfig( -[peers_in_max] -10 -[peers_out_max] -1001 -)xrpldConfig"); - } - - void - run() override - { - testBackoff1(); - testBackoff2(); - testDuplicateOutIn(); - testDuplicateInOut(); - testConfig(); - testInvalidConfig(); - testPeerLimitExceeded(); - testActivateDuplicatePeer(); - testActivateInboundDisabled(); - testAddFixedPeerNoPort(); - testOnConnectedSelfConnection(); - testIsValidAddress(); - testVerifyEndpoints(); - } -}; - -BEAST_DEFINE_TESTSUITE(PeerFinder, peerfinder, xrpl); - -} // namespace xrpl::PeerFinder diff --git a/src/tests/libxrpl/CMakeLists.txt b/src/tests/libxrpl/CMakeLists.txt index cafe72eff9..2e131b895e 100644 --- a/src/tests/libxrpl/CMakeLists.txt +++ b/src/tests/libxrpl/CMakeLists.txt @@ -21,7 +21,7 @@ set_target_properties( ) # Lets test sources include the shared helpers as . target_include_directories(xrpl_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(xrpl_tests PRIVATE GTest::gtest xrpl.libxrpl) +target_link_libraries(xrpl_tests PRIVATE GTest::gtest GTest::gmock xrpl.libxrpl) # One source subdirectory per module. Network unit tests are currently not # supported on Windows. @@ -29,6 +29,7 @@ set(test_modules basics crypto json + peerfinder resource shamap tx diff --git a/src/tests/libxrpl/main.cpp b/src/tests/libxrpl/main.cpp index 5142bbe08a..f9114bffc4 100644 --- a/src/tests/libxrpl/main.cpp +++ b/src/tests/libxrpl/main.cpp @@ -1,8 +1,9 @@ +#include #include int main(int argc, char** argv) { - ::testing::InitGoogleTest(&argc, argv); + ::testing::InitGoogleMock(&argc, argv); return RUN_ALL_TESTS(); } diff --git a/src/tests/libxrpl/peerfinder/Livecache.cpp b/src/tests/libxrpl/peerfinder/Livecache.cpp new file mode 100644 index 0000000000..464ec0e5da --- /dev/null +++ b/src/tests/libxrpl/peerfinder/Livecache.cpp @@ -0,0 +1,294 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl::PeerFinder { +namespace { + +class LivecacheTest : public ::testing::Test +{ +protected: + static beast::Journal + journal() + { + return beast::Journal{TestSink::instance()}; + } + + static beast::IP::Endpoint + endpoint(std::uint16_t index, bool v4 = true) + { + auto const port = static_cast(10000 + index); + + if (v4) + { + auto bytes = beast::IP::AddressV4::bytes_type{ + {54, + static_cast((index / 256) % 256), + static_cast(index % 256), + 1}}; + return beast::IP::Endpoint{beast::IP::Address{beast::IP::AddressV4{bytes}}, port}; + } + + auto bytes = beast::IP::AddressV6::bytes_type{ + {0x20, + 0x01, + 0x0d, + 0xb8, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + static_cast((index / 256) % 256), + static_cast(index % 256), + 1}}; + return beast::IP::Endpoint{beast::IP::Address{beast::IP::AddressV6{bytes}}, port}; + } + + void + addEndpoint(beast::IP::Endpoint const& ep, std::uint32_t hops = 0) + { + cache_.insert(Endpoint{ep, hops}); + } + + TestStopwatch clock_; + Livecache<> cache_{clock_, journal()}; +}; + +} // namespace + +TEST_F(LivecacheTest, basic_insert) +{ + EXPECT_TRUE(cache_.empty()); + + for (auto i = 0; i < 10; ++i) + addEndpoint(endpoint(i, true)); + + EXPECT_FALSE(cache_.empty()); + EXPECT_EQ(cache_.size(), 10u); + + for (auto i = 10; i < 20; ++i) + addEndpoint(endpoint(i, false)); + + EXPECT_FALSE(cache_.empty()); + EXPECT_EQ(cache_.size(), 20u); +} + +TEST_F(LivecacheTest, insert_update_keeps_lowest_hop_count) +{ + auto const ep1 = Endpoint{endpoint(1), 2}; + cache_.insert(ep1); + ASSERT_EQ(cache_.size(), 1u); + EXPECT_EQ((cache_.hops.begin() + 2)->begin()->hops, 2u); + + auto const ep2 = Endpoint{ep1.address, 4}; + cache_.insert(ep2); + EXPECT_EQ(cache_.size(), 1u); + EXPECT_EQ((cache_.hops.begin() + 2)->begin()->hops, 2u); + + auto const ep3 = Endpoint{ep1.address, 2}; + cache_.insert(ep3); + EXPECT_EQ(cache_.size(), 1u); + EXPECT_EQ((cache_.hops.begin() + 2)->begin()->hops, 2u); + + auto const ep4 = Endpoint{ep1.address, 1}; + cache_.insert(ep4); + EXPECT_EQ(cache_.size(), 1u); + EXPECT_EQ((cache_.hops.begin() + 1)->begin()->hops, 1u); +} + +TEST_F(LivecacheTest, hop_iterators_support_const_reverse_and_move_back) +{ + auto const ep1 = Endpoint{endpoint(1), 1}; + auto const ep2 = Endpoint{endpoint(2), 1}; + cache_.insert(ep1); + cache_.insert(ep2); + + auto hop = *(cache_.hops.begin() + 1); + ASSERT_NE(hop.begin(), hop.end()); + ASSERT_NE(hop.cbegin(), hop.cend()); + ASSERT_NE(hop.rbegin(), hop.rend()); + ASSERT_NE(hop.crbegin(), hop.crend()); + + auto const firstAddress = hop.begin()->address; + hop.moveBack(hop.begin()); + EXPECT_EQ(hop.rbegin()->address, firstAddress); + + auto const& constHops = cache_.hops; + EXPECT_NE(constHops.begin(), constHops.end()); + EXPECT_NE(constHops.cbegin(), constHops.cend()); + EXPECT_NE(constHops.rbegin(), constHops.rend()); + EXPECT_NE(constHops.crbegin(), constHops.crend()); + + auto const constHop = *(constHops.cbegin() + 1); + EXPECT_EQ(std::distance(constHop.begin(), constHop.end()), 2); + EXPECT_EQ(std::distance(constHop.cbegin(), constHop.cend()), 2); + EXPECT_EQ(std::distance(constHop.rbegin(), constHop.rend()), 2); + EXPECT_EQ(std::distance(constHop.crbegin(), constHop.crend()), 2); +} + +TEST_F(LivecacheTest, on_write_reports_entries_and_expiration) +{ + cache_.insert(Endpoint{endpoint(1), 1}); + cache_.insert(Endpoint{endpoint(2), Tuning::kMaxHops + 1}); + + JsonPropertyStream stream; + { + beast::PropertyStream::Map map(stream); + cache_.onWrite(map); + } + + auto const& top = stream.top(); + EXPECT_EQ(top["size"].asUInt(), 2u); + EXPECT_FALSE(top["hist"].asString().empty()); + ASSERT_TRUE(top.isMember("entries")); + ASSERT_EQ(top["entries"].size(), 2u); + auto const& entry = top["entries"][json::UInt{0}]; + EXPECT_TRUE(entry.isMember("hops")); + EXPECT_TRUE(entry.isMember("address")); + EXPECT_TRUE(entry.isMember("expires")); +} + +TEST_F(LivecacheTest, expire_removes_entries_after_ttl) +{ + using namespace std::chrono_literals; + + cache_.insert(Endpoint{endpoint(1), 1}); + ASSERT_EQ(cache_.size(), 1u); + + cache_.expire(); + EXPECT_EQ(cache_.size(), 1u); + + clock_.advance(Tuning::kLiveCacheSecondsToLive - 1s); + cache_.expire(); + EXPECT_EQ(cache_.size(), 1u); + + clock_.advance(1s); + cache_.expire(); + EXPECT_TRUE(cache_.empty()); +} + +TEST_F(LivecacheTest, expire_removes_multiple_entries_after_ttl) +{ + using namespace std::chrono_literals; + + cache_.insert(Endpoint{endpoint(1), 1}); + cache_.insert(Endpoint{endpoint(2), 2}); + + clock_.advance(Tuning::kLiveCacheSecondsToLive); + cache_.expire(); + EXPECT_TRUE(cache_.empty()); +} + +TEST_F(LivecacheTest, histogram_counts_all_entries) +{ + constexpr auto kNumEndpoints = 40; + + for (auto i = 0; i < kNumEndpoints; ++i) + { + addEndpoint(endpoint(static_cast(i)), xrpl::randInt()); + } + + auto const histogram = cache_.hops.histogram(); + ASSERT_FALSE(histogram.empty()); + + std::vector values; + boost::split(values, histogram, boost::algorithm::is_any_of(",")); + + auto sum = 0; + for (auto const& value : values) + { + auto const count = boost::lexical_cast(boost::trim_copy(value)); + sum += count; + EXPECT_GE(count, 0); + } + EXPECT_EQ(sum, kNumEndpoints); +} + +TEST_F(LivecacheTest, shuffle_preserves_bucket_contents) +{ + for (auto i = 0; i < 100; ++i) + { + addEndpoint(endpoint(static_cast(i)), xrpl::randInt(Tuning::kMaxHops + 1)); + } + + using AtHop = std::vector; + using AllHops = std::array; + + auto const compareEndpoint = [](Endpoint const& lhs, Endpoint const& rhs) { + return rhs.hops < lhs.hops || (rhs.hops == lhs.hops && rhs.address < lhs.address); + }; + auto const sameEndpoint = [](Endpoint const& lhs, Endpoint const& rhs) { + return lhs.hops == rhs.hops && lhs.address == rhs.address; + }; + auto const sameEndpoints = + [&sameEndpoint](std::vector const& lhs, std::vector const& rhs) { + return lhs.size() == rhs.size() && + std::equal(lhs.begin(), lhs.end(), rhs.begin(), sameEndpoint); + }; + + AllHops before; + AllHops beforeSorted; + for (auto i = std::make_pair(0, cache_.hops.begin()); i.second != cache_.hops.end(); + ++i.first, ++i.second) + { + std::ranges::copy(*i.second, std::back_inserter(before[i.first])); + std::ranges::copy(*i.second, std::back_inserter(beforeSorted[i.first])); + std::ranges::sort(beforeSorted[i.first], compareEndpoint); + } + + cache_.hops.shuffle(); + + AllHops after; + AllHops afterSorted; + for (auto i = std::make_pair(0, cache_.hops.begin()); i.second != cache_.hops.end(); + ++i.first, ++i.second) + { + std::ranges::copy(*i.second, std::back_inserter(after[i.first])); + std::ranges::copy(*i.second, std::back_inserter(afterSorted[i.first])); + std::ranges::sort(afterSorted[i.first], compareEndpoint); + } + + auto allBucketsKeptOriginalOrder = true; + for (auto i = 0u; i < before.size(); ++i) + { + EXPECT_EQ(before[i].size(), after[i].size()); + allBucketsKeptOriginalOrder = + allBucketsKeptOriginalOrder && sameEndpoints(before[i], after[i]); + EXPECT_TRUE(sameEndpoints(beforeSorted[i], afterSorted[i])); + } + EXPECT_FALSE(allBucketsKeptOriginalOrder); +} + +} // namespace xrpl::PeerFinder diff --git a/src/tests/libxrpl/peerfinder/PeerFinder.cpp b/src/tests/libxrpl/peerfinder/PeerFinder.cpp new file mode 100644 index 0000000000..31fa59d1ce --- /dev/null +++ b/src/tests/libxrpl/peerfinder/PeerFinder.cpp @@ -0,0 +1,1270 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl::PeerFinder { +namespace { + +using ::testing::_; +using ::testing::NiceMock; +using ::testing::Return; + +beast::Journal +journal() +{ + return beast::Journal{TestSink::instance()}; +} + +beast::IP::Endpoint +endpoint(std::string const& value) +{ + return beast::IP::Endpoint::fromString(value); +} + +class MockStore : public Store +{ +public: + MOCK_METHOD(std::size_t, load, (Store::load_callback const& cb), (override)); + MOCK_METHOD(void, save, (std::vector const& entries), (override)); +}; + +class CapturingStore : public Store +{ +public: + std::vector entriesToLoad; + std::vector> saves; + + std::size_t + load(Store::load_callback const& cb) override + { + for (auto const& entry : entriesToLoad) + cb(entry.endpoint, entry.valence); + return entriesToLoad.size(); + } + + void + save(std::vector const& entries) override + { + saves.push_back(entries); + } +}; + +Store::Entry +storeEntry(beast::IP::Endpoint const& endpoint, int valence) +{ + Store::Entry entry; + entry.endpoint = endpoint; + entry.valence = valence; + return entry; +} + +void +allowEmptyStore(MockStore& store) +{ + ON_CALL(store, load(_)).WillByDefault(Return(0)); + ON_CALL(store, save(_)).WillByDefault([](std::vector const&) {}); +} + +class MockChecker +{ +public: + MOCK_METHOD(void, stop, ()); + MOCK_METHOD(void, wait, ()); + MOCK_METHOD(void, recordAsyncConnect, (beast::IP::Endpoint const& ep)); + + boost::system::error_code nextError; + bool completeAsync = true; + std::vector asyncConnects; + + template + void + asyncConnect(beast::IP::Endpoint const& ep, Handler&& handler) + { + asyncConnects.push_back(ep); + recordAsyncConnect(ep); + if (completeAsync) + std::forward(handler)(nextError); + } +}; + +class TestSource : public Source +{ +public: + explicit TestSource(std::string name) : name_(std::move(name)) + { + } + + std::string const& + name() override + { + return name_; + } + + void + cancel() override + { + ++cancelCount; + } + + void + fetch(Results& results, beast::Journal) override + { + ++fetchCount; + results = resultsToFetch; + } + + Results resultsToFetch; + int fetchCount = 0; + int cancelCount = 0; + +private: + std::string name_; +}; + +class DefaultCancelSource : public Source +{ +public: + std::string const& + name() override + { + return name_; + } + + void + fetch(Results& results, beast::Journal) override + { + results = resultsToFetch; + } + + Results resultsToFetch; + +private: + std::string name_{"default"}; +}; + +class PeerFinderTest : public ::testing::Test +{ +public: + PeerFinderTest() + { + allowEmptyStore(store_); + } + +protected: + void + configure(std::size_t ipLimit = 2) + { + Config config; + config.autoConnect = false; + config.listeningPort = 1024; + config.ipLimit = static_cast(ipLimit); + logic_.config(config); + } + + NiceMock store_; + NiceMock checker_; + TestStopwatch clock_; + Logic> logic_{clock_, store_, checker_, journal()}; +}; + +int +savedValence(std::vector const& entries, beast::IP::Endpoint const& endpoint) +{ + for (auto const& entry : entries) + { + if (entry.endpoint == endpoint) + return entry.valence; + } + + ADD_FAILURE() << "missing saved endpoint " << endpoint.toString(); + return 0; +} + +TEST_F(PeerFinderTest, backoff_limits_repeated_connection_attempts) +{ + auto constexpr kSECONDS = 10000; + + logic_.addFixedPeer("test", endpoint("65.0.0.1:5")); + configure(); + + std::size_t attempts = 0; + for (std::size_t i = 0; i < kSECONDS; ++i) + { + auto const list = logic_.autoconnect(); + if (!list.empty()) + { + ASSERT_EQ(list.size(), 1u); + auto const [slot, result] = logic_.newOutboundSlot(list.front()); + ASSERT_NE(slot, nullptr); + ASSERT_EQ(result, Result::Success); + EXPECT_TRUE(logic_.onConnected(slot, endpoint("65.0.0.2:5"))); + logic_.onClosed(slot); + ++attempts; + } + clock_.advance(std::chrono::seconds(1)); + logic_.oncePerSecond(); + } + + EXPECT_LT(attempts, 20u); +} + +TEST_F(PeerFinderTest, activated_peer_backoff_allows_at_most_one_attempt_per_minute) +{ + auto constexpr kSECONDS = 10000; + + logic_.addFixedPeer("test", endpoint("65.0.0.1:5")); + configure(); + + PublicKey const publicKey(randomKeyPair(KeyType::Secp256k1).first); + + std::size_t attempts = 0; + for (std::size_t i = 0; i < kSECONDS; ++i) + { + auto const list = logic_.autoconnect(); + if (!list.empty()) + { + ASSERT_EQ(list.size(), 1u); + auto const [slot, result] = logic_.newOutboundSlot(list.front()); + ASSERT_NE(slot, nullptr); + ASSERT_EQ(result, Result::Success); + ASSERT_TRUE(logic_.onConnected(slot, endpoint("65.0.0.2:5"))); + ASSERT_EQ(logic_.activate(slot, publicKey, false), Result::Success); + logic_.onClosed(slot); + ++attempts; + } + clock_.advance(std::chrono::seconds(1)); + logic_.oncePerSecond(); + } + + EXPECT_LE(attempts, (kSECONDS + 59u) / 60u); +} + +TEST_F(PeerFinderTest, duplicate_inbound_slot_is_rejected_for_existing_outbound_slot) +{ + configure(); + + auto const remote = endpoint("65.0.0.1:5"); + auto const [slot1, result1] = logic_.newOutboundSlot(remote); + ASSERT_NE(slot1, nullptr); + EXPECT_EQ(result1, Result::Success); + EXPECT_EQ(logic_.connectedAddresses.count(remote.address()), 1u); + + auto const local = endpoint("65.0.0.2:1024"); + auto const [slot2, result2] = logic_.newInboundSlot(local, remote); + EXPECT_EQ(logic_.connectedAddresses.count(remote.address()), 1u); + EXPECT_EQ(result2, Result::DuplicatePeer); + EXPECT_EQ(slot2, nullptr); + + if (slot2) + logic_.onClosed(slot2); + logic_.onClosed(slot1); +} + +TEST_F(PeerFinderTest, duplicate_outbound_slot_is_rejected_for_existing_inbound_slot) +{ + configure(); + + auto const remote = endpoint("65.0.0.1:5"); + auto const local = endpoint("65.0.0.2:1024"); + + auto const [slot1, result1] = logic_.newInboundSlot(local, remote); + ASSERT_NE(slot1, nullptr); + EXPECT_EQ(result1, Result::Success); + EXPECT_EQ(logic_.connectedAddresses.count(remote.address()), 1u); + + auto const [slot2, result2] = logic_.newOutboundSlot(remote); + EXPECT_EQ(result2, Result::DuplicatePeer); + EXPECT_EQ(logic_.connectedAddresses.count(remote.address()), 1u); + EXPECT_EQ(slot2, nullptr); + + if (slot2) + logic_.onClosed(slot2); + logic_.onClosed(slot1); +} + +TEST_F(PeerFinderTest, peer_limit_exceeded_rejects_additional_inbound_slot) +{ + configure(); + + auto const local = endpoint("65.0.0.2:1024"); + auto const [slot, result] = logic_.newInboundSlot(local, endpoint("55.104.0.2:1025")); + ASSERT_NE(slot, nullptr); + EXPECT_EQ(result, Result::Success); + + auto const [slot1, result1] = logic_.newInboundSlot(local, endpoint("55.104.0.2:1026")); + ASSERT_NE(slot1, nullptr); + EXPECT_EQ(result1, Result::Success); + + auto const [slot2, result2] = logic_.newInboundSlot(local, endpoint("55.104.0.2:1027")); + EXPECT_EQ(result2, Result::IpLimitExceeded); + EXPECT_EQ(slot2, nullptr); + + if (slot2) + logic_.onClosed(slot2); + logic_.onClosed(slot1); + logic_.onClosed(slot); +} + +TEST_F(PeerFinderTest, activate_rejects_duplicate_public_key) +{ + configure(); + + auto const local = endpoint("65.0.0.2:1024"); + PublicKey const publicKey(randomKeyPair(KeyType::Secp256k1).first); + + auto const [slot, result] = logic_.newOutboundSlot(endpoint("55.104.0.2:1025")); + ASSERT_NE(slot, nullptr); + EXPECT_EQ(result, Result::Success); + + auto const [slot2, result2] = logic_.newOutboundSlot(endpoint("55.104.0.2:1026")); + ASSERT_NE(slot2, nullptr); + EXPECT_EQ(result2, Result::Success); + + EXPECT_TRUE(logic_.onConnected(slot, local)); + EXPECT_TRUE(logic_.onConnected(slot2, local)); + + EXPECT_EQ(logic_.activate(slot, publicKey, false), Result::Success); + EXPECT_EQ(logic_.activate(slot2, publicKey, false), Result::DuplicatePeer); + + logic_.onClosed(slot); + + EXPECT_EQ(logic_.activate(slot2, publicKey, false), Result::Success); + logic_.onClosed(slot2); +} + +TEST_F(PeerFinderTest, activate_rejects_inbound_when_inbound_connections_are_disabled) +{ + configure(); + + PublicKey const publicKey(randomKeyPair(KeyType::Secp256k1).first); + auto const local = endpoint("65.0.0.2:1024"); + + auto const [slot, result] = logic_.newInboundSlot(local, endpoint("55.104.0.2:1025")); + ASSERT_NE(slot, nullptr); + EXPECT_EQ(result, Result::Success); + + EXPECT_EQ(logic_.activate(slot, publicKey, false), Result::InboundDisabled); + + { + Config config; + config.autoConnect = false; + config.listeningPort = 1024; + config.ipLimit = 2; + config.inPeers = 1; + logic_.config(config); + } + + EXPECT_EQ(logic_.activate(slot, publicKey, false), Result::Success); + + auto const [slot2, result2] = logic_.newInboundSlot(local, endpoint("55.104.0.2:1026")); + ASSERT_NE(slot2, nullptr); + EXPECT_EQ(result2, Result::Success); + + PublicKey const publicKey2(randomKeyPair(KeyType::Secp256k1).first); + EXPECT_EQ(logic_.activate(slot2, publicKey2, false), Result::Full); + + logic_.onClosed(slot2); + logic_.onClosed(slot); +} + +TEST_F(PeerFinderTest, add_fixed_peer_rejects_endpoint_without_port) +{ + EXPECT_THROW(logic_.addFixedPeer("test", endpoint("65.0.0.2")), std::runtime_error); +} + +TEST_F(PeerFinderTest, on_connected_rejects_self_connection) +{ + auto const local = endpoint("65.0.0.2:1234"); + auto const [slot, result] = logic_.newOutboundSlot(local); + ASSERT_NE(slot, nullptr); + EXPECT_EQ(result, Result::Success); + + EXPECT_FALSE(logic_.onConnected(slot, local)); + logic_.onClosed(slot); +} + +TEST(PeerFinderResult, converts_all_result_values_to_strings) +{ + EXPECT_EQ(to_string(Result::InboundDisabled), "inbound disabled"); + EXPECT_EQ(to_string(Result::DuplicatePeer), "peer already connected"); + EXPECT_EQ(to_string(Result::IpLimitExceeded), "ip limit exceeded"); + EXPECT_EQ(to_string(Result::Full), "slots full"); + EXPECT_EQ(to_string(Result::Success), "success"); + EXPECT_EQ(to_string(static_cast(-1)), "unknown"); +} + +TEST(PeerFinderEndpoint, orders_by_address) +{ + Endpoint const high{endpoint("65.0.0.2:10002"), 1}; + Endpoint const low{endpoint("65.0.0.1:10001"), 2}; + std::vector endpoints{high, low}; + + std::ranges::sort( + endpoints, [](Endpoint const& lhs, Endpoint const& rhs) { return lhs < rhs; }); + + EXPECT_EQ(endpoints.front().address, low.address); + EXPECT_EQ(endpoints.back().address, high.address); +} + +TEST(PeerFinderCounts, tracks_slot_states_and_capacity) +{ + TestStopwatch clock; + Counts counts; + Config config; + config.outPeers = 1; + config.inPeers = 1; + config.wantIncoming = true; + counts.onConfig(config); + + EXPECT_EQ(counts.outMax(), 1); + EXPECT_EQ(counts.inMax(), 1); + EXPECT_EQ(counts.inboundSlotsFree(), 1); + EXPECT_EQ(counts.outboundSlotsFree(), 1); + EXPECT_EQ(counts.totalActive(), 0); + EXPECT_FALSE(counts.isConnectedToNetwork()); + EXPECT_EQ(counts.attemptsNeeded(), Tuning::kMaxConnectAttempts); + EXPECT_EQ(counts.stateString(), "0/1 out, 0/1 in, 0 connecting, 0 closing"); + + SlotImp inbound(endpoint("65.0.0.1:10001"), endpoint("65.0.0.2:10002"), false, clock); + counts.add(inbound); + EXPECT_EQ(counts.acceptCount(), 1); + EXPECT_TRUE(counts.canActivate(inbound)); + counts.remove(inbound); + EXPECT_EQ(counts.acceptCount(), 0); + + inbound.activate(clock.now()); + counts.add(inbound); + EXPECT_EQ(counts.inboundActive(), 1); + EXPECT_EQ(counts.totalActive(), 1); + EXPECT_EQ(counts.inboundSlotsFree(), 0); + + SlotImp const extraInbound( + endpoint("65.0.0.3:10003"), endpoint("65.0.0.4:10004"), false, clock); + EXPECT_FALSE(counts.canActivate(extraInbound)); + counts.remove(inbound); + + SlotImp outbound(endpoint("65.0.0.5:10005"), false, clock); + counts.add(outbound); + EXPECT_EQ(counts.attempts(), 1); + EXPECT_EQ(counts.connectCount(), 1); + EXPECT_EQ(counts.attemptsNeeded(), Tuning::kMaxConnectAttempts - 1); + counts.remove(outbound); + + outbound.state(Slot::State::Connected); + EXPECT_TRUE(counts.canActivate(outbound)); + outbound.activate(clock.now()); + counts.add(outbound); + EXPECT_EQ(counts.outActive(), 1); + EXPECT_EQ(counts.outboundSlotsFree(), 0); + + SlotImp extraOutbound(endpoint("65.0.0.6:10006"), false, clock); + extraOutbound.state(Slot::State::Connected); + EXPECT_FALSE(counts.canActivate(extraOutbound)); + + SlotImp fixedOutbound(endpoint("65.0.0.7:10007"), true, clock); + fixedOutbound.state(Slot::State::Connected); + EXPECT_TRUE(counts.canActivate(fixedOutbound)); + fixedOutbound.activate(clock.now()); + counts.add(fixedOutbound); + EXPECT_EQ(counts.fixed(), 1u); + EXPECT_EQ(counts.fixedActive(), 1u); + counts.remove(fixedOutbound); + + SlotImp reservedOutbound(endpoint("65.0.0.8:10008"), false, clock); + reservedOutbound.reserved(true); + reservedOutbound.state(Slot::State::Connected); + EXPECT_TRUE(counts.canActivate(reservedOutbound)); + reservedOutbound.activate(clock.now()); + counts.add(reservedOutbound); + + JsonPropertyStream stream; + { + beast::PropertyStream::Map map(stream); + counts.onWrite(map); + } + EXPECT_TRUE(stream.top().isMember("accept")); + EXPECT_TRUE(stream.top().isMember("connect")); + EXPECT_TRUE(stream.top().isMember("close")); + EXPECT_TRUE(stream.top().isMember("reserved")); + EXPECT_TRUE(stream.top().isMember("total")); + counts.remove(reservedOutbound); + counts.remove(outbound); + + SlotImp closing(endpoint("65.0.0.9:10009"), endpoint("65.0.0.10:10010"), false, clock); + closing.state(Slot::State::Closing); + counts.add(closing); + EXPECT_EQ(counts.closingCount(), 1); + counts.remove(closing); + + Counts saturatedAttempts; + saturatedAttempts.onConfig(config); + std::vector> attempts; + for (int i = 0; i < Tuning::kMaxConnectAttempts; ++i) + { + attempts.push_back( + std::make_unique( + endpoint("65.1.0." + std::to_string(i + 1) + ":" + std::to_string(11000 + i)), + false, + clock)); + saturatedAttempts.add(*attempts.back()); + } + EXPECT_EQ(saturatedAttempts.attempts(), Tuning::kMaxConnectAttempts); + EXPECT_EQ(saturatedAttempts.attemptsNeeded(), 0u); + + Config disconnected; + disconnected.outPeers = 0; + counts.onConfig(disconnected); + EXPECT_TRUE(counts.isConnectedToNetwork()); +} + +TEST(PeerFinderHandouts, filters_redirect_slot_and_connect_targets) +{ + TestStopwatch clock; + auto const remote = endpoint("65.0.0.2:10002"); + auto const slot = std::make_shared(endpoint("65.0.0.1:10001"), remote, false, clock); + + RedirectHandouts redirects(slot); + EXPECT_EQ(redirects.slot(), slot); + EXPECT_TRUE(redirects.list().empty()); + EXPECT_FALSE(redirects.full()); + EXPECT_FALSE(redirects.tryInsert(Endpoint{endpoint("65.0.0.3:10003"), Tuning::kMaxHops + 1})); + EXPECT_FALSE(redirects.tryInsert(Endpoint{endpoint("65.0.0.3:10003"), 0})); + EXPECT_FALSE(redirects.tryInsert(Endpoint{remote.atPort(12000), 1})); + EXPECT_TRUE(redirects.tryInsert(Endpoint{endpoint("65.0.0.3:10003"), 1})); + EXPECT_FALSE(redirects.tryInsert(Endpoint{endpoint("65.0.0.3:12000"), 1})); + EXPECT_EQ(redirects.list().size(), 1u); + + SlotHandouts slotHandouts(slot); + EXPECT_EQ(slotHandouts.slot(), slot); + EXPECT_FALSE(slotHandouts.full()); + EXPECT_FALSE( + slotHandouts.tryInsert(Endpoint{endpoint("65.0.0.4:10004"), Tuning::kMaxHops + 1})); + EXPECT_FALSE(slotHandouts.tryInsert(Endpoint{remote.atPort(12001), 1})); + + auto const recent = endpoint("65.0.0.5:10005"); + slot->recent.insert(recent, 2); + EXPECT_FALSE(slotHandouts.tryInsert(Endpoint{recent, 2})); + EXPECT_TRUE(slotHandouts.tryInsert(Endpoint{endpoint("65.0.0.6:10006"), 2})); + EXPECT_FALSE(slotHandouts.tryInsert(Endpoint{endpoint("65.0.0.6:12000"), 2})); + slotHandouts.insert(Endpoint{endpoint("65.0.0.7:10007"), 1}); + EXPECT_EQ(slotHandouts.list().size(), 2u); + + ConnectHandouts::Squelches squelches(clock); + ConnectHandouts connects(2, squelches); + EXPECT_TRUE(connects.empty()); + EXPECT_TRUE(connects.tryInsert(endpoint("65.0.0.8:10008"))); + EXPECT_FALSE(connects.empty()); + EXPECT_FALSE(connects.tryInsert(endpoint("65.0.0.8:12000"))); + EXPECT_TRUE(connects.tryInsert(Endpoint{endpoint("65.0.0.9:10009"), 1})); + EXPECT_TRUE(connects.full()); + EXPECT_FALSE(connects.tryInsert(endpoint("65.0.0.10:10010"))); + EXPECT_EQ(connects.list().size(), 2u); + + ConnectHandouts squelched(1, squelches); + EXPECT_FALSE(squelched.tryInsert(endpoint("65.0.0.9:12000"))); +} + +TEST(PeerFinderHandouts, distributes_livecache_entries) +{ + TestStopwatch clock; + Livecache<> cache(clock, journal()); + cache.insert(Endpoint{endpoint("65.0.0.10:10010"), 1}); + cache.insert(Endpoint{endpoint("65.0.0.11:10011"), 2}); + + auto const slot1 = std::make_shared( + endpoint("65.0.0.1:10001"), endpoint("65.0.0.2:10002"), false, clock); + auto const slot2 = std::make_shared( + endpoint("65.0.0.3:10003"), endpoint("65.0.0.4:10004"), false, clock); + std::vector targets; + targets.emplace_back(slot1); + targets.emplace_back(slot2); + + handout(targets.begin(), targets.end(), cache.hops.begin(), cache.hops.end()); + + EXPECT_FALSE(targets.front().list().empty()); + EXPECT_FALSE(targets.back().list().empty()); + + for (std::uint32_t i = 0; i < Tuning::kNumberOfEndpoints; ++i) + targets.front().insert(Endpoint{endpoint("65.1.0." + std::to_string(i + 1) + ":12000"), 1}); + + handout(targets.begin(), targets.begin() + 1, cache.hops.begin(), cache.hops.end()); + EXPECT_TRUE(targets.front().full()); +} + +TEST_F(PeerFinderTest, preprocess_filters_invalid_duplicate_and_extra_self_endpoints) +{ + auto const local = endpoint("65.0.0.1:10001"); + auto const remote = endpoint("65.0.0.2:10002"); + auto const slot = std::make_shared(local, remote, false, clock_); + Endpoints endpoints{ + Endpoint{endpoint("65.0.0.3:10003"), Tuning::kMaxHops + 1}, + Endpoint{endpoint("0.0.0.0:2459"), 0}, + Endpoint{endpoint("0.0.0.0:2460"), 0}, + Endpoint{endpoint("10.0.0.1:10004"), 1}, + Endpoint{endpoint("65.0.0.5"), 1}, + Endpoint{endpoint("65.0.0.6:10006"), 1}, + Endpoint{endpoint("65.0.0.6:10006"), 2}}; + + logic_.preprocess(slot, endpoints); + + ASSERT_EQ(endpoints.size(), 2u); + EXPECT_EQ(endpoints.front().address, remote.atPort(2459)); + EXPECT_EQ(endpoints.front().hops, 1u); + EXPECT_EQ(endpoints.back().address, endpoint("65.0.0.6:10006")); + EXPECT_EQ(endpoints.back().hops, 2u); +} + +TEST_F(PeerFinderTest, on_endpoints_checks_neighbor_before_caching_it) +{ + Config config; + config.autoConnect = false; + config.listeningPort = 1024; + config.ipLimit = 2; + config.inPeers = 1; + logic_.config(config); + + auto const local = endpoint("65.0.0.1:10001"); + auto const remote = endpoint("55.104.0.2:1025"); + auto const [slot, result] = logic_.newInboundSlot(local, remote); + ASSERT_NE(slot, nullptr); + EXPECT_EQ(result, Result::Success); + PublicKey const publicKey(randomKeyPair(KeyType::Secp256k1).first); + ASSERT_EQ(logic_.activate(slot, publicKey, false), Result::Success); + + Endpoints const advertised{Endpoint{endpoint("0.0.0.0:2459"), 0}}; + logic_.onEndpoints(slot, advertised); + + ASSERT_EQ(checker_.asyncConnects.size(), 1u); + EXPECT_EQ(checker_.asyncConnects.front(), remote.atPort(2459)); + EXPECT_EQ(slot->listeningPort(), std::optional{2459}); + EXPECT_TRUE(slot->checked); + EXPECT_TRUE(slot->canAccept); + EXPECT_TRUE(logic_.livecache.empty()); + + clock_.advance(Tuning::kSecondsPerMessage); + logic_.onEndpoints(slot, advertised); + EXPECT_EQ(logic_.livecache.size(), 1u); + EXPECT_EQ(logic_.bootcache.size(), 1u); + + logic_.onEndpoints(slot, Endpoints{Endpoint{endpoint("65.0.0.9:10009"), 1}}); + EXPECT_EQ(logic_.livecache.size(), 1u); + + logic_.onClosed(slot); +} + +TEST_F(PeerFinderTest, on_endpoints_skips_failed_neighbor_connectivity_checks) +{ + Config config; + config.autoConnect = false; + config.listeningPort = 1024; + config.ipLimit = 2; + config.inPeers = 1; + logic_.config(config); + + checker_.nextError = boost::asio::error::host_unreachable; + auto const local = endpoint("65.0.0.1:10001"); + auto const remote = endpoint("55.104.0.3:1025"); + auto const [slot, result] = logic_.newInboundSlot(local, remote); + ASSERT_NE(slot, nullptr); + EXPECT_EQ(result, Result::Success); + PublicKey const publicKey(randomKeyPair(KeyType::Secp256k1).first); + ASSERT_EQ(logic_.activate(slot, publicKey, false), Result::Success); + + Endpoints const advertised{Endpoint{endpoint("0.0.0.0:2459"), 0}}; + logic_.onEndpoints(slot, advertised); + EXPECT_TRUE(slot->checked); + EXPECT_FALSE(slot->canAccept); + + clock_.advance(Tuning::kSecondsPerMessage); + logic_.onEndpoints(slot, advertised); + EXPECT_TRUE(logic_.livecache.empty()); + + logic_.onClosed(slot); +} + +TEST_F(PeerFinderTest, on_endpoints_waits_for_pending_connectivity_check) +{ + Config config; + config.autoConnect = false; + config.listeningPort = 1024; + config.ipLimit = 2; + config.inPeers = 1; + logic_.config(config); + + checker_.completeAsync = false; + auto const local = endpoint("65.0.0.1:10001"); + auto const remote = endpoint("55.104.0.4:1025"); + auto const [slot, result] = logic_.newInboundSlot(local, remote); + ASSERT_NE(slot, nullptr); + EXPECT_EQ(result, Result::Success); + PublicKey const publicKey(randomKeyPair(KeyType::Secp256k1).first); + ASSERT_EQ(logic_.activate(slot, publicKey, false), Result::Success); + + Endpoints const advertised{Endpoint{endpoint("0.0.0.0:2459"), 0}}; + logic_.onEndpoints(slot, advertised); + EXPECT_TRUE(slot->connectivityCheckInProgress); + + clock_.advance(Tuning::kSecondsPerMessage); + logic_.onEndpoints(slot, advertised); + EXPECT_EQ(checker_.asyncConnects.size(), 1u); + EXPECT_TRUE(logic_.livecache.empty()); + + checker_.completeAsync = true; + logic_.checkComplete(remote, remote.atPort(2459), boost::asio::error::operation_aborted); + slot->connectivityCheckInProgress = false; + logic_.onClosed(slot); +} + +TEST_F(PeerFinderTest, builds_endpoint_messages_and_redirects_from_livecache) +{ + Config config; + config.autoConnect = false; + config.wantIncoming = true; + config.listeningPort = 2459; + config.inPeers = 2; + config.outPeers = 2; + config.ipLimit = 2; + logic_.config(config); + + auto const remote = endpoint("55.104.0.5:1025"); + auto const live = endpoint("65.0.0.10:10010"); + logic_.livecache.insert(Endpoint{live, 1}); + + auto const [slot, result] = logic_.newOutboundSlot(remote); + ASSERT_NE(slot, nullptr); + EXPECT_EQ(result, Result::Success); + ASSERT_TRUE(logic_.onConnected(slot, endpoint("65.0.0.1:10001"))); + PublicKey const publicKey(randomKeyPair(KeyType::Secp256k1).first); + ASSERT_EQ(logic_.activate(slot, publicKey, false), Result::Success); + + auto const messages = logic_.buildEndpointsForPeers(); + ASSERT_EQ(messages.size(), 1u); + auto const& sent = messages.front().second; + EXPECT_TRUE(std::ranges::any_of(sent, [](Endpoint const& ep) { return ep.hops == 0; })); + EXPECT_TRUE( + std::ranges::any_of(sent, [&live](Endpoint const& ep) { return ep.address == live; })); + EXPECT_TRUE(logic_.buildEndpointsForPeers().empty()); + + auto const redirects = logic_.redirect(slot); + EXPECT_FALSE(redirects.empty()); + + logic_.onClosed(slot); +} + +TEST_F(PeerFinderTest, autoconnect_uses_livecache_then_bootcache) +{ + Config config; + config.autoConnect = true; + config.wantIncoming = false; + config.outPeers = 1; + config.inPeers = 0; + config.ipLimit = 1; + logic_.config(config); + + auto const live = endpoint("65.0.0.11:10011"); + logic_.livecache.insert(Endpoint{live, 1}); + auto const liveAddresses = logic_.autoconnect(); + ASSERT_EQ(liveAddresses.size(), 1u); + EXPECT_EQ(liveAddresses.front(), live); + + auto const boot = endpoint("65.0.0.12:10012"); + EXPECT_TRUE(logic_.bootcache.insertStatic(boot)); + auto const bootAddresses = logic_.autoconnect(); + ASSERT_EQ(bootAddresses.size(), 1u); + EXPECT_EQ(bootAddresses.front(), boot); +} + +TEST_F(PeerFinderTest, sources_redirects_status_and_validation_paths_are_exercised) +{ + auto const source = std::make_shared("static"); + source->resultsToFetch.addresses = {endpoint("65.0.0.13:10013")}; + logic_.addStaticSource(source); + EXPECT_EQ(source->fetchCount, 1); + EXPECT_EQ(logic_.bootcache.size(), 1u); + + auto const failing = std::make_shared("failing"); + failing->resultsToFetch.error = boost::asio::error::host_unreachable; + logic_.fetch(failing); + EXPECT_EQ(failing->fetchCount, 1); + + auto const dynamic = std::make_shared("dynamic"); + logic_.addSource(dynamic); + ASSERT_EQ(logic_.sources.size(), 1u); + EXPECT_EQ(logic_.sources.front(), dynamic); + + std::vector redirects{ + {boost::asio::ip::make_address("65.0.0.14"), 10014}, + {boost::asio::ip::make_address("65.0.0.15"), 10015}}; + logic_.onRedirects(redirects.begin(), redirects.end(), redirects.front()); + EXPECT_EQ(logic_.bootcache.size(), 3u); + + EXPECT_FALSE(logic_.isValidAddress(endpoint("0.0.0.0:10016"))); + EXPECT_FALSE(logic_.isValidAddress(endpoint("10.0.0.1:10017"))); + EXPECT_FALSE(logic_.isValidAddress(endpoint("65.0.0.16"))); + EXPECT_TRUE(logic_.isValidAddress(endpoint("65.0.0.16:10016"))); + + JsonPropertyStream stream; + { + beast::PropertyStream::Map map(stream); + logic_.onWrite(map); + } + EXPECT_TRUE(stream.top().isMember("peers")); + EXPECT_TRUE(stream.top().isMember("counts")); + EXPECT_TRUE(stream.top().isMember("config")); + EXPECT_TRUE(stream.top().isMember("livecache")); + EXPECT_TRUE(stream.top().isMember("bootcache")); + + DefaultCancelSource defaultCancel; + Source::Results results; + EXPECT_TRUE(results.addresses.empty()); + defaultCancel.cancel(); + defaultCancel.fetch(results, journal()); + + logic_.fetchSource = dynamic; + logic_.stop(); + EXPECT_TRUE(logic_.stopping); + EXPECT_EQ(dynamic->cancelCount, 1); + + auto const ignored = std::make_shared("ignored"); + logic_.fetch(ignored); + EXPECT_EQ(ignored->fetchCount, 0); + + logic_.checkComplete( + endpoint("65.0.0.18:10018"), endpoint("65.0.0.19:10019"), boost::system::error_code{}); +} + +TEST(PeerFinderBootcache, loads_unique_entries_and_clears_cache) +{ + CapturingStore store; + TestStopwatch clock; + auto const ep1 = endpoint("65.0.0.1:10001"); + auto const ep2 = endpoint("65.0.0.2:10002"); + store.entriesToLoad = {storeEntry(ep1, 3), storeEntry(ep2, -2), storeEntry(ep1, 4)}; + + Bootcache cache(store, clock, journal()); + cache.load(); + + EXPECT_FALSE(cache.empty()); + EXPECT_EQ(cache.size(), 2u); + EXPECT_EQ(*cache.begin(), ep1); + EXPECT_EQ(*cache.cbegin(), ep1); + EXPECT_NE(cache.begin(), cache.end()); + EXPECT_NE(cache.cbegin(), cache.cend()); + + cache.clear(); + EXPECT_TRUE(cache.empty()); + EXPECT_EQ(cache.begin(), cache.end()); +} + +TEST(PeerFinderBootcache, records_connection_outcomes_and_persists_pending_updates) +{ + CapturingStore store; + TestStopwatch clock; + auto const ep1 = endpoint("65.0.0.1:10001"); + auto const ep2 = endpoint("65.0.0.2:10002"); + auto const ep3 = endpoint("65.0.0.3:10003"); + auto const ep4 = endpoint("65.0.0.4:10004"); + + { + Bootcache cache(store, clock, journal()); + + EXPECT_TRUE(cache.insert(ep1)); + EXPECT_FALSE(cache.insert(ep1)); + + cache.onSuccess(ep1); + EXPECT_TRUE(cache.insertStatic(ep1)); + EXPECT_FALSE(cache.insertStatic(ep1)); + + EXPECT_TRUE(cache.insertStatic(ep2)); + cache.onSuccess(ep3); + cache.onFailure(ep3); + cache.onFailure(ep4); + + EXPECT_EQ(cache.size(), 4u); + + JsonPropertyStream stream; + { + beast::PropertyStream::Map map(stream); + cache.onWrite(map); + } + EXPECT_TRUE(stream.top().isMember("entries")); + EXPECT_EQ(stream.top()["entries"].size(), 4u); + } + + ASSERT_EQ(store.saves.size(), 1u); + auto const& saved = store.saves.front(); + ASSERT_EQ(saved.size(), 4u); + EXPECT_EQ(savedValence(saved, ep1), Bootcache::kStaticValence); + EXPECT_EQ(savedValence(saved, ep2), Bootcache::kStaticValence); + EXPECT_EQ(savedValence(saved, ep3), -1); + EXPECT_EQ(savedValence(saved, ep4), -1); +} + +TEST(PeerFinderBootcache, periodic_activity_saves_after_cooldown) +{ + using namespace std::chrono_literals; + + CapturingStore store; + TestStopwatch clock; + + { + Bootcache cache(store, clock, journal()); + EXPECT_TRUE(cache.insert(endpoint("65.0.0.1:10001"))); + + cache.periodicActivity(); + EXPECT_TRUE(store.saves.empty()); + + clock.advance(Tuning::kBootcacheCooldownTime + 1s); + cache.periodicActivity(); + ASSERT_EQ(store.saves.size(), 1u); + + cache.periodicActivity(); + EXPECT_EQ(store.saves.size(), 1u); + } + + EXPECT_EQ(store.saves.size(), 1u); +} + +TEST(PeerFinderBootcache, prunes_when_cache_exceeds_limit) +{ + CapturingStore store; + TestStopwatch clock; + Bootcache cache(store, clock, journal()); + + for (std::uint16_t i = 0; i <= Tuning::kBootcacheSize; ++i) + { + EXPECT_TRUE(cache.insert(endpoint( + "65.0." + std::to_string((i / 256) % 256) + "." + std::to_string(i % 256) + ":" + + std::to_string(10000 + i)))); + } + + EXPECT_LE(cache.size(), Tuning::kBootcacheSize); +} + +TEST(PeerFinderEndpoint, clamps_hops_to_overflow_bucket) +{ + auto const address = endpoint("65.0.0.1:10001"); + Endpoint const ep(address, Tuning::kMaxHops + 10); + + EXPECT_EQ(ep.address, address); + EXPECT_EQ(ep.hops, Tuning::kMaxHops + 1); +} + +TEST(PeerFinderSlotImp, tracks_state_and_recent_endpoints) +{ + using State = Slot::State; + using namespace std::chrono_literals; + + TestStopwatch clock; + auto const local = endpoint("65.0.0.1:10000"); + auto const remote = endpoint("65.0.0.2:10001"); + SlotImp inbound(local, remote, true, clock); + + EXPECT_TRUE(inbound.inbound()); + EXPECT_TRUE(inbound.fixed()); + EXPECT_FALSE(inbound.reserved()); + EXPECT_EQ(inbound.state(), State::Accept); + EXPECT_EQ(inbound.remoteEndpoint(), remote); + EXPECT_EQ(inbound.localEndpoint(), std::optional{local}); + EXPECT_FALSE(inbound.publicKey()); + EXPECT_FALSE(inbound.listeningPort()); + EXPECT_FALSE(inbound.checked); + EXPECT_FALSE(inbound.canAccept); + EXPECT_FALSE(inbound.connectivityCheckInProgress); + + auto const newLocal = endpoint("65.0.0.3:10002"); + auto const newRemote = endpoint("65.0.0.4:10003"); + PublicKey const publicKey(randomKeyPair(KeyType::Secp256k1).first); + + inbound.localEndpoint(newLocal); + inbound.remoteEndpoint(newRemote); + inbound.publicKey(publicKey); + inbound.reserved(true); + inbound.setListeningPort(2459); + + EXPECT_EQ(inbound.localEndpoint(), std::optional{newLocal}); + EXPECT_EQ(inbound.remoteEndpoint(), newRemote); + EXPECT_EQ(inbound.publicKey(), std::optional{publicKey}); + EXPECT_TRUE(inbound.reserved()); + EXPECT_EQ(inbound.listeningPort(), std::optional{2459}); + EXPECT_FALSE(inbound.prefix().empty()); + + inbound.state(State::Closing); + EXPECT_EQ(inbound.state(), State::Closing); + + SlotImp outbound(remote, false, clock); + EXPECT_FALSE(outbound.inbound()); + EXPECT_FALSE(outbound.fixed()); + EXPECT_EQ(outbound.state(), State::Connect); + EXPECT_TRUE(outbound.checked); + EXPECT_TRUE(outbound.canAccept); + + outbound.state(State::Connected); + outbound.activate(clock.now()); + EXPECT_EQ(outbound.state(), State::Active); + EXPECT_EQ(outbound.whenAcceptEndpoints, clock.now()); + + auto const recent = endpoint("65.0.0.5:10004"); + EXPECT_FALSE(outbound.recent.filter(recent, 2)); + + outbound.recent.insert(recent, 2); + EXPECT_TRUE(outbound.recent.filter(recent, 2)); + EXPECT_TRUE(outbound.recent.filter(recent, 3)); + EXPECT_FALSE(outbound.recent.filter(recent, 1)); + + outbound.recent.insert(recent, 4); + EXPECT_FALSE(outbound.recent.filter(recent, 1)); + + outbound.recent.insert(recent, 1); + EXPECT_TRUE(outbound.recent.filter(recent, 1)); + EXPECT_FALSE(outbound.recent.filter(recent, 0)); + + clock.advance(Tuning::kLiveCacheSecondsToLive + 1s); + outbound.expire(); + EXPECT_FALSE(outbound.recent.filter(recent, 1)); +} + +TEST(PeerFinderConfig, writes_property_stream_and_compares_verify_endpoints) +{ + Config config; + config.maxPeers = 42; + config.outPeers = 12; + config.inPeers = 30; + config.peerPrivate = false; + config.wantIncoming = true; + config.autoConnect = false; + config.listeningPort = 2459; + config.features = "feature"; + config.ipLimit = 4; + config.verifyEndpoints = false; + + JsonPropertyStream stream; + { + beast::PropertyStream::Map map(stream); + config.onWrite(map); + } + + auto const& json = stream.top(); + EXPECT_EQ(json["max_peers"].asUInt(), config.maxPeers); + EXPECT_EQ(json["out_peers"].asUInt(), config.outPeers); + EXPECT_TRUE(json.isMember("want_incoming")); + EXPECT_TRUE(json.isMember("auto_connect")); + EXPECT_EQ(json["port"].asUInt(), config.listeningPort); + EXPECT_EQ(json["features"].asString(), config.features); + EXPECT_EQ(json["ip_limit"].asInt(), config.ipLimit); + EXPECT_TRUE(json.isMember("verify_endpoints")); + + Config same = config; + EXPECT_EQ(config, same); + same.verifyEndpoints = true; + EXPECT_NE(config, same); +} + +TEST(PeerFinderConfig, validator_and_standalone_settings_disable_auto_connect) +{ + PeerLimitConfig const limits{.maxPeers = 50, .inPeers = {}, .outPeers = {}}; + + Config const config = Config::makeConfig(false, true, limits, 2459, true, 7, false); + + EXPECT_TRUE(config.peerPrivate); + EXPECT_FALSE(config.autoConnect); + EXPECT_FALSE(config.verifyEndpoints); + EXPECT_EQ(config.ipLimit, 7); +} + +TEST(PeerFinderConfig, calculates_outbound_peers_and_clamps_ip_limits) +{ + Config config; + config.maxPeers = 1; + EXPECT_EQ(config.calcOutPeers(), Tuning::kMinOutCount); + + config.maxPeers = 100; + EXPECT_EQ(config.calcOutPeers(), 15u); + + config.inPeers = 1; + config.ipLimit = 0; + config.applyTuning(); + EXPECT_EQ(config.ipLimit, 1); + + Config explicitLimit; + explicitLimit.inPeers = 8; + explicitLimit.ipLimit = 99; + explicitLimit.applyTuning(); + EXPECT_EQ(explicitLimit.ipLimit, 4); + + Config largeInbound; + largeInbound.inPeers = 200; + largeInbound.ipLimit = 0; + largeInbound.applyTuning(); + EXPECT_EQ(largeInbound.ipLimit, 7); +} + +TEST(PeerFinderConfig, applies_legacy_and_explicit_peer_limits) +{ + struct ConfigCase + { + std::string name; + std::optional maxPeers; + std::optional maxIn; + std::optional maxOut; + std::uint16_t port; + std::uint16_t expectedOut; + std::uint16_t expectedIn; + std::uint16_t expectedIpLimit; + }; + + std::vector const cases{ + {.name = "legacy no config", + .maxPeers = {}, + .maxIn = {}, + .maxOut = {}, + .port = 4000, + .expectedOut = 10, + .expectedIn = 11, + .expectedIpLimit = 2}, + {.name = "legacy max_peers 0", + .maxPeers = 0, + .maxIn = 100, + .maxOut = 10, + .port = 4000, + .expectedOut = 10, + .expectedIn = 11, + .expectedIpLimit = 2}, + {.name = "legacy max_peers 5", + .maxPeers = 5, + .maxIn = 100, + .maxOut = 10, + .port = 4000, + .expectedOut = 10, + .expectedIn = 0, + .expectedIpLimit = 1}, + {.name = "legacy max_peers 20", + .maxPeers = 20, + .maxIn = 100, + .maxOut = 10, + .port = 4000, + .expectedOut = 10, + .expectedIn = 10, + .expectedIpLimit = 2}, + {.name = "legacy max_peers 100", + .maxPeers = 100, + .maxIn = 100, + .maxOut = 10, + .port = 4000, + .expectedOut = 15, + .expectedIn = 85, + .expectedIpLimit = 6}, + {.name = "legacy max_peers 20, private", + .maxPeers = 20, + .maxIn = 100, + .maxOut = 10, + .port = 0, + .expectedOut = 20, + .expectedIn = 0, + .expectedIpLimit = 1}, + {.name = "new in 100/out 10", + .maxPeers = {}, + .maxIn = 100, + .maxOut = 10, + .port = 4000, + .expectedOut = 10, + .expectedIn = 100, + .expectedIpLimit = 6}, + {.name = "new in 0/out 10", + .maxPeers = {}, + .maxIn = 0, + .maxOut = 10, + .port = 4000, + .expectedOut = 10, + .expectedIn = 0, + .expectedIpLimit = 1}, + {.name = "new in 100/out 10, private", + .maxPeers = {}, + .maxIn = 100, + .maxOut = 10, + .port = 0, + .expectedOut = 10, + .expectedIn = 0, + .expectedIpLimit = 6}}; + + for (auto const& testCase : cases) + { + SCOPED_TRACE(testCase.name); + + PeerLimitConfig const limits{ + .maxPeers = testCase.maxPeers, .inPeers = testCase.maxIn, .outPeers = testCase.maxOut}; + + Config const config = + Config::makeConfig(false, false, limits, testCase.port, false, 0, true); + + Counts counts; + counts.onConfig(config); + EXPECT_EQ(counts.outMax(), testCase.expectedOut); + EXPECT_EQ(counts.inMax(), testCase.expectedIn); + EXPECT_EQ(config.ipLimit, testCase.expectedIpLimit); + + NiceMock store; + allowEmptyStore(store); + NiceMock checker; + TestStopwatch clock; + Logic> logic(clock, store, checker, journal()); + logic.config(config); + + EXPECT_EQ(logic.config(), config); + } +} + +TEST(PeerFinderConfig, rejects_incomplete_or_out_of_range_peer_limits) +{ + std::vector const configs{ + {.maxPeers = {}, .inPeers = 100, .outPeers = {}}, + {.maxPeers = {}, .inPeers = {}, .outPeers = 100}, + {.maxPeers = {}, .inPeers = 100, .outPeers = 5}, + {.maxPeers = {}, .inPeers = 1001, .outPeers = 10}, + {.maxPeers = {}, .inPeers = 10, .outPeers = 1001}}; + + for (auto const& limits : configs) + { + EXPECT_THROW( + Config::makeConfig(false, false, limits, 4000, false, 0, true), std::exception); + } +} + +} // namespace +} // namespace xrpl::PeerFinder diff --git a/src/xrpld/app/rdb/PeerFinder.h b/src/xrpld/app/rdb/PeerFinder.h index 4f186ff7e2..5d916000a3 100644 --- a/src/xrpld/app/rdb/PeerFinder.h +++ b/src/xrpld/app/rdb/PeerFinder.h @@ -1,9 +1,8 @@ #pragma once -#include - #include #include +#include #include #include diff --git a/src/xrpld/app/rdb/detail/PeerFinder.cpp b/src/xrpld/app/rdb/detail/PeerFinder.cpp index abdcd1c61a..72a275c7cd 100644 --- a/src/xrpld/app/rdb/detail/PeerFinder.cpp +++ b/src/xrpld/app/rdb/detail/PeerFinder.cpp @@ -1,12 +1,11 @@ #include -#include - #include #include #include #include #include +#include #include #include // IWYU pragma: keep diff --git a/src/xrpld/overlay/detail/ConnectAttempt.cpp b/src/xrpld/overlay/detail/ConnectAttempt.cpp index 064b4ecd3e..0f0b3242de 100644 --- a/src/xrpld/overlay/detail/ConnectAttempt.cpp +++ b/src/xrpld/overlay/detail/ConnectAttempt.cpp @@ -7,8 +7,6 @@ #include #include #include -#include -#include #include #include @@ -16,6 +14,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/src/xrpld/overlay/detail/ConnectAttempt.h b/src/xrpld/overlay/detail/ConnectAttempt.h index d7836e3c84..f9ba33571f 100644 --- a/src/xrpld/overlay/detail/ConnectAttempt.h +++ b/src/xrpld/overlay/detail/ConnectAttempt.h @@ -3,12 +3,12 @@ #include #include #include -#include #include #include #include #include +#include #include #include diff --git a/src/xrpld/overlay/detail/OverlayImpl.cpp b/src/xrpld/overlay/detail/OverlayImpl.cpp index 6a6a6edace..f9972548d1 100644 --- a/src/xrpld/overlay/detail/OverlayImpl.cpp +++ b/src/xrpld/overlay/detail/OverlayImpl.cpp @@ -10,8 +10,6 @@ #include #include #include -#include -#include #include #include #include @@ -39,6 +37,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -179,12 +180,13 @@ OverlayImpl::OverlayImpl( , journal_(app_.getJournal("Overlay")) , serverHandler_(serverHandler) , resourceManager_(resourceManager) + , store_(app_.getJournal("PeerFinder")) , peerFinder_( PeerFinder::makeManager( ioContext, stopwatch(), app_.getJournal("PeerFinder"), - config, + store_, collector)) , resolver_(resolver) , nextId_(1) @@ -201,6 +203,7 @@ OverlayImpl::OverlayImpl( return ret; }()) { + store_.open(config); beast::PropertyStream::Source::add(peerFinder_.get()); } @@ -505,7 +508,7 @@ OverlayImpl::remove(std::shared_ptr const& slot) void OverlayImpl::start() { - PeerFinder::Config const config = PeerFinder::Config::makeConfig( + PeerFinder::Config const config = PeerFinder::makeConfig( app_.config(), serverHandler_.setup().overlay.port(), app_.getValidationPublicKey().has_value(), diff --git a/src/xrpld/overlay/detail/OverlayImpl.h b/src/xrpld/overlay/detail/OverlayImpl.h index 092ac86a6d..cd2c7d630b 100644 --- a/src/xrpld/overlay/detail/OverlayImpl.h +++ b/src/xrpld/overlay/detail/OverlayImpl.h @@ -8,8 +8,7 @@ #include #include #include -#include -#include +#include #include #include @@ -24,6 +23,8 @@ #include #include #include +#include +#include #include #include #include @@ -109,6 +110,7 @@ private: beast::Journal const journal_; ServerHandler& serverHandler_; Resource::Manager& resourceManager_; + PeerFinder::StoreSqdb store_; std::unique_ptr peerFinder_; TrafficCount traffic_; hash_map, std::weak_ptr> peers_; diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index 8838970b5f..c720bdf30b 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -19,8 +19,6 @@ #include #include #include -#include -#include #include #include @@ -43,6 +41,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/src/xrpld/overlay/detail/PeerImp.h b/src/xrpld/overlay/detail/PeerImp.h index ea6eccd656..90f8a917f4 100644 --- a/src/xrpld/overlay/detail/PeerImp.h +++ b/src/xrpld/overlay/detail/PeerImp.h @@ -9,8 +9,6 @@ #include #include #include -#include -#include #include #include @@ -24,6 +22,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/src/xrpld/peerfinder/PeerfinderManager.h b/src/xrpld/peerfinder/PeerfinderManager.h index 0530343641..f96ea31943 100644 --- a/src/xrpld/peerfinder/PeerfinderManager.h +++ b/src/xrpld/peerfinder/PeerfinderManager.h @@ -1,368 +1,19 @@ #pragma once #include -#include -#include -#include -#include -#include -#include +#include -#include - -#include -#include #include -#include -#include -#include -#include -#include namespace xrpl::PeerFinder { -using clock_type = beast::AbstractClock; - -/** - * Represents a set of addresses. - */ -using IPAddresses = std::vector; - -//------------------------------------------------------------------------------ - -/** - * PeerFinder configuration settings. - */ -struct Config -{ - /** - * The largest number of public peer slots to allow. - * This includes both inbound and outbound, but does not include - * fixed peers. - */ - std::size_t maxPeers{Tuning::kDefaultMaxPeers}; - - /** - * The number of automatic outbound connections to maintain. - * Outbound connections are only maintained if autoConnect - * is `true`. - */ - 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{0}; - - /** - * `true` if we want our IP address kept private. - */ - bool peerPrivate = true; - - /** - * `true` if we want to accept incoming connections. - */ - bool wantIncoming{true}; - - /** - * `true` if we want to establish connections automatically - */ - bool autoConnect{true}; - - /** - * The listening port number. - */ - std::uint16_t listeningPort{0}; - - /** - * The set of features we advertise. - */ - std::string features; - - /** - * Limit how many incoming connections we allow per IP - */ - int ipLimit{0}; - - /** - * `true` if we want to verify endpoints in TMEndpoints messages - */ - bool verifyEndpoints = true; - - //-------------------------------------------------------------------------- - - /** - * Create a configuration with default values. - */ - Config(); - - /** - * Returns a suitable value for outPeers according to the rules. - */ - [[nodiscard]] std::size_t - calcOutPeers() const; - - /** - * Adjusts the values so they follow the business rules. - */ - void - applyTuning(); - - /** - * Write the configuration into a property stream - */ - void - onWrite(beast::PropertyStream::Map& map) const; - - /** - * 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 - * @param verifyEndpoints `true` if we want to verify endpoints in - * TMEndpoints messages - * @return PeerFinder::Config - */ - static Config - makeConfig( - xrpl::Config const& config, - std::uint16_t port, - bool validationPublicKey, - int ipLimit, - bool verifyEndpoints); - - friend bool - operator==(Config const& lhs, Config const& rhs) = default; -}; - -//------------------------------------------------------------------------------ - -/** - * Describes a connectable peer address along with some metadata. - */ -struct Endpoint -{ - Endpoint() = default; - - Endpoint(beast::IP::Endpoint ep, std::uint32_t hops); - - std::uint32_t hops = 0; - beast::IP::Endpoint address; -}; - -inline bool -operator<(Endpoint const& lhs, Endpoint const& rhs) -{ - return lhs.address < rhs.address; -} - -/** - * A set of Endpoint used for connecting. - */ -using Endpoints = std::vector; - -//------------------------------------------------------------------------------ - -/** - * Possible results from activating a slot. - */ -enum class Result { InboundDisabled, DuplicatePeer, IpLimitExceeded, Full, Success }; - -/** - * @brief Converts a `Result` enum value to its string representation. - * - * This function provides a human-readable string for a given `Result` enum, - * which is useful for logging, debugging, or displaying status messages. - * - * @param result The `Result` enum value to convert. - * @return A `std::string_view` representing the enum value. Returns "unknown" - * if the enum value is not explicitly handled. - * - * @note This function returns a `std::string_view` for performance. - * A `std::string` would need to allocate memory on the heap and copy the - * string literal into it every time the function is called. - */ -inline std::string_view -to_string(Result result) noexcept -{ - switch (result) - { - case Result::InboundDisabled: - return "inbound disabled"; - case Result::DuplicatePeer: - return "peer already connected"; - case Result::IpLimitExceeded: - return "ip limit exceeded"; - case Result::Full: - return "slots full"; - case Result::Success: - return "success"; - } - - return "unknown"; -} - -/** - * Maintains a set of IP addresses used for getting into the network. - */ -class Manager : public beast::PropertyStream::Source -{ -protected: - Manager() noexcept; - -public: - /** - * Destroy the object. - * Any pending source fetch operations are aborted. - * There may be some listener calls made before the - * destructor returns. - */ - ~Manager() override = default; - - /** - * Set the configuration for the manager. - * The new settings will be applied asynchronously. - * Thread safety: - * Can be called from any threads at any time. - */ - virtual void - setConfig(Config const& config) = 0; - - /** - * Transition to the started state, synchronously. - */ - virtual void - start() = 0; - - /** - * Transition to the stopped state, synchronously. - */ - virtual void - stop() = 0; - - /** - * Returns the configuration for the manager. - */ - virtual Config - config() = 0; - - /** - * Add a peer that should always be connected. - * This is useful for maintaining a private cluster of peers. - * The string is the name as specified in the configuration - * file, along with the set of corresponding IP addresses. - */ - virtual void - addFixedPeer(std::string_view name, std::vector const& addresses) = 0; - - /** - * Add a set of strings as fallback IP::Endpoint sources. - * @param name A label used for diagnostics. - */ - virtual void - addFallbackStrings(std::string const& name, std::vector const& strings) = 0; - - /** - * Add a URL as a fallback location to obtain IP::Endpoint sources. - * @param name A label used for diagnostics. - */ - /* VFALCO NOTE Unimplemented - virtual void addFallbackURL (std::string const& name, - std::string const& url) = 0; - */ - - //-------------------------------------------------------------------------- - - /** - * Create a new inbound slot with the specified remote endpoint. - * If nullptr is returned, then the slot could not be assigned. - * Usually this is because of a detected self-connection. - */ - virtual std::pair, Result> - newInboundSlot( - beast::IP::Endpoint const& localEndpoint, - beast::IP::Endpoint const& remoteEndpoint) = 0; - - /** - * Create a new outbound slot with the specified remote endpoint. - * If nullptr is returned, then the slot could not be assigned. - * Usually this is because of a duplicate connection. - */ - virtual std::pair, Result> - newOutboundSlot(beast::IP::Endpoint const& remoteEndpoint) = 0; - - /** - * Called when mtENDPOINTS is received. - */ - virtual void - onEndpoints(std::shared_ptr const& slot, Endpoints const& endpoints) = 0; - - /** - * Called when the slot is closed. - * This always happens when the socket is closed, unless the socket - * was canceled. - */ - virtual void - onClosed(std::shared_ptr const& slot) = 0; - - /** - * Called when an outbound connection is deemed to have failed - */ - virtual void - onFailure(std::shared_ptr const& slot) = 0; - - /** - * Called when we received redirect IPs from a busy peer. - */ - virtual void - onRedirects( - boost::asio::ip::tcp::endpoint const& remoteAddress, - std::vector const& eps) = 0; - - //-------------------------------------------------------------------------- - - /** - * Called when an outbound connection attempt succeeds. - * The local endpoint must be valid. If the caller receives an error - * when retrieving the local endpoint from the socket, it should - * proceed as if the connection attempt failed by calling on_closed - * instead of on_connected. - * @return `true` if the connection should be kept - */ - virtual bool - onConnected(std::shared_ptr const& slot, beast::IP::Endpoint const& localEndpoint) = 0; - - /** - * Request an active slot type. - */ - virtual Result - activate(std::shared_ptr const& slot, PublicKey const& key, bool reserved) = 0; - - /** - * Returns a set of endpoints suitable for redirection. - */ - virtual std::vector - redirect(std::shared_ptr const& slot) = 0; - - /** - * Return a set of addresses we should connect to. - */ - virtual std::vector - autoconnect() = 0; - - virtual std::vector, std::vector>> - buildEndpointsForPeers() = 0; - - /** - * Perform periodic activity. - * This should be called once per second. - */ - virtual void - oncePerSecond() = 0; -}; +Config +makeConfig( + xrpl::Config const& config, + std::uint16_t port, + bool validationPublicKey, + int ipLimit, + bool verifyEndpoints); } // namespace xrpl::PeerFinder diff --git a/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp b/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp index 5d276dc9c5..a893b969e0 100644 --- a/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp +++ b/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp @@ -1,124 +1,39 @@ #include #include -#include -#include +#include -#include -#include #include namespace xrpl::PeerFinder { -Config::Config() : outPeers(calcOutPeers()) - -{ -} - -std::size_t -Config::calcOutPeers() const -{ - return std::max( - ((maxPeers * Tuning::kOutPercent) + 50) / 100, std::size_t(Tuning::kMinOutCount)); -} - -void -Config::applyTuning() -{ - if (ipLimit == 0) - { - // Unless a limit is explicitly set, we allow between - // 2 and 5 connections from non RFC-1918 "private" - // IP addresses. - ipLimit = 2; - - if (inPeers > Tuning::kDefaultMaxPeers) - ipLimit += std::min(5, static_cast(inPeers / Tuning::kDefaultMaxPeers)); - } - - // We don't allow a single IP to consume all incoming slots, - // unless we only have one incoming slot available. - ipLimit = std::max(1, std::min(ipLimit, static_cast(inPeers / 2))); -} - -void -Config::onWrite(beast::PropertyStream::Map& map) const -{ - map["max_peers"] = maxPeers; - map["out_peers"] = outPeers; - map["want_incoming"] = wantIncoming; - map["auto_connect"] = autoConnect; - map["port"] = listeningPort; - map["features"] = features; - map["ip_limit"] = ipLimit; - map["verify_endpoints"] = verifyEndpoints; -} - Config -Config::makeConfig( +makeConfig( xrpl::Config const& cfg, std::uint16_t port, bool validationPublicKey, int ipLimit, bool verifyEndpoints) { - PeerFinder::Config config; - - config.peerPrivate = cfg.peerPrivate; - - // Servers with peer privacy don't want to allow incoming connections - config.wantIncoming = (!config.peerPrivate) && (port != 0); - + PeerLimitConfig limits; if ((cfg.peersOutMax == 0u) && (cfg.peersInMax == 0u)) { - if (cfg.peersMax != 0) - config.maxPeers = cfg.peersMax; - - config.maxPeers = std::max(config.maxPeers, Tuning::kMinOutCount); - 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; - } + limits.maxPeers = cfg.peersMax; } else { - config.outPeers = cfg.peersOutMax; - config.inPeers = cfg.peersInMax; - config.maxPeers = 0; + limits.inPeers = cfg.peersInMax; + limits.outPeers = cfg.peersOutMax; } - // 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.peerPrivate; - config.listeningPort = port; - config.features = ""; - config.ipLimit = ipLimit; - config.verifyEndpoints = verifyEndpoints; - - // Enforce business rules - config.applyTuning(); - - return config; + return Config::makeConfig( + cfg.peerPrivate, + cfg.standalone(), + limits, + port, + validationPublicKey, + ipLimit, + verifyEndpoints); } } // namespace xrpl::PeerFinder diff --git a/src/xrpld/peerfinder/detail/StoreSqdb.h b/src/xrpld/peerfinder/detail/StoreSqdb.h index b17d2fdc5b..b0973a42b3 100644 --- a/src/xrpld/peerfinder/detail/StoreSqdb.h +++ b/src/xrpld/peerfinder/detail/StoreSqdb.h @@ -1,11 +1,11 @@ #pragma once #include -#include #include #include #include +#include #include #include diff --git a/src/xrpld/peerfinder/detail/iosformat.h b/src/xrpld/peerfinder/detail/iosformat.h deleted file mode 100644 index 46c69ef602..0000000000 --- a/src/xrpld/peerfinder/detail/iosformat.h +++ /dev/null @@ -1,201 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -namespace beast { - -// A collection of handy stream manipulators and -// functions to produce nice looking log output. - -/** - * Left justifies a field at the specified width. - */ -struct Leftw -{ - explicit Leftw(int width) : width(width) - { - } - int const width; - template - friend std::basic_ios& - operator<<(std::basic_ios& ios, Leftw const& p) - { - ios.setf(std::ios_base::left, std::ios_base::adjustfield); - ios.width(p.width); - return ios; - } -}; - -/** - * Produce a section heading and fill the rest of the line with dashes. - */ -template -std::basic_string -heading(std::basic_string title, int width = 80, CharT fill = CharT('-')) -{ - title.reserve(width); - title.push_back(CharT(' ')); - title.resize(width, fill); - return title; -} - -/** - * Produce a dashed line separator, with a specified or default size. - */ -struct Divider -{ - using CharT = char; - explicit Divider(int width = 80, CharT fill = CharT('-')) : width(width), fill(fill) - { - } - int const width; - CharT const fill; - template - friend std::basic_ostream& - operator<<(std::basic_ostream& os, Divider const& d) - { - os << std::basic_string(d.width, d.fill); - return os; - } -}; - -/** - * Creates a padded field with an optional fill character. - */ -struct Fpad -{ - explicit Fpad(int width, int pad = 0, char fill = ' ') : width(width + pad), fill(fill) - { - } - int const width; - char const fill; - template - friend std::basic_ostream& - operator<<(std::basic_ostream& os, Fpad const& f) - { - os << std::basic_string(f.width, f.fill); - return os; - } -}; - -//------------------------------------------------------------------------------ - -namespace detail { - -template -std::string -to_string(T const& t) -{ - std::stringstream ss; - ss << t; - return ss.str(); -} - -} // namespace detail - -/** - * Justifies a field at the specified width. - */ -/** @{ */ -template < - class CharT, - class Traits = std::char_traits, - class Allocator = std::allocator> -class FieldT -{ -public: - using string_t = std::basic_string; - FieldT(string_t const& text, int width, int pad, bool right) - : text(text), width(width), pad(pad), right(right) - { - } - string_t const text; - int const width; - int const pad; - bool const right; - template - friend std::basic_ostream& - operator<<(std::basic_ostream& os, FieldT const& f) - { - std::size_t const length(f.text.length()); - if (f.right) - { - if (length < f.width) - os << std::basic_string(f.width - length, CharT2(' ')); - os << f.text; - } - else - { - os << f.text; - if (length < f.width) - os << std::basic_string(f.width - length, CharT2(' ')); - } - if (f.pad != 0) - os << string_t(f.pad, CharT(' ')); - return os; - } -}; - -template -FieldT -field( - std::basic_string const& text, - int width = 8, - int pad = 0, - bool right = false) -{ - return FieldT(text, width, pad, right); -} - -template -FieldT -field(CharT const* text, int width = 8, int pad = 0, bool right = false) -{ - return FieldT, std::allocator>( - std::basic_string, std::allocator>(text), - width, - pad, - right); -} - -template -FieldT -field(T const& t, int width = 8, int pad = 0, bool right = false) -{ - std::string const text(detail::to_string(t)); - return field(text, width, pad, right); -} - -template -FieldT -rField(std::basic_string const& text, int width = 8, int pad = 0) -{ - return FieldT(text, width, pad, true); -} - -template -FieldT -rField(CharT const* text, int width = 8, int pad = 0) -{ - return FieldT, std::allocator>( - std::basic_string, std::allocator>(text), - width, - pad, - true); -} - -template -FieldT -rField(T const& t, int width = 8, int pad = 0) -{ - std::string const text(detail::to_string(t)); - return field(text, width, pad, true); -} -/** @} */ - -} // namespace beast diff --git a/src/xrpld/peerfinder/make_Manager.h b/src/xrpld/peerfinder/make_Manager.h deleted file mode 100644 index 1c13d7a4ca..0000000000 --- a/src/xrpld/peerfinder/make_Manager.h +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include - -#include -#include -#include - -#include - -#include - -namespace xrpl::PeerFinder { - -/** - * Create a new Manager. - */ -std::unique_ptr -makeManager( - boost::asio::io_context& ioContext, - clock_type& clock, - beast::Journal journal, - BasicConfig const& config, - beast::insight::Collector::ptr const& collector); - -} // namespace xrpl::PeerFinder