Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ jobs:
core_test sharechain_test share_test btc_share_test \
test_threading test_weights \
test_header_chain test_mempool test_template_builder \
test_doge_chain test_compact_blocks test_dash_x11_kat test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_broadcaster_full test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer test_dash_embedded_relay_e2e test_dash_block_relay_plan test_dash_version_activation_latch test_dash_block_relay_binding test_dash_block_relay_dual_arm test_dash_coinbase_parity test_dash_donation_combined test_dash_g3_assembled test_dash_work_target test_dash_work_job_targets test_dash_stratum_binding test_dash_job_notify_roundtrip test_dash_cb_payee test_dash_stratum_notify_roundtrip test_dash_poolnode_messages test_dash_peer test_dash_share_tracker test_dash_node test_dash_share_messages test_dash_stratum_extranonce_split test_dash_stratum_submit_reassembly test_dash_difficulty_parity test_dash_auto_ratchet \
test_doge_chain test_compact_blocks test_dash_x11_kat test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_broadcaster_full test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer test_dash_embedded_relay_e2e test_dash_block_relay_plan test_dash_version_activation_latch test_dash_block_relay_binding test_dash_block_relay_dual_arm test_dash_coinbase_parity test_dash_donation_combined test_dash_g3_assembled test_dash_work_target test_dash_work_job_targets test_dash_stratum_binding test_dash_job_notify_roundtrip test_dash_cb_payee test_dash_stratum_notify_roundtrip test_dash_poolnode_messages test_dash_peer test_dash_share_tracker test_dash_node test_dash_share_messages test_dash_stratum_extranonce_split test_dash_stratum_submit_reassembly test_dash_difficulty_parity test_dash_auto_ratchet test_dash_min_protocol_gate \
test_multiaddress_pplns test_pplns_stress \
test_hash_link test_decay_pplns \
test_pplns_consensus \
Expand Down Expand Up @@ -252,7 +252,7 @@ jobs:
test_threading test_weights \
test_header_chain test_mempool test_template_builder \
test_doge_chain test_compact_blocks test_dash_x11_kat \
test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_broadcaster_full test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer test_dash_embedded_relay_e2e test_dash_block_relay_plan test_dash_version_activation_latch test_dash_block_relay_binding test_dash_block_relay_dual_arm test_dash_coinbase_parity test_dash_donation_combined test_dash_g3_assembled test_dash_work_target test_dash_work_job_targets test_dash_stratum_binding test_dash_job_notify_roundtrip test_dash_cb_payee test_dash_stratum_notify_roundtrip test_dash_poolnode_messages test_dash_peer test_dash_share_tracker test_dash_node test_dash_share_messages test_dash_stratum_extranonce_split test_dash_stratum_submit_reassembly test_dash_difficulty_parity test_dash_auto_ratchet \
test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_broadcaster_full test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer test_dash_embedded_relay_e2e test_dash_block_relay_plan test_dash_version_activation_latch test_dash_block_relay_binding test_dash_block_relay_dual_arm test_dash_coinbase_parity test_dash_donation_combined test_dash_g3_assembled test_dash_work_target test_dash_work_job_targets test_dash_stratum_binding test_dash_job_notify_roundtrip test_dash_cb_payee test_dash_stratum_notify_roundtrip test_dash_poolnode_messages test_dash_peer test_dash_share_tracker test_dash_node test_dash_share_messages test_dash_stratum_extranonce_split test_dash_stratum_submit_reassembly test_dash_difficulty_parity test_dash_auto_ratchet test_dash_min_protocol_gate \
test_hash_link test_decay_pplns \
test_pplns_consensus \
test_v36_script_sorting test_v36_cross_impl_refhash \
Expand Down
53 changes: 53 additions & 0 deletions src/impl/dash/min_protocol_gate.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#pragma once

// DASH sharechain minimum-protocol-version gate (v36 min-proto ratchet, #643).
//
// SCOPE: a parameterized, per-instance policy for the reject-if-proto<floor
// decision at the version handshake. This leaf carries ONLY the policy object
// and its accept/reject predicate; the LIVE call site in NodeImpl::handle_version
// is the reception/handshake slice (B) and is deliberately out of scope here.
//
// RATCHET SEMANTICS (operator #643, integrator-concurred 2026-07-06 option a):
// - The floor defaults to dash::SharechainConfig::MINIMUM_PROTOCOL_VERSION
// (1700), which is the current DASH oracle accept-all floor. At the default
// this gate is a NO-OP: a peer advertising protocol=1700 MUST be accepted.
// - The floor is a per-instance MEMBER, not a baked-in constant, so the actual
// v36 ratchet value is a one-line operator knob set at G2-migration time.
// This leaf commits NO premature v36 constant (DASH share ver 16->36 is
// older-than-v35; the floor is a chosen constant, not an oracle-derived one).
//
// ISOLATION: fenced to src/impl/dash/. Touches NO shared bitcoin_family / core
// version-handshake types (per-coin isolation primitive boundary).

#include <cstdint>

#include "config_pool.hpp"

namespace dash
{

// Per-instance minimum-protocol-version policy. Default-constructed it holds the
// oracle accept-all floor (1700) and accepts every peer at or above it.
struct MinProtocolGate
{
// The rejection floor. A peer is admitted iff peer_protocol >= min_version.
uint32_t min_version = SharechainConfig::MINIMUM_PROTOCOL_VERSION;

MinProtocolGate() = default;
explicit MinProtocolGate(uint32_t floor) : min_version(floor) {}

// reject-if-proto<floor predicate. True => admit the peer.
// At the default floor (1700) this is accept-all for every real DASH peer.
[[nodiscard]] bool accepts(uint32_t peer_protocol) const
{
return peer_protocol >= min_version;
}

// Convenience inverse for handshake sites that branch on rejection.
[[nodiscard]] bool rejects(uint32_t peer_protocol) const
{
return !accepts(peer_protocol);
}
};

} // namespace dash
98 changes: 92 additions & 6 deletions src/impl/dash/node.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
#include "share_chain.hpp"
#include "share_tracker.hpp"
#include "peer.hpp"
#include "min_protocol_gate.hpp"
#include "messages.hpp"
#include "coin/transaction.hpp"

#include <pool/node.hpp>
Expand Down Expand Up @@ -208,19 +210,103 @@ class NodeImpl : public pool::BaseNode<dash::Config, dash::ShareChain, dash::Pee
// pure-virtual INetwork contract.
void disconnect() override { }

// ICommunicator: the message-dispatch entry point. Slice A drops inbound
// messages (no-op) to satisfy the pure-virtual contract; the reception slice
// (B) replaces this with the real Legacy/Actual protocol routing.
void handle(std::unique_ptr<RawMessage> /*rmsg*/, const NetService& /*service*/) override { }
// ICommunicator: the message-dispatch entry point. Slice B wires the real
// version-handshake routing so handle_version() (and its live #646 gate) is
// actually reachable -- mirrors the pool::NodeBridge unknown-peer branch
// (src/pool/node.hpp). Full Legacy/Actual established-peer message dispatch
// (shares, txs, getaddrs) rides a later slice; until then, post-handshake
// messages are dropped. The handshake + min-protocol discrimination is live.
void handle(std::unique_ptr<RawMessage> rmsg, const NetService& service) override
{
// Guard: the peer may have been removed by a prior error/timeout while an
// async_read callback was still in-flight for the same socket.
if (!m_connections.contains(service))
return;
auto peer = m_connections[service];
peer->m_timeout->restart();

if (peer->type() == pool::PeerConnectionType::unknown)
{
// The first message from an unknown peer MUST be `version`.
if (rmsg->m_command.compare(0, 7, "version") != 0)
return error("expected version message", service);

std::optional<pool::PeerConnectionType> peer_type;
try {
peer_type = handle_version(std::move(rmsg), peer);
} catch (const std::exception& ex) {
error(ex.what(), service);
return;
}
if (!peer_type.has_value()) {
// self- or duplicate-connection: graceful close.
close_connection(service);
return;
}
peer->stable(*peer_type, PEER_TIMEOUT_TIME);
return;
}

// Established peer: full Legacy/Actual message dispatch rides a later
// slice; post-handshake messages are dropped for now.
}

// BaseNode pure-virtuals. Slice A provides minimal stubs so NodeImpl is a
// concrete, instantiable type for the KAT; the reception/handshake slice (B)
// replaces these with the real ping + version handshake.
void send_ping(peer_ptr /*peer*/) override { }

// #646 min-protocol ratchet gate -- LIVE per-instance floor. Default-
// constructed it holds the oracle accept-all floor (1700), so at the default
// this gate is a no-op; the operator lifts min_version at G2-migration time
// and this reception then rejects any peer advertising a sub-floor protocol.
dash::MinProtocolGate m_min_protocol_gate;

// Slice B: real version-handshake reception. Mirrors the ltc::NodeImpl
// reference (src/impl/ltc/node.cpp:265) reconciled to DASH's header-only
// node and message set. Parses message_version, guards self- and duplicate
// connections, fires the #646 min-protocol discrimination, registers the
// peer, and returns the negotiated connection type. The share-download /
// getaddrs machinery LTC drives here is not yet present on the DASH node and
// rides a later slice; this reception is what flips G2 dual-pool from a
// dormant seam to a live cross-peer.
std::optional<pool::PeerConnectionType>
handle_version(std::unique_ptr<RawMessage> /*rmsg*/, peer_ptr /*peer*/) override
handle_version(std::unique_ptr<RawMessage> rmsg, peer_ptr peer) override
{
return std::nullopt;
auto msg = dash::message_version::make(rmsg->m_data);

if (peer->m_other_version.has_value())
throw std::runtime_error("more than one version message");

peer->m_other_version = msg->m_version;
peer->m_other_subversion = msg->m_subversion;

// Self-connection guard: we dialled our own advertised nonce.
if (m_nonce == msg->m_nonce)
return std::nullopt;

// Duplicate-connection guard: already peered with this nonce.
if (m_peers.contains(msg->m_nonce))
return std::nullopt;

// #646 min-protocol ratchet -- LIVE discrimination. Throwing here makes
// the NodeBridge close the connection (pool/node.hpp handle()). At the
// default floor (1700) every real DASH peer is admitted (accept-all).
if (m_min_protocol_gate.rejects(msg->m_version))
throw std::runtime_error("peer protocol below min-protocol floor");

peer->m_nonce = msg->m_nonce;
m_peers[peer->m_nonce] = peer;

// Legacy/actual split for the G2 dual-pool: only once the operator has
// ratcheted the floor above the oracle baseline do at-or-above-floor
// peers negotiate the actual (v36) protocol; otherwise legacy (parity
// with the ltc reference, which returns legacy unconditionally).
const bool ratcheted =
m_min_protocol_gate.min_version > SharechainConfig::MINIMUM_PROTOCOL_VERSION;
return (ratcheted && msg->m_version >= m_min_protocol_gate.min_version)
? pool::PeerConnectionType::actual
: pool::PeerConnectionType::legacy;
}

// ── Tracker accessors ───────────────────────────────────────────────
Expand Down
12 changes: 12 additions & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,18 @@ if (BUILD_TESTING AND GTest_FOUND)
target_link_libraries(test_dash_conformance PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining) # OBJECT-lib SCC direct-naming (#22/#39): core stratum/web_server TUs
gtest_add_tests(test_dash_conformance "" AUTO)

# DASH min-protocol-version ratchet gate KAT (#643, option a): pure, header-only
# policy object; no socket/ltc/pool dependency. Mirrors the test_dash_conformance link set.
add_executable(test_dash_min_protocol_gate test_dash_min_protocol_gate.cpp)
target_link_libraries(test_dash_min_protocol_gate PRIVATE
GTest::gtest_main GTest::gtest
dash_x11 core
nlohmann_json::nlohmann_json
${Boost_LIBRARIES}
)
target_link_libraries(test_dash_min_protocol_gate PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining) # OBJECT-lib SCC direct-naming (#22/#39): core stratum/web_server TUs
gtest_add_tests(test_dash_min_protocol_gate "" AUTO)

# DASH S8 pool-node leaf 4 — share_tracker.hpp KAT: DensePPLNSWindow decayed
# weight ring buffer (compute_v36_weights), decay table, donation split,
# window slides. PoW-independent consensus core; socket-free, chain-free.
Expand Down
64 changes: 64 additions & 0 deletions test/test_dash_min_protocol_gate.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// KAT for the DASH min-protocol-version ratchet gate (#643, option a).
//
// Pins the two invariants integrator required for the SAFE-ADDITIVE leaf:
// 1. NO-OP AT DEFAULT: a default-constructed gate carries the oracle floor
// (1700) and MUST admit a peer at protocol=1700. Nobody may later read the
// landed leaf as an already-live ratchet.
// 2. PARAMETERIZED, NOT BAKED: the floor is a per-instance member, so raising
// it rejects below-floor peers -- proving the ratchet MECHANISM works while
// the default stays inert.

#include <gtest/gtest.h>

#include "impl/dash/config_pool.hpp"
#include "impl/dash/min_protocol_gate.hpp"

using dash::MinProtocolGate;
using dash::SharechainConfig;

// (1) Default gate == oracle accept-all floor, and it is a genuine no-op there.
TEST(DashMinProtocolGate, DefaultFloorIsOracleAcceptAll)
{
MinProtocolGate gate;
EXPECT_EQ(gate.min_version, SharechainConfig::MINIMUM_PROTOCOL_VERSION);
EXPECT_EQ(gate.min_version, 1700u);

// A peer AT the floor MUST be accepted -- this is the no-op assertion.
EXPECT_TRUE(gate.accepts(1700u));
EXPECT_FALSE(gate.rejects(1700u));

// Real DASH peers advertise >= 1700, so the default admits every one.
EXPECT_TRUE(gate.accepts(1700u));
EXPECT_TRUE(gate.accepts(1800u));
EXPECT_TRUE(gate.accepts(70210u));
}

// (2) Below the default floor is rejected -- boundary is >=, not >.
TEST(DashMinProtocolGate, BelowDefaultFloorRejected)
{
MinProtocolGate gate;
EXPECT_FALSE(gate.accepts(1699u));
EXPECT_TRUE(gate.rejects(1699u));
EXPECT_FALSE(gate.accepts(0u));
// Exact boundary: floor-1 out, floor in.
EXPECT_FALSE(gate.accepts(gate.min_version - 1));
EXPECT_TRUE(gate.accepts(gate.min_version));
}

// (3) The floor is a settable per-instance member -- the ratchet mechanism.
// Raising it to a hypothetical v36 value rejects the old-floor peer, but
// this is a per-instance choice, NOT a committed constant.
TEST(DashMinProtocolGate, RatchetIsParameterizedNotBaked)
{
// Operator knob: raise the floor at G2-migration time.
MinProtocolGate ratcheted(1800u);
EXPECT_EQ(ratcheted.min_version, 1800u);
EXPECT_FALSE(ratcheted.accepts(1700u)); // old-floor peer now rejected
EXPECT_TRUE(ratcheted.accepts(1800u)); // at-new-floor peer admitted

// The default gate is UNAFFECTED -- proving the ratchet is per-instance and
// the leaf commits no premature v36 constant.
MinProtocolGate defaulted;
EXPECT_TRUE(defaulted.accepts(1700u));
EXPECT_EQ(defaulted.min_version, 1700u);
}
Loading