|
| 1 | +// SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | +// LTC lookbehind chain-walk WINDOW clamp + activation guard -- KAT, exercised |
| 3 | +// through LTC's REAL ShareTracker API. |
| 4 | +// |
| 5 | +// FENCED, additive (no production code touched this slice). Pins the pure |
| 6 | +// integer window idiom that governs every backward sharechain accessor: |
| 7 | +// actual = min(lookbehind, height); if (actual <= 0) -> empty result |
| 8 | +// |
| 9 | +// Unlike dgb -- which lifts the clamp into a header (chain_walk_window.hpp) and |
| 10 | +// KATs the free functions -- LTC is the V36 reference impl and does NOT |
| 11 | +// modularize this guard: the identical three-line pattern is open-coded inline |
| 12 | +// in FOUR share_tracker.hpp accessors: |
| 13 | +// auto height = chain.get_height(share_hash); // depth of the head |
| 14 | +// auto actual = std::min(lookbehind, height); // clamp |
| 15 | +// if (actual <= 0) return <empty>; // guard |
| 16 | +// auto view = chain.get_chain(share_hash, actual); // walk |
| 17 | +// get_average_stale_prop (share_tracker.hpp:2091) |
| 18 | +// get_stale_counts (share_tracker.hpp:2114) |
| 19 | +// get_desired_version_counts (share_tracker.hpp:2156) |
| 20 | +// get_desired_version_weights (share_tracker.hpp:2185) |
| 21 | +// |
| 22 | +// APPROACH (mirrors the LTC desired_version_tally precedent, commit 0c8a83a3): |
| 23 | +// LTC has no header to lift, and this KAT must NOT touch production code, so it |
| 24 | +// drives the clamp+guard through the real chain-walk API rather than a lifted |
| 25 | +// oracle. It builds a resolved ltc::ShareTracker of ltc::MergedMiningShare (V36) |
| 26 | +// of a KNOWN depth H, then observes the REALIZED window -- the number of shares |
| 27 | +// get_desired_version_counts actually walks -- across a dense (H, lookbehind) |
| 28 | +// matrix. All shares carry one desired_version, so the single tally bucket's |
| 29 | +// count IS the realized walk length. The realized window is asserted equal to an |
| 30 | +// INDEPENDENT inline oracle min(lookbehind, H) computed from the construction |
| 31 | +// depth H and the loop variable -- NOT read from the code under test -- and the |
| 32 | +// empty/non-empty result is asserted to track the (actual > 0) guard. |
| 33 | +// |
| 34 | +// Why H == the share count: get_height returns get_delta_to_last().height, which |
| 35 | +// accumulates 1 per contained ancestor from the head to the null-parent chain |
| 36 | +// end, so a chain of H shares (share 0 has a null parent) reports height H. The |
| 37 | +// high-lookbehind matrix cells (lookbehind >> H) self-validate this: they observe |
| 38 | +// exactly H walked, proving height == H and available == H before the sub-H cells |
| 39 | +// exercise the clamp. This is the non-circular basis for the min() oracle. |
| 40 | +// |
| 41 | +// The clamp feeds two consensus-bearing consumers via get_desired_version_weights |
| 42 | +// -- the check()-phase 60% work-weighted v36 switch gate (share_check step 2) and |
| 43 | +// the #288 AutoRatchet VOTING tail guard -- so a drifted window would re-scope the |
| 44 | +// accept gate. share_tracker.hpp is NOT rewired here. This target joins the |
| 45 | +// existing `share_test` executable (already on the build.yml --target allowlist), |
| 46 | +// so it cannot become a #143 NOT_BUILT sentinel. |
| 47 | + |
| 48 | +#include <cstddef> |
| 49 | +#include <cstdint> |
| 50 | +#include <cstdio> |
| 51 | +#include <algorithm> |
| 52 | +#include <map> |
| 53 | +#include <string> |
| 54 | +#include <vector> |
| 55 | + |
| 56 | +#include <gtest/gtest.h> |
| 57 | + |
| 58 | +#include <core/uint256.hpp> |
| 59 | +#include <impl/ltc/share.hpp> |
| 60 | +#include <impl/ltc/share_tracker.hpp> |
| 61 | + |
| 62 | +namespace { |
| 63 | + |
| 64 | +// A short hex tail -> uint256 (zero-padded to 64 nibbles), matching the LTC |
| 65 | +// desired_version_tally / dgb share_test hx() convention. |
| 66 | +uint256 hx(const std::string& tail) { |
| 67 | + uint256 v; |
| 68 | + v.SetHex(std::string(64 - tail.size(), '0') + tail); |
| 69 | + return v; |
| 70 | +} |
| 71 | + |
| 72 | +// Build a resolved ltc::ShareTracker with `count` uniform V36 shares (share i's |
| 73 | +// parent is i-1; share 0 has a null parent = resolved chain genesis) and return |
| 74 | +// the tip hash. Every share carries the same desired_version so the single tally |
| 75 | +// bucket's count equals the realized walk length. Distinct `salt` gives disjoint |
| 76 | +// hash spaces across the matrix so no two trackers collide (each tracker is |
| 77 | +// independent, but keeping hashes disjoint is defensive). |
| 78 | +uint256 build_uniform_chain(ltc::ShareTracker& tracker, int32_t count, |
| 79 | + uint64_t dv, uint32_t bits, size_t salt) { |
| 80 | + uint256 prev; |
| 81 | + prev.SetNull(); |
| 82 | + uint256 tip; |
| 83 | + tip.SetNull(); |
| 84 | + for (int32_t i = 0; i < count; ++i) { |
| 85 | + char buf[24]; |
| 86 | + std::snprintf(buf, sizeof(buf), "%zx", |
| 87 | + static_cast<size_t>((salt << 16) + 0x1c70 + i)); |
| 88 | + uint256 h = hx(buf); |
| 89 | + auto* sh = new ltc::MergedMiningShare(); |
| 90 | + sh->m_hash = h; |
| 91 | + if (i == 0) sh->m_prev_hash.SetNull(); else sh->m_prev_hash = prev; |
| 92 | + sh->m_desired_version = dv; |
| 93 | + sh->m_bits = bits; |
| 94 | + sh->m_max_bits = bits; |
| 95 | + ltc::ShareType st; |
| 96 | + st = sh; |
| 97 | + tracker.add(st); |
| 98 | + prev = h; |
| 99 | + tip = h; |
| 100 | + } |
| 101 | + return tip; |
| 102 | +} |
| 103 | + |
| 104 | +// Realized window = number of shares the backward walk actually tallied. All |
| 105 | +// shares share one desired_version, so the map has at most one bucket whose |
| 106 | +// count IS the walk length; sum defensively. |
| 107 | +int32_t realized_window(ltc::ShareTracker& tracker, const uint256& tip, |
| 108 | + int32_t lookbehind) { |
| 109 | + int32_t total = 0; |
| 110 | + for (const auto& [dv, n] : tracker.get_desired_version_counts(tip, lookbehind)) |
| 111 | + total += n; |
| 112 | + return total; |
| 113 | +} |
| 114 | + |
| 115 | +// The walk runs (non-empty result) iff the realized window is positive. |
| 116 | +bool walk_ran(ltc::ShareTracker& tracker, const uint256& tip, |
| 117 | + int32_t lookbehind) { |
| 118 | + return !tracker.get_desired_version_counts(tip, lookbehind).empty(); |
| 119 | +} |
| 120 | + |
| 121 | +constexpr uint64_t kDv = 36; |
| 122 | +constexpr uint32_t kBits = 0x1e0fffff; |
| 123 | + |
| 124 | +} // namespace |
| 125 | + |
| 126 | +// --- realized window = min(lookbehind, height) ------------------------------ |
| 127 | +TEST(LtcChainWalkWindow, ClampsToHeight) { |
| 128 | + // A head 6 shares deep. |
| 129 | + ltc::ShareTracker t6; |
| 130 | + const uint256 tip6 = build_uniform_chain(t6, 6, kDv, kBits, 1); |
| 131 | + // lookbehind below height -> identity (the ask is satisfiable). |
| 132 | + EXPECT_EQ(realized_window(t6, tip6, 3), 3); |
| 133 | + // lookbehind above height -> clamped to what the chain can yield. |
| 134 | + EXPECT_EQ(realized_window(t6, tip6, 720), 6); |
| 135 | + // exact boundary: equal -> that value. |
| 136 | + EXPECT_EQ(realized_window(t6, tip6, 6), 6); |
| 137 | + |
| 138 | + // One-deep chain: at most one ancestor to walk. |
| 139 | + ltc::ShareTracker t1; |
| 140 | + const uint256 tip1 = build_uniform_chain(t1, 1, kDv, kBits, 2); |
| 141 | + EXPECT_EQ(realized_window(t1, tip1, 720), 1); |
| 142 | + EXPECT_EQ(realized_window(t1, tip1, 1), 1); |
| 143 | +} |
| 144 | + |
| 145 | +// --- degenerate / defensive inputs ------------------------------------------ |
| 146 | +TEST(LtcChainWalkWindow, NonPositiveInputs) { |
| 147 | + // Genesis head (no such share present) -> zero window: chain.contains()==false |
| 148 | + // is the height-0 analog (a present head is always >=1 deep), empty map. |
| 149 | + ltc::ShareTracker t; |
| 150 | + EXPECT_EQ(realized_window(t, hx("deadbeef"), 720), 0); |
| 151 | + EXPECT_FALSE(walk_ran(t, hx("deadbeef"), 720)); |
| 152 | + |
| 153 | + ltc::ShareTracker t3; |
| 154 | + const uint256 tip3 = build_uniform_chain(t3, 3, kDv, kBits, 3); |
| 155 | + // zero lookbehind -> min(0,height)=0 -> actual<=0 -> zero window. |
| 156 | + EXPECT_EQ(realized_window(t3, tip3, 0), 0); |
| 157 | + EXPECT_FALSE(walk_ran(t3, tip3, 0)); |
| 158 | + // negative lookbehind -> min(-5,height)=-5 -> actual<=0 -> zero window. |
| 159 | + EXPECT_EQ(realized_window(t3, tip3, -5), 0); |
| 160 | + EXPECT_FALSE(walk_ran(t3, tip3, -5)); |
| 161 | +} |
| 162 | + |
| 163 | +// --- activation guard: actual > 0 ------------------------------------------- |
| 164 | +TEST(LtcChainWalkWindow, ActivationGuard) { |
| 165 | + ltc::ShareTracker t; |
| 166 | + const uint256 tip = build_uniform_chain(t, 4, kDv, kBits, 4); |
| 167 | + // The accessors early-return the empty result when actual <= 0. |
| 168 | + EXPECT_FALSE(walk_ran(t, tip, 0)); |
| 169 | + EXPECT_FALSE(walk_ran(t, tip, -3)); |
| 170 | + // Absent tip is also empty (contains() guard, the height-0 analog). |
| 171 | + EXPECT_FALSE(walk_ran(t, hx("cafe"), 720)); |
| 172 | + // A positive realized window runs. |
| 173 | + EXPECT_TRUE(walk_ran(t, tip, 1)); |
| 174 | + EXPECT_TRUE(walk_ran(t, tip, 720)); |
| 175 | +} |
| 176 | + |
| 177 | +// --- composite: the realized window is walked iff it is positive ------------- |
| 178 | +TEST(LtcChainWalkWindow, ClampThenActivateComposite) { |
| 179 | + // Present head, lookbehind 0: clamp to 0, guard fails -> no walk. |
| 180 | + ltc::ShareTracker t8; |
| 181 | + const uint256 tip8 = build_uniform_chain(t8, 8, kDv, kBits, 5); |
| 182 | + EXPECT_FALSE(walk_ran(t8, tip8, 0)); |
| 183 | + |
| 184 | + // One-deep: clamp to 1, guard passes -> walk one ancestor. |
| 185 | + ltc::ShareTracker t1; |
| 186 | + const uint256 tip1 = build_uniform_chain(t1, 1, kDv, kBits, 6); |
| 187 | + EXPECT_TRUE(walk_ran(t1, tip1, 720)); |
| 188 | + EXPECT_EQ(realized_window(t1, tip1, 720), 1); |
| 189 | + |
| 190 | + // Deep chain, real lookbehind: clamp to 8, walk 8. |
| 191 | + EXPECT_TRUE(walk_ran(t8, tip8, 720)); |
| 192 | + EXPECT_EQ(realized_window(t8, tip8, 720), 8); |
| 193 | +} |
| 194 | + |
| 195 | +// --- NON-CIRCULAR: realized window == inline min()+guard over a dense matrix -- |
| 196 | +// Re-implements the exact clamp+guard pattern from the four share_tracker.hpp |
| 197 | +// accessors WITHOUT reading their `actual`, then proves the OBSERVABLE realized |
| 198 | +// window matches it across a dense (height, lookbehind) grid. The high-lookbehind |
| 199 | +// cells (lookbehind >> height) self-validate height == the construction depth, |
| 200 | +// giving the min() oracle a non-circular basis. |
| 201 | +TEST(LtcChainWalkWindow, RealizedWindowMatchesInlineClampMatrix) { |
| 202 | + for (int32_t height = 1; height <= 8; ++height) { |
| 203 | + ltc::ShareTracker t; |
| 204 | + const uint256 tip = |
| 205 | + build_uniform_chain(t, height, kDv, kBits, 100 + height); |
| 206 | + for (int32_t lookbehind = -2; lookbehind <= 12; ++lookbehind) { |
| 207 | + // verbatim pre-lift inline (see share_tracker.hpp:2155-2160): |
| 208 | + const int32_t inline_actual = std::min(lookbehind, height); |
| 209 | + const bool inline_runs = !(inline_actual <= 0); |
| 210 | + |
| 211 | + EXPECT_EQ(realized_window(t, tip, lookbehind), |
| 212 | + inline_runs ? inline_actual : 0) |
| 213 | + << "height=" << height << " lookbehind=" << lookbehind; |
| 214 | + EXPECT_EQ(walk_ran(t, tip, lookbehind), inline_runs) |
| 215 | + << "height=" << height << " lookbehind=" << lookbehind; |
| 216 | + } |
| 217 | + } |
| 218 | +} |
0 commit comments