@@ -34,6 +34,7 @@ inline uint64_t mul128_shift(uint64_t a, uint64_t b, unsigned shift) {
3434#include < chrono>
3535#include < deque>
3636#include < functional>
37+ #include < iomanip>
3738#include < map>
3839#include < optional>
3940#include < set>
@@ -960,23 +961,24 @@ class ShareTracker
960961 LOG_INFO << " [PPLNS-RING] built: window=" << head_pplns.window_size ()
961962 << " chain_len=" << CL_i32 << " build_time=" << ring_us << " us" ;
962963 } else {
963- // Subsequent shares: find the new deepest entry and slide
964- // The new PPLNS start is prev_hash (one step deeper)
964+ // Subsequent shares: slide PPLNS window one step deeper.
965+ // The new window drops the shallowest entry and adds the
966+ // share at depth (chain_len - 1) from the new start.
967+ //
968+ // FIX: deep_hash IS the correct new tail entry (at depth
969+ // chain_len-1 from new start). The previous code took
970+ // deep_hash.tail (one step FURTHER), causing an off-by-one
971+ // that shifted the tail entry one position too deep each
972+ // extension — accumulating PPLNS weight errors and causing
973+ // GENTX mismatches for all V36 shares.
965974 auto ring_depth = head_pplns.window_size ();
966975 if (ring_depth > 0 ) {
967- // Get the hash one past the current deepest entry
968976 auto deep_hash = chain.get_nth_parent_via_skip (
969977 prev_hash, std::min (ring_depth - 1 , CL_i32 - 1 ));
970978 if (!deep_hash.IsNull () && chain.contains (deep_hash)) {
971- auto * deep_idx = chain.get_index (deep_hash);
972- auto deeper = deep_idx ? deep_idx->tail : uint256 ();
973- if (!deeper.IsNull () && chain.contains (deeper)) {
974- head_pplns.extend_backward (chain, deeper);
975- } else {
976- // Can't extend — rebuild
977- head_pplns.rebuild (chain, prev_hash, CL_i32);
978- }
979+ head_pplns.extend_backward (chain, deep_hash);
979980 } else {
981+ // Can't extend — rebuild from scratch
980982 head_pplns.rebuild (chain, prev_hash, CL_i32);
981983 }
982984 }
@@ -1753,6 +1755,115 @@ class ShareTracker
17531755 return result;
17541756 }
17551757
1758+ // -- Diagnostic: per-share V36 PPLNS walk dump --
1759+ // Walks from start_hash backward, logging every share's contribution.
1760+ // Output matches p2pool's [PARENT-PPLNS] stderr format for diff comparison.
1761+ // Call only from GENTX mismatch handler — never on the hot path.
1762+ void dump_v36_pplns_walk (const uint256& start_hash, int32_t max_shares)
1763+ {
1764+ if (start_hash.IsNull () || !chain.contains (start_hash))
1765+ {
1766+ LOG_WARNING << " [PPLNS-WALK] start=" << start_hash.GetHex ().substr (0 ,16 )
1767+ << " NOT IN CHAIN — walk aborted" ;
1768+ return ;
1769+ }
1770+
1771+ static constexpr uint64_t DECAY_PRECISION = 40 ;
1772+ static constexpr uint64_t DECAY_SCALE = uint64_t (1 ) << DECAY_PRECISION ;
1773+ static constexpr uint64_t LN2_MICRO = 693147 ;
1774+
1775+ uint32_t half_life = std::max (PoolConfig::chain_length () / 4 , uint32_t (1 ));
1776+ uint64_t decay_per = DECAY_SCALE - (DECAY_SCALE * LN2_MICRO ) / (uint64_t (1000000 ) * half_life);
1777+
1778+ int32_t share_count = 0 ;
1779+ uint64_t decay_fp = DECAY_SCALE ;
1780+ uint288 running_total;
1781+ uint288 running_donation;
1782+ std::map<std::vector<unsigned char >, uint288> per_addr_weight;
1783+
1784+ auto height = chain.get_height (start_hash);
1785+ auto last = chain.get_last (start_hash);
1786+
1787+ LOG_WARNING << " [PPLNS-WALK] start=" << start_hash.GetHex ().substr (0 , 16 )
1788+ << " max_shares=" << max_shares
1789+ << " height=" << height
1790+ << " last=" << (last.IsNull () ? " null" : last.GetHex ().substr (0 , 16 ))
1791+ << " half_life=" << half_life
1792+ << " decay_per=" << decay_per;
1793+
1794+ auto cur = start_hash;
1795+ while (!cur.IsNull () && chain.contains (cur) && share_count < max_shares)
1796+ {
1797+ chain.get_share (cur).invoke ([&](auto * obj) {
1798+ auto target = chain::bits_to_target (obj->m_bits );
1799+ auto att = chain::target_to_average_attempts (target);
1800+ uint32_t don = obj->m_donation ;
1801+
1802+ uint288 decayed_att = (att * uint288 (decay_fp)) >> DECAY_PRECISION ;
1803+ auto addr_w = decayed_att * static_cast <uint32_t >(65535 - don);
1804+ auto don_w = decayed_att * don;
1805+
1806+ auto script = get_share_script (obj);
1807+ per_addr_weight[script] += addr_w;
1808+ running_total += addr_w + don_w;
1809+ running_donation += don_w;
1810+
1811+ // Script hex prefix (first 10 bytes, like p2pool's key.encode('hex')[:40])
1812+ static const char * HX = " 0123456789abcdef" ;
1813+ std::string sh;
1814+ for (size_t i = 0 ; i < std::min (script.size (), size_t (20 )); ++i) {
1815+ sh += HX [script[i] >> 4 ];
1816+ sh += HX [script[i] & 0xf ];
1817+ }
1818+
1819+ LOG_WARNING << " [PPLNS-WALK] #" << share_count
1820+ << " hash=" << cur.GetHex ().substr (0 , 16 )
1821+ << " script=" << sh
1822+ << " bits=0x" << std::hex << obj->m_bits << std::dec
1823+ << " don=" << don
1824+ << " att=" << att.GetLow64 ()
1825+ << " decay_fp=" << decay_fp
1826+ << " decayed=" << decayed_att.GetLow64 ()
1827+ << " addr_w=" << addr_w.GetLow64 ()
1828+ << " running=" << running_total.GetLow64 ();
1829+ });
1830+
1831+ ++share_count;
1832+
1833+ decay_fp = mul128_shift (decay_fp, decay_per, DECAY_PRECISION );
1834+ auto * idx = chain.get_index (cur);
1835+ cur = idx ? idx->tail : uint256 ();
1836+ }
1837+
1838+ // Summary matching p2pool's [PARENT-PPLNS] format
1839+ LOG_WARNING << " [PPLNS-WALK] SUMMARY: shares=" << share_count
1840+ << " addrs=" << per_addr_weight.size ()
1841+ << " total_w=" << running_total.GetLow64 ()
1842+ << " don_w=" << running_donation.GetLow64 ();
1843+ for (const auto & [script, weight] : per_addr_weight)
1844+ {
1845+ static const char * HX = " 0123456789abcdef" ;
1846+ std::string sh;
1847+ for (size_t i = 0 ; i < std::min (script.size (), size_t (20 )); ++i) {
1848+ sh += HX [script[i] >> 4 ];
1849+ sh += HX [script[i] & 0xf ];
1850+ }
1851+ double pct = running_total.IsNull () ? 0.0 :
1852+ static_cast <double >(weight.GetLow64 ()) / static_cast <double >(running_total.GetLow64 ()) * 100.0 ;
1853+ LOG_WARNING << " [PPLNS-WALK] " << sh
1854+ << " w=" << weight.GetLow64 ()
1855+ << " pct=" << std::fixed << std::setprecision (2 ) << pct << " %" ;
1856+ }
1857+
1858+ // Chain gap detection: check if walk terminated early (not at chain bottom)
1859+ if (share_count < max_shares && !cur.IsNull ()) {
1860+ LOG_WARNING << " [PPLNS-WALK] CHAIN GAP: walk stopped at share #" << share_count
1861+ << " — next hash " << cur.GetHex ().substr (0 , 16 )
1862+ << " is " << (chain.contains (cur) ? " IN chain (walk bug)" : " NOT IN chain (missing share)" )
1863+ << " . Expected " << max_shares << " shares." ;
1864+ }
1865+ }
1866+
17561867 // -- Expected payouts from PPLNS weights --
17571868 // Uses exact integer arithmetic matching generate_share_transaction():
17581869 // V36: amount = (uint288(subsidy) * weight / total_weight).GetLow64()
0 commit comments