diff --git a/src/cryptonote_config.h b/src/cryptonote_config.h index 5960ec76b7..36434456e6 100644 --- a/src/cryptonote_config.h +++ b/src/cryptonote_config.h @@ -69,6 +69,7 @@ static_assert(STAKING_PORTIONS % 12 == 0, "Use a multiple of twelve, so that it #define STORAGE_SERVER_PING_LIFETIME UPTIME_PROOF_FREQUENCY_IN_SECONDS #define LOKINET_PING_LIFETIME UPTIME_PROOF_FREQUENCY_IN_SECONDS +#define LOKINET_RC_RECEIVED_MAX_IN_SECONDS (2*24*3600) // very relaxed restriction -- we must have seen an RC (RouterContact) within the last 2 days. TODO: tune #define CRYPTONOTE_REWARD_BLOCKS_WINDOW 100 #define CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V1 20000 // NOTE(loki): For testing suite, //size of block (bytes) after which reward for block calculated using block size - before first fork diff --git a/src/cryptonote_core/cryptonote_core.cpp b/src/cryptonote_core/cryptonote_core.cpp index 829556908b..74fdb7dbb9 100644 --- a/src/cryptonote_core/cryptonote_core.cpp +++ b/src/cryptonote_core/cryptonote_core.cpp @@ -2080,6 +2080,17 @@ namespace cryptonote m_service_node_list.copy_active_x25519_pubkeys(std::inserter(active_sns, active_sns.end())); m_lmq->set_active_sns(std::move(active_sns)); } + + void core::request_peer_stats( + std::vector router_ids, + std::function data)> results_handler) const + { + if (not m_lokinet_lmq_connection) + throw std::runtime_error("cannot request peer stats without a lokinet connected"); + + m_lmq->request(*m_lokinet_lmq_connection, "lokid.get_peer_stats", std::move(results_handler), lokimq::bt_serialize(router_ids)); + } + //----------------------------------------------------------------------------------------------- crypto::hash core::get_tail_id() const { diff --git a/src/cryptonote_core/cryptonote_core.h b/src/cryptonote_core/cryptonote_core.h index 16f0b05a32..dc40f20719 100644 --- a/src/cryptonote_core/cryptonote_core.h +++ b/src/cryptonote_core/cryptonote_core.h @@ -328,6 +328,11 @@ namespace cryptonote /// active SNs. void update_lmq_sns(); + /// Called (from service_node_quorum_cop) to request peer stats from the connected lokinet daemon + void request_peer_stats( + std::vector router_ids, + std::function data)> results_handler) const; + /** * @brief get the cryptonote protocol instance * @@ -992,6 +997,11 @@ namespace cryptonote uint16_t storage_port() const { return m_storage_port; } uint16_t quorumnet_port() const { return m_quorumnet_port; } + // when lokinet connects via lokimq, we keep up with the connection so that we can make + // bi-directional requests. TODO: this is a messy place to put this, but currently the + // lmq server is not accessible to quorum_cop where this connection is needed + std::optional m_lokinet_lmq_connection = std::nullopt; + /** * @brief attempts to relay any transactions in the mempool which need it * diff --git a/src/cryptonote_core/service_node_list.cpp b/src/cryptonote_core/service_node_list.cpp index 22c079be3f..2ef88648be 100644 --- a/src/cryptonote_core/service_node_list.cpp +++ b/src/cryptonote_core/service_node_list.cpp @@ -33,12 +33,17 @@ #include +#include +#include +#include + extern "C" { #include } #include "ringct/rctSigs.h" #include "wallet/wallet2.h" +#include "cryptonote_core/cryptonote_core.h" #include "cryptonote_tx_utils.h" #include "cryptonote_basic/tx_extra.h" #include "cryptonote_basic/hardfork.h" @@ -292,6 +297,44 @@ namespace service_nodes return false; } + std::future service_node_list::update_peer_stats( + const cryptonote::core& core, + std::vector router_ids) + { + std::promise promise; + try + { + core.request_peer_stats(router_ids, [&](bool success, std::vector data) { + if (not success) + throw std::runtime_error("Failed to request peer stats from lokinet"); + + if (data.empty()) + throw std::runtime_error("Empty response from lokinet"); + + peer_stats_map stats_map = bt_decode_peer_stats_map(data[0]); + + std::lock_guard lock{m_sn_mutex}; + // TODO: avoid scan of map here + for (auto& [pubkey, proof] : proofs) + { + auto itr = stats_map.find(proof.pubkey_ed25519); + if (itr != stats_map.end()) + { + proof.last_rc_updated_ms = itr->second.last_rc_updated; + } + } + + promise.set_value(std::move(stats_map)); + }); + } + catch (const std::exception& e) + { + promise.set_exception(std::current_exception()); + } + + return promise.get_future(); + } + bool reg_tx_extract_fields(const cryptonote::transaction& tx, contributor_args_t &contributor_args, uint64_t& expiration_timestamp, crypto::public_key& service_node_key, crypto::signature& signature) { cryptonote::tx_extra_service_node_register registration; @@ -1957,6 +2000,116 @@ namespace service_nodes return result; } + void lokinet_peer_stats::bt_decode(std::string_view data) + { + bt_decode(lokimq::bt_deserialize(data)); + } + + void lokinet_peer_stats::bt_decode(const lokimq::bt_dict& dict) + { + constexpr auto RouterIdKey = "RouterIdKey"; + constexpr auto NumConnectionAttemptsKey = "numConnectionAttempts"; + constexpr auto NumConnectionSuccessesKey = "numConnectionSuccesses"; + constexpr auto NumConnectionRejectionsKey = "numConnectionRejections"; + constexpr auto NumConnectionTimeoutsKey = "numConnectionTimeouts"; + constexpr auto NumPathBuildsKey = "numPathBuilds"; + constexpr auto NumPacketsAttemptedKey = "numPacketsAttempted"; + constexpr auto NumPacketsSentKey = "numPacketsSent"; + constexpr auto NumPacketsDroppedKey = "numPacketsDropped"; + constexpr auto NumPacketsResentKey = "numPacketsResent"; + constexpr auto NumDistinctRCsReceivedKey = "numDistinctRCsReceived"; + constexpr auto NumLateRCsKey = "numLateRCs"; + constexpr auto PeakBandwidthBytesPerSecKey = "peakBandwidthBytesPerSec"; + constexpr auto LongestRCReceiveIntervalKey = "longestRCReceiveInterval"; + constexpr auto LeastRCRemainingLifetimeKey = "leastRCRemainingLifetime"; + constexpr auto LastRCUpdatedKey = "lastRCUpdated"; + + // TODO: validation -- do we reject if we don't get a full message? or types are wrong? + + router_id = std::get(dict.at(RouterIdKey)); + + num_connection_attempts = lokimq::get_int(dict.at(NumConnectionAttemptsKey)); + num_connection_successes = lokimq::get_int(dict.at(NumConnectionSuccessesKey)); + num_connection_rejections = lokimq::get_int(dict.at(NumConnectionRejectionsKey)); + num_connection_timeouts = lokimq::get_int(dict.at(NumConnectionTimeoutsKey)); + + num_path_builds = lokimq::get_int(dict.at(NumPathBuildsKey)); + num_packets_attempted = lokimq::get_int(dict.at(NumPacketsAttemptedKey)); + num_packets_sent = lokimq::get_int(dict.at(NumPacketsSentKey)); + num_packets_dropped = lokimq::get_int(dict.at(NumPacketsDroppedKey)); + num_packets_resent = lokimq::get_int(dict.at(NumPacketsResentKey)); + + num_distinct_rcs_received = lokimq::get_int(dict.at(NumDistinctRCsReceivedKey)); + num_late_rcs = lokimq::get_int(dict.at(NumLateRCsKey)); + + peak_bandwidth_bytes_per_sec = lokimq::get_int(dict.at(PeakBandwidthBytesPerSecKey)); + longest_rc_receive_interval = std::chrono::milliseconds(lokimq::get_int(dict.at(LongestRCReceiveIntervalKey))); + least_rc_remaining_lifetime = std::chrono::milliseconds(lokimq::get_int(dict.at(LeastRCRemainingLifetimeKey))); + last_rc_updated = std::chrono::milliseconds(lokimq::get_int(dict.at(LastRCUpdatedKey))); + } + + peer_stats_map bt_decode_peer_stats_map(std::string_view data) + { + // this is expected to be encoded as: + // dict [router_id -> [dict representing peer stats, e.g. lokinet_peer_stats::bt_decode()]] + + std::unordered_map stats_map; + + auto dict = lokimq::bt_deserialize(data); + for (auto [router_id, value] : dict) + { + if (not std::holds_alternative(value)) + throw std::invalid_argument("invalid bt-encoded list of stats, dict contains invalid entry"); + + auto pubkey = parse_router_id(router_id); + + lokinet_peer_stats stats; + stats.bt_decode(std::get(value)); + stats_map[pubkey] = std::move(stats); + } + + return stats_map; + } + + constexpr int router_id_size = 58; + constexpr int ed25519_pubkey_size = 32; + + crypto::ed25519_public_key parse_router_id(std::string_view router_id) + { + // lokinet's RouterID is serialized as 32 bytes base32z encoded (52 chars), followed by ".snode" + // 52 chars for pubkey and 6 for ".snode" + if (router_id.size() != router_id_size) + throw std::invalid_argument("invalid router_id length"); + + std::string_view encoded = router_id.substr(0, 52); + std::string_view tld = router_id.substr(52); + + if (tld != ".snode") + throw std::invalid_argument("invalid routerId tld"); + + std::string raw = lokimq::from_base32z(encoded.begin(), encoded.end()); + if (raw.size() != sizeof(ed25519_pubkey_size)) + throw std::invalid_argument("routerId contains invalid pubkey"); + + // TODO: byte order? + crypto::ed25519_public_key pubkey; + memcpy(pubkey.data, raw.data(), sizeof(ed25519_pubkey_size)); + + return pubkey; + } + + std::string ed25519_pubkey_to_router_id(const crypto::ed25519_public_key& pubkey) + { + std::string router_id; + router_id.reserve(router_id_size); + + router_id.append(lokimq::to_base32z(std::string_view((const char*)&pubkey.data, ed25519_pubkey_size))); + router_id.append(".snode"); + + return router_id; + } + + #ifdef __cpp_lib_erase_if // # (C++20) using std::erase_if; #else diff --git a/src/cryptonote_core/service_node_list.h b/src/cryptonote_core/service_node_list.h index fabc7a8f17..0ccb62e00c 100644 --- a/src/cryptonote_core/service_node_list.h +++ b/src/cryptonote_core/service_node_list.h @@ -28,9 +28,12 @@ #pragma once +#include #include +#include #include #include +#include #include "serialization/serialization.h" #include "cryptonote_basic/cryptonote_basic_impl.h" #include "cryptonote_core/service_node_rules.h" @@ -38,8 +41,11 @@ #include "cryptonote_core/service_node_quorum_cop.h" #include "common/util.h" +using namespace std::chrono_literals; + namespace cryptonote { +class core; class Blockchain; class BlockchainDB; struct checkpoint_t; @@ -60,6 +66,44 @@ namespace service_nodes END_KV_SERIALIZE_MAP() }; + // tracks lokinet's PeerStats struct and is used to reflect the behavior of a service node's peers on the lokinet network + struct lokinet_peer_stats + { + std::string router_id; + + int32_t num_connection_attempts = 0; + int32_t num_connection_successes = 0; + int32_t num_connection_rejections = 0; + int32_t num_connection_timeouts = 0; + + int32_t num_path_builds = 0; + int64_t num_packets_attempted = 0; + int64_t num_packets_sent = 0; + int64_t num_packets_dropped = 0; + int64_t num_packets_resent = 0; + + int32_t num_distinct_rcs_received = 0; + int32_t num_late_rcs = 0; + + int64_t peak_bandwidth_bytes_per_sec = 0; + std::chrono::milliseconds longest_rc_receive_interval = 0ms; + std::chrono::milliseconds least_rc_remaining_lifetime = 0ms; + std::chrono::milliseconds last_rc_updated = 0ms; + + // Decodes a peerstats into this existing struct + void bt_decode(std::string_view data); + void bt_decode(const lokimq::bt_dict& dict); + }; + using peer_stats_map = std::unordered_map; + + // functions for converting lokinet's RouterID string representation to/from an ed25519 public key + crypto::ed25519_public_key parse_router_id(std::string_view router_id); + std::string ed25519_pubkey_to_router_id(const crypto::ed25519_public_key& pubkey); + + // Decodes a list of peer stats + peer_stats_map bt_decode_peer_stats_map(std::string_view data); + + struct proof_info { uint64_t timestamp = 0; // The actual time we last received an uptime proof (serialized) @@ -83,6 +127,8 @@ namespace service_nodes // Derived from pubkey_ed25519, not serialized crypto::x25519_public_key pubkey_x25519 = crypto::x25519_public_key::null(); + std::chrono::milliseconds last_rc_updated_ms = 0ms; + // Updates pubkey_ed25519 to the given key, re-deriving the x25519 key if it actually changes // (does nothing if the key is the same as the current value). If x25519 derivation fails then // both pubkeys are set to null. @@ -324,6 +370,9 @@ namespace service_nodes bool is_key_image_locked(crypto::key_image const &check_image, uint64_t *unlock_height = nullptr, service_node_info::contribution_t *the_locked_contribution = nullptr) const; uint64_t height() const { return m_state.height; } + // makes a request to lokinet to retrieve peer stats for a given list of service nodes + std::future update_peer_stats(const cryptonote::core& core, std::vector router_ids); + /// Note(maxim): this should not affect thread-safety as the returned object is const /// /// For checkpointing, quorums are only generated when height % CHECKPOINT_INTERVAL == 0 (and diff --git a/src/cryptonote_core/service_node_quorum_cop.cpp b/src/cryptonote_core/service_node_quorum_cop.cpp index b3a99d1b52..092c52df1a 100644 --- a/src/cryptonote_core/service_node_quorum_cop.cpp +++ b/src/cryptonote_core/service_node_quorum_cop.cpp @@ -59,6 +59,7 @@ namespace service_nodes { buf_ptr += snprintf(buf_ptr, buf_end - buf_ptr, "Service Node is currently failing the following tests: "); if (!uptime_proved) buf_ptr += snprintf(buf_ptr, buf_end - buf_ptr, "Uptime proof missing. "); + if (!rc_received_recently) buf_ptr += snprintf(buf_ptr, buf_end - buf_ptr, "RC not received recently. "); if (!voted_in_checkpoints) buf_ptr += snprintf(buf_ptr, buf_end - buf_ptr, "Skipped voting in at least %d checkpoints. ", (int)(CHECKPOINT_NUM_QUORUMS_TO_PARTICIPATE_IN - CHECKPOINT_MAX_MISSABLE_VOTES)); buf_ptr += snprintf(buf_ptr, buf_end - buf_ptr, "Note: Storage server may not be reachable. This is only testable by an external Service Node."); } @@ -83,20 +84,25 @@ namespace service_nodes service_node_test_results result; // Defaults to true for individual tests bool ss_reachable = true; uint64_t timestamp = 0; + uint64_t rc_timestamp = 0; decltype(std::declval().public_ips) ips{}; decltype(std::declval().votes) votes; m_core.get_service_node_list().access_proof(pubkey, [&](const proof_info &proof) { ss_reachable = proof.storage_server_reachable; timestamp = std::max(proof.timestamp, proof.effective_timestamp); + rc_timestamp = proof.last_rc_updated_ms.count() / 1000; ips = proof.public_ips; votes = proof.votes; }); uint64_t time_since_last_uptime_proof = std::time(nullptr) - timestamp; + uint64_t time_since_last_rc_received = std::time(nullptr) - rc_timestamp; bool check_uptime_obligation = true; + bool check_rc_received_obligation= true; bool check_checkpoint_obligation = true; #if defined(LOKI_ENABLE_INTEGRATION_TEST_HOOKS) + // TODO: provide similar override for rc received obligation if (integration_test::state.disable_obligation_uptime_proof) check_uptime_obligation = false; if (integration_test::state.disable_obligation_checkpointing) check_checkpoint_obligation = false; #endif @@ -110,6 +116,15 @@ namespace service_nodes result.uptime_proved = false; } + if (check_rc_received_obligation && time_since_last_rc_received > LOKINET_RC_RECEIVED_MAX_IN_SECONDS) + { + LOG_PRINT_L1( + "Service Node: " << pubkey << ", failed RouterContact publishing obligation check: last RC was older than: " + << LOKINET_RC_RECEIVED_MAX_IN_SECONDS << "s. Time since last RC receipt was: " + << tools::get_human_readable_timespan(std::chrono::seconds(time_since_last_rc_received))); + result.rc_received_recently = false; + } + if (!ss_reachable) { LOG_PRINT_L1("Service Node storage server is not reachable for node: " << pubkey); @@ -203,9 +218,13 @@ namespace service_nodes void quorum_cop::process_quorums(cryptonote::block const &block) { + MFATAL("process_quorums"); uint8_t const hf_version = block.major_version; if (hf_version < cryptonote::network_version_9_service_nodes) + { + MFATAL("hf_version < version_9"); return; + } uint64_t const REORG_SAFETY_BUFFER_BLOCKS = (hf_version >= cryptonote::network_version_12_checkpointing) ? REORG_SAFETY_BUFFER_BLOCKS_POST_HF12 @@ -216,11 +235,17 @@ namespace service_nodes uint64_t const height = cryptonote::get_block_height(block); uint64_t const latest_height = std::max(m_core.get_current_blockchain_height(), m_core.get_target_blockchain_height()); if (latest_height < VOTE_LIFETIME) + { + MFATAL("< VOTE_LIFETIME"); return; + } uint64_t const start_voting_from_height = latest_height - VOTE_LIFETIME; if (height < start_voting_from_height) + { + MFATAL("< start_voting_from_height"); return; + } service_nodes::quorum_type const max_quorum_type = service_nodes::max_quorum_type_for_hf(hf_version); bool tested_myself_once_per_block = false; @@ -247,6 +272,7 @@ namespace service_nodes case quorum_type::obligations: { + MFATAL("obligations quorum"); m_obligations_height = std::max(m_obligations_height, start_voting_from_height); for (; m_obligations_height < (height - REORG_SAFETY_BUFFER_BLOCKS); m_obligations_height++) @@ -283,10 +309,16 @@ namespace service_nodes // NOTE: Wait at least 2 hours before we're allowed to vote so that we collect necessary voting information from people on the network bool alive_for_min_time = live_time >= MIN_TIME_IN_S_BEFORE_VOTING; if (!alive_for_min_time) + { + MFATAL("not up long enough"); continue; + } if (!m_core.service_node()) + { + MFATAL("not a service node"); continue; + } auto quorum = m_core.get_quorum(quorum_type::obligations, m_obligations_height); if (!quorum) @@ -296,16 +328,49 @@ namespace service_nodes continue; } - if (quorum->workers.empty()) continue; + MFATAL("ALMOST THERE..."); + + if (quorum->workers.empty()) + { + MFATAL("No workers..."); + continue; + } + + if (not voting_enabled) + { + MFATAL("voting disabled..."); + } + int index_in_group = voting_enabled ? find_index_in_quorum_group(quorum->validators, my_keys.pub) : -1; + MFATAL("index_in_group" << index_in_group); if (index_in_group >= 0) { // // NOTE: I am in the quorum // auto worker_states = m_core.get_service_node_list_state(quorum->workers); - auto worker_it = worker_states.begin(); std::unique_lock lock{m_lock}; + + // make first pass to calculate nodes we are going to vote on + std::vector router_ids; + router_ids.reserve(worker_states.size()); + for (const auto& state : worker_states) + { + m_core.get_service_node_list().access_proof(state.pubkey, [&](const proof_info &proof) { + router_ids.push_back(ed25519_pubkey_to_router_id(proof.pubkey_ed25519)); + }); + } + + // TODO: we unlock our mutex, make our request, and then wait for it to return. this should be done + // with a better asynchronous model. + lock.unlock(); + + auto future = m_core.get_service_node_list().update_peer_stats(m_core, router_ids); + auto peer_stats = future.get(); + // TODO: inspect peer stats and modify proofs + + lock.lock(); + auto worker_it = worker_states.begin(); int good = 0, total = 0; for (size_t node_index = 0; node_index < quorum->workers.size(); ++worker_it, ++node_index) { @@ -475,6 +540,7 @@ namespace service_nodes bool quorum_cop::block_added(const cryptonote::block& block, const std::vector& txs, cryptonote::checkpoint_t const * /*checkpoint*/) { + process_quorums(block); uint64_t const height = cryptonote::get_block_height(block) + 1; // chain height = new top block height + 1 m_vote_pool.remove_expired_votes(height); diff --git a/src/cryptonote_core/service_node_quorum_cop.h b/src/cryptonote_core/service_node_quorum_cop.h index f40d2b2edf..88b1920e8f 100644 --- a/src/cryptonote_core/service_node_quorum_cop.h +++ b/src/cryptonote_core/service_node_quorum_cop.h @@ -78,6 +78,7 @@ namespace service_nodes struct service_node_test_results { bool uptime_proved = true; + bool rc_received_recently = true; bool single_ip = true; bool voted_in_checkpoints = true; bool storage_server_reachable = true; diff --git a/src/rpc/core_rpc_server.cpp b/src/rpc/core_rpc_server.cpp index fbaa5bbcab..0dbda987b6 100644 --- a/src/rpc/core_rpc_server.cpp +++ b/src/rpc/core_rpc_server.cpp @@ -3145,7 +3145,12 @@ namespace cryptonote { namespace rpc { return handle_ping( req.version, service_nodes::MIN_LOKINET_VERSION, "Lokinet", m_core.m_last_lokinet_ping, LOKINET_PING_LIFETIME, - [this](bool significant) { if (significant) m_core.reset_proof_interval(); }); + [this, &context](bool significant) { + if (significant) + m_core.reset_proof_interval(); + + m_core.m_lokinet_lmq_connection = context.lmq_connection_id; + }); } //------------------------------------------------------------------------------------------------------------------------------ GET_STAKING_REQUIREMENT::response core_rpc_server::invoke(GET_STAKING_REQUIREMENT::request&& req, rpc_context context) diff --git a/src/rpc/core_rpc_server.h b/src/rpc/core_rpc_server.h index a718f4831b..d28a836a6c 100644 --- a/src/rpc/core_rpc_server.h +++ b/src/rpc/core_rpc_server.h @@ -110,6 +110,9 @@ namespace cryptonote { namespace rpc { // A free-form identifier identifiying the remote address of the request; this might be IP:PORT, // or could contain a pubkey, or ... std::string_view remote; + + // If RPC source is rpc_source::lmq, this stores the ConnectionID associated with the request. + std::optional lmq_connection_id = std::nullopt; }; struct rpc_request { diff --git a/src/rpc/lmq_server.cpp b/src/rpc/lmq_server.cpp index 77bb29fa72..76f308ddc1 100644 --- a/src/rpc/lmq_server.cpp +++ b/src/rpc/lmq_server.cpp @@ -150,6 +150,7 @@ lmq_rpc::lmq_rpc(cryptonote::core& core, core_rpc_server& rpc, const boost::prog request.context.admin = m.access.auth >= AuthLevel::admin; request.context.source = rpc_source::lmq; request.context.remote = m.remote; + request.context.lmq_connection_id = m.conn; request.body = m.data.empty() ? ""sv : m.data[0]; try { diff --git a/tests/network_tests/daemons.py b/tests/network_tests/daemons.py index ad506bbfa5..afc2e1b16c 100644 --- a/tests/network_tests/daemons.py +++ b/tests/network_tests/daemons.py @@ -153,8 +153,6 @@ def __init__(self, *, '--p2p-bind-port={}'.format(self.p2p_port), '--rpc-bind-ip={}'.format(self.listen_ip), '--rpc-bind-port={}'.format(self.rpc_port), - '--zmq-rpc-bind-ip={}'.format(self.listen_ip), - '--zmq-rpc-bind-port={}'.format(self.zmq_port), '--quorumnet-port={}'.format(self.qnet_port), ) diff --git a/tests/network_tests/service_node_network.py b/tests/network_tests/service_node_network.py index 45196c614b..1adf6c0005 100644 --- a/tests/network_tests/service_node_network.py +++ b/tests/network_tests/service_node_network.py @@ -54,6 +54,9 @@ def vprint(*args, timestamp=True, **kwargs): print(datetime.now(), end=" ") print(*args, **kwargs) +''' +sleep a few seconds, then call code that calls snn.mine +''' class SNNetwork: def __init__(self, datadir, *, binpath='../../build/bin', sns=20, nodes=3): diff --git a/tests/unit_tests/service_nodes.cpp b/tests/unit_tests/service_nodes.cpp index 34175a758b..c028958e59 100644 --- a/tests/unit_tests/service_nodes.cpp +++ b/tests/unit_tests/service_nodes.cpp @@ -557,3 +557,47 @@ TEST(service_nodes, service_node_get_locked_key_image_unlock_height) ASSERT_EQ(unlock_height, expected); } } + +TEST(service_nodes, lokinet_peer_stats_deserialization) +{ + constexpr std::string_view data = + "d" + "21:numConnectionAttempts" "i1e" + "22:numConnectionSuccesses" "i2e" + "23:numConnectionRejections" "i3e" + "21:numConnectionTimeouts" "i4e" + "13:numPathBuilds" "i5e" + "19:numPacketsAttempted" "i6e" + "14:numPacketsSent" "i7e" + "17:numPacketsDropped" "i8e" + "16:numPacketsResent" "i9e" + "22:numDistinctRCsReceived" "i10e" + "10:numLateRCs" "i11e" + "24:peakBandwidthBytesPerSec" "i12e" + "24:longestRCReceiveInterval" "i13e" + "24:leastRCRemainingLifetime" "i14e" + "13:lastRCUpdated" "i15e" + "e"; + + service_nodes::lokinet_peer_stats stats; + + ASSERT_NO_THROW(stats.bt_decode(data)); + + // TODO: router_id + + ASSERT_EQ(1, stats.num_connection_attempts); + ASSERT_EQ(2, stats.num_connection_successes); + ASSERT_EQ(3, stats.num_connection_rejections); + ASSERT_EQ(4, stats.num_connection_timeouts); + ASSERT_EQ(5, stats.num_path_builds); + ASSERT_EQ(6, stats.num_packets_attempted); + ASSERT_EQ(7, stats.num_packets_sent); + ASSERT_EQ(8, stats.num_packets_dropped); + ASSERT_EQ(9, stats.num_packets_resent); + ASSERT_EQ(10, stats.num_distinct_rcs_received); + ASSERT_EQ(11, stats.num_late_rcs); + ASSERT_EQ(12, stats.peak_bandwidth_bytes_per_sec); + ASSERT_EQ(13, stats.longest_rc_receive_interval.count()); + ASSERT_EQ(14, stats.least_rc_remaining_lifetime.count()); + ASSERT_EQ(15, stats.last_rc_updated.count()); +}