From 42a7a4dd320f002457843b4418314f43a70c0c8f Mon Sep 17 00:00:00 2001 From: xiphon Date: Wed, 26 Feb 2020 12:37:28 +0000 Subject: [PATCH] daemon: auto public nodes - cache and prioritize most stable nodes --- src/rpc/CMakeLists.txt | 1 + src/rpc/bootstrap_daemon.cpp | 40 +++-- src/rpc/bootstrap_daemon.h | 34 +++- src/rpc/bootstrap_node_selector.cpp | 117 +++++++++++++ src/rpc/bootstrap_node_selector.h | 103 +++++++++++ src/rpc/core_rpc_server.cpp | 50 +++--- src/rpc/core_rpc_server.h | 2 +- tests/unit_tests/CMakeLists.txt | 1 + tests/unit_tests/bootstrap_node_selector.cpp | 172 +++++++++++++++++++ 9 files changed, 477 insertions(+), 43 deletions(-) create mode 100644 src/rpc/bootstrap_node_selector.cpp create mode 100644 src/rpc/bootstrap_node_selector.h create mode 100644 tests/unit_tests/bootstrap_node_selector.cpp diff --git a/src/rpc/CMakeLists.txt b/src/rpc/CMakeLists.txt index 65d88b57e..fe5e5a85b 100644 --- a/src/rpc/CMakeLists.txt +++ b/src/rpc/CMakeLists.txt @@ -35,6 +35,7 @@ set(rpc_base_sources set(rpc_sources bootstrap_daemon.cpp + bootstrap_node_selector.cpp core_rpc_server.cpp rpc_payment.cpp rpc_version_str.cpp diff --git a/src/rpc/bootstrap_daemon.cpp b/src/rpc/bootstrap_daemon.cpp index c97b2c95a..6a0833f19 100644 --- a/src/rpc/bootstrap_daemon.cpp +++ b/src/rpc/bootstrap_daemon.cpp @@ -2,6 +2,8 @@ #include +#include + #include "crypto/crypto.h" #include "cryptonote_core/cryptonote_core.h" #include "misc_log_ex.h" @@ -12,15 +14,22 @@ namespace cryptonote { - bootstrap_daemon::bootstrap_daemon(std::function()> get_next_public_node) - : m_get_next_public_node(get_next_public_node) + bootstrap_daemon::bootstrap_daemon( + std::function()> get_public_nodes, + bool rpc_payment_enabled) + : m_selector(new bootstrap_node::selector_auto(std::move(get_public_nodes))) + , m_rpc_payment_enabled(rpc_payment_enabled) { } - bootstrap_daemon::bootstrap_daemon(const std::string &address, const boost::optional &credentials) - : bootstrap_daemon(nullptr) + bootstrap_daemon::bootstrap_daemon( + const std::string &address, + boost::optional credentials, + bool rpc_payment_enabled) + : m_selector(nullptr) + , m_rpc_payment_enabled(rpc_payment_enabled) { - if (!set_server(address, credentials)) + if (!set_server(address, std::move(credentials))) { throw std::runtime_error("invalid bootstrap daemon address or credentials"); } @@ -54,11 +63,16 @@ namespace cryptonote return res.height; } - bool bootstrap_daemon::handle_result(bool success) + bool bootstrap_daemon::handle_result(bool success, const std::string &status) { - if (!success && m_get_next_public_node) + const bool failed = !success || (!m_rpc_payment_enabled && status == CORE_RPC_STATUS_PAYMENT_REQUIRED); + if (failed && m_selector) { + const std::string current_address = address(); m_http_client.disconnect(); + + const boost::unique_lock lock(m_selector_mutex); + m_selector->handle_result(current_address, !failed); } return success; @@ -79,14 +93,18 @@ namespace cryptonote bool bootstrap_daemon::switch_server_if_needed() { - if (!m_get_next_public_node || m_http_client.is_connected()) + if (m_http_client.is_connected() || !m_selector) { return true; } - const boost::optional address = m_get_next_public_node(); - if (address) { - return set_server(*address); + boost::optional node; + { + const boost::unique_lock lock(m_selector_mutex); + node = m_selector->next_node(); + } + if (node) { + return set_server(node->address, node->credentials); } return false; diff --git a/src/rpc/bootstrap_daemon.h b/src/rpc/bootstrap_daemon.h index 6276b1b21..bedc255b5 100644 --- a/src/rpc/bootstrap_daemon.h +++ b/src/rpc/bootstrap_daemon.h @@ -1,26 +1,34 @@ #pragma once #include -#include +#include #include +#include #include #include "net/http_client.h" #include "storages/http_abstract_invoke.h" +#include "bootstrap_node_selector.h" + namespace cryptonote { class bootstrap_daemon { public: - bootstrap_daemon(std::function()> get_next_public_node); - bootstrap_daemon(const std::string &address, const boost::optional &credentials); + bootstrap_daemon( + std::function()> get_public_nodes, + bool rpc_payment_enabled); + bootstrap_daemon( + const std::string &address, + boost::optional credentials, + bool rpc_payment_enabled); std::string address() const noexcept; boost::optional get_height(); - bool handle_result(bool success); + bool handle_result(bool success, const std::string &status); template bool invoke_http_json(const boost::string_ref uri, const t_request &out_struct, t_response &result_struct) @@ -30,7 +38,8 @@ namespace cryptonote return false; } - return handle_result(epee::net_utils::invoke_http_json(uri, out_struct, result_struct, m_http_client)); + const bool result = epee::net_utils::invoke_http_json(uri, out_struct, result_struct, m_http_client); + return handle_result(result, result_struct.status); } template @@ -41,7 +50,8 @@ namespace cryptonote return false; } - return handle_result(epee::net_utils::invoke_http_bin(uri, out_struct, result_struct, m_http_client)); + const bool result = epee::net_utils::invoke_http_bin(uri, out_struct, result_struct, m_http_client); + return handle_result(result, result_struct.status); } template @@ -52,7 +62,13 @@ namespace cryptonote return false; } - return handle_result(epee::net_utils::invoke_http_json_rpc("/json_rpc", std::string(command_name.begin(), command_name.end()), out_struct, result_struct, m_http_client)); + const bool result = epee::net_utils::invoke_http_json_rpc( + "/json_rpc", + std::string(command_name.begin(), command_name.end()), + out_struct, + result_struct, + m_http_client); + return handle_result(result, result_struct.status); } private: @@ -61,7 +77,9 @@ namespace cryptonote private: epee::net_utils::http::http_simple_client m_http_client; - std::function()> m_get_next_public_node; + const bool m_rpc_payment_enabled; + const std::unique_ptr m_selector; + boost::mutex m_selector_mutex; }; } diff --git a/src/rpc/bootstrap_node_selector.cpp b/src/rpc/bootstrap_node_selector.cpp new file mode 100644 index 000000000..34845060e --- /dev/null +++ b/src/rpc/bootstrap_node_selector.cpp @@ -0,0 +1,117 @@ +// Copyright (c) 2020, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "bootstrap_node_selector.h" + +#include "crypto/crypto.h" + +namespace cryptonote +{ +namespace bootstrap_node +{ + + void selector_auto::node::handle_result(bool success) + { + if (!success) + { + fails = std::min(std::numeric_limits::max() - 2, fails) + 2; + } + else + { + fails = std::max(std::numeric_limits::min() + 2, fails) - 2; + } + } + + void selector_auto::handle_result(const std::string &address, bool success) + { + auto &nodes_by_address = m_nodes.get(); + const auto it = nodes_by_address.find(address); + if (it != nodes_by_address.end()) + { + nodes_by_address.modify(it, [success](node &entry) { + entry.handle_result(success); + }); + } + } + + boost::optional selector_auto::next_node() + { + if (!has_at_least_one_good_node()) + { + append_new_nodes(); + } + + if (m_nodes.empty()) + { + return {}; + } + + auto node = m_nodes.get().begin(); + const size_t count = std::distance(node, m_nodes.get().upper_bound(node->fails)); + std::advance(node, crypto::rand_idx(count)); + + return {{node->address, {}}}; + } + + bool selector_auto::has_at_least_one_good_node() const + { + return !m_nodes.empty() && m_nodes.get().begin()->fails == 0; + } + + void selector_auto::append_new_nodes() + { + bool updated = false; + + for (const auto &node : m_get_nodes()) + { + const auto &address = node.first; + const auto &white = node.second; + const size_t initial_score = white ? 0 : 1; + updated |= m_nodes.get().insert({address, initial_score}).second; + } + + if (updated) + { + truncate(); + } + } + + void selector_auto::truncate() + { + const size_t total = m_nodes.size(); + if (total > m_max_nodes) + { + auto &nodes_by_fails = m_nodes.get(); + auto from = nodes_by_fails.rbegin(); + std::advance(from, total - m_max_nodes); + nodes_by_fails.erase(from.base(), nodes_by_fails.end()); + } + } + +} +} diff --git a/src/rpc/bootstrap_node_selector.h b/src/rpc/bootstrap_node_selector.h new file mode 100644 index 000000000..fc993719b --- /dev/null +++ b/src/rpc/bootstrap_node_selector.h @@ -0,0 +1,103 @@ +// Copyright (c) 2020, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "net/http_client.h" + +namespace cryptonote +{ +namespace bootstrap_node +{ + + struct node_info + { + std::string address; + boost::optional credentials; + }; + + struct selector + { + virtual void handle_result(const std::string &address, bool success) = 0; + virtual boost::optional next_node() = 0; + }; + + class selector_auto : public selector + { + public: + selector_auto(std::function()> get_nodes, size_t max_nodes = 1000) + : m_get_nodes(std::move(get_nodes)) + , m_max_nodes(max_nodes) + {} + + void handle_result(const std::string &address, bool success) final; + boost::optional next_node() final; + + private: + bool has_at_least_one_good_node() const; + void append_new_nodes(); + void truncate(); + + private: + struct node + { + std::string address; + size_t fails; + + void handle_result(bool success); + }; + + struct by_address {}; + struct by_fails {}; + + typedef boost::multi_index_container< + node, + boost::multi_index::indexed_by< + boost::multi_index::ordered_unique, boost::multi_index::member>, + boost::multi_index::ordered_non_unique, boost::multi_index::member> + > + > nodes_list; + + const std::function()> m_get_nodes; + const size_t m_max_nodes; + nodes_list m_nodes; + }; + +} +} diff --git a/src/rpc/core_rpc_server.cpp b/src/rpc/core_rpc_server.cpp index e92ae7c08..eaded5726 100644 --- a/src/rpc/core_rpc_server.cpp +++ b/src/rpc/core_rpc_server.cpp @@ -170,7 +170,7 @@ namespace cryptonote return set_bootstrap_daemon(address, credentials); } //------------------------------------------------------------------------------------------------------------------------------ - boost::optional core_rpc_server::get_random_public_node() + std::map core_rpc_server::get_public_nodes(uint32_t credits_per_hash_threshold/* = 0*/) { COMMAND_RPC_GET_PUBLIC_NODES::request request; COMMAND_RPC_GET_PUBLIC_NODES::response response; @@ -179,47 +179,51 @@ namespace cryptonote request.white = true; if (!on_get_public_nodes(request, response) || response.status != CORE_RPC_STATUS_OK) { - return boost::none; + return {}; } - const auto get_random_node_address = [](const std::vector& public_nodes) -> std::string { - const auto& random_node = public_nodes[crypto::rand_idx(public_nodes.size())]; - const auto address = random_node.host + ":" + std::to_string(random_node.rpc_port); - return address; + std::map result; + + const auto append = [&result, &credits_per_hash_threshold](const std::vector &nodes, bool white) { + for (const auto &node : nodes) + { + const bool rpc_payment_enabled = credits_per_hash_threshold > 0; + const bool node_rpc_payment_enabled = node.rpc_credits_per_hash > 0; + if (!node_rpc_payment_enabled || + (rpc_payment_enabled && node.rpc_credits_per_hash >= credits_per_hash_threshold)) + { + result.insert(std::make_pair(node.host + ":" + std::to_string(node.rpc_port), white)); + } + } }; - if (!response.white.empty()) - { - return get_random_node_address(response.white); - } + append(response.white, true); + append(response.gray, false); - MDEBUG("No white public node found, checking gray peers"); - - if (!response.gray.empty()) - { - return get_random_node_address(response.gray); - } - - MERROR("Failed to find any suitable public node"); - - return boost::none; + return result; } //------------------------------------------------------------------------------------------------------------------------------ bool core_rpc_server::set_bootstrap_daemon(const std::string &address, const boost::optional &credentials) { boost::unique_lock lock(m_bootstrap_daemon_mutex); + constexpr const uint32_t credits_per_hash_threshold = 0; + constexpr const bool rpc_payment_enabled = credits_per_hash_threshold != 0; + if (address.empty()) { m_bootstrap_daemon.reset(nullptr); } else if (address == "auto") { - m_bootstrap_daemon.reset(new bootstrap_daemon([this]{ return get_random_public_node(); })); + auto get_nodes = [this, credits_per_hash_threshold]() { + return get_public_nodes(credits_per_hash_threshold); + }; + m_bootstrap_daemon.reset(new bootstrap_daemon(std::move(get_nodes), rpc_payment_enabled)); } else { - m_bootstrap_daemon.reset(new bootstrap_daemon(address, credentials)); + m_bootstrap_daemon.reset(new bootstrap_daemon(address, credentials, rpc_payment_enabled)); } m_should_use_bootstrap_daemon = m_bootstrap_daemon.get() != nullptr; @@ -1931,7 +1935,7 @@ namespace cryptonote if (*bootstrap_daemon_height < target_height) { MINFO("Bootstrap daemon is out of sync"); - return m_bootstrap_daemon->handle_result(false); + return m_bootstrap_daemon->handle_result(false, {}); } uint64_t top_height = m_core.get_current_blockchain_height(); diff --git a/src/rpc/core_rpc_server.h b/src/rpc/core_rpc_server.h index 23c611470..218e92ca8 100644 --- a/src/rpc/core_rpc_server.h +++ b/src/rpc/core_rpc_server.h @@ -265,7 +265,7 @@ private: //utils uint64_t get_block_reward(const block& blk); bool fill_block_header_response(const block& blk, bool orphan_status, uint64_t height, const crypto::hash& hash, block_header_response& response, bool fill_pow_hash); - boost::optional get_random_public_node(); + std::map get_public_nodes(uint32_t credits_per_hash_threshold = 0); bool set_bootstrap_daemon(const std::string &address, const std::string &username_password); bool set_bootstrap_daemon(const std::string &address, const boost::optional &credentials); enum invoke_http_mode { JON, BIN, JON_RPC }; diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index cda25dfc9..13ac48f1f 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -34,6 +34,7 @@ set(unit_tests_sources blockchain_db.cpp block_queue.cpp block_reward.cpp + bootstrap_node_selector.cpp bulletproofs.cpp canonical_amounts.cpp chacha.cpp diff --git a/tests/unit_tests/bootstrap_node_selector.cpp b/tests/unit_tests/bootstrap_node_selector.cpp new file mode 100644 index 000000000..c609d1223 --- /dev/null +++ b/tests/unit_tests/bootstrap_node_selector.cpp @@ -0,0 +1,172 @@ +// Copyright (c) 2020, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include + +#include "rpc/bootstrap_node_selector.h" + +class bootstrap_node_selector : public ::testing::Test +{ +protected: + void SetUp() override + { + nodes.insert(white_nodes.begin(), white_nodes.end()); + nodes.insert(gray_nodes.begin(), gray_nodes.end()); + } + + const std::map white_nodes = { + { + "white_node_1:18089", true + }, + { + "white_node_2:18081", true + } + }; + const std::map gray_nodes = { + { + "gray_node_1:18081", false + }, + { + "gray_node_2:18089", false + } + }; + + std::map nodes; +}; + +TEST_F(bootstrap_node_selector, selector_auto_empty) +{ + cryptonote::bootstrap_node::selector_auto selector([]() { + return std::map(); + }); + + EXPECT_FALSE(selector.next_node()); +} + +TEST_F(bootstrap_node_selector, selector_auto_no_credentials) +{ + cryptonote::bootstrap_node::selector_auto selector([this]() { + return nodes; + }); + + for (size_t fails = 0; fails < nodes.size(); ++fails) + { + const auto current = selector.next_node(); + EXPECT_FALSE(current->credentials); + + selector.handle_result(current->address, false); + } +} + +TEST_F(bootstrap_node_selector, selector_auto_success) +{ + cryptonote::bootstrap_node::selector_auto selector([this]() { + return nodes; + }); + + auto current = selector.next_node(); + for (size_t fails = 0; fails < nodes.size(); ++fails) + { + selector.handle_result(current->address, true); + + current = selector.next_node(); + EXPECT_TRUE(white_nodes.count(current->address) > 0); + } +} + +TEST_F(bootstrap_node_selector, selector_auto_failure) +{ + cryptonote::bootstrap_node::selector_auto selector([this]() { + return nodes; + }); + + auto current = selector.next_node(); + for (size_t fails = 0; fails < nodes.size(); ++fails) + { + const auto previous = current; + + selector.handle_result(current->address, false); + + current = selector.next_node(); + EXPECT_NE(current->address, previous->address); + } +} + +TEST_F(bootstrap_node_selector, selector_auto_white_nodes_first) +{ + cryptonote::bootstrap_node::selector_auto selector([this]() { + return nodes; + }); + + for (size_t iterations = 0; iterations < 2; ++iterations) + { + for (size_t fails = 0; fails < white_nodes.size(); ++fails) + { + const auto current = selector.next_node(); + EXPECT_TRUE(white_nodes.count(current->address) > 0); + + selector.handle_result(current->address, false); + } + + for (size_t fails = 0; fails < gray_nodes.size(); ++fails) + { + const auto current = selector.next_node(); + EXPECT_TRUE(gray_nodes.count(current->address) > 0); + + selector.handle_result(current->address, false); + } + } +} + +TEST_F(bootstrap_node_selector, selector_auto_max_nodes) +{ + const size_t max_nodes = nodes.size() / 2; + + bool populated_once = false; + cryptonote::bootstrap_node::selector_auto selector([this, &populated_once]() { + if (!populated_once) + { + populated_once = true; + return nodes; + } + + return std::map(); + }, max_nodes); + + std::set unique_nodes; + + for (size_t fails = 0; fails < nodes.size(); ++fails) + { + const auto current = selector.next_node(); + unique_nodes.insert(current->address); + + selector.handle_result(current->address, false); + } + + EXPECT_EQ(unique_nodes.size(), max_nodes); +}