Skip to content

Commit 192bfe8

Browse files
committed
Fix V36 GENTX mismatch: off-by-one in HeadPPLNS ring buffer extension
The extend_backward slide in think() Phase 2 took deep_hash.tail (depth 400) instead of deep_hash itself (depth 399) when adding the new tail entry to the ring buffer. Each extension shifted the last entry one share too deep, accumulating PPLNS weight errors of 300-700 satoshi per address — enough to fail every V36 GENTX verification. Fix: pass deep_hash directly to extend_backward instead of deeper. Testnet: verified gap 391 → 0. Mainnet: 86 V36 failures now resolved. Also adds GENTX mismatch diagnostics (PPLNS walk dump, hash_link comparison, PPLNS-empty trace) and fixes current_payouts zero-address display during bootstrap.
1 parent 244128d commit 192bfe8

4 files changed

Lines changed: 211 additions & 13 deletions

File tree

.gitignore

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,5 +151,13 @@ test/test_*_include.cmake
151151
!test/test_*.cpp
152152
!test/test_*.hpp
153153
!test/test_*.h
154-
CLAUDE.md
154+
155+
# Release artifacts
156+
*.tar.gz
157+
*.tar.bz2
158+
*.dmg
159+
*.msi
160+
*.deb
161+
SHA256SUMS
162+
155163
CLAUDE.md

src/core/web_server.cpp

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2129,8 +2129,24 @@ void MiningInterface::refresh_work()
21292129
}
21302130

21312131
// Fallback: single output to zero-key (burn) so coinbase is always valid
2132-
if (pplns_outputs.empty())
2132+
if (pplns_outputs.empty()) {
21332133
pplns_outputs.push_back({"0000000000000000000000000000000000000000", coinbase_value});
2134+
// Diagnostic: trace WHY PPLNS is empty — this causes invalid coinbase
2135+
static int zero_warn = 0;
2136+
if (zero_warn++ < 10 || zero_warn % 100 == 0) {
2137+
bool has_pplns = static_cast<bool>(m_pplns_fn);
2138+
bool has_best = static_cast<bool>(m_best_share_hash_fn);
2139+
uint256 best_hash;
2140+
if (has_best) best_hash = m_best_share_hash_fn();
2141+
LOG_WARNING << "[PPLNS-EMPTY] Falling back to zero-address burn!"
2142+
<< " has_pplns_fn=" << has_pplns
2143+
<< " has_best_fn=" << has_best
2144+
<< " best_hash=" << (best_hash.IsNull() ? "null" : best_hash.GetHex().substr(0, 16))
2145+
<< " subsidy=" << coinbase_value
2146+
<< " donation_script_len=" << m_donation_script.size()
2147+
<< " (#" << zero_warn << ")";
2148+
}
2149+
}
21342150

21352151
// Get merged mining commitment if an MM manager is wired.
21362152
// Only rebuild when PPLNS weights changed (new best_share).
@@ -2909,7 +2925,14 @@ nlohmann::json MiningInterface::rest_current_payouts()
29092925
// These are always up-to-date with the latest share template and subsidy.
29102926
auto cached = get_cached_pplns_outputs();
29112927
if (!cached.empty()) {
2928+
// Skip zero-address burn entries (20-byte zero hash from PPLNS-empty fallback).
2929+
// These are not real payouts — they indicate PPLNS is not yet computed.
2930+
static const std::string zero_burn = "0000000000000000000000000000000000000000";
2931+
29122932
for (const auto& [script_hex, amount] : cached) {
2933+
if (script_hex == zero_burn)
2934+
continue; // Don't expose the burn placeholder to the API
2935+
29132936
// script_hex is a hex-encoded scriptPubKey — decode to bytes, then to address
29142937
auto script_bytes = ParseHex(script_hex);
29152938
std::string addr = core::script_to_address(script_bytes, is_ltc, m_testnet);

src/impl/ltc/share_check.hpp

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1374,6 +1374,53 @@ uint256 generate_share_transaction(const ShareT& share, TrackerT& tracker, bool
13741374
reinterpret_cast<const unsigned char*>(tx.data()), tx.size());
13751375
auto txid = Hash(tx_span);
13761376

1377+
// V36 hash_link cross-check: compute prefix hash_link from our coinbase
1378+
// and compare with the share's stored hash_link. If states differ,
1379+
// the prefix (outputs/amounts) differs from what p2pool built.
1380+
if (dump_diag && use_v36_pplns)
1381+
{
1382+
if constexpr (requires { share.m_hash_link.m_extra_data; })
1383+
{
1384+
auto gbr = compute_gentx_before_refhash(ver);
1385+
// prefix = full coinbase minus last 44 bytes (ref_hash 32 + nonce 8 + locktime 4)
1386+
size_t prefix_len = tx.size() - 44;
1387+
std::vector<unsigned char> prefix(
1388+
reinterpret_cast<const unsigned char*>(tx.data()),
1389+
reinterpret_cast<const unsigned char*>(tx.data()) + prefix_len);
1390+
auto computed_hl = prefix_to_hash_link(prefix, gbr);
1391+
1392+
bool state_match = (computed_hl.m_state.m_data == share.m_hash_link.m_state.m_data);
1393+
bool extra_match = (computed_hl.m_extra_data.m_data == share.m_hash_link.m_extra_data.m_data);
1394+
bool len_match = (computed_hl.m_length == share.m_hash_link.m_length);
1395+
1396+
static const char* HXD = "0123456789abcdef";
1397+
auto hex_fn = [&](const auto& v) {
1398+
std::string h; for (auto b : v) { h += HXD[(uint8_t)b >> 4]; h += HXD[(uint8_t)b & 0xf]; } return h;
1399+
};
1400+
1401+
LOG_WARNING << "[HASHLINK-CMP] state_match=" << (state_match ? "YES" : "NO")
1402+
<< " extra_match=" << (extra_match ? "YES" : "NO")
1403+
<< " len_match=" << (len_match ? "YES" : "NO")
1404+
<< " c2pool_len=" << computed_hl.m_length
1405+
<< " share_len=" << share.m_hash_link.m_length;
1406+
if (!state_match) {
1407+
LOG_WARNING << "[HASHLINK-CMP] c2pool_state=" << hex_fn(computed_hl.m_state.m_data);
1408+
LOG_WARNING << "[HASHLINK-CMP] share_state =" << hex_fn(share.m_hash_link.m_state.m_data);
1409+
}
1410+
if (!extra_match) {
1411+
LOG_WARNING << "[HASHLINK-CMP] c2pool_extra=" << hex_fn(computed_hl.m_extra_data.m_data)
1412+
<< " (" << computed_hl.m_extra_data.m_data.size() << " bytes)";
1413+
LOG_WARNING << "[HASHLINK-CMP] share_extra =" << hex_fn(share.m_hash_link.m_extra_data.m_data)
1414+
<< " (" << share.m_hash_link.m_extra_data.m_data.size() << " bytes)";
1415+
}
1416+
// Also dump prefix length and the last 60 bytes for comparison
1417+
LOG_WARNING << "[HASHLINK-CMP] prefix_len=" << prefix_len
1418+
<< " gbr_len=" << gbr.size()
1419+
<< " prefix_tail=" << hex_fn(std::vector<unsigned char>(
1420+
prefix.end() - std::min(prefix.size(), size_t(60)), prefix.end()));
1421+
}
1422+
}
1423+
13771424
// One-time full coinbase hex dump for cross-implementation debugging
13781425
{
13791426
static int coinbase_dump_count = 0;
@@ -1798,6 +1845,15 @@ bool share_check(const ShareT& share,
17981845
{
17991846
LOG_WARNING << "[GENTX-DIAG] Re-running generate_share_transaction with full dump (v36_active=" << v36_active << "):";
18001847
generate_share_transaction(share, tracker, true, v36_active);
1848+
1849+
// Per-share PPLNS walk dump — compare with p2pool's [PARENT-PPLNS] output.
1850+
// Uses same parameters as generate_share_transaction's V36 path.
1851+
if (v36_active && !share.m_prev_hash.IsNull()) {
1852+
auto diag_chain_len = static_cast<int32_t>(PoolConfig::real_chain_length());
1853+
LOG_WARNING << "[GENTX-DIAG] Per-share V36 PPLNS walk from prev="
1854+
<< share.m_prev_hash.GetHex().substr(0, 16) << ":";
1855+
tracker.dump_v36_pplns_walk(share.m_prev_hash, diag_chain_len);
1856+
}
18011857
}
18021858

18031859
throw std::invalid_argument("GenerateShareTransaction mismatch — coinbase does not match PPLNS payouts");

src/impl/ltc/share_tracker.hpp

Lines changed: 122 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)