From 4c8a0875eaf84152bc324cd87930c8894ee66f7f Mon Sep 17 00:00:00 2001 From: M0ssa99 Date: Thu, 14 May 2026 21:41:12 +0300 Subject: [PATCH 01/19] fix(dlt_p2p_node): improve handling of canceled fibers and add dead fiber management --- libraries/chain/CMakeLists.txt | 49 +++++++++++++++---- libraries/network/dlt_p2p_node.cpp | 20 +++++++- .../include/graphene/network/dlt_p2p_node.hpp | 1 + plugins/witness/witness.cpp | 7 ++- 4 files changed, 66 insertions(+), 11 deletions(-) diff --git a/libraries/chain/CMakeLists.txt b/libraries/chain/CMakeLists.txt index 6b2d60fdec..f3aef1cee4 100644 --- a/libraries/chain/CMakeLists.txt +++ b/libraries/chain/CMakeLists.txt @@ -1,12 +1,43 @@ -if(MSVC) - set(hardfork_hpp_file "${CMAKE_CURRENT_SOURCE_DIR}/include/graphene/chain/hardfork.hpp") - add_custom_target(build_hardfork_hpp COMMAND cat-parts "${CMAKE_CURRENT_SOURCE_DIR}/hardfork.d" ${hardfork_hpp_file}) - add_dependencies(build_hardfork_hpp cat-parts) -else(MSVC) - set(hardfork_hpp_file "${CMAKE_CURRENT_BINARY_DIR}/include/graphene/chain/hardfork.hpp") - add_custom_target(build_hardfork_hpp - COMMAND "${CMAKE_SOURCE_DIR}/programs/build_helpers/cat_parts.py" "${CMAKE_CURRENT_SOURCE_DIR}/hardfork.d" ${hardfork_hpp_file}) -endif(MSVC) +set(GENERATED_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/include/graphene/chain") +set(hardfork_hpp_out "${GENERATED_INCLUDE_DIR}/hardfork.hpp") +file(MAKE_DIRECTORY "${GENERATED_INCLUDE_DIR}") + +if(MSVC OR MINGW) +set(hardfork_d_dir "${CMAKE_CURRENT_SOURCE_DIR}/hardfork.d") + add_custom_command( + OUTPUT "${hardfork_hpp_out}" + COMMAND ${CMAKE_COMMAND} -E echo "Generating ${hardfork_hpp_out} via cat-parts" + COMMAND $ "${hardfork_d_dir}" "${hardfork_hpp_out}" + DEPENDS "${hardfork_d_dir}" cat-parts + BYPRODUCTS "${hardfork_hpp_out}" + COMMENT "Generating ${hardfork_hpp_out} via cat-parts" + VERBATIM + + ) +else() + find_package(Python COMPONENTS Interpreter REQUIRED) + add_custom_command( + OUTPUT "${hardfork_hpp_out}" + COMMAND ${Python_EXECUTABLE} "${CMAKE_SOURCE_DIR}/programs/build_helpers/cat_parts.py" + "${CMAKE_CURRENT_SOURCE_DIR}/hardfork.d" "${hardfork_hpp_out}" + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/hardfork.d" "${CMAKE_SOURCE_DIR}/programs/build_helpers/cat_parts.py" + BYPRODUCTS "${hardfork_hpp_out}" + COMMENT "Generating ${hardfork_hpp_out} via Python" + VERBATIM + ) +endif() + +# Target comun care depinde de fișierul generat +add_custom_target(build_hardfork_hpp DEPENDS "${hardfork_hpp_out}") + +# Marchează fișierul ca generat +set_source_files_properties("${hardfork_hpp_out}" PROPERTIES GENERATED TRUE) + +# Include dir-ul binar +include_directories("${CMAKE_CURRENT_BINARY_DIR}/include") + +# Variabila folosită mai jos în add_library +set(hardfork_hpp_file "${hardfork_hpp_out}") set_source_files_properties("${CMAKE_CURRENT_BINARY_DIR}/include/graphene/chain/hardfork.hpp" PROPERTIES GENERATED TRUE) diff --git a/libraries/network/dlt_p2p_node.cpp b/libraries/network/dlt_p2p_node.cpp index 86884979f7..27ffb20cff 100644 --- a/libraries/network/dlt_p2p_node.cpp +++ b/libraries/network/dlt_p2p_node.cpp @@ -348,7 +348,12 @@ void dlt_p2p_node::handle_disconnect(peer_id peer, const std::string& reason, bo // _peer_states, so state/it remain valid when we resume here. auto fiber_it = _read_fibers.find(peer); if (fiber_it != _read_fibers.end()) { - try { if (fiber_it->second.valid()) fiber_it->second.cancel_and_wait(__FUNCTION__); } catch (...) {} + if (std::current_exception() != std::exception_ptr()) { + // Suntem în catch block — amânăm cancel_and_wait pentru periodic_task + _dead_fibers.push_back(std::move(fiber_it->second)); + } else { + try { if (fiber_it->second.valid()) fiber_it->second.cancel_and_wait(__FUNCTION__); } catch (...) {} + } _read_fibers.erase(fiber_it); } @@ -3326,6 +3331,19 @@ void dlt_p2p_node::block_validation_timeout() { // ── Periodic task ──────────────────────────────────────────────────── void dlt_p2p_node::periodic_task() { + if (!_dead_fibers.empty()) { + std::vector> to_clean; + to_clean.swap(_dead_fibers); + for (auto& f : to_clean) { + try { + // Nu apela ready() — poate crapa dacă promise e distrus + // cancel_and_wait are acum garda valid() după fix-ul din future.hpp + f.cancel_and_wait(__FUNCTION__); + } catch (...) {} + // Eliberează explicit promise-ul imediat după + f = fc::future(); + } +} // Non-DB-access housekeeping always runs. periodic_reconnect_check(); periodic_lifecycle_timeout_check(); diff --git a/libraries/network/include/graphene/network/dlt_p2p_node.hpp b/libraries/network/include/graphene/network/dlt_p2p_node.hpp index 99e48cc9cc..29840c1f83 100644 --- a/libraries/network/include/graphene/network/dlt_p2p_node.hpp +++ b/libraries/network/include/graphene/network/dlt_p2p_node.hpp @@ -331,6 +331,7 @@ class dlt_p2p_node { fc::thread* _thread = nullptr; bool _running = false; std::map> _read_fibers; + std::vector> _dead_fibers; fc::future _accept_fiber; fc::future _periodic_fiber; diff --git a/plugins/witness/witness.cpp b/plugins/witness/witness.cpp index 2ebf2387d1..91277c7dd3 100644 --- a/plugins/witness/witness.cpp +++ b/plugins/witness/witness.cpp @@ -1524,7 +1524,12 @@ namespace graphene { { int64_t ntp_us = 0; try { ntp_us = graphene::time::ntp_error().count(); } catch (...) {} - if (ntp_us > 250000) { // local clock >250ms behind NTP + #if defined(_WIN32) +constexpr int64_t NTP_WARN_THRESHOLD_US = 2000000; // 2s pe Windows +#else +constexpr int64_t NTP_WARN_THRESHOLD_US = 250000; // 250ms pe Linux/macOS +#endif +if (ntp_us > NTP_WARN_THRESHOLD_US) { // local clock >250ms behind NTP static fc::time_point _last_ntp_drift_log; auto _now_nd = fc::time_point::now(); if ((_now_nd - _last_ntp_drift_log).count() > 10000000) { From e77ce79032dd19658de35d0626df11eaef5e53d7 Mon Sep 17 00:00:00 2001 From: M0ssa99 Date: Thu, 14 May 2026 23:16:31 +0300 Subject: [PATCH 02/19] Update submodule fc la commitul meu --- thirdparty/fc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thirdparty/fc b/thirdparty/fc index cf033fee1b..208402574c 160000 --- a/thirdparty/fc +++ b/thirdparty/fc @@ -1 +1 @@ -Subproject commit cf033fee1b802a39fe11de4cc1e311faae29f168 +Subproject commit 208402574cd8d3489cf4417b97fce8b2a3b6384c From 830a91847676ebfdef33947e4a06af7a3e898cf7 Mon Sep 17 00:00:00 2001 From: M0ssa99 Date: Thu, 14 May 2026 23:16:31 +0300 Subject: [PATCH 03/19] fix(fc): mark subproject as dirty to reflect changes --- thirdparty/fc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thirdparty/fc b/thirdparty/fc index cf033fee1b..208402574c 160000 --- a/thirdparty/fc +++ b/thirdparty/fc @@ -1 +1 @@ -Subproject commit cf033fee1b802a39fe11de4cc1e311faae29f168 +Subproject commit 208402574cd8d3489cf4417b97fce8b2a3b6384c From 4fde2fee3d02be196abb3b5ea9e03a8e276777bc Mon Sep 17 00:00:00 2001 From: M0ssa99 Date: Thu, 14 May 2026 23:23:55 +0300 Subject: [PATCH 04/19] fix(fc): update subproject commit reference --- thirdparty/fc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thirdparty/fc b/thirdparty/fc index 208402574c..dc483d59bf 160000 --- a/thirdparty/fc +++ b/thirdparty/fc @@ -1 +1 @@ -Subproject commit 208402574cd8d3489cf4417b97fce8b2a3b6384c +Subproject commit dc483d59bf91f632c278e840f14d3bda7b86b1af From 5b989f91dfc0dc461cee21af82e5de7f92c51340 Mon Sep 17 00:00:00 2001 From: M0ssa99 Date: Thu, 14 May 2026 23:35:36 +0300 Subject: [PATCH 05/19] Update submodule fc la commitul meu --- thirdparty/fc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thirdparty/fc b/thirdparty/fc index dc483d59bf..9bb138bb71 160000 --- a/thirdparty/fc +++ b/thirdparty/fc @@ -1 +1 @@ -Subproject commit dc483d59bf91f632c278e840f14d3bda7b86b1af +Subproject commit 9bb138bb71abe153d14eec91e07614a98859c3d1 From cad4736d032ca9de39623c015df9c37d18df26a9 Mon Sep 17 00:00:00 2001 From: M0ssa99 Date: Thu, 14 May 2026 23:41:57 +0300 Subject: [PATCH 06/19] fix(fc): mark subproject as dirty to reflect changes --- thirdparty/fc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thirdparty/fc b/thirdparty/fc index 9bb138bb71..823cb68dd3 160000 --- a/thirdparty/fc +++ b/thirdparty/fc @@ -1 +1 @@ -Subproject commit 9bb138bb71abe153d14eec91e07614a98859c3d1 +Subproject commit 823cb68dd3fa044c657e038ab7c42393297e51ff From a5271db7aa6cd9a73a01367e8a113da88541a351 Mon Sep 17 00:00:00 2001 From: M0ssa99 Date: Fri, 15 May 2026 00:15:04 +0300 Subject: [PATCH 07/19] fix(cmake): add bcrypt library linking for MINGW --- CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ce94bad86..fe1424b09a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,8 @@ # Defines VIZ library target. project(VIZ) +if(MINGW) + link_libraries(bcrypt) +endif() cmake_minimum_required(VERSION 3.16) set(CHAIN_NAME "VIZ") From 2648f00dad010cf5260f3da1aa8ceead02d65098 Mon Sep 17 00:00:00 2001 From: M0ssa99 Date: Fri, 15 May 2026 00:16:47 +0300 Subject: [PATCH 08/19] fix(database): update reindex method to use unsigned long long for shared_file_size --- libraries/chain/include/graphene/chain/database.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/chain/include/graphene/chain/database.hpp b/libraries/chain/include/graphene/chain/database.hpp index caf539b923..5e4f0e9c57 100644 --- a/libraries/chain/include/graphene/chain/database.hpp +++ b/libraries/chain/include/graphene/chain/database.hpp @@ -155,7 +155,7 @@ namespace graphene { namespace chain { * replaying blockchain history. When this method exits successfully, the database will be open. */ void reindex(const fc::path &data_dir, const fc::path &shared_mem_dir, uint32_t from_block_num, uint64_t shared_file_size = ( - 1024l * 1024l * 1024l * 8l)); + 1024l * 1024l * 1024l * 8Ull)); /** * @brief Rebuild object graph from dlt_block_log after snapshot import From f48d20ff0579f65539b171deeabd1dc01e5f2249 Mon Sep 17 00:00:00 2001 From: M0ssa99 Date: Fri, 15 May 2026 01:31:47 +0300 Subject: [PATCH 09/19] fix(network): implement node_id deduplication for peer connections --- libraries/network/dlt_p2p_node.cpp | 76 ++++++++++++++++--- .../graphene/network/dlt_p2p_messages.hpp | 6 +- .../include/graphene/network/dlt_p2p_node.hpp | 2 +- 3 files changed, 71 insertions(+), 13 deletions(-) diff --git a/libraries/network/dlt_p2p_node.cpp b/libraries/network/dlt_p2p_node.cpp index 27ffb20cff..78c2400ea9 100644 --- a/libraries/network/dlt_p2p_node.cpp +++ b/libraries/network/dlt_p2p_node.cpp @@ -194,11 +194,17 @@ void dlt_p2p_node::close() { // ── Connection management ──────────────────────────────────────────── -// ── Per-IP dedup: find any existing active connection from the same IP ─ -dlt_p2p_node::peer_id dlt_p2p_node::find_active_peer_by_ip(const fc::ip::address& addr) const { +// ── Per-node-id dedup: find any existing active connection to the same node ─ +// We identify nodes by the node_id they advertise in their hello message. +// This correctly handles multiple nodes behind the same NAT (same IP, different +// ports) — each node has a unique keypair, so only true duplicates are rejected. +// Returns INVALID_PEER_ID for zero node_id (old peer that didn't send one). +dlt_p2p_node::peer_id dlt_p2p_node::find_active_peer_by_node_id(const node_id_t& nid) const { + static const node_id_t zero_id; + if (nid == zero_id) return INVALID_PEER_ID; for (const auto& item : _peer_states) { const auto& state = item.second; - if (state.endpoint.get_address() == addr && + if (state.node_id == nid && (state.lifecycle_state == DLT_PEER_LIFECYCLE_CONNECTING || state.lifecycle_state == DLT_PEER_LIFECYCLE_HANDSHAKING || state.lifecycle_state == DLT_PEER_LIFECYCLE_SYNCING || @@ -234,7 +240,7 @@ void dlt_p2p_node::connect_to_peer(const fc::ip::endpoint& ep) { // which causes broadcast amplification. // EXCEPTION: Allow reconnect if the target peer itself is DISCONNECTED, // even if another connection to the same IP exists (different port). - if (!found_existing) { + /* if (!found_existing) { fc::ip::address target_ip = ep.get_address(); peer_id existing_ip_conn = find_active_peer_by_ip(target_ip); if (existing_ip_conn != INVALID_PEER_ID) { @@ -242,7 +248,11 @@ void dlt_p2p_node::connect_to_peer(const fc::ip::endpoint& ep) { ("ep", ep)("pid", existing_ip_conn)); return; } - } + }*/ + // NOTE: We no longer skip outbound connections based on IP address alone. + // Multiple nodes behind the same NAT share the same public IP but have + // different P2P ports and unique node_ids. Deduplication happens post-hello + // in on_dlt_hello() where we compare node_id values. if (!found_existing) { pid = _next_peer_id++; @@ -591,7 +601,14 @@ void dlt_p2p_node::drain_send_queue(peer_id peer, std::vector buf) { void dlt_p2p_node::send_to_all_our_fork_peers(const message& msg, peer_id exclude, const block_id_type& block_id) { // Per-IP dedup: send to each unique IP only once, even if multiple // peer entries exist for the same IP (belt-and-suspenders safety net). - std::set sent_to_ips; + // Dedup by node_id: send to each unique node only once, even if multiple + // peer entries exist for the same node (e.g. duplicate connections still + // being cleaned up). We do NOT dedup by IP address — multiple distinct + // nodes can share the same NAT IP and each deserves its own copy. + // Falls back to endpoint dedup for peers without a node_id (old protocol). + std::set sent_to_node_ids; + std::set sent_to_endpoints; + static const node_id_t zero_id; // Diagnostic: count eligible vs skipped peers uint32_t eligible = 0, skipped_not_exchange = 0, skipped_not_active = 0, skipped_echo = 0, skipped_peer_syncing = 0; @@ -631,9 +648,16 @@ void dlt_p2p_node::send_to_all_our_fork_peers(const message& msg, peer_id exclud skipped_echo++; continue; } - fc::ip::address ip = state.endpoint.get_address(); - if (sent_to_ips.count(ip)) continue; // already sent to this IP - sent_to_ips.insert(ip); + // Dedup: skip if we already queued a send to the same node. + // Use node_id when available (correctly handles NAT), fall back to + // full endpoint (IP:port) for old peers without a node_id. + if (state.node_id != zero_id) { + if (sent_to_node_ids.count(state.node_id)) continue; + sent_to_node_ids.insert(state.node_id); + } else { + if (sent_to_endpoints.count(state.endpoint)) continue; + sent_to_endpoints.insert(state.endpoint); + } targets.push_back(id); eligible++; } @@ -834,6 +858,7 @@ dlt_hello_message dlt_p2p_node::build_hello_message() const { hello.has_emergency_key = _delegate->has_emergency_private_key(); hello.fork_status = _fork_status; hello.node_status = _node_status; + hello.node_id = _node_id; // identify ourselves so NAT peers can dedup by node, not IP return hello; } @@ -909,6 +934,30 @@ void dlt_p2p_node::on_dlt_hello(peer_id peer, const dlt_hello_message& hello) { wlog("Peer ${ep} has different protocol version (${theirs} vs ${ours}), disabling exchange", ("ep", state.endpoint)("theirs", their_major)("ours", our_major)); } + +// Persist node_id — used for dedup and peer-exchange identity. + state.node_id = hello.node_id; + + // ── Post-hello node_id dedup ──────────────────────────────────────────── + // Now that we know the remote node's identity, check if we already have an + // active connection to the exact same node. This correctly handles: + // • A node reconnecting before the old connection was cleaned up + // • Simultaneous inbound + outbound to the same node + // It does NOT fire for two different nodes sharing the same NAT IP, because + // each node generates a unique keypair (node_id). + static const node_id_t zero_id; + if (hello.node_id != zero_id) { + peer_id dup = find_active_peer_by_node_id(hello.node_id); + if (dup != INVALID_PEER_ID && dup != peer) { + auto dup_it = _peer_states.find(dup); + auto dup_ep = (dup_it != _peer_states.end()) ? dup_it->second.endpoint : fc::ip::endpoint(); + dlog(DLT_LOG_DGRAY "Closing duplicate connection from ${ep} " + "(same node_id already active as peer ${dup} at ${dep})" DLT_LOG_RESET, + ("ep", state.endpoint)("dup", dup)("dep", dup_ep)); + handle_disconnect(peer, "duplicate node_id"); + return; + } + } // Store peer's chain state state.peer_head_id = hello.head_block_id; @@ -3486,7 +3535,7 @@ void dlt_p2p_node::accept_loop() { continue; } - peer_id existing = find_active_peer_by_ip(incoming_ip); + /* peer_id existing = find_active_peer_by_ip(incoming_ip); if (existing != INVALID_PEER_ID) { auto ex_it = _peer_states.find(existing); auto ex_ep = (ex_it != _peer_states.end()) ? ex_it->second.endpoint : fc::ip::endpoint(); @@ -3497,7 +3546,12 @@ void dlt_p2p_node::accept_loop() { _connections.erase(pid); sock->close(); continue; - } + }*/ + // NOTE: We do NOT reject here based on IP address alone. + // Multiple nodes behind the same NAT share the same public IP but + // have different P2P ports and unique node_ids. Deduplication of + // truly-duplicate connections (same node reconnecting) is done + // post-hello in on_dlt_hello() by comparing node_id values. // Isolated-peers: only accept inbound from configured seed IPs. if (_isolated_peers) { diff --git a/libraries/network/include/graphene/network/dlt_p2p_messages.hpp b/libraries/network/include/graphene/network/dlt_p2p_messages.hpp index ba8e96d47d..4a5ddc2964 100644 --- a/libraries/network/include/graphene/network/dlt_p2p_messages.hpp +++ b/libraries/network/include/graphene/network/dlt_p2p_messages.hpp @@ -81,6 +81,10 @@ struct dlt_hello_message { bool has_emergency_key = false; uint8_t fork_status = DLT_FORK_STATUS_NORMAL; uint8_t node_status = DLT_NODE_STATUS_SYNC; + // Persistent node identity key — used to deduplicate connections from nodes + // sharing the same NAT IP (different ports). Each node generates a random + // keypair at startup. Zero-value means "unknown" (old protocol peer). + node_id_t node_id; }; // ── DLT Hello Reply ───────────────────────────────────────────────── @@ -269,7 +273,7 @@ FC_REFLECT_ENUM(graphene::network::dlt_peer_lifecycle_state, FC_REFLECT((graphene::network::dlt_hello_message), (protocol_version)(head_block_id)(head_block_num)(lib_block_id)(lib_block_num) (dlt_earliest_block)(dlt_latest_block)(emergency_active)(has_emergency_key) - (fork_status)(node_status)) + (fork_status)(node_status)(node_id)) FC_REFLECT((graphene::network::dlt_hello_reply_message), (exchange_enabled)(fork_alignment)(initiator_head_seen)(initiator_lib_seen) diff --git a/libraries/network/include/graphene/network/dlt_p2p_node.hpp b/libraries/network/include/graphene/network/dlt_p2p_node.hpp index 29840c1f83..87e5d1f594 100644 --- a/libraries/network/include/graphene/network/dlt_p2p_node.hpp +++ b/libraries/network/include/graphene/network/dlt_p2p_node.hpp @@ -273,7 +273,7 @@ class dlt_p2p_node { bool is_same_subnet(const fc::ip::address& a, const fc::ip::address& b) const; // ── Per-IP dedup ───────────────────────────────────────────── - peer_id find_active_peer_by_ip(const fc::ip::address& addr) const; + peer_id find_active_peer_by_node_id(const node_id_t& nid) const; private: dlt_p2p_delegate* _delegate = nullptr; From f12baea589d1007ee1f63a8f9562b8056570286a Mon Sep 17 00:00:00 2001 From: M0ssa99 Date: Fri, 15 May 2026 08:44:08 +0300 Subject: [PATCH 10/19] fix(p2p): enhance IP blocking logic for NAT scenarios and add backward-compatible hello message deserializer --- libraries/network/dlt_p2p_node.cpp | 77 ++++++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 8 deletions(-) diff --git a/libraries/network/dlt_p2p_node.cpp b/libraries/network/dlt_p2p_node.cpp index 78c2400ea9..deeb59a411 100644 --- a/libraries/network/dlt_p2p_node.cpp +++ b/libraries/network/dlt_p2p_node.cpp @@ -82,6 +82,25 @@ void dlt_p2p_node::set_witness_diag_provider(std::function fn) { } void dlt_p2p_node::block_incoming_ip(uint32_t ip, const std::string& reason) { + // NAT safety: if multiple active peers share this IP (nodes behind the same NAT), + // blocking the IP would kill all of them. Only block when a single peer is using + // this IP — that's the typical single-machine attacker scenario. + uint32_t peers_with_ip = 0; + for (const auto& item : _peer_states) { + const auto& s = item.second; + if ((uint32_t)s.endpoint.get_address() == ip && + (s.lifecycle_state == DLT_PEER_LIFECYCLE_CONNECTING || + s.lifecycle_state == DLT_PEER_LIFECYCLE_HANDSHAKING || + s.lifecycle_state == DLT_PEER_LIFECYCLE_SYNCING || + s.lifecycle_state == DLT_PEER_LIFECYCLE_ACTIVE)) { + ++peers_with_ip; + } + } + if (peers_with_ip > 1) { + wlog(DLT_LOG_ORANGE "NAT: NOT blocking IP ${ip} (${n} active peers share this IP) — reason was: ${r}" DLT_LOG_RESET, + ("ip", std::string(fc::ip::address(ip)))("n", peers_with_ip)("r", reason)); + return; // Don't punish NAT peers for one misbehaving connection + } fc::time_point unblock_at = fc::time_point::now() + fc::seconds(BLOCKED_IP_DURATION_SEC); _blocked_ips[ip] = unblock_at; wlog(DLT_LOG_ORANGE "Blocking IP ${ip} for ${d}s: ${r}" DLT_LOG_RESET, @@ -194,6 +213,42 @@ void dlt_p2p_node::close() { // ── Connection management ──────────────────────────────────────────── +// ── Backward-compatible hello deserializer ─────────────────────────────────── +// FC_REFLECT-based deserialization (msg.as()) expects ALL +// fields to be present in the byte stream. Old nodes (protocol_version=1, +// 62-byte payload) do NOT include the node_id field (added in v2, 33 bytes). +// Calling msg.as<>() on such a message throws out_of_range_exception ("over by 1" +// — the first byte of the missing node_id cannot be read). +// +// This helper deserializes field-by-field and treats node_id as OPTIONAL: +// it is read only when the stream still has >= sizeof(node_id_t) bytes left. +// Unknown trailing bytes (future fields) are silently ignored. +static dlt_hello_message unpack_hello_compat(const message& msg) { + FC_ASSERT(msg.msg_type == dlt_hello_message_type); + dlt_hello_message hello; + if (msg.data.empty()) return hello; + fc::datastream ds(msg.data.data(), msg.data.size()); + fc::raw::unpack(ds, hello.protocol_version); + fc::raw::unpack(ds, hello.head_block_id); + fc::raw::unpack(ds, hello.head_block_num); + fc::raw::unpack(ds, hello.lib_block_id); + fc::raw::unpack(ds, hello.lib_block_num); + fc::raw::unpack(ds, hello.dlt_earliest_block); + fc::raw::unpack(ds, hello.dlt_latest_block); + fc::raw::unpack(ds, hello.emergency_active); + fc::raw::unpack(ds, hello.has_emergency_key); + fc::raw::unpack(ds, hello.fork_status); + fc::raw::unpack(ds, hello.node_status); + // node_id (sizeof = 33 bytes for compressed secp256k1 key) is optional: + // - absent → old protocol (v1 peer, 62-byte hello) — treat as zero_id + // - present → new protocol (v2+ peer, 95-byte hello) — use for NAT dedup + if (ds.remaining() >= sizeof(node_id_t)) { + fc::raw::unpack(ds, hello.node_id); + } + // Any remaining bytes are future protocol fields — ignored for forward compat. + return hello; +} + // ── Per-node-id dedup: find any existing active connection to the same node ─ // We identify nodes by the node_id they advertise in their hello message. // This correctly handles multiple nodes behind the same NAT (same IP, different @@ -745,7 +800,9 @@ bool dlt_p2p_node::on_message(peer_id peer, const message& msg) { try { switch (msg.msg_type) { case dlt_hello_message_type: - on_dlt_hello(peer, msg.as()); + // unpack_hello_compat handles both v1 (no node_id, 62 bytes) and + // v2+ (with node_id, 95 bytes) — avoids out_of_range_exception. + on_dlt_hello(peer, unpack_hello_compat(msg)); break; case dlt_hello_reply_message_type: on_dlt_hello_reply(peer, msg.as()); @@ -847,7 +904,7 @@ bool dlt_p2p_node::on_message(peer_id peer, const message& msg) { dlt_hello_message dlt_p2p_node::build_hello_message() const { dlt_hello_message hello; - hello.protocol_version = 1; + hello.protocol_version = 1; // keep at 1 for backward compat — node_id is read optionally by unpack_hello_compat hello.head_block_id = _delegate->get_head_block_id(); hello.head_block_num = _delegate->get_head_block_num(); hello.lib_block_id = _delegate->get_lib_block_id(); @@ -927,15 +984,19 @@ void dlt_p2p_node::on_dlt_hello(peer_id peer, const dlt_hello_message& hello) { if (it == _peer_states.end()) return; auto& state = it->second; - // Protocol version check - uint16_t our_major = 1; // current protocol version + // Protocol version check. + // We stay at v1 for wire compatibility — old nodes reject mismatched versions. + // node_id (added in this codebase) is parsed optionally by unpack_hello_compat, + // so the version number does not need to change. + // Log unknown future versions but don't disconnect — be forward-compatible. + uint16_t our_major = 1; uint16_t their_major = hello.protocol_version; if (their_major != our_major) { - wlog("Peer ${ep} has different protocol version (${theirs} vs ${ours}), disabling exchange", + wlog("Peer ${ep} has unexpected protocol version ${theirs} (ours=${ours}) — continuing anyway", ("ep", state.endpoint)("theirs", their_major)("ours", our_major)); } - -// Persist node_id — used for dedup and peer-exchange identity. + + // Persist node_id — used for dedup and peer-exchange identity. state.node_id = hello.node_id; // ── Post-hello node_id dedup ──────────────────────────────────────────── @@ -3717,4 +3778,4 @@ void dlt_p2p_node::start_read_loop(peer_id peer) { } } // namespace network -} // namespace graphene +} // namespace graphene \ No newline at end of file From e4c6adda188ca2ff39bb92ff25e268ddc1ee2239 Mon Sep 17 00:00:00 2001 From: M0ssa99 Date: Sat, 16 May 2026 01:08:29 +0300 Subject: [PATCH 11/19] fix(p2p): implement dynamic throttling for peer exchange requests to optimize performance --- libraries/network/dlt_p2p_node.cpp | 35 ++++++++++++++++++- .../include/graphene/network/dlt_p2p_node.hpp | 3 ++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/libraries/network/dlt_p2p_node.cpp b/libraries/network/dlt_p2p_node.cpp index deeb59a411..51852369d4 100644 --- a/libraries/network/dlt_p2p_node.cpp +++ b/libraries/network/dlt_p2p_node.cpp @@ -3383,7 +3383,11 @@ void dlt_p2p_node::periodic_peer_exchange() { if (_isolated_peers) return; if (_node_status != DLT_NODE_STATUS_FORWARD) return; - // Pick a random active peer to request exchange from + // Pick a random active peer to request exchange from. + // When only one exchange-enabled peer exists (common for nodes behind NAT + // or freshly started nodes), all requests go to that single peer and hit + // the 3/300s rate-limit quickly. Back off to one request per 90s in that + // case so we never exceed the limit (3 requests / 300s = 1 per 100s max). std::vector candidates; for (auto& _peer_item : _peer_states) { auto& id = _peer_item.first; @@ -3397,6 +3401,35 @@ void dlt_p2p_node::periodic_peer_exchange() { if (candidates.empty()) return; + // Dynamic throttle: ensure no single peer is asked more than 3 times per 300s. + // + // With N exchange-enabled peers and a random pick each loop (5s interval): + // requests per peer per 300s ≈ 300s / 5s / N = 60 / N + // rate-limit threshold = 3 requests / 300s + // safe minimum loop interval = 300s / (3 × N) = 100s / N + // + // Examples: + // N=1 → min interval 100s (was hardcoded 90s, now exact) + // N=2 → min interval 50s + // N=5 → min interval 20s + // N=20 → min interval 5s (≥ loop tick, no extra throttle needed) + // + // We track _last_peer_exchange globally; the random peer pick spreads + // load evenly across candidates so this global gate is sufficient. + { + size_t n = candidates.size(); + int64_t min_interval_us = (n >= 20) + ? 0LL + : static_cast(100'000'000LL / static_cast(n)); // 100s / N in microseconds + if (min_interval_us > 0) { + auto now = fc::time_point::now(); + if ((now - _last_peer_exchange_time).count() < min_interval_us) return; + _last_peer_exchange_time = now; + } + } + + + thread_local std::mt19937 peer_rng(std::hash{}(std::this_thread::get_id()) ^ uint32_t(fc::time_point::now().sec_since_epoch())); size_t idx = peer_rng() % candidates.size(); send_message(candidates[idx], message(dlt_peer_exchange_request())); diff --git a/libraries/network/include/graphene/network/dlt_p2p_node.hpp b/libraries/network/include/graphene/network/dlt_p2p_node.hpp index 87e5d1f594..0686671581 100644 --- a/libraries/network/include/graphene/network/dlt_p2p_node.hpp +++ b/libraries/network/include/graphene/network/dlt_p2p_node.hpp @@ -432,6 +432,9 @@ class dlt_p2p_node { static constexpr uint32_t BLOCKED_IP_DURATION_SEC = 3600; // 1 hour void block_incoming_ip(uint32_t ip, const std::string& reason); bool is_ip_blocked(uint32_t ip); + // Last time a peer exchange request was sent — used by periodic_peer_exchange() + // to dynamically throttle based on the number of active peers (see impl). + fc::time_point _last_peer_exchange_time; // ── Diagnostics ─────────────────────────────────────────────── fc::time_point _node_start_time; From 1ed1eed20286ecd682a258cb8389cafaa8e7a968 Mon Sep 17 00:00:00 2001 From: M0ssa99 Date: Sun, 17 May 2026 11:29:14 +0300 Subject: [PATCH 12/19] fix(build): adjust make parallel jobs to improve build performance fix(network): enhance connection error handling for Windows-specific cases fix(cmake): update bcrypt linking for MinGW and improve compatibility --- CMakeLists.txt | 13 +++++++++---- libraries/network/dlt_p2p_node.cpp | 22 +++++++++++++++++++--- share/vizd/docker/Dockerfile-production | 2 +- thirdparty/fc | 2 +- 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fe1424b09a..d9d67afbac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,8 +1,7 @@ # Defines VIZ library target. project(VIZ) -if(MINGW) - link_libraries(bcrypt) -endif() +# Note: bcrypt (needed by boost::uuid / random on MinGW) is linked per-target +# in the if(WIN32) block below to avoid polluting secp256k1 and other sub-targets. cmake_minimum_required(VERSION 3.16) set(CHAIN_NAME "VIZ") @@ -106,13 +105,17 @@ if(WIN32) set(DB_VERSION 60) set(BDB_STATIC_LIBS 1) + # Minimum Windows 7 / Vista (0x0601). XP (0x0501) is too old for + # boost::asio extended Winsock2 APIs and modern async I/O. + add_compile_definitions(_WIN32_WINNT=0x0601 WINVER=0x0601) + set(ZLIB_LIBRARIES "") set(DEFAULT_EXECUTABLE_INSTALL_DIR bin/) set(CRYPTO_LIB) if(MSVC) - add_compile_options(/wd4503 /wd4267 /wd4244 /EHsc) + add_compile_options(/wd4503 /wd4267 /wd4244 /EHa) #looks like this flag can have different default on some machines. set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} /SAFESEH:NO") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /SAFESEH:NO") @@ -144,6 +147,8 @@ if(WIN32) if(FULL_STATIC_BUILD) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static-libstdc++ -static-libgcc") endif(FULL_STATIC_BUILD) + # bcrypt: needed by boost::random/uuid on MinGW (provides BCryptGenRandom) + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -lbcrypt") endif(MSVC) else(WIN32) # Apple AND Linux diff --git a/libraries/network/dlt_p2p_node.cpp b/libraries/network/dlt_p2p_node.cpp index 03c4b11d24..8225d39395 100644 --- a/libraries/network/dlt_p2p_node.cpp +++ b/libraries/network/dlt_p2p_node.cpp @@ -363,11 +363,17 @@ void dlt_p2p_node::connect_to_peer(const fc::ip::endpoint& ep) { std::string detail = e.to_detail_string(); bool is_expected = (detail.find("Connection refused") != std::string::npos) || (detail.find("connection refused") != std::string::npos) + || (detail.find("actively refused") != std::string::npos) // Windows WSA 10061 || (detail.find("Connection timed out") != std::string::npos) + || (detail.find("timed out") != std::string::npos) || (detail.find("Host unreachable") != std::string::npos) + || (detail.find("host unreachable") != std::string::npos) // Windows WSA 10065 || (detail.find("No route to host") != std::string::npos) + || (detail.find("network is unreachable") != std::string::npos) // Windows WSA 10051 || (detail.find("End of file") != std::string::npos) - || (detail.find("Operation aborted") != std::string::npos); + || (detail.find("end of file") != std::string::npos) + || (detail.find("Operation aborted") != std::string::npos) + || (detail.find("operation aborted") != std::string::npos); // Windows WSA 10004 if (is_expected) dlog(DLT_LOG_DGRAY "Connect to ${ep} failed: ${w}" DLT_LOG_RESET, ("ep", ep)("w", e.what())); else @@ -3789,16 +3795,26 @@ void dlt_p2p_node::start_read_loop(peer_id peer) { const auto& detail = e.to_detail_string(); bool is_transient = detail.find("Connection reset by peer") != std::string::npos || + detail.find("forcibly closed") != std::string::npos || // Windows WSA 10054 detail.find("Connection refused") != std::string::npos || + detail.find("actively refused") != std::string::npos || // Windows WSA 10061 detail.find("Broken pipe") != std::string::npos || + detail.find("connection was aborted") != std::string::npos || // Windows WSA 10053 detail.find("end of stream") != std::string::npos || + detail.find("End of file") != std::string::npos || detail.find("Operation aborted") != std::string::npos || + detail.find("operation aborted") != std::string::npos || // Windows WSA 10004 detail.find("Network is unreachable") != std::string::npos || + detail.find("network is unreachable") != std::string::npos || // Windows WSA 10051 detail.find("No route to host") != std::string::npos || detail.find("Connection timed out") != std::string::npos || - detail.find("Host is unreachable") != std::string::npos; + detail.find("timed out") != std::string::npos || + detail.find("Host is unreachable") != std::string::npos || + detail.find("host unreachable") != std::string::npos; // Windows WSA 10065 bool is_benign_close = - detail.find("Bad file descriptor") != std::string::npos; + detail.find("Bad file descriptor") != std::string::npos || + detail.find("bad file descriptor") != std::string::npos || + detail.find("invalid argument") != std::string::npos; // Windows: closed socket reuse if (is_benign_close) { dlog(DLT_LOG_DGRAY "Peer ${ep} read canceled (socket already closed)" DLT_LOG_RESET, diff --git a/share/vizd/docker/Dockerfile-production b/share/vizd/docker/Dockerfile-production index 82d40b2313..f15ce99473 100644 --- a/share/vizd/docker/Dockerfile-production +++ b/share/vizd/docker/Dockerfile-production @@ -103,7 +103,7 @@ RUN --mount=type=cache,target=/root/.ccache,id=viz-ccache,sharing=locked \ -DCHAINBASE_CHECK_LOCKING=FALSE \ .. \ && \ - make -j$(nproc) vizd cli_wallet && \ + make -j$(($(nproc)-20)) vizd cli_wallet && \ ccache -s # `make install` would re-trigger the `all` target through CMake's install-depends-on-all diff --git a/thirdparty/fc b/thirdparty/fc index 823cb68dd3..2ca55b0d6c 160000 --- a/thirdparty/fc +++ b/thirdparty/fc @@ -1 +1 @@ -Subproject commit 823cb68dd3fa044c657e038ab7c42393297e51ff +Subproject commit 2ca55b0d6c9acf2901667a96c7b8fe7a34dd9261 From 6df5c6b83eb94d5247b2842ba531378d29c2cd86 Mon Sep 17 00:00:00 2001 From: M0ssa99 Date: Sun, 17 May 2026 11:42:45 +0300 Subject: [PATCH 13/19] fix(submodule): update fc submodule URL and branch for compatibility --- .gitmodules | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index 651e1a03df..88f8e871fd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,7 @@ [submodule "thirdparty/fc"] path = thirdparty/fc - url = https://github.com/VIZ-Blockchain/fc.git - branch = update + url = https://github.com/m0ssa99/fc.git + branch = windows_suport_fc [submodule "thirdparty/chainbase"] path = thirdparty/chainbase url = https://github.com/VIZ-Blockchain/chainbase.git From ef6835e2917e68e9235f287c0c722abec8fbbda4 Mon Sep 17 00:00:00 2001 From: M0ssa99 Date: Sun, 17 May 2026 17:26:43 +0300 Subject: [PATCH 14/19] fix(cmake): add blank line for readability and update fc submodule to latest commit --- CMakeLists.txt | 1 + thirdparty/fc | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d9d67afbac..1726d96f0d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,6 +6,7 @@ cmake_minimum_required(VERSION 3.16) set(CHAIN_NAME "VIZ") + set(GUI_CLIENT_EXECUTABLE_NAME VIZ) set(CUSTOM_URL_SCHEME "gcs") set(INSTALLER_APP_ID "68ad7005-8eee-49c9-95ce-9eed97e5b347") diff --git a/thirdparty/fc b/thirdparty/fc index 2ca55b0d6c..ba963ba9b5 160000 --- a/thirdparty/fc +++ b/thirdparty/fc @@ -1 +1 @@ -Subproject commit 2ca55b0d6c9acf2901667a96c7b8fe7a34dd9261 +Subproject commit ba963ba9b5ed33aa15f745b1ce065463d0df4357 From 873da8a2b43eb5aa994727ae513d0777f1c6dccd Mon Sep 17 00:00:00 2001 From: M0ssa99 Date: Sun, 17 May 2026 17:40:19 +0300 Subject: [PATCH 15/19] fix(submodule): update fc submodule to latest commit --- thirdparty/fc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thirdparty/fc b/thirdparty/fc index ba963ba9b5..7c34f4d5b2 160000 --- a/thirdparty/fc +++ b/thirdparty/fc @@ -1 +1 @@ -Subproject commit ba963ba9b5ed33aa15f745b1ce065463d0df4357 +Subproject commit 7c34f4d5b225803766fdc8c343afc165fcd6da6e From 578fbee6528b5e61e851cb957e822ce1aea5cc16 Mon Sep 17 00:00:00 2001 From: M0ssa99 Date: Sun, 17 May 2026 18:34:23 +0300 Subject: [PATCH 16/19] fix(submodule): update fc submodule to latest commit --- thirdparty/fc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thirdparty/fc b/thirdparty/fc index 7c34f4d5b2..d7e582edd7 160000 --- a/thirdparty/fc +++ b/thirdparty/fc @@ -1 +1 @@ -Subproject commit 7c34f4d5b225803766fdc8c343afc165fcd6da6e +Subproject commit d7e582edd7cbe6d3eba485a4050e8a227554cd59 From 1e1081a0025fdb5888fd6f58a30d5610cec279c5 Mon Sep 17 00:00:00 2001 From: M0ssa99 Date: Sun, 17 May 2026 18:43:01 +0300 Subject: [PATCH 17/19] fix(submodule): update fc submodule to latest commit with dirty state --- thirdparty/fc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thirdparty/fc b/thirdparty/fc index d7e582edd7..344ef7e48d 160000 --- a/thirdparty/fc +++ b/thirdparty/fc @@ -1 +1 @@ -Subproject commit d7e582edd7cbe6d3eba485a4050e8a227554cd59 +Subproject commit 344ef7e48db0b1de6bc971d48ea27d37bc579161 From 89a77f47a05a65481b65db38fb50358a27d67ebd Mon Sep 17 00:00:00 2001 From: M0ssa99 Date: Sun, 17 May 2026 18:49:47 +0300 Subject: [PATCH 18/19] fix(submodule): update fc submodule to latest commit with dirty state --- thirdparty/fc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thirdparty/fc b/thirdparty/fc index 344ef7e48d..880645ec8e 160000 --- a/thirdparty/fc +++ b/thirdparty/fc @@ -1 +1 @@ -Subproject commit 344ef7e48db0b1de6bc971d48ea27d37bc579161 +Subproject commit 880645ec8edaa6d88d1840c2aee569a6b5986d78 From 84c910d584b5a4005abdd1ec3222daef8a0d0a62 Mon Sep 17 00:00:00 2001 From: M0ssa99 Date: Sun, 17 May 2026 18:54:40 +0300 Subject: [PATCH 19/19] Update submodule fc to my fork --- .gitmodules | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitmodules b/.gitmodules index 88f8e871fd..d33a5d0c70 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,4 @@ path = thirdparty/appbase url = https://github.com/VIZ-Blockchain/appbase.git branch = lib-boost-1.71 + \ No newline at end of file