From 7ea35a001239c5193267cb5e1d1ba304d33659e3 Mon Sep 17 00:00:00 2001 From: everoddandeven Date: Sun, 4 Jan 2026 16:39:04 +0100 Subject: [PATCH] Fix daemon rpc and add more tests * Implement set_peer_ban/set_peer_bans * Implement get_block* * Add more daemon rpc tests --- src/cpp/common/py_monero_common.cpp | 7 + src/cpp/common/py_monero_common.h | 1 + src/cpp/daemon/py_monero_daemon.cpp | 118 +++++++++++-- src/cpp/daemon/py_monero_daemon.h | 20 ++- src/cpp/daemon/py_monero_daemon_model.cpp | 87 ++++++++-- src/cpp/daemon/py_monero_daemon_model.h | 20 ++- src/cpp/py_monero.cpp | 15 +- src/python/monero_daemon.pyi | 16 +- tests/test_monero_daemon_rpc.py | 200 +++++++++++++++++++++- tests/utils/monero_test_utils.py | 55 +++++- 10 files changed, 475 insertions(+), 64 deletions(-) diff --git a/src/cpp/common/py_monero_common.cpp b/src/cpp/common/py_monero_common.cpp index c701728..af04fd0 100644 --- a/src/cpp/common/py_monero_common.cpp +++ b/src/cpp/common/py_monero_common.cpp @@ -251,6 +251,13 @@ void PyMoneroUtils::binary_blocks_to_json(const std::string &bin, std::string &j monero_utils::binary_blocks_to_json(bin, json); } +void PyMoneroUtils::binary_blocks_to_property_tree(const std::string &bin, boost::property_tree::ptree &node) { + std::string response_json; + monero_utils::binary_blocks_to_json(bin, response_json); + std::istringstream iss = response_json.empty() ? std::istringstream() : std::istringstream(response_json); + boost::property_tree::read_json(iss, node); +} + bool PyMoneroUtils::is_valid_language(const std::string& language) { return monero_utils::is_valid_language(language); } diff --git a/src/cpp/common/py_monero_common.h b/src/cpp/common/py_monero_common.h index 40e850f..404a9d2 100644 --- a/src/cpp/common/py_monero_common.h +++ b/src/cpp/common/py_monero_common.h @@ -128,6 +128,7 @@ class PyMoneroUtils { static py::dict binary_to_dict(const std::string& bin); static std::string binary_to_json(const std::string &bin); static void binary_blocks_to_json(const std::string &bin, std::string &json); + static void binary_blocks_to_property_tree(const std::string &bin, boost::property_tree::ptree &node); static bool is_valid_language(const std::string& language); static std::vector> get_blocks_from_txs(std::vector> txs); static std::vector> get_blocks_from_transfers(std::vector> transfers); diff --git a/src/cpp/daemon/py_monero_daemon.cpp b/src/cpp/daemon/py_monero_daemon.cpp index e2de4de..9abaae9 100644 --- a/src/cpp/daemon/py_monero_daemon.cpp +++ b/src/cpp/daemon/py_monero_daemon.cpp @@ -1,5 +1,7 @@ #include "py_monero_daemon.h" +static const uint64_t MAX_REQ_SIZE = 3000000; +static const uint64_t NUM_HEADERS_PER_REQ = 750; void PyMoneroDaemonDefault::add_listener(const std::shared_ptr &listener) { boost::lock_guard lock(m_listeners_mutex); @@ -64,6 +66,13 @@ void PyMoneroDaemonDefault::submit_block(const std::string& block_blob) { return submit_blocks(block_blobs); } +void PyMoneroDaemonDefault::set_peer_ban(const std::shared_ptr& ban) { + if (ban == nullptr) throw std::runtime_error("Ban is none"); + std::vector> bans; + bans.push_back(ban); + set_peer_bans(bans); +} + PyMoneroDaemonPoller::~PyMoneroDaemonPoller() { set_is_polling(false); } @@ -275,6 +284,23 @@ std::shared_ptr PyMoneroDaemonRpc::get_block_by_hash(const return block; } +std::shared_ptr PyMoneroDaemonRpc::get_block_header_by_height_cached(uint64_t height, uint64_t max_height) { + // get header from cache + auto found = m_cached_headers.find(height); + if (found != m_cached_headers.end()) return found->second; + + // fetch and cache headers if not in cache + uint64_t end_height = std::min(max_height, height + NUM_HEADERS_PER_REQ - 1); + auto headers = get_block_headers_by_range(height, end_height); + + for(const auto& header : headers) { + m_cached_headers[header->m_height.get()] = header; + } + + return m_cached_headers[height]; +} + + std::vector> PyMoneroDaemonRpc::get_blocks_by_hash(const std::vector& block_hashes, uint64_t start_height, bool prune) { throw std::runtime_error("PyMoneroDaemonRpc::get_blocks_by_hash(): not implemented"); } @@ -292,25 +318,91 @@ std::shared_ptr PyMoneroDaemonRpc::get_block_by_height(uin return block; } -std::vector> PyMoneroDaemonRpc::get_blocks_by_height(std::vector heights) { - // TODO Binary Request - throw std::runtime_error("PyMoneroDaemonRpc::get_blocks_by_height(): not implemented"); +std::vector> PyMoneroDaemonRpc::get_blocks_by_height(const std::vector& heights) { + // fetch blocks in binary + PyMoneroGetBlocksByHeightRequest request(heights); + auto response = m_rpc->send_binary_request(request); + if (response->m_binary == boost::none) throw std::runtime_error("Invalid Monero Binary response"); + boost::property_tree::ptree node; + PyMoneroUtils::binary_blocks_to_property_tree(response->m_binary.get(), node); + check_response_status(node); + std::vector> blocks; + PyMoneroBlock::from_property_tree(node, heights, blocks); + return blocks; } -std::vector> PyMoneroDaemonRpc::get_blocks_by_range(std::optional start_height, std::optional end_height) { - if (!start_height.has_value()) { +std::vector> PyMoneroDaemonRpc::get_blocks_by_range(boost::optional start_height, boost::optional end_height) { + if (start_height == boost::none) { start_height = 0; } - if (!end_height.has_value()) { + if (end_height == boost::none) { end_height = get_height() - 1; } - + std::vector heights; - for (uint64_t height = start_height.value(); height <= end_height.value(); height++) heights.push_back(height); + for (uint64_t height = start_height.get(); height <= end_height.get(); height++) heights.push_back(height); return get_blocks_by_height(heights); } +std::vector> PyMoneroDaemonRpc::get_blocks_by_range_chunked(boost::optional start_height, boost::optional end_height, boost::optional max_chunk_size) { + if (start_height == boost::none) start_height = 0; + if (end_height == boost::none) end_height = get_height() - 1; + uint64_t from_height = start_height.get(); + bool from_zero = from_height == 0; + uint64_t last_height = (!from_zero) ? from_height - 1 : from_height; + std::vector> blocks; + while (last_height < end_height) { + uint64_t height_to_get = last_height + 1; + if (from_zero) { + height_to_get = 0; + from_zero = false; + } + auto max_blocks = get_max_blocks(height_to_get, end_height, max_chunk_size); + blocks.insert(blocks.end(), max_blocks.begin(), max_blocks.end()); + last_height = blocks[blocks.size() - 1]->m_height.get(); + } + return blocks; +} + +std::vector> PyMoneroDaemonRpc::get_max_blocks(boost::optional start_height, boost::optional max_height, boost::optional chunk_size) { + if (start_height == boost::none) start_height = 0; + if (max_height == boost::none) max_height = get_height() - 1; + if (chunk_size == boost::none) chunk_size = MAX_REQ_SIZE; + + // determine end height to fetch + uint64_t req_size = 0; + uint64_t from_height = start_height.get(); + bool from_zero = from_height == 0; + uint64_t end_height = (!from_zero) ? from_height - 1 : 0; + + while (req_size < chunk_size && end_height < max_height) { + // get header of next block + uint64_t height_to_get = end_height + 1; + if (from_zero) { + height_to_get = 0; + from_zero = false; + } + auto header = get_block_header_by_height_cached(height_to_get, max_height.get()); + uint64_t header_size = header->m_size.get(); + // block cannot be bigger than max request size + if (header_size > chunk_size) throw std::runtime_error("Block exceeds maximum request size: " + std::to_string(header_size)); + + // done iterating if fetching block would exceed max request size + if (req_size + header_size > chunk_size) break; + + // otherwise block is included + req_size += header_size; + end_height++; + } + + if (end_height >= start_height) { + return get_blocks_by_range(start_height, end_height); + } + + return std::vector>(); +} + std::vector PyMoneroDaemonRpc::get_block_hashes(std::vector block_hashes, uint64_t start_height) { throw std::runtime_error("PyMoneroDaemonRpc::get_block_hashes(): not implemented"); } @@ -320,7 +412,13 @@ std::vector> PyMoneroDaemonRpc::get_txs(const auto params = std::make_shared(tx_hashes, prune); PyMoneroPathRequest request("get_transactions", params); std::shared_ptr response = m_rpc->send_path_request(request); - check_response_status(response); + try { check_response_status(response); } + catch (const std::exception& ex) { + if (std::string(ex.what()).find("Failed to parse hex representation of transaction hash") != std::string::npos) { + throw std::runtime_error("Invalid transaction hash"); + } + throw; + } auto res = response->m_response.get(); std::vector> txs; PyMoneroTx::from_property_tree(res, txs); @@ -624,7 +722,7 @@ std::vector> PyMoneroDaemonRpc::get_peer_bans() { return bans; } -void PyMoneroDaemonRpc::set_peer_bans(std::vector> bans) { +void PyMoneroDaemonRpc::set_peer_bans(const std::vector>& bans) { auto params = std::make_shared(bans); PyMoneroJsonRequest request("set_bans", params); std::shared_ptr response = m_rpc->send_json_request(request); diff --git a/src/cpp/daemon/py_monero_daemon.h b/src/cpp/daemon/py_monero_daemon.h index 3e277ab..eec1a7f 100644 --- a/src/cpp/daemon/py_monero_daemon.h +++ b/src/cpp/daemon/py_monero_daemon.h @@ -55,10 +55,9 @@ class PyMoneroDaemon { virtual std::shared_ptr get_block_by_hash(const std::string& hash) { throw std::runtime_error("PyMoneroDaemon: not implemented"); } virtual std::vector> get_blocks_by_hash(const std::vector& block_hashes, uint64_t start_height, bool prune) { throw std::runtime_error("PyMoneroDaemon: not implemented"); } virtual std::shared_ptr get_block_by_height(uint64_t height) { throw std::runtime_error("PyMoneroDaemon: not implemented"); } - virtual std::vector> get_blocks_by_height(std::vector heights) { throw std::runtime_error("PyMoneroDaemon: not implemented"); } - virtual std::vector> get_blocks_by_range(std::optional start_height, std::optional end_height) { throw std::runtime_error("PyMoneroDaemon: not implemented"); } - virtual std::vector> get_blocks_by_range_chunked(uint64_t start_height, uint64_t end_height) { throw std::runtime_error("PyMoneroDaemon: not implemented"); } - virtual std::vector> get_blocks_by_range_chunked(uint64_t start_height, uint64_t end_height, uint64_t max_chunk_size) { throw std::runtime_error("PyMoneroDaemon: not implemented"); } + virtual std::vector> get_blocks_by_height(const std::vector& heights) { throw std::runtime_error("PyMoneroDaemon: not implemented"); } + virtual std::vector> get_blocks_by_range(boost::optional start_height, boost::optional end_height) { throw std::runtime_error("PyMoneroDaemon: not implemented"); } + virtual std::vector> get_blocks_by_range_chunked(boost::optional start_height, boost::optional end_height, boost::optional max_chunk_size) { throw std::runtime_error("PyMoneroDaemon: not implemented"); } virtual std::vector get_block_hashes(std::vector block_hashes, uint64_t start_height) { throw std::runtime_error("PyMoneroDaemon: not implemented"); } virtual std::optional> get_tx(const std::string& tx_hash, bool prune = false) { throw std::runtime_error("PyMoneroDaemon: not implemented"); } virtual std::vector> get_txs(const std::vector& tx_hashes, bool prune = false) { throw std::runtime_error("PyMoneroDaemon: not implemented"); } @@ -98,7 +97,7 @@ class PyMoneroDaemon { virtual void set_outgoing_peer_limit(int limit) { throw std::runtime_error("PyMoneroDaemon: not implemented"); } virtual void set_incoming_peer_limit(int limit) { throw std::runtime_error("PyMoneroDaemon: not implemented"); } virtual std::vector> get_peer_bans() { throw std::runtime_error("PyMoneroDaemon: not implemented"); } - virtual void set_peer_bans(std::vector> bans) { throw std::runtime_error("PyMoneroDaemon: not implemented"); } + virtual void set_peer_bans(const std::vector>& bans) { throw std::runtime_error("PyMoneroDaemon: not implemented"); } virtual void set_peer_ban(const std::shared_ptr& ban) { throw std::runtime_error("PyMoneroDaemon: not implemented"); } virtual void start_mining(const std::string &address, int num_threads, bool is_background, bool ignore_battery) { throw std::runtime_error("PyMoneroDaemon: not implemented"); } virtual void stop_mining() { throw std::runtime_error("PyMoneroDaemon: not implemented"); } @@ -125,6 +124,7 @@ class PyMoneroDaemonDefault : public PyMoneroDaemon { PyMoneroKeyImageSpentStatus get_key_image_spent_status(std::string& key_image) override; std::optional get_tx_hex(const std::string& tx_hash, bool prune = false); void submit_block(const std::string& block_blob) override; + void set_peer_ban(const std::shared_ptr& ban) override; protected: mutable boost::recursive_mutex m_listeners_mutex; @@ -190,8 +190,9 @@ class PyMoneroDaemonRpc : public PyMoneroDaemonDefault { std::shared_ptr get_block_by_hash(const std::string& hash) override; std::vector> get_blocks_by_hash(const std::vector& block_hashes, uint64_t start_height, bool prune) override; std::shared_ptr get_block_by_height(uint64_t height) override; - std::vector> get_blocks_by_height(std::vector heights) override; - std::vector> get_blocks_by_range(std::optional start_height, std::optional end_height) override; + std::vector> get_blocks_by_height(const std::vector& heights) override; + std::vector> get_blocks_by_range(boost::optional start_height, boost::optional end_height) override; + std::vector> get_blocks_by_range_chunked(boost::optional start_height, boost::optional end_height, boost::optional max_chunk_size) override; std::vector get_block_hashes(std::vector block_hashes, uint64_t start_height) override; std::vector> get_txs(const std::vector& tx_hashes, bool prune = false) override; std::vector get_tx_hexes(const std::vector& tx_hashes, bool prune = false) override; @@ -224,7 +225,7 @@ class PyMoneroDaemonRpc : public PyMoneroDaemonDefault { void set_outgoing_peer_limit(int limit) override; void set_incoming_peer_limit(int limit) override; std::vector> get_peer_bans() override; - void set_peer_bans(std::vector> bans) override; + void set_peer_bans(const std::vector>& bans) override; void start_mining(const std::string &address, int num_threads, bool is_background, bool ignore_battery) override; void stop_mining() override; std::shared_ptr get_mining_status() override; @@ -241,7 +242,10 @@ class PyMoneroDaemonRpc : public PyMoneroDaemonDefault { protected: std::shared_ptr m_rpc; std::shared_ptr m_poller; + std::unordered_map> m_cached_headers; + std::vector> get_max_blocks(boost::optional start_height, boost::optional max_height, boost::optional chunk_size); + std::shared_ptr get_block_header_by_height_cached(uint64_t height, uint64_t max_height); std::shared_ptr get_bandwidth_limits(); std::shared_ptr set_bandwidth_limits(int up, int down); void refresh_listening() override; diff --git a/src/cpp/daemon/py_monero_daemon_model.cpp b/src/cpp/daemon/py_monero_daemon_model.cpp index 0fb8647..c296d43 100644 --- a/src/cpp/daemon/py_monero_daemon_model.cpp +++ b/src/cpp/daemon/py_monero_daemon_model.cpp @@ -77,14 +77,7 @@ std::shared_ptr PyMoneroPathResponse::deserialize(const st } std::shared_ptr PyMoneroBinaryResponse::deserialize(const std::string& response_binary) { - // deserialize json to property node - std::string response_json; - monero_utils::binary_to_json(response_binary, response_json); - std::istringstream iss = response_json.empty() ? std::istringstream() : std::istringstream(response_json); - boost::property_tree::ptree node; - boost::property_tree::read_json(iss, node); auto response = std::make_shared(); - response->m_response = node; response->m_binary = response_binary; return response; } @@ -292,7 +285,7 @@ void PyMoneroBlockHeader::from_property_tree(const boost::property_tree::ptree& else if (key == std::string("miner_tx_hash")) header->m_miner_tx_hash = it->second.data(); else if (key == std::string("num_txes")) header->m_num_txs = it->second.get_value(); else if (key == std::string("orphan_status")) header->m_orphan_status = it->second.get_value(); - else if (key == std::string("prev_hash")) header->m_prev_hash = it->second.data(); + else if (key == std::string("prev_hash") || key == std::string("prev_id")) header->m_prev_hash = it->second.data(); else if (key == std::string("reward")) header->m_reward = it->second.get_value(); else if (key == std::string("pow_hash")) { std::string pow_hash = it->second.data(); @@ -349,6 +342,71 @@ void PyMoneroBlock::from_property_tree(const boost::property_tree::ptree& node, } } +void PyMoneroBlock::from_property_tree(const boost::property_tree::ptree& node, const std::vector& heights, std::vector>& blocks) { + const auto& rpc_blocks = node.get_child("blocks"); + const auto& rpc_txs = node.get_child("txs"); + + if (rpc_blocks.size() != rpc_txs.size()) { + throw std::runtime_error("blocks and txs size mismatch"); + } + + auto it_block = rpc_blocks.begin(); + auto it_txs = rpc_txs.begin(); + size_t idx = 0; + + for (; it_block != rpc_blocks.end(); ++it_block, ++it_txs, ++idx) { + // build block + auto block = std::make_shared(); + boost::property_tree::ptree block_n; + std::istringstream block_iis = std::istringstream(it_block->second.get_value()); + boost::property_tree::read_json(block_iis, block_n); + PyMoneroBlock::from_property_tree(block_n, block); + block->m_height = heights.at(idx); + blocks.push_back(block); + + std::vector tx_hashes; + if (auto hashes = it_block->second.get_child_optional("tx_hashes")) { + for (const auto& h : *hashes) { + tx_hashes.push_back(h.second.get_value()); + } + } + + // build transactions + std::vector> txs; + size_t tx_idx = 0; + + for (const auto& tx_node : it_txs->second) { + auto tx = std::make_shared(); + tx->m_hash = tx_hashes.at(tx_idx++); + tx->m_is_confirmed = true; + tx->m_in_tx_pool = false; + tx->m_is_miner_tx = false; + tx->m_relay = true; + tx->m_is_relayed = true; + tx->m_is_failed = false; + tx->m_is_double_spend_seen = false; + boost::property_tree::ptree tx_n; + std::istringstream tx_iis = std::istringstream(tx_node.second.get_value()); + boost::property_tree::read_json(tx_iis, tx_n); + PyMoneroTx::from_property_tree(tx_n, tx); + txs.push_back(tx); + } + + // merge into one block + block->m_txs.clear(); + for (auto& tx : txs) { + if (tx->m_block != boost::none) { + block->merge(block, tx->m_block.get()); + } + else { + tx->m_block = block; + block->m_txs.push_back(tx); + } + } + } +} + + void PyMoneroOutput::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& output) { for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { std::string key = it->first; @@ -639,7 +697,7 @@ void PyMoneroBan::from_property_tree(const boost::property_tree::ptree& node, st auto node2 = it->second; for (auto it2 = node2.begin(); it2 != node2.end(); ++it2) { auto ban = std::make_shared(); - PyMoneroBan::from_property_tree(node2, ban); + PyMoneroBan::from_property_tree(it2->second, ban); bans.push_back(ban); } } @@ -684,8 +742,8 @@ std::vector PyMoneroTxHashes::from_property_tree(const boost::prope void PyMoneroMinerTxSum::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& sum) { for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { std::string key = it->first; - if (key == std::string("emission_sum")) sum->m_emission_sum = it->second.get_value(); - else if (key == std::string("fee_sum")) sum->m_fee_sum = it->second.get_value(); + if (key == std::string("emission_amount")) sum->m_emission_sum = it->second.get_value(); + else if (key == std::string("fee_amount")) sum->m_fee_sum = it->second.get_value(); } } @@ -1111,6 +1169,12 @@ rapidjson::Value PyMoneroGetBlockTemplateParams::to_rapidjson_val(rapidjson::Doc return root; } +rapidjson::Value PyMoneroGetBlocksByHeightRequest::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { + rapidjson::Value root(rapidjson::kObjectType); + if (!m_heights.empty()) root.AddMember("heights", monero_utils::to_rapidjson_val(allocator, m_heights), allocator); + return root; +} + rapidjson::Value PyMoneroBan::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { rapidjson::Value root(rapidjson::kObjectType); rapidjson::Value value_str(rapidjson::kStringType); @@ -1132,7 +1196,6 @@ rapidjson::Value PyMoneroSubmitTxParams::to_rapidjson_val(rapidjson::Document::A rapidjson::Value PyMoneroRelayTxParams::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { rapidjson::Value root(rapidjson::kObjectType); - rapidjson::Value value_str(rapidjson::kStringType); if (!m_tx_hashes.empty()) root.AddMember("txids", monero_utils::to_rapidjson_val(allocator, m_tx_hashes), allocator); return root; } diff --git a/src/cpp/daemon/py_monero_daemon_model.h b/src/cpp/daemon/py_monero_daemon_model.h index 5fdc09b..1832b9a 100644 --- a/src/cpp/daemon/py_monero_daemon_model.h +++ b/src/cpp/daemon/py_monero_daemon_model.h @@ -24,6 +24,7 @@ class PyMoneroBlockHeader : public monero::monero_block_header { class PyMoneroBlock : public PyMoneroBlockHeader { public: static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& block); + static void from_property_tree(const boost::property_tree::ptree& node, const std::vector& heights, std::vector>& blocks); }; class PyMoneroOutput : public monero::monero_output { @@ -307,6 +308,18 @@ class PyMoneroGetBlockTemplateParams : public PyMoneroJsonRequestParams { rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override; }; +class PyMoneroGetBlocksByHeightRequest : public PyMoneroBinaryRequest { +public: + std::vector m_heights; + + PyMoneroGetBlocksByHeightRequest(const std::vector& heights) { + m_method = "get_blocks_by_height.bin"; + m_heights = heights; + } + + rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override; +}; + class PyMoneroJsonResponse { public: boost::optional m_jsonrpc; @@ -927,12 +940,13 @@ class PyMoneroRpcConnection : public monero_rpc_connection { std::string body = request.to_binary_val(); const epee::net_utils::http::http_response_info* response = invoke_post(uri, body, timeout); - int result = response->m_response_code; - if (result != 200) throw std::runtime_error("HTTP error: code " + std::to_string(result)); - return PyMoneroBinaryResponse::deserialize(response->m_body); + auto res = std::make_shared(); + res->m_binary = response->m_body; + + return res; } // exposed python methods diff --git a/src/cpp/py_monero.cpp b/src/cpp/py_monero.cpp index b3836ec..bb763a5 100644 --- a/src/cpp/py_monero.cpp +++ b/src/cpp/py_monero.cpp @@ -1244,18 +1244,15 @@ PYBIND11_MODULE(monero, m) { .def("get_block_by_height", [](PyMoneroDaemon& self, uint64_t height) { MONERO_CATCH_AND_RETHROW(self.get_block_by_height(height)); }, py::arg("height")) - .def("get_blocks_by_height", [](PyMoneroDaemon& self, std::vector heights) { + .def("get_blocks_by_height", [](PyMoneroDaemon& self, const std::vector& heights) { MONERO_CATCH_AND_RETHROW(self.get_blocks_by_height(heights)); }, py::arg("heights")) - .def("get_blocks_by_range", [](PyMoneroDaemon& self, uint64_t start_height, uint64_t end_height) { + .def("get_blocks_by_range", [](PyMoneroDaemon& self, boost::optional start_height, boost::optional end_height) { MONERO_CATCH_AND_RETHROW(self.get_blocks_by_range(start_height, end_height)); }, py::arg("start_height"), py::arg("end_height")) - .def("get_blocks_by_range_chunked", [](PyMoneroDaemon& self, uint64_t start_height, uint64_t end_height) { - MONERO_CATCH_AND_RETHROW(self.get_blocks_by_range_chunked(start_height, end_height)); - }, py::arg("start_height"), py::arg("end_height")) - .def("get_blocks_by_range_chunked", [](PyMoneroDaemon& self, uint64_t start_height, uint64_t end_height, uint64_t max_chunk_size) { + .def("get_blocks_by_range_chunked", [](PyMoneroDaemon& self, boost::optional start_height, boost::optional end_height, boost::optional max_chunk_size) { MONERO_CATCH_AND_RETHROW(self.get_blocks_by_range_chunked(start_height, end_height, max_chunk_size)); - }, py::arg("start_height"), py::arg("end_height"), py::arg("max_chunk_size")) + }, py::arg("start_height"), py::arg("end_height"), py::arg("max_chunk_size") = py::none()) .def("get_block_hashes", [](PyMoneroDaemon& self, const std::vector& block_hashes, uint64_t start_height) { MONERO_CATCH_AND_RETHROW(self.get_block_hashes(block_hashes, start_height)); }, py::arg("block_hashes"), py::arg("start_height")) @@ -1373,10 +1370,10 @@ PYBIND11_MODULE(monero, m) { .def("get_peer_bans", [](PyMoneroDaemon& self) { MONERO_CATCH_AND_RETHROW(self.get_peer_bans()); }) - .def("set_peer_bans", [](PyMoneroDaemon& self, std::vector> bans) { + .def("set_peer_bans", [](PyMoneroDaemon& self, const std::vector>& bans) { MONERO_CATCH_AND_RETHROW(self.set_peer_bans(bans)); }, py::arg("bans")) - .def("set_peer_ban", [](PyMoneroDaemon& self, std::shared_ptr ban) { + .def("set_peer_ban", [](PyMoneroDaemon& self, const std::shared_ptr& ban) { MONERO_CATCH_AND_RETHROW(self.set_peer_ban(ban)); }, py::arg("ban")) .def("start_mining", [](PyMoneroDaemon& self, std::string& address, uint64_t num_threads, bool is_background, bool ignore_battery) { diff --git a/src/python/monero_daemon.pyi b/src/python/monero_daemon.pyi index d88c14e..60d071a 100644 --- a/src/python/monero_daemon.pyi +++ b/src/python/monero_daemon.pyi @@ -194,7 +194,7 @@ class MoneroDaemon: :return list[MoneroBlock]: blocks at the given heights """ ... - def get_blocks_by_range(self, start_height: int, end_height: int) -> list[MoneroBlock]: + def get_blocks_by_range(self, start_height: typing.Optional[int], end_height: typing.Optional[int]) -> list[MoneroBlock]: """ Get blocks in the given height range. @@ -203,19 +203,7 @@ class MoneroDaemon: :return list[MoneroBlock]: blocks in the given height range """ ... - @typing.overload - def get_blocks_by_range_chunked(self, start_height: int, end_height: int) -> list[MoneroBlock]: - """ - Get blocks in the given height range as chunked requests so that each request is - not too big. - - :param int start_height: is the start height lower bound inclusive (optional) - :param int end_height: is the end height upper bound inclusive (optional) - :return list[MoneroBlock]: blocks in the given height range - """ - ... - @typing.overload - def get_blocks_by_range_chunked(self, start_height: int, end_height: int, max_chunk_size: int) -> list[MoneroBlock]: + def get_blocks_by_range_chunked(self, start_height: typing.Optional[int], end_height: typing.Optional[int], max_chunk_size: typing.Optional[int] = None) -> list[MoneroBlock]: """ Get blocks in the given height range as chunked requests so that each request is not too big. diff --git a/tests/test_monero_daemon_rpc.py b/tests/test_monero_daemon_rpc.py index 69c2ce0..6bdcc07 100644 --- a/tests/test_monero_daemon_rpc.py +++ b/tests/test_monero_daemon_rpc.py @@ -8,7 +8,7 @@ MoneroDaemonUpdateCheckResult, MoneroDaemonUpdateDownloadResult, MoneroDaemonListener, MoneroPeer, MoneroDaemonInfo, MoneroDaemonSyncInfo, MoneroHardForkInfo, MoneroAltChain, MoneroTx, MoneroSubmitTxResult, - MoneroTxPoolStats + MoneroTxPoolStats, MoneroBan, MoneroTxConfig, MoneroDestination ) from utils import MoneroTestUtils as Utils, TestContext, BinaryBlockContext, MiningUtils @@ -212,6 +212,134 @@ def test_get_blocks_by_height_binary(self): Utils.assert_true(tx_found, "No transactions found to test") + # Can get blocks by range in a single request + @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") + def test_get_blocks_by_range(self): + # get height range + num_blocks = 100 + num_blocks_ago = 102 + Utils.assert_true(num_blocks > 0) + Utils.assert_true(num_blocks_ago >= num_blocks) + height = self._daemon.get_height() + Utils.assert_true(height - num_blocks_ago + num_blocks - 1 < height) + start_height = height - num_blocks_ago + end_height = height - num_blocks_ago + num_blocks - 1 + + # test known start and end heights + Utils.test_get_blocks_range(self._daemon, start_height, end_height, height, False, self.BINARY_BLOCK_CTX) + + # test unspecified start + Utils.test_get_blocks_range(self._daemon, None, num_blocks - 1, height, False, self.BINARY_BLOCK_CTX) + + # test unspecified end + Utils.test_get_blocks_range(self._daemon, height - num_blocks - 1, None, height, False, self.BINARY_BLOCK_CTX) + + # Can get blocks by range using chunked requests + @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") + def test_get_blocks_by_range_chunked(self): + # get long height range + num_blocks = min(self._daemon.get_height() - 2, 1440) # test up to ~2 days of blocks + Utils.assert_true(num_blocks > 0) + height = self._daemon.get_height() + Utils.assert_true(height - num_blocks - 1 < height) + start_height = height - num_blocks + end_height = height - 1 + + # test known start and end heights + Utils.test_get_blocks_range(self._daemon, start_height, end_height, height, True, self.BINARY_BLOCK_CTX) + + # test unspecified start + Utils.test_get_blocks_range(self._daemon, None, num_blocks - 1, height, True, self.BINARY_BLOCK_CTX) + + # test unspecified end + Utils.test_get_blocks_range(self._daemon, end_height - num_blocks - 1, None, height, True, self.BINARY_BLOCK_CTX) + + # Can get block hashes (binary) + #@pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") + @pytest.mark.skip(reason="Binary request not implemented") + def test_get_block_ids_binary(self) -> None: + # get_hashes.bin + raise NotImplementedError("Binary request not implemented") + + # Can get a transaction by hash and without pruning + @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") + def test_get_tx_by_hash(self) -> None: + # fetch tx hashses to test + tx_hashes = Utils.get_confirmed_tx_hashes(self._daemon) + + # context for creating txs + ctx = TestContext() + ctx.is_pruned = False + ctx.is_confirmed = True + ctx.from_get_tx_pool = False + + # fetch each tx by hash without pruning + for tx_hash in tx_hashes: + tx = self._daemon.get_tx(tx_hash) + Utils.test_tx(tx, ctx) + + # fetch each tx by hash with pruning + for tx_hash in tx_hashes: + tx = self._daemon.get_tx(tx_hash, True) + ctx.is_pruned = True + Utils.test_tx(tx, ctx) + + # fetch invalid hash + try: + self._daemon.get_tx("invalid tx hash") + raise Exception("fail") + except Exception as e: + Utils.assert_equals("Invalid transaction hash", str(e)) + + # Can get transactions by hashes with and without pruning + #@pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") + @pytest.mark.skip(reason="Needs wallet rpc") + def test_get_txs_by_hashes(self) -> None: + # fetch tx hashses to test + tx_hashes = Utils.get_confirmed_tx_hashes(self._daemon) + assert len(tx_hashes) > 0, "No tx hashes found" + + # context for creating txs + ctx = TestContext() + ctx.is_pruned = False + ctx.is_confirmed = True + ctx.from_get_tx_pool = False + + # fetch each tx by hash without pruning + txs = self._daemon.get_txs(tx_hashes) + assert len(txs) == len(tx_hashes), f"Expected len(txs) == len(tx_hashes), got {len(txs)} == {len(tx_hashes)}" + for tx in txs: + Utils.test_tx(tx, ctx) + + # fetch each tx by hash with pruning + txs = self._daemon.get_txs(tx_hashes, True) + ctx.is_pruned = True + assert len(txs) == len(tx_hashes), f"Expected len(txs) == len(tx_hashes), got {len(txs)} == {len(tx_hashes)}" + for tx in txs: + Utils.test_tx(tx, ctx) + + # fetch missing hash + dest = MoneroDestination() + dest.address = self._wallet.get_primary_address() + dest.amount = Utils.MAX_FEE + config = MoneroTxConfig() + config.account_index = 0 + config.destinations.append(dest) + tx = self._wallet.create_tx(config) + assert tx.hash is not None + assert self._daemon.get_tx(tx.hash) is None + tx_hashes.append(tx.hash) + num_txs = len(txs) + txs = self._daemon.get_txs(tx_hashes) + assert num_txs == len(txs) + + # fetch invalid hash + try: + self._daemon.get_txs(["invalid tx hash"]) + raise Exception("fail") + except Exception as e: + Utils.assert_equals("Invalid transaction hash", str(e)) + # Can get transaction pool statistics #@pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @pytest.mark.skip("TODO") @@ -244,6 +372,22 @@ def test_get_tx_pool_statistics(self): self._daemon.flush_tx_pool(tx_ids) raise e + # Can get the miner tx sum + @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") + def test_get_miner_tx_sum(self) -> None: + sum = self._daemon.get_miner_tx_sum(0, min(5000, self._daemon.get_height())) + Utils.test_miner_tx_sum(sum) + + # Can get fee estimate + @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") + def test_get_fee_estimate(self) -> None: + fee_estimate = self._daemon.get_fee_estimate() + Utils.test_unsigned_big_integer(fee_estimate.fee, True) + assert len(fee_estimate.fees) == 4, "Exptected 4 fees" + for fee in fee_estimate.fees: + Utils.test_unsigned_big_integer(fee, True) + Utils.test_unsigned_big_integer(fee_estimate.quantization_mask, True) + # Can get general information @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_general_information(self): @@ -383,6 +527,60 @@ def test_block_listener(self): except Exception as e: print(f"[!]: {str(e)}") + # Can ban a peer + @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") + def test_ban_peer(self): + # set ban + host = "192.168.1.51" + ban = MoneroBan() + ban.host = host + ban.is_banned = True + ban.seconds = 60 + self._daemon.set_peer_ban(ban) + + # test ban + bans = self._daemon.get_peer_bans() + found: bool = False + for peer_ban in bans: + Utils.test_ban(peer_ban) + if host == peer_ban.host: + found = True + + assert found, f"Could not find peer ban {host}" + + # Can ban peers + @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") + def test_ban_peers(self): + # set bans + addr1 = "192.168.1.52" + addr2 = "192.168.1.53" + ban1 = MoneroBan() + ban1.host = addr1 + ban1.is_banned = True + ban1.seconds = 60 + ban2 = MoneroBan() + ban2.host = addr2 + ban2.is_banned = True + ban2.seconds = 60 + bans: list[MoneroBan] = [] + bans.append(ban1) + bans.append(ban2) + self._daemon.set_peer_bans(bans) + + # test bans + bans = self._daemon.get_peer_bans() + found1: bool = False + found2: bool = False + for aBan in bans: + Utils.test_ban(aBan) + if addr1 == aBan.host: + found1 = True + if addr2 == aBan.host: + found2 = True + + assert found1, f"Could not find peer ban1 {addr1}" + assert found2, f"Could not find peer ban2 {addr2}" + # Can start and stop mining #@pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @pytest.mark.skip("TODO") diff --git a/tests/utils/monero_test_utils.py b/tests/utils/monero_test_utils.py index a184360..8feba43 100644 --- a/tests/utils/monero_test_utils.py +++ b/tests/utils/monero_test_utils.py @@ -9,14 +9,16 @@ MoneroWalletConfig, MoneroDaemonRpc, MoneroWalletRpc, MoneroBlockHeader, MoneroBlockTemplate, MoneroBlock, MoneroDaemonUpdateCheckResult, MoneroDaemonUpdateDownloadResult, MoneroWalletKeys, MoneroSubaddress, MoneroPeer, MoneroDaemonInfo, MoneroDaemonSyncInfo, MoneroHardForkInfo, - MoneroAltChain, MoneroTxPoolStats, MoneroWallet, MoneroRpcError, MoneroTxConfig, - MoneroAccount, MoneroTxWallet, MoneroTxQuery, MoneroConnectionSpan, SerializableStruct + MoneroAltChain, MoneroTxPoolStats, MoneroWallet, MoneroRpcError, MoneroTxConfig, MoneroBan, + MoneroAccount, MoneroTxWallet, MoneroTxQuery, MoneroConnectionSpan, SerializableStruct, + MoneroMinerTxSum, MoneroDaemon ) from .wallet_sync_printer import WalletSyncPrinter from .wallet_tx_tracker import WalletTxTracker from .test_context import TestContext from .tx_context import TxContext +from .binary_block_context import BinaryBlockContext class MoneroTestUtils(ABC): @@ -68,7 +70,7 @@ class MoneroTestUtils(ABC): # test wallet constants MAX_FEE = 7500000*10000 NETWORK_TYPE: MoneroNetworkType = MoneroNetworkType.MAINNET - REGTEST: bool = getenv("REGTEST") == "true" + REGTEST: bool = getenv("REGTEST") == "true" or True LANGUAGE: str = "English" SEED: str = "vortex degrees outbreak teeming gimmick school rounded tonic observant injury leech ought problems ahead upcoming ledge textbook cigar atrium trash dunes eavesdrop dullness evolved vortex" ADDRESS: str = "48W9YHwPzRz9aPTeXCA6kmSpW6HsvmWx578jj3of2gT3JwZzwTf33amESBoNDkL6SVK34Q2HTKqgYbGyE1hBws3wCrcBDR2" @@ -487,16 +489,16 @@ def test_miner_tx(cls, miner_tx: MoneroTx): # cls.test_tx(miner_tx, ctx) @classmethod - def test_tx(cls, tx: MoneroTx, ctx: Optional[TestContext]) -> None: + def test_tx(cls, tx: Optional[MoneroTx], ctx: Optional[TestContext]) -> None: raise NotImplementedError() # TODO: test block deep copy @classmethod - def test_block(cls, block: MoneroBlock, ctx: TestContext): + def test_block(cls, block: Optional[MoneroBlock], ctx: TestContext): # test required fields - cls.assert_not_none(block) - assert block.miner_tx is not None + assert block is not None, "Expected MoneroBlock, got None" + assert block.miner_tx is not None, "Expected block miner tx" cls.test_miner_tx(block.miner_tx) # TODO: miner tx doesn't have as much stuff, can't call testTx? cls.test_block_header(block, ctx.header_is_full) @@ -800,6 +802,19 @@ def test_alt_chain(cls, alt_chain: MoneroAltChain): cls.assert_true(alt_chain.length > 0) cls.assert_equals(64, len(alt_chain.main_chain_parent_block_hash)) + @classmethod + def test_ban(cls, ban: Optional[MoneroBan]) -> None: + assert ban is not None + assert ban.host is not None + assert ban.ip is not None + assert ban.seconds is not None + + @classmethod + def test_miner_tx_sum(cls, sum: Optional[MoneroMinerTxSum]) -> None: + assert sum is not None + cls.test_unsigned_big_integer(sum.emission_sum, True) + cls.test_unsigned_big_integer(sum.fee_sum, True) + @classmethod def get_unrelayed_tx(cls, wallet: MoneroWallet, account_idx: int): # TODO monero-project @@ -908,9 +923,35 @@ def get_random_transactions( def test_tx_wallet(cls, tx: MoneroTxWallet, ctx: TxContext) -> None: raise NotImplementedError() + @classmethod + def get_confirmed_tx_hashes(cls, daemon: MoneroDaemon) -> list[str]: + hashes: list[str] = [] + height: int = daemon.get_height() + i = 0 + while i < 5 and height > 0: + height -= 1 + block = daemon.get_block_by_height(height) + for tx_hash in block.tx_hashes: + hashes.append(tx_hash) + return hashes + @classmethod def test_rpc_connection(cls, connection: Optional[MoneroRpcConnection], uri: Optional[str]) -> None: assert connection is not None assert uri is not None assert len(uri) > 0 assert connection.uri == uri + + @classmethod + def test_get_blocks_range(cls, daemon: MoneroDaemonRpc, start_height: Optional[int], end_height: Optional[int], chain_height: int, chunked: bool, block_ctx: BinaryBlockContext) -> None: + # fetch blocks by range + real_start_height = 0 if start_height is None else start_height + real_end_height = chain_height - 1 if end_height is None else end_height + print(f"get_blocks_by_range_chunked(): start_height: {start_height}, end_height: {end_height}") + blocks = daemon.get_blocks_by_range_chunked(start_height, end_height) if chunked else daemon.get_blocks_by_range(start_height, end_height) + cls.assert_equals(real_end_height - real_start_height + 1, len(blocks)) + + # test each block + for i in range(len(blocks)): + cls.assert_equals(real_start_height + i, blocks[i].height) + cls.test_block(blocks[i], block_ctx)