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
29 changes: 29 additions & 0 deletions src/impl/dgb/coin/desired_version_tally.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,33 @@ inline uint32_t ratchet_min_protocol_version(
return current_floor;
}

// Runtime WIRING decision -- the call-site guard the pure ratchet above deliberately
// omits. ratchet_min_protocol_version mirrors ONLY the oracle dict-branch
// (data.py:861): over an empty/partial window it ratchets (0 >= 0), because the
// oracle's None / full-window guard lives at the CALL SITE (main.py:212):
// if len(shares) > CHAIN_LENGTH:
// counts = get_desired_version_counts(tracker,
// get_nth_parent_hash(previous_share.hash, CHAIN_LENGTH*9//10), CHAIN_LENGTH//10)
// update_min_protocol_version(counts, best_share)
// Without this guard a fresh node (parent_height < CHAIN_LENGTH -> empty window) would
// spuriously lift the floor to 3500 and reject every legitimate peer. This function is
// that guard: no lift until a FULL window exists behind the best share's parent, then
// delegate to the pure ratchet over the [CHAIN_LENGTH*9/10, CHAIN_LENGTH] window the
// caller sampled (the SAME window as the 60% version-switch gate, share_check step 2).
inline uint32_t apply_min_protocol_ratchet_decision(
int32_t parent_height,
int32_t chain_length,
const std::map<uint64_t, uint288>& window_weights,
int64_t best_version,
uint32_t current_floor,
uint32_t target_floor)
{
if (current_floor >= target_floor) // oracle guard: minpver < newminpver
return current_floor;
if (parent_height < chain_length) // oracle main.py:212: len(shares) > CHAIN_LENGTH
return current_floor; // no full window yet -> never lift
return ratchet_min_protocol_version(
window_weights, best_version, current_floor, target_floor);
}

} // namespace dgb
66 changes: 63 additions & 3 deletions src/impl/dgb/node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -309,12 +309,17 @@ std::optional<pool::PeerConnectionType> NodeImpl::handle_version(std::unique_ptr
peer->write(std::move(getaddrs_msg));
}

// Reject peers running too-old protocol
if (msg->m_version < m_tracker.m_params->minimum_protocol_version)
// Reject peers running too-old protocol. The floor is the RUNTIME
// ratcheted value (cold 1400, lifted to 3500 once >=95% of window work
// desires the best share's version -- apply_min_protocol_ratchet), NOT the
// immutable config floor. Read lock-free; compute thread publishes via store.
const uint32_t min_proto =
m_runtime_min_protocol_version.load(std::memory_order_relaxed);
if (msg->m_version < min_proto)
{
LOG_WARNING << "Peer " << msg->m_addr_from.m_endpoint.to_string()
<< " protocol " << msg->m_version
<< " < minimum " << m_tracker.m_params->minimum_protocol_version
<< " < minimum " << min_proto
<< ", disconnecting";
throw std::runtime_error("peer protocol too old");
}
Expand Down Expand Up @@ -806,6 +811,58 @@ void NodeImpl::notify_local_share(const uint256& share_hash)
run_think();
}

void NodeImpl::apply_min_protocol_ratchet()
{
// Runtime wiring of the accept-floor ratchet #602 landed as a pure function.
// Mirrors oracle main.py:213-216 (run on each best-share advance) + data.py:857
// update_min_protocol_version: over the window [CHAIN_LENGTH*9/10, CHAIN_LENGTH]
// behind the best share's PARENT, lift the inbound P2P accept-floor 1400 -> 3500
// once the best share's VERSION holds >=95% of the work-weighted desired-version
// tally. Runtime sibling of the 60% version-switch gate (share_check.hpp step 2),
// reading the SAME window. MUST be called under exclusive m_tracker_mutex.
const uint32_t target = PoolConfig::SHARE_MINIMUM_PROTOCOL_VERSION; // 3500
const uint32_t current =
m_runtime_min_protocol_version.load(std::memory_order_relaxed);
if (current >= target) // already ratcheted -> latched, no-op
return;
if (m_best_share_hash.IsNull() || !m_tracker.chain.contains(m_best_share_hash))
return;

// best share's VERSION (share TYPE version) + its parent hash
// (oracle: previous_share = shares[best_share.previous_share_hash]).
int64_t best_version = 0;
uint256 prev_hash;
m_tracker.chain.get_share(m_best_share_hash).invoke([&](auto* obj) {
best_version = std::remove_pointer_t<decltype(obj)>::version;
prev_hash = obj->m_prev_hash;
});
if (prev_hash.IsNull() || !m_tracker.chain.contains(prev_hash))
return;

const int32_t CL = static_cast<int32_t>(m_tracker.m_params->chain_length);
const int32_t parent_height = m_tracker.chain.get_height(prev_hash);

// Sample the SAME window the 60% switch gate reads (share_check.hpp:1610):
// anchor = nth_parent(parent, CHAIN_LENGTH*9/10), size = CHAIN_LENGTH/10.
const uint32_t window_start = (static_cast<uint32_t>(CL) * 9) / 10;
const uint32_t window_size = static_cast<uint32_t>(CL) / 10;
auto anchor = m_tracker.chain.get_nth_parent_key(prev_hash, window_start);
auto weights = m_tracker.get_desired_version_weights(
anchor, static_cast<int32_t>(window_size));

// apply_min_protocol_ratchet_decision applies the main.py:212 full-window
// guard (parent_height >= CHAIN_LENGTH) the pure ratchet omits, then delegates.
const uint32_t lifted = dgb::apply_min_protocol_ratchet_decision(
parent_height, CL, weights, best_version, current, target);
if (lifted != current) {
m_runtime_min_protocol_version.store(lifted, std::memory_order_relaxed);
LOG_INFO << "[min-proto-ratchet] MINIMUM_PROTOCOL_VERSION " << current
<< " -> " << lifted << " (>=95% window work desires v"
<< best_version << ", best="
<< m_best_share_hash.GetHex().substr(0, 16) << ")";
}
}

uint256 NodeImpl::best_share_hash()
{
// p2pool's think() returns the best VERIFIED head — not the raw chain tip.
Expand Down Expand Up @@ -1465,6 +1522,7 @@ void NodeImpl::run_think()
if (!result.best.IsNull()) {
best_changed = (m_best_share_hash != result.best);
m_best_share_hash = result.best;
apply_min_protocol_ratchet(); // oracle main.py:216 (per best-share advance)
}

// Phase 1c: drive the whale-departure detector with the fresh best
Expand Down Expand Up @@ -1843,6 +1901,7 @@ void NodeImpl::clean_tracker()

if (!result.best.IsNull()) {
m_best_share_hash = result.best;
apply_min_protocol_ratchet(); // oracle main.py:216 (per best-share advance)
}

flush_verified_to_leveldb();
Expand Down Expand Up @@ -2014,6 +2073,7 @@ void NodeImpl::clean_tracker()
if (!result.best.IsNull()) {
clean_best_changed = (m_best_share_hash != result.best);
m_best_share_hash = result.best;
apply_min_protocol_ratchet(); // oracle main.py:216 (per best-share advance)
}
publish_snapshot();
flush_verified_to_leveldb();
Expand Down
15 changes: 15 additions & 0 deletions src/impl/dgb/node.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,21 @@ class NodeImpl : public pool::BaseNode<dgb::Config, dgb::ShareChain, dgb::Peer>
WhaleDepartureDetector m_whale_departure;
// Lock-free publication of the detector verdict (see local_desired_target).
std::atomic<bool> m_whale_departure_active{false};

// Runtime P2P accept-floor, ratcheted 1400 -> 3500 by the desired-version
// tally (oracle net.MINIMUM_PROTOCOL_VERSION, mutated at main.py:216). Seeded
// from the cold config floor (PoolConfig::MINIMUM_PROTOCOL_VERSION) and lifted
// by apply_min_protocol_ratchet() on the compute thread under m_tracker_mutex;
// read lock-free by handle_version() on the IO thread. Latching (never lowers).
std::atomic<uint32_t> m_runtime_min_protocol_version{
PoolConfig::MINIMUM_PROTOCOL_VERSION};

// Runtime sibling of the 60% version-switch gate (share_check step 2): on each
// best-share advance, lift m_runtime_min_protocol_version once >=95% of the
// window work behind the best share's parent desires the best share's VERSION.
// Mirrors oracle main.py:213-216 + data.py:857. MUST be called under the
// exclusive m_tracker_mutex (reads tracker chain + desired-version weights).
void apply_min_protocol_ratchet();
// Phase 3L non-consensus log-based pool monitor (attack/anomaly detection).
// Driven from run_think() under the exclusive tracker lock on a ~30s cadence;
// emits [MONITOR-*] log lines only — no consensus / sharechain effect.
Expand Down
52 changes: 52 additions & 0 deletions src/impl/dgb/test/min_protocol_ratchet_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,55 @@ TEST(DgbMinProtocolRatchet, ConsensusMagnitudeNoOverflow) {
auto w = std::map<uint64_t, uint288>{{36, best}, {35, rest}};
EXPECT_EQ(dgb::ratchet_min_protocol_version(w, 36, COLD, TARGET), TARGET);
}

// ============================================================================
// apply_min_protocol_ratchet_decision -- the RUNTIME WIRING guard (follow-on to
// the pure ratchet above). Adds the oracle main.py:212 full-window guard
// (len(shares) > CHAIN_LENGTH) the pure function deliberately omits. CL below is
// a stand-in CHAIN_LENGTH; parent_height is the height of the best share's parent.
// ============================================================================

constexpr int32_t CL = 100; // stand-in CHAIN_LENGTH for the window guard

// --- FULL-WINDOW GUARD: parent_height < CHAIN_LENGTH -> NEVER lift, even when the
// (partial) window is unanimous. This is the fresh-node bug the wiring exists to
// prevent: without it a young chain would lock the floor to 3500 and reject every
// legitimate peer. Contrast EmptyWindowMirrorsOracleDictBranch: the PURE fn
// ratchets on the same weights; the DECISION fn must NOT (no full window yet).
TEST(DgbMinProtocolRatchetWiring, ShortChainNeverLifts) {
auto w = std::map<uint64_t, uint288>{{36, u(100)}}; // unanimous
// pure fn would ratchet...
EXPECT_EQ(dgb::ratchet_min_protocol_version(w, 36, COLD, TARGET), TARGET);
// ...but the decision guard holds it cold while parent_height < CL.
EXPECT_EQ(dgb::apply_min_protocol_ratchet_decision(CL - 1, CL, w, 36, COLD, TARGET), COLD);
// exactly one short of a full window still holds.
EXPECT_EQ(dgb::apply_min_protocol_ratchet_decision(0, CL, w, 36, COLD, TARGET), COLD);
}

// --- empty window + short chain -> no lift (guards the EmptyWindowMirrorsOracle
// dict-branch behaviour behind the full-window precondition).
TEST(DgbMinProtocolRatchetWiring, ShortChainEmptyWindowNoLift) {
EXPECT_EQ(dgb::apply_min_protocol_ratchet_decision(CL - 1, CL, {}, 36, COLD, TARGET), COLD);
}

// --- FULL WINDOW present (parent_height >= CHAIN_LENGTH) + unanimous -> lift.
TEST(DgbMinProtocolRatchetWiring, FullWindowUnanimousLifts) {
auto w = std::map<uint64_t, uint288>{{36, u(100)}};
EXPECT_EQ(dgb::apply_min_protocol_ratchet_decision(CL, CL, w, 36, COLD, TARGET), TARGET);
EXPECT_EQ(dgb::apply_min_protocol_ratchet_decision(CL * 5, CL, w, 36, COLD, TARGET), TARGET);
}

// --- FULL WINDOW present but best < 95% -> stays cold (delegates to pure fn).
TEST(DgbMinProtocolRatchetWiring, FullWindowBelow95StaysCold) {
auto w = std::map<uint64_t, uint288>{{36, u(94)}, {35, u(6)}}; // sum=100, 94<95
EXPECT_EQ(dgb::apply_min_protocol_ratchet_decision(CL, CL, w, 36, COLD, TARGET), COLD);
}

// --- already at/above target short-circuits regardless of window/height.
TEST(DgbMinProtocolRatchetWiring, AlreadyAtTargetShortCircuits) {
auto w = std::map<uint64_t, uint288>{{36, u(100)}};
EXPECT_EQ(dgb::apply_min_protocol_ratchet_decision(CL, CL, w, 36, TARGET, TARGET), TARGET);
// even with a short chain the latch holds at target (never lowers).
EXPECT_EQ(dgb::apply_min_protocol_ratchet_decision(0, CL, w, 36, TARGET, TARGET), TARGET);
}

Loading