Skip to content

Commit c75b3b2

Browse files
authored
Merge pull request #604 from frstrtr/dgb/min-proto-ratchet-runtime-wire
dgb: wire runtime min-protocol 95%-ratchet into per-best-share loop (#602 follow-on)
2 parents c72b493 + 4a2fe3e commit c75b3b2

4 files changed

Lines changed: 159 additions & 3 deletions

File tree

src/impl/dgb/coin/desired_version_tally.hpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,4 +128,33 @@ inline uint32_t ratchet_min_protocol_version(
128128
return current_floor;
129129
}
130130

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

src/impl/dgb/node.cpp

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -309,12 +309,17 @@ std::optional<pool::PeerConnectionType> NodeImpl::handle_version(std::unique_ptr
309309
peer->write(std::move(getaddrs_msg));
310310
}
311311

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

814+
void NodeImpl::apply_min_protocol_ratchet()
815+
{
816+
// Runtime wiring of the accept-floor ratchet #602 landed as a pure function.
817+
// Mirrors oracle main.py:213-216 (run on each best-share advance) + data.py:857
818+
// update_min_protocol_version: over the window [CHAIN_LENGTH*9/10, CHAIN_LENGTH]
819+
// behind the best share's PARENT, lift the inbound P2P accept-floor 1400 -> 3500
820+
// once the best share's VERSION holds >=95% of the work-weighted desired-version
821+
// tally. Runtime sibling of the 60% version-switch gate (share_check.hpp step 2),
822+
// reading the SAME window. MUST be called under exclusive m_tracker_mutex.
823+
const uint32_t target = PoolConfig::SHARE_MINIMUM_PROTOCOL_VERSION; // 3500
824+
const uint32_t current =
825+
m_runtime_min_protocol_version.load(std::memory_order_relaxed);
826+
if (current >= target) // already ratcheted -> latched, no-op
827+
return;
828+
if (m_best_share_hash.IsNull() || !m_tracker.chain.contains(m_best_share_hash))
829+
return;
830+
831+
// best share's VERSION (share TYPE version) + its parent hash
832+
// (oracle: previous_share = shares[best_share.previous_share_hash]).
833+
int64_t best_version = 0;
834+
uint256 prev_hash;
835+
m_tracker.chain.get_share(m_best_share_hash).invoke([&](auto* obj) {
836+
best_version = std::remove_pointer_t<decltype(obj)>::version;
837+
prev_hash = obj->m_prev_hash;
838+
});
839+
if (prev_hash.IsNull() || !m_tracker.chain.contains(prev_hash))
840+
return;
841+
842+
const int32_t CL = static_cast<int32_t>(m_tracker.m_params->chain_length);
843+
const int32_t parent_height = m_tracker.chain.get_height(prev_hash);
844+
845+
// Sample the SAME window the 60% switch gate reads (share_check.hpp:1610):
846+
// anchor = nth_parent(parent, CHAIN_LENGTH*9/10), size = CHAIN_LENGTH/10.
847+
const uint32_t window_start = (static_cast<uint32_t>(CL) * 9) / 10;
848+
const uint32_t window_size = static_cast<uint32_t>(CL) / 10;
849+
auto anchor = m_tracker.chain.get_nth_parent_key(prev_hash, window_start);
850+
auto weights = m_tracker.get_desired_version_weights(
851+
anchor, static_cast<int32_t>(window_size));
852+
853+
// apply_min_protocol_ratchet_decision applies the main.py:212 full-window
854+
// guard (parent_height >= CHAIN_LENGTH) the pure ratchet omits, then delegates.
855+
const uint32_t lifted = dgb::apply_min_protocol_ratchet_decision(
856+
parent_height, CL, weights, best_version, current, target);
857+
if (lifted != current) {
858+
m_runtime_min_protocol_version.store(lifted, std::memory_order_relaxed);
859+
LOG_INFO << "[min-proto-ratchet] MINIMUM_PROTOCOL_VERSION " << current
860+
<< " -> " << lifted << " (>=95% window work desires v"
861+
<< best_version << ", best="
862+
<< m_best_share_hash.GetHex().substr(0, 16) << ")";
863+
}
864+
}
865+
809866
uint256 NodeImpl::best_share_hash()
810867
{
811868
// p2pool's think() returns the best VERIFIED head — not the raw chain tip.
@@ -1465,6 +1522,7 @@ void NodeImpl::run_think()
14651522
if (!result.best.IsNull()) {
14661523
best_changed = (m_best_share_hash != result.best);
14671524
m_best_share_hash = result.best;
1525+
apply_min_protocol_ratchet(); // oracle main.py:216 (per best-share advance)
14681526
}
14691527

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

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

18481907
flush_verified_to_leveldb();
@@ -2014,6 +2073,7 @@ void NodeImpl::clean_tracker()
20142073
if (!result.best.IsNull()) {
20152074
clean_best_changed = (m_best_share_hash != result.best);
20162075
m_best_share_hash = result.best;
2076+
apply_min_protocol_ratchet(); // oracle main.py:216 (per best-share advance)
20172077
}
20182078
publish_snapshot();
20192079
flush_verified_to_leveldb();

src/impl/dgb/node.hpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,21 @@ class NodeImpl : public pool::BaseNode<dgb::Config, dgb::ShareChain, dgb::Peer>
6363
WhaleDepartureDetector m_whale_departure;
6464
// Lock-free publication of the detector verdict (see local_desired_target).
6565
std::atomic<bool> m_whale_departure_active{false};
66+
67+
// Runtime P2P accept-floor, ratcheted 1400 -> 3500 by the desired-version
68+
// tally (oracle net.MINIMUM_PROTOCOL_VERSION, mutated at main.py:216). Seeded
69+
// from the cold config floor (PoolConfig::MINIMUM_PROTOCOL_VERSION) and lifted
70+
// by apply_min_protocol_ratchet() on the compute thread under m_tracker_mutex;
71+
// read lock-free by handle_version() on the IO thread. Latching (never lowers).
72+
std::atomic<uint32_t> m_runtime_min_protocol_version{
73+
PoolConfig::MINIMUM_PROTOCOL_VERSION};
74+
75+
// Runtime sibling of the 60% version-switch gate (share_check step 2): on each
76+
// best-share advance, lift m_runtime_min_protocol_version once >=95% of the
77+
// window work behind the best share's parent desires the best share's VERSION.
78+
// Mirrors oracle main.py:213-216 + data.py:857. MUST be called under the
79+
// exclusive m_tracker_mutex (reads tracker chain + desired-version weights).
80+
void apply_min_protocol_ratchet();
6681
// Phase 3L non-consensus log-based pool monitor (attack/anomaly detection).
6782
// Driven from run_think() under the exclusive tracker lock on a ~30s cadence;
6883
// emits [MONITOR-*] log lines only — no consensus / sharechain effect.

src/impl/dgb/test/min_protocol_ratchet_test.cpp

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,55 @@ TEST(DgbMinProtocolRatchet, ConsensusMagnitudeNoOverflow) {
110110
auto w = std::map<uint64_t, uint288>{{36, best}, {35, rest}};
111111
EXPECT_EQ(dgb::ratchet_min_protocol_version(w, 36, COLD, TARGET), TARGET);
112112
}
113+
114+
// ============================================================================
115+
// apply_min_protocol_ratchet_decision -- the RUNTIME WIRING guard (follow-on to
116+
// the pure ratchet above). Adds the oracle main.py:212 full-window guard
117+
// (len(shares) > CHAIN_LENGTH) the pure function deliberately omits. CL below is
118+
// a stand-in CHAIN_LENGTH; parent_height is the height of the best share's parent.
119+
// ============================================================================
120+
121+
constexpr int32_t CL = 100; // stand-in CHAIN_LENGTH for the window guard
122+
123+
// --- FULL-WINDOW GUARD: parent_height < CHAIN_LENGTH -> NEVER lift, even when the
124+
// (partial) window is unanimous. This is the fresh-node bug the wiring exists to
125+
// prevent: without it a young chain would lock the floor to 3500 and reject every
126+
// legitimate peer. Contrast EmptyWindowMirrorsOracleDictBranch: the PURE fn
127+
// ratchets on the same weights; the DECISION fn must NOT (no full window yet).
128+
TEST(DgbMinProtocolRatchetWiring, ShortChainNeverLifts) {
129+
auto w = std::map<uint64_t, uint288>{{36, u(100)}}; // unanimous
130+
// pure fn would ratchet...
131+
EXPECT_EQ(dgb::ratchet_min_protocol_version(w, 36, COLD, TARGET), TARGET);
132+
// ...but the decision guard holds it cold while parent_height < CL.
133+
EXPECT_EQ(dgb::apply_min_protocol_ratchet_decision(CL - 1, CL, w, 36, COLD, TARGET), COLD);
134+
// exactly one short of a full window still holds.
135+
EXPECT_EQ(dgb::apply_min_protocol_ratchet_decision(0, CL, w, 36, COLD, TARGET), COLD);
136+
}
137+
138+
// --- empty window + short chain -> no lift (guards the EmptyWindowMirrorsOracle
139+
// dict-branch behaviour behind the full-window precondition).
140+
TEST(DgbMinProtocolRatchetWiring, ShortChainEmptyWindowNoLift) {
141+
EXPECT_EQ(dgb::apply_min_protocol_ratchet_decision(CL - 1, CL, {}, 36, COLD, TARGET), COLD);
142+
}
143+
144+
// --- FULL WINDOW present (parent_height >= CHAIN_LENGTH) + unanimous -> lift.
145+
TEST(DgbMinProtocolRatchetWiring, FullWindowUnanimousLifts) {
146+
auto w = std::map<uint64_t, uint288>{{36, u(100)}};
147+
EXPECT_EQ(dgb::apply_min_protocol_ratchet_decision(CL, CL, w, 36, COLD, TARGET), TARGET);
148+
EXPECT_EQ(dgb::apply_min_protocol_ratchet_decision(CL * 5, CL, w, 36, COLD, TARGET), TARGET);
149+
}
150+
151+
// --- FULL WINDOW present but best < 95% -> stays cold (delegates to pure fn).
152+
TEST(DgbMinProtocolRatchetWiring, FullWindowBelow95StaysCold) {
153+
auto w = std::map<uint64_t, uint288>{{36, u(94)}, {35, u(6)}}; // sum=100, 94<95
154+
EXPECT_EQ(dgb::apply_min_protocol_ratchet_decision(CL, CL, w, 36, COLD, TARGET), COLD);
155+
}
156+
157+
// --- already at/above target short-circuits regardless of window/height.
158+
TEST(DgbMinProtocolRatchetWiring, AlreadyAtTargetShortCircuits) {
159+
auto w = std::map<uint64_t, uint288>{{36, u(100)}};
160+
EXPECT_EQ(dgb::apply_min_protocol_ratchet_decision(CL, CL, w, 36, TARGET, TARGET), TARGET);
161+
// even with a short chain the latch holds at target (never lowers).
162+
EXPECT_EQ(dgb::apply_min_protocol_ratchet_decision(0, CL, w, 36, TARGET, TARGET), TARGET);
163+
}
164+

0 commit comments

Comments
 (0)