Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Lokinet peer stats to deregistration voting process #1198

Open
wants to merge 9 commits into
base: dev
Choose a base branch
from
1 change: 1 addition & 0 deletions src/cryptonote_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions src/cryptonote_core/cryptonote_core.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string> router_ids,
std::function<void(bool success, std::vector<std::string> 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
{
Expand Down
10 changes: 10 additions & 0 deletions src/cryptonote_core/cryptonote_core.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string> router_ids,
std::function<void(bool success, std::vector<std::string> data)> results_handler) const;

/**
* @brief get the cryptonote protocol instance
*
Expand Down Expand Up @@ -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<lokimq::ConnectionID> m_lokinet_lmq_connection = std::nullopt;

/**
* @brief attempts to relay any transactions in the mempool which need it
*
Expand Down
153 changes: 153 additions & 0 deletions src/cryptonote_core/service_node_list.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,17 @@

#include <boost/endian/conversion.hpp>

#include <lokimq/bt_serialize.h>
#include <lokimq/base32z.h>
#include <stdexcept>

extern "C" {
#include <sodium.h>
}

#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"
Expand Down Expand Up @@ -292,6 +297,44 @@ namespace service_nodes
return false;
}

std::future<peer_stats_map> service_node_list::update_peer_stats(
const cryptonote::core& core,
std::vector<std::string> router_ids)
{
std::promise<peer_stats_map> promise;
try
{
core.request_peer_stats(router_ids, [&](bool success, std::vector<std::string> 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;
Expand Down Expand Up @@ -1957,6 +2000,116 @@ namespace service_nodes
return result;
}

void lokinet_peer_stats::bt_decode(std::string_view data)
{
bt_decode(lokimq::bt_deserialize<lokimq::bt_dict>(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";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clearly Jeff did not name these.


// TODO: validation -- do we reject if we don't get a full message? or types are wrong?

router_id = std::get<std::string>(dict.at(RouterIdKey));

num_connection_attempts = lokimq::get_int<int32_t>(dict.at(NumConnectionAttemptsKey));
num_connection_successes = lokimq::get_int<int32_t>(dict.at(NumConnectionSuccessesKey));
num_connection_rejections = lokimq::get_int<int32_t>(dict.at(NumConnectionRejectionsKey));
num_connection_timeouts = lokimq::get_int<int32_t>(dict.at(NumConnectionTimeoutsKey));

num_path_builds = lokimq::get_int<int32_t>(dict.at(NumPathBuildsKey));
num_packets_attempted = lokimq::get_int<int64_t>(dict.at(NumPacketsAttemptedKey));
num_packets_sent = lokimq::get_int<int64_t>(dict.at(NumPacketsSentKey));
num_packets_dropped = lokimq::get_int<int64_t>(dict.at(NumPacketsDroppedKey));
num_packets_resent = lokimq::get_int<int64_t>(dict.at(NumPacketsResentKey));

num_distinct_rcs_received = lokimq::get_int<int32_t>(dict.at(NumDistinctRCsReceivedKey));
num_late_rcs = lokimq::get_int<int32_t>(dict.at(NumLateRCsKey));

peak_bandwidth_bytes_per_sec = lokimq::get_int<int64_t>(dict.at(PeakBandwidthBytesPerSecKey));
longest_rc_receive_interval = std::chrono::milliseconds(lokimq::get_int<int64_t>(dict.at(LongestRCReceiveIntervalKey)));
least_rc_remaining_lifetime = std::chrono::milliseconds(lokimq::get_int<int64_t>(dict.at(LeastRCRemainingLifetimeKey)));
last_rc_updated = std::chrono::milliseconds(lokimq::get_int<int64_t>(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<crypto::ed25519_public_key, lokinet_peer_stats> stats_map;

auto dict = lokimq::bt_deserialize<lokimq::bt_dict>(data);
for (auto [router_id, value] : dict)
{
if (not std::holds_alternative<lokimq::bt_dict>(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<lokimq::bt_dict>(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
Expand Down
49 changes: 49 additions & 0 deletions src/cryptonote_core/service_node_list.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,24 @@

#pragma once

#include <chrono>
#include <mutex>
#include <future>
#include <shared_mutex>
#include <string_view>
#include <lokimq/bt_serialize.h>
#include "serialization/serialization.h"
#include "cryptonote_basic/cryptonote_basic_impl.h"
#include "cryptonote_core/service_node_rules.h"
#include "cryptonote_core/service_node_voting.h"
#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;
Expand All @@ -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<crypto::ed25519_public_key, lokinet_peer_stats>;

// 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)
Expand All @@ -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.
Expand Down Expand Up @@ -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<peer_stats_map> update_peer_stats(const cryptonote::core& core, std::vector<std::string> 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
Expand Down
Loading