#include #include #include #include #include #include #include #include namespace xrpl { namespace 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 async_connect(beast::IP::Endpoint const& ep, Handler&& handler) { boost::system::error_code ec; handler(ep, ep, ec); } }; void test_backoff1() { 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::from_string("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.new_outbound_slot(list.front()); BEAST_EXPECT( logic.onConnected(slot, beast::IP::Endpoint::from_string("65.0.0.2:5"))); logic.on_closed(slot); ++n; } clock.advance(std::chrono::seconds(1)); logic.once_per_second(); } // Less than 20 attempts BEAST_EXPECT(n < 20); } // with activate void test_backoff2() { 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::from_string("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.new_outbound_slot(list.front()); if (!BEAST_EXPECT( logic.onConnected(slot, beast::IP::Endpoint::from_string("65.0.0.2:5")))) return; if (!BEAST_EXPECT(logic.activate(slot, pk, false) == PeerFinder::Result::success)) return; logic.on_closed(slot); ++n; } clock.advance(std::chrono::seconds(1)); logic.once_per_second(); } // 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 test_duplicateOutIn() { 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::from_string("65.0.0.1:5"); auto const [slot1, r] = logic.new_outbound_slot(remote); BEAST_EXPECT(slot1 != nullptr); BEAST_EXPECT(r == Result::success); BEAST_EXPECT(logic.connectedAddresses_.count(remote.address()) == 1); auto const local = beast::IP::Endpoint::from_string("65.0.0.2:1024"); auto const [slot2, r2] = logic.new_inbound_slot(local, remote); BEAST_EXPECT(logic.connectedAddresses_.count(remote.address()) == 1); BEAST_EXPECT(r2 == Result::duplicatePeer); if (!BEAST_EXPECT(slot2 == nullptr)) logic.on_closed(slot2); logic.on_closed(slot1); } // test establishing outgoing slot for an already existing incoming slot void test_duplicateInOut() { 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::from_string("65.0.0.1:5"); auto const local = beast::IP::Endpoint::from_string("65.0.0.2:1024"); auto const [slot1, r] = logic.new_inbound_slot(local, remote); BEAST_EXPECT(slot1 != nullptr); BEAST_EXPECT(r == Result::success); BEAST_EXPECT(logic.connectedAddresses_.count(remote.address()) == 1); auto const [slot2, r2] = logic.new_outbound_slot(remote); BEAST_EXPECT(r2 == Result::duplicatePeer); BEAST_EXPECT(logic.connectedAddresses_.count(remote.address()) == 1); if (!BEAST_EXPECT(slot2 == nullptr)) logic.on_closed(slot2); logic.on_closed(slot1); } void test_peerLimitExceeded() { 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::from_string("65.0.0.2:1024"); auto const [slot, r] = logic.new_inbound_slot(local, beast::IP::Endpoint::from_string("55.104.0.2:1025")); BEAST_EXPECT(slot != nullptr); BEAST_EXPECT(r == Result::success); auto const [slot1, r1] = logic.new_inbound_slot(local, beast::IP::Endpoint::from_string("55.104.0.2:1026")); BEAST_EXPECT(slot1 != nullptr); BEAST_EXPECT(r1 == Result::success); auto const [slot2, r2] = logic.new_inbound_slot(local, beast::IP::Endpoint::from_string("55.104.0.2:1027")); BEAST_EXPECT(r2 == Result::ipLimitExceeded); if (!BEAST_EXPECT(slot2 == nullptr)) logic.on_closed(slot2); logic.on_closed(slot1); logic.on_closed(slot); } void test_activate_duplicate_peer() { 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::from_string("65.0.0.2:1024"); PublicKey const pk1(randomKeyPair(KeyType::secp256k1).first); auto const [slot, rSlot] = logic.new_outbound_slot(beast::IP::Endpoint::from_string("55.104.0.2:1025")); BEAST_EXPECT(slot != nullptr); BEAST_EXPECT(rSlot == Result::success); auto const [slot2, r2Slot] = logic.new_outbound_slot(beast::IP::Endpoint::from_string("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.on_closed(slot); // accept the same key for a new slot after removing the old slot BEAST_EXPECT(logic.activate(slot2, pk1, false) == Result::success); logic.on_closed(slot2); } void test_activate_inbound_disabled() { 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::from_string("65.0.0.2:1024"); auto const [slot, rSlot] = logic.new_inbound_slot(local, beast::IP::Endpoint::from_string("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.new_inbound_slot(local, beast::IP::Endpoint::from_string("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.on_closed(slot2); logic.on_closed(slot); } void test_addFixedPeer_no_port() { testcase("test addFixedPeer no port"); TestStore store; TestChecker checker; TestStopwatch clock; Logic logic(clock, store, checker, journal_); try { logic.addFixedPeer("test", beast::IP::Endpoint::from_string("65.0.0.2")); fail("invalid endpoint successfully added"); } catch (std::runtime_error const& e) { pass(); } } void test_onConnected_self_connection() { testcase("test onConnected self connection"); TestStore store; TestChecker checker; TestStopwatch clock; Logic logic(clock, store, checker, journal_); auto const local = beast::IP::Endpoint::from_string("65.0.0.2:1234"); auto const [slot, r] = logic.new_outbound_slot(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.on_closed(slot); } void test_config() { // if peers_max is configured then peers_in_max and peers_out_max // are ignored auto run = [&](std::string const& test, std::optional maxPeers, std::optional maxIn, std::optional maxOut, std::uint16_t port, std::uint16_t expectOut, std::uint16_t expectIn, std::uint16_t expectIpLimit) { 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.PEERS_MAX == max && c.PEERS_IN_MAX == 0 && c.PEERS_OUT_MAX == 0) || (c.PEERS_IN_MAX == *maxIn && c.PEERS_OUT_MAX == *maxOut)); Config config = Config::makeConfig(c, port, false, 0); Counts counts; counts.onConfig(config); BEAST_EXPECT( counts.out_max() == expectOut && counts.in_max() == 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 test_invalid_config() { testcase("invalid config"); auto run = [&](std::string const& toLoad) { xrpl::Config c; try { c.loadFromString(toLoad); fail(); } catch (...) { pass(); } }; run(R"rippleConfig( [peers_in_max] 100 )rippleConfig"); run(R"rippleConfig( [peers_out_max] 100 )rippleConfig"); run(R"rippleConfig( [peers_in_max] 100 [peers_out_max] 5 )rippleConfig"); run(R"rippleConfig( [peers_in_max] 1001 [peers_out_max] 10 )rippleConfig"); run(R"rippleConfig( [peers_in_max] 10 [peers_out_max] 1001 )rippleConfig"); } void run() override { test_backoff1(); test_backoff2(); test_duplicateOutIn(); test_duplicateInOut(); test_config(); test_invalid_config(); test_peerLimitExceeded(); test_activate_duplicate_peer(); test_activate_inbound_disabled(); test_addFixedPeer_no_port(); test_onConnected_self_connection(); } }; BEAST_DEFINE_TESTSUITE(PeerFinder, peerfinder, xrpl); } // namespace PeerFinder } // namespace xrpl