Skip to content

Commit 8699490

Browse files
committed
Fix merged PPLNS hash: remove desired_weight cap from compute_merged_payout_hash
Multiple interrelated fixes for c2pool↔p2pool V36 consensus: 1. merged_payout_hash: walk VERIFIED chain only (not raw chain) to exclude c2pool's own unverified shares from the weight computation. Defer check until verified depth >= CHAIN_LENGTH to avoid bootstrap false positives. 2. desired_target: pass MAX_TARGET instead of block difficulty to compute_share_target(). Block difficulty made shares 2634x too hard. The [pre_target3//30, pre_target3] clipping provides the correct range. 3. Bootstrap share target: use hardest (lowest target) bits from received peer shares instead of MAX_TARGET during bootstrap. Prevents flooding the network with easy shares when joining an existing chain. 4. Add /miner_thresholds API endpoint: returns minimum viable hashrate, minimum payout per share, pool hashrate, and DUST range for dashboard. 5. Remove single-address merged coinbase fallback (submitauxblock path) — always use multi-address PPLNS block builder.
1 parent 341af1c commit 8699490

5 files changed

Lines changed: 245 additions & 39 deletions

File tree

src/c2pool/c2pool_refactored.cpp

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2056,8 +2056,12 @@ int main(int argc, char* argv[]) {
20562056
params.donation = static_cast<uint16_t>(std::round(65535.0 * dev_donation / 100.0));
20572057
params.desired_version = 36;
20582058

2059-
// Compute pool-level share target from tracker state
2060-
auto desired_target = chain::bits_to_target(bits);
2059+
// Compute pool-level share target from tracker state.
2060+
// Pass MAX_TARGET as desired_target so compute_share_target
2061+
// clips to pool share difficulty (pre_target3). Block difficulty
2062+
// is NOT the share target — it would make shares 1000x+ too hard.
2063+
// Per-miner difficulty adjustment happens via VARDIFF in stratum.
2064+
auto desired_target = ltc::PoolConfig::max_target();
20612065
auto [share_max_bits, share_bits] = p2p_node->tracker().compute_share_target(
20622066
params.prev_share, timestamp, desired_target);
20632067
params.max_bits = share_max_bits;
@@ -2125,7 +2129,7 @@ int main(int argc, char* argv[]) {
21252129

21262130
// Recompute share target with the clipped timestamp
21272131
{
2128-
auto desired_target2 = chain::bits_to_target(bits);
2132+
auto desired_target2 = ltc::PoolConfig::max_target();
21292133
auto [sm, sb] = tracker.compute_share_target(
21302134
params.prev_share, params.timestamp, desired_target2);
21312135
params.max_bits = sm;

src/c2pool/merged/merged_mining.cpp

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -769,27 +769,11 @@ void MergedMiningManager::try_submit_merged_blocks(
769769
proof.index,
770770
parent_header_hex);
771771

772-
if (chain.config.multiaddress && m_payout_provider) {
773-
// Strategy: submit via BOTH paths for maximum reliability.
774-
//
775-
// 1. submitauxblock FIRST (fast path) — uses daemon/adapter template,
776-
// arrives instantly because daemon already has the block ready.
777-
// The mm-adapter builds PPLNS coinbase, so payouts are correct.
778-
//
779-
// 2. submitblock SECOND (THE commitment path) — uses our custom
780-
// coinbase with /c2pool/ tag + THE state_root. If #1 already
781-
// won, this gets "duplicate" (harmless). If #1 failed, this
782-
// is the backup. If this wins the race, the block carries
783-
// our THE commitment on-chain.
784-
//
785-
// Both are valid blocks with identical PoW. The daemon picks
786-
// whichever arrives first. Different scriptSig text doesn't
787-
// invalidate the block — it just changes the merkle_root.
788-
789-
// Fast path: submitauxblock (daemon template)
790-
submit_aux_and_relay(chain, auxpow, parent_hash.GetHex());
791-
792-
// THE commitment path: build custom block with /c2pool/ + state_root
772+
// Always use the PPLNS multiaddress path with /c2pool/ tag +
773+
// THE state_root + COMBINED_DONATION_SCRIPT marker.
774+
// Even solo mode gets proper donation marker and fee outputs.
775+
// submitauxblock is never used — it bypasses our coinbase builder.
776+
if (m_payout_provider) {
793777
auto payouts = m_payout_provider(chain.config.chain_id,
794778
chain.current_work.coinbase_value);
795779
uint256 state_root;
@@ -799,19 +783,20 @@ void MergedMiningManager::try_submit_merged_blocks(
799783
chain.current_work.block_template, payouts, auxpow, state_root);
800784
if (!block_hex.empty()) {
801785
LOG_INFO << "[MM:" << chain.config.symbol
802-
<< "] THE block submitted (" << block_hex.size()/2 << " bytes, "
786+
<< "] PPLNS block submitted (" << block_hex.size()/2 << " bytes, "
803787
<< payouts.size() << " outputs, /c2pool/ + state_root)";
804788
chain.rpc->submit_block(block_hex);
805-
// Relay custom block via P2P too
806789
if (m_block_relay_fn) {
807790
try { m_block_relay_fn(chain.config.chain_id, block_hex); }
808791
catch (...) {}
809792
}
810793
}
811794
}
812795
} else {
813-
LOG_INFO << "[MM:" << chain.config.symbol << "] Single-address mode (multiaddress="
814-
<< chain.config.multiaddress << " payout_provider=" << !!m_payout_provider << ")";
796+
// No payout provider — cannot build coinbase. Use daemon template
797+
// as absolute last resort (no PPLNS, no donation marker).
798+
LOG_WARNING << "[MM:" << chain.config.symbol
799+
<< "] No payout_provider — falling back to submitauxblock (no PPLNS)";
815800
submit_aux_and_relay(chain, auxpow, parent_hash.GetHex());
816801
}
817802
LOG_INFO << "[MM:" << chain.config.symbol << "] Post-submission processing complete";

src/core/web_server.cpp

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,8 @@ void HttpSession::process_request()
266266
// ── p2pool legacy compatibility endpoints ──────────────────────
267267
else if (target == "/local_stats")
268268
rest_result = mining_interface_->rest_local_stats();
269+
else if (target == "/miner_thresholds")
270+
rest_result = mining_interface_->rest_miner_thresholds();
269271
else if (target == "/web/version")
270272
rest_result = mining_interface_->rest_web_version();
271273
else if (target == "/web/currency_info")
@@ -2387,6 +2389,64 @@ nlohmann::json MiningInterface::rest_stratum_stats()
23872389
return result;
23882390
}
23892391

2392+
nlohmann::json MiningInterface::rest_miner_thresholds()
2393+
{
2394+
nlohmann::json result = nlohmann::json::object();
2395+
2396+
// Get pool hashrate from global_stats computation
2397+
auto gs = rest_global_stats();
2398+
double pool_hr = gs.value("pool_hash_rate", 0.0);
2399+
2400+
// Get block subsidy from cached GBT template
2401+
uint64_t subsidy = 156250000; // default 1.5625 LTC
2402+
if (m_cached_template.contains("coinbasevalue"))
2403+
subsidy = m_cached_template.value("coinbasevalue", subsidy);
2404+
2405+
// Network params (testnet defaults, overridden by config)
2406+
uint32_t chain_length = 400; // REAL_CHAIN_LENGTH
2407+
uint32_t share_period = 4; // SHARE_PERIOD
2408+
if (m_node) {
2409+
auto hs = m_node->get_hashrate_stats();
2410+
if (hs.contains("chain_length"))
2411+
chain_length = hs.value("chain_length", chain_length);
2412+
if (hs.contains("share_period"))
2413+
share_period = hs.value("share_period", share_period);
2414+
}
2415+
2416+
double window_sec = static_cast<double>(chain_length) * share_period;
2417+
2418+
// min_hashrate = pool_hashrate / chain_length (1 share per window)
2419+
double min_hr_normal = pool_hr / chain_length;
2420+
double min_hr_dust = min_hr_normal / 30.0; // 30x DUST range
2421+
2422+
// min_payout = subsidy / chain_length
2423+
double min_payout = static_cast<double>(subsidy) / 1e8 / chain_length;
2424+
2425+
result["pool_hashrate"] = pool_hr;
2426+
result["min_hashrate_normal"] = min_hr_normal;
2427+
result["min_hashrate_dust"] = min_hr_dust;
2428+
result["min_payout_ltc"] = min_payout;
2429+
result["block_subsidy_ltc"] = static_cast<double>(subsidy) / 1e8;
2430+
result["chain_length"] = chain_length;
2431+
result["share_period"] = share_period;
2432+
result["window_seconds"] = window_sec;
2433+
result["dust_range"] = 30;
2434+
2435+
// Human-readable formatting
2436+
auto fmt_hr = [](double hr) -> std::string {
2437+
char buf[32];
2438+
if (hr >= 1e12) { snprintf(buf, sizeof(buf), "%.2f TH/s", hr / 1e12); return buf; }
2439+
if (hr >= 1e9) { snprintf(buf, sizeof(buf), "%.2f GH/s", hr / 1e9); return buf; }
2440+
if (hr >= 1e6) { snprintf(buf, sizeof(buf), "%.2f MH/s", hr / 1e6); return buf; }
2441+
if (hr >= 1e3) { snprintf(buf, sizeof(buf), "%.2f KH/s", hr / 1e3); return buf; }
2442+
snprintf(buf, sizeof(buf), "%.0f H/s", hr); return buf;
2443+
};
2444+
result["min_hashrate_display"] = fmt_hr(min_hr_dust);
2445+
result["pool_hashrate_display"] = fmt_hr(pool_hr);
2446+
2447+
return result;
2448+
}
2449+
23902450
nlohmann::json MiningInterface::rest_global_stats()
23912451
{
23922452
// Return p2pool-compatible pool statistics

src/core/web_server.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ class MiningInterface : public jsonrpccxx::JsonRpc2Server
168168
nlohmann::json rest_uptime();
169169
nlohmann::json rest_connected_miners();
170170
nlohmann::json rest_stratum_stats();
171+
nlohmann::json rest_miner_thresholds();
171172
nlohmann::json rest_global_stats();
172173
nlohmann::json rest_sharechain_stats();
173174
nlohmann::json rest_sharechain_window();

0 commit comments

Comments
 (0)