@@ -2754,16 +2754,21 @@ int main(int argc, char* argv[]) {
27542754 d.version_counts [std::to_string (s->version )] = 1 ;
27552755 d.desired_version_counts [std::to_string (s->m_desired_version )] = 1 ;
27562756
2757+ auto target = chain::bits_to_target (s->m_bits );
2758+ d.difficulty_sum = chain::target_to_difficulty (target);
2759+
2760+ // Work-weighted desired version (matches p2pool's target_to_average_attempts weighting)
2761+ auto att = chain::target_to_average_attempts (target);
2762+ d.desired_version_weights [std::to_string (s->m_desired_version )] =
2763+ static_cast <double >(att.GetLow64 ());
2764+
27572765 std::string miner;
27582766 if constexpr (requires { s->m_address ; })
27592767 miner = HexStr (s->m_address .m_data );
27602768 else if constexpr (requires { s->m_pubkey_hash ; })
27612769 miner = s->m_pubkey_hash .GetHex ();
27622770 if (!miner.empty ())
27632771 d.miner_counts [miner] = 1 ;
2764-
2765- auto target = chain::bits_to_target (s->m_bits );
2766- d.difficulty_sum = chain::target_to_difficulty (target);
27672772 });
27682773 } catch (...) {}
27692774 return d;
@@ -2805,6 +2810,48 @@ int main(int argc, char* argv[]) {
28052810 static_cast <int >(ltc::PoolConfig::chain_length ()));
28062811 auto sr = stats_skiplist->query (best, walk);
28072812
2813+ // Sampling window: oldest CHAIN_LENGTH/10 shares in the active chain
2814+ // Matches p2pool consensus: get_nth_parent(tip, CHAIN_LENGTH*9//10) then count CHAIN_LENGTH//10
2815+ int chain_len = static_cast <int >(ltc::PoolConfig::chain_length ());
2816+ int chain_ht = best.IsNull () ? 0 : static_cast <int >(chain.get_height (best));
2817+ int skip_count = std::min (chain_ht, chain_len * 9 / 10 );
2818+ int sample_count = std::min (chain_ht - skip_count, chain_len / 10 );
2819+
2820+ // Walk active chain: find sampling start hash AND track V36 propagation
2821+ uint256 sampling_start;
2822+ int deepest_v36_pos = 0 ;
2823+ int v36_contiguous_from_tip = 0 ;
2824+ bool contiguous = true ;
2825+ int full_walk = std::min (chain_ht, chain_len);
2826+ {
2827+ uint256 pos = best;
2828+ for (int i = 0 ; i < full_walk && !pos.IsNull (); ++i) {
2829+ if (!chain.contains (pos)) break ;
2830+ if (i == skip_count) sampling_start = pos;
2831+ try {
2832+ auto & cd = chain.get (pos);
2833+ cd.share .invoke ([&](auto * s) {
2834+ if (static_cast <int >(s->m_desired_version ) >= 36 ) {
2835+ deepest_v36_pos = i + 1 ;
2836+ if (contiguous) v36_contiguous_from_tip = i + 1 ;
2837+ } else if (contiguous) {
2838+ contiguous = false ;
2839+ }
2840+ pos = s->m_prev_hash ;
2841+ });
2842+ } catch (...) { break ; }
2843+ }
2844+ }
2845+ result[" deepest_v36_position" ] = deepest_v36_pos;
2846+ result[" v36_contiguous_from_tip" ] = v36_contiguous_from_tip;
2847+
2848+ // Query the oldest CHAIN_LENGTH/10 shares from that position
2849+ auto sampling_sr = (sample_count > 0 && !sampling_start.IsNull ())
2850+ ? stats_skiplist->query (sampling_start, sample_count)
2851+ : chain::StatsResult{};
2852+ result[" sampling_desired_version" ] = sampling_sr.desired_version_weights ;
2853+ result[" sampling_total" ] = sampling_sr.share_count ;
2854+
28082855 result[" total_shares" ] = sr.share_count ;
28092856 result[" orphan_shares" ] = sr.orphan_count ;
28102857 result[" dead_shares" ] = sr.dead_count ;
@@ -3024,9 +3071,166 @@ int main(int argc, char* argv[]) {
30243071 result[" blocks" ] = std::move (blocks_arr);
30253072 result[" doge_blocks" ] = std::move (doge_arr);
30263073 result[" total" ] = static_cast <int >(chain.size ());
3074+ // Include per-share PPLNS + current as fallback
3075+ if (mi) {
3076+ result[" pplns_current" ] = mi->rest_current_payouts ();
3077+ // Per-share PPLNS from cache (available for shares since server start)
3078+ nlohmann::json pplns_map = nlohmann::json::object ();
3079+ for (const auto & s : result[" shares" ]) {
3080+ std::string sh = s[" h" ].get <std::string>();
3081+ auto p = mi->get_pplns_for_tip (sh);
3082+ if (!p.empty ()) pplns_map[sh] = std::move (p);
3083+ }
3084+ if (!pplns_map.empty ()) result[" pplns" ] = std::move (pplns_map);
3085+ }
3086+ return result;
3087+ });
3088+
3089+ // Lightweight tip endpoint for RealTime polling
3090+ web_server.get_mining_interface ()->set_sharechain_tip_fn ([&p2p_node]() {
3091+ auto & chain = p2p_node->tracker ().chain ;
3092+ uint256 best;
3093+ int32_t best_height = -1 ;
3094+ for (const auto & [head_hash, tail_hash] : chain.get_heads ()) {
3095+ auto h = chain.get_height (head_hash);
3096+ if (h > best_height) { best = head_hash; best_height = h; }
3097+ }
3098+ return nlohmann::json::object ({
3099+ {" hash" , best.IsNull () ? " " : best.GetHex ().substr (0 , 16 )},
3100+ {" height" , best_height},
3101+ {" total" , static_cast <int >(chain.size ())}
3102+ });
3103+ });
3104+
3105+ // Delta endpoint: return only shares newer than `since` hash
3106+ web_server.get_mining_interface ()->set_sharechain_delta_fn (
3107+ [&p2p_node, &web_server](const std::string& since_hash) {
3108+ nlohmann::json result;
3109+ auto & chain = p2p_node->tracker ().chain ;
3110+ auto & verified = p2p_node->tracker ().verified ;
3111+ bool testnet = ltc::PoolConfig::is_testnet;
3112+
3113+ uint256 best;
3114+ int32_t best_height = -1 ;
3115+ for (const auto & [head_hash, tail_hash] : chain.get_heads ()) {
3116+ auto h = chain.get_height (head_hash);
3117+ if (h > best_height) { best = head_hash; best_height = h; }
3118+ }
3119+
3120+ auto mi = web_server.get_mining_interface ();
3121+ std::string fee_addr;
3122+ if (mi) fee_addr = mi->get_node_fee_hash160 ();
3123+
3124+ nlohmann::json shares_arr = nlohmann::json::array ();
3125+ int count = 0 ;
3126+
3127+ if (!best.IsNull ()) {
3128+ int walk = std::min (static_cast <int >(chain.get_height (best)),
3129+ static_cast <int >(ltc::PoolConfig::chain_length ()));
3130+ try {
3131+ auto view = chain.get_chain (best, walk);
3132+ for (auto [hash, data] : view) {
3133+ std::string short_h = hash.GetHex ().substr (0 , 16 );
3134+ // Stop when we reach the share the client already has
3135+ if (short_h == since_hash || hash.GetHex () == since_hash)
3136+ break ;
3137+
3138+ nlohmann::json s;
3139+ s[" h" ] = short_h;
3140+ s[" H" ] = hash.GetHex ();
3141+ s[" p" ] = count;
3142+ s[" v" ] = verified.contains (hash) ? 1 : 0 ;
3143+
3144+ auto * idx = chain.get_index (hash);
3145+ if (idx && idx->is_block_solution )
3146+ s[" blk" ] = 1 ;
3147+
3148+ data.share .invoke ([&](auto * obj) {
3149+ s[" t" ] = obj->m_timestamp ;
3150+ s[" V" ] = obj->version ;
3151+ s[" s" ] = static_cast <int >(obj->m_stale_info );
3152+ s[" b" ] = obj->m_bits ;
3153+ s[" a" ] = obj->m_absheight ;
3154+ s[" dv" ] = obj->m_desired_version ;
3155+
3156+ auto script = get_share_script (obj);
3157+ std::string addr = core::script_to_address (script, true , testnet);
3158+ s[" m" ] = addr.empty () ? HexStr (script) : addr;
3159+
3160+ if (!obj->m_coinbase .m_data .empty ()) {
3161+ std::string best_run, cur_run;
3162+ for (auto c : obj->m_coinbase .m_data ) {
3163+ if (c >= 32 && c <= 126 ) cur_run += static_cast <char >(c);
3164+ else { if (cur_run.size () > best_run.size ()) best_run = cur_run; cur_run.clear (); }
3165+ }
3166+ if (cur_run.size () > best_run.size ()) best_run = cur_run;
3167+ bool has_letter = false ;
3168+ for (auto c : best_run) if ((c>=' A' &&c<=' Z' )||(c>=' a' &&c<=' z' )) { has_letter=true ; break ; }
3169+ if (best_run.size () >= 10 && has_letter) {
3170+ if (best_run.size () > 48 ) best_run.resize (48 );
3171+ s[" cb" ] = best_run;
3172+ }
3173+ }
3174+
3175+ if (!fee_addr.empty () && script.size () >= 22 ) {
3176+ int off = -1 ;
3177+ if (script.size ()==25 && script[0 ]==0x76 ) off=3 ;
3178+ else if (script.size ()==22 && script[0 ]==0x00 ) off=2 ;
3179+ else if (script.size ()==23 && script[0 ]==0xa9 ) off=2 ;
3180+ if (off >= 0 ) {
3181+ std::string h160 = HexStr (std::vector<unsigned char >(
3182+ script.begin ()+off, script.begin ()+off+20 ));
3183+ if (h160 == fee_addr) s[" fee" ] = 1 ;
3184+ }
3185+ }
3186+ });
3187+ shares_arr.push_back (std::move (s));
3188+ if (++count >= 200 ) break ; // safety cap
3189+ }
3190+ } catch (...) {}
3191+ }
3192+
3193+ // Updated heads/blocks for the client to merge
3194+ nlohmann::json heads_arr = nlohmann::json::array ();
3195+ for (auto & [hh, _] : chain.get_heads ())
3196+ heads_arr.push_back (hh.GetHex ().substr (0 , 16 ));
3197+
3198+ nlohmann::json blocks_arr = nlohmann::json::array ();
3199+ if (mi) {
3200+ for (const auto & fb : mi->get_found_blocks ())
3201+ if (!fb.share_hash .empty ()) blocks_arr.push_back (fb.share_hash .substr (0 , 16 ));
3202+ }
3203+ nlohmann::json doge_arr = nlohmann::json::array ();
3204+ if (mi) {
3205+ auto * mm = mi->get_mm_manager ();
3206+ if (mm) for (const auto & db : mm->get_discovered_blocks ())
3207+ if (!db.parent_hash .empty ()) doge_arr.push_back (db.parent_hash .substr (0 , 16 ));
3208+ }
3209+
3210+ result[" shares" ] = std::move (shares_arr);
3211+ result[" count" ] = count;
3212+ result[" tip" ] = best.IsNull () ? " " : best.GetHex ().substr (0 , 16 );
3213+ result[" heads" ] = std::move (heads_arr);
3214+ result[" blocks" ] = std::move (blocks_arr);
3215+ result[" doge_blocks" ] = std::move (doge_arr);
3216+
3217+ // Include per-share PPLNS snapshots from server cache
3218+ // Each share's tip had a unique PPLNS computed at arrival time
3219+ if (count > 0 && mi) {
3220+ nlohmann::json pplns_map = nlohmann::json::object ();
3221+ for (const auto & s : result[" shares" ]) {
3222+ std::string sh = s[" h" ].get <std::string>();
3223+ auto p = mi->get_pplns_for_tip (sh);
3224+ if (!p.empty ()) pplns_map[sh] = std::move (p);
3225+ }
3226+ result[" pplns" ] = std::move (pplns_map);
3227+ }
30273228 return result;
30283229 });
30293230
3231+ // Start background PPLNS pre-computation (separate thread, waits for sync)
3232+ web_server.get_mining_interface ()->start_pplns_precompute ();
3233+
30303234 // Wire individual share lookup for /web/share/<hash> detail page
30313235 web_server.get_mining_interface ()->set_share_lookup_fn (
30323236 [&p2p_node, &web_server](const std::string& hash_hex) -> nlohmann::json {
0 commit comments