Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions docs/DASHBOARD_INTEGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,64 @@ custom dashboard, or scraping pool metrics.
| `/ban_stats` | P2P ban statistics |
| `/api/coin_peers` | Parent-coin daemon peer info |
| `/api/node_topology` | Sharechain topology graph |
| `/p2p_stats` | Per-message-type p2p wire counters + tx-pool and embedded-timestamp gauges |

### `/p2p_stats`

Read-only observability for the pool p2p protocol. Before it existed, none of
the pool message types emitted anything at any verbosity, so "is this message
type actually moving on the wire?" had no observable answer and a log-grep
returning zero was easy to misread as "not implemented".

```json
{
"messages": { "shares": { "in": 412, "out": 388 }, "remember_tx": { "in": 9, "out": 14 }, "...": {} },
"totals": { "in": 1204, "out": 1189 },
"trace_enabled": false,
"txpool": {
"known_txs_size": 12043,
"known_txs_order_size": 12043,
"last_have_tx_advert_size": 500,
"last_losing_tx_advert_size": 0,
"have_tx_adverts_sent": 37,
"updated_at": 1785049468
},
"sharechain_timestamps": {
"tip_embedded_timestamp": 1785024952,
"tip_lag_seconds": 24516,
"clip_upper_bound": 39,
"delta_samples": 100,
"delta_saturated": 97,
"saturation_fraction": 0.97,
"updated_at": 1785049468
}
}
```

* `messages` — one `in`/`out` pair per canonical p2pool message type, counted at
the two shared choke points every pool message passes through, so the numbers
are identical in meaning across every coin. `unknown` buckets anything that
did not match a known command. `verack`/`pong` belong to the coin-daemon p2p
layer, not the pool protocol, and read 0 on a healthy pool node — that zero is
an answer, not a gap.
* `txpool` — settles whether a peer dashboard showing `TXPOOL=0` for this node
means the pool is genuinely empty (`known_txs_size` 0) or the advert is being
suppressed (`known_txs_size` > 0 with no adverts sent).
`known_txs_order_size` is `null` on lanes with no recency deque (DASH), which
is deliberately distinct from `0`.
* `sharechain_timestamps` — `tip_lag_seconds` is wall-clock now minus the chain
tip's **embedded** `share_data.timestamp`; `saturation_fraction` is the share
of the last 100 embedded deltas pinned exactly to the clip upper bound
(`2*SHARE_PERIOD - 1`; 39 s on DASH). Upstream p2pool clips embedded
timestamps to `[prev+1, prev+2*SHARE_PERIOD-1]`, so once real cadence outruns
that bound the embedded clock saturates, the difficulty retarget goes blind to
hashrate, and the floor decays. **These two fields are the honest
early-warning signal; `pool_hash_rate` and `min_difficulty` are not** — under
saturation they are computed from that same saturated history and report floor
decay rather than the pool.

`trace_enabled` reports the per-message debug log, which is off by default and
is not switched on by any code path in-tree; the counters are the mechanism.

---

Expand Down
1 change: 1 addition & 0 deletions src/core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ set(source
opscript.hpp
random.hpp random.cpp
addr_store.hpp addr_store.cpp
p2p_message_stats.hpp
web_server.hpp web_server.cpp
http_session.cpp
stratum_server.hpp stratum_server.cpp
Expand Down
62 changes: 62 additions & 0 deletions src/core/http_session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

#include "web_server.hpp"
#include "filesystem.hpp"
#include "p2p_message_stats.hpp"

#include <filesystem>
#include <fstream>
Expand All @@ -27,6 +28,65 @@

namespace core {

// ── /p2p_stats — read-only p2p wire observability ──────────────────────
//
// Serialises core::obs::p2p_stats() (src/core/p2p_message_stats.hpp). Pure
// reader: it loads relaxed atomics and formats them. It calls NOTHING on the
// node, takes no lock, and can never block the IO or compute thread — which is
// also why it lives here rather than behind a MiningInterface accessor.
//
// Answers, per message type and direction, "did this actually move on the
// wire?" — a question that previously had NO observable answer at any
// verbosity, so a log-grep returning zero was misread as "not implemented".
static nlohmann::json build_p2p_stats_json()
{
const auto& s = obs::p2p_stats();

nlohmann::json messages = nlohmann::json::object();
for (std::size_t i = 0; i < obs::P2P_MESSAGE_COUNT; ++i) {
const auto m = static_cast<obs::P2PMessage>(i);
messages[std::string(obs::p2p_message_name(m))] = {
{"in", s.get_in(m)},
{"out", s.get_out(m)}
};
}

const auto samples = s.ts_delta_samples.load(std::memory_order_relaxed);
const auto saturated = s.ts_delta_saturated.load(std::memory_order_relaxed);
const auto order_size = s.known_txs_order_size.load(std::memory_order_relaxed);

nlohmann::json out;
out["messages"] = std::move(messages);
out["totals"] = {{"in", s.total_in()}, {"out", s.total_out()}};
out["trace_enabled"] = s.trace_enabled.load(std::memory_order_relaxed);

// Tx-pool visibility. known_txs_order_size is null on lanes with no
// recency deque (DASH), NOT 0 — an absent sidecar is not an empty one.
out["txpool"] = {
{"known_txs_size", s.known_txs_size.load(std::memory_order_relaxed)},
{"known_txs_order_size", order_size < 0 ? nlohmann::json(nullptr)
: nlohmann::json(order_size)},
{"last_have_tx_advert_size", s.last_have_tx_advert_size.load(std::memory_order_relaxed)},
{"last_losing_tx_advert_size", s.last_losing_tx_advert_size.load(std::memory_order_relaxed)},
{"have_tx_adverts_sent", s.have_tx_adverts_sent.load(std::memory_order_relaxed)},
{"updated_at", s.known_txs_updated_at.load(std::memory_order_relaxed)}
};

// Sharechain embedded-timestamp health. tip_lag_seconds and
// saturation_fraction are the honest early-warning pair; pool_hash_rate /
// min_difficulty are NOT (under saturation they measure floor history).
out["sharechain_timestamps"] = {
{"tip_embedded_timestamp", s.tip_embedded_timestamp.load(std::memory_order_relaxed)},
{"tip_lag_seconds", s.tip_lag_seconds.load(std::memory_order_relaxed)},
{"clip_upper_bound", s.ts_clip_upper_bound.load(std::memory_order_relaxed)},
{"delta_samples", samples},
{"delta_saturated", saturated},
{"saturation_fraction", samples ? static_cast<double>(saturated) / samples : 0.0},
{"updated_at", s.sharechain_updated_at.load(std::memory_order_relaxed)}
};
return out;
}


// ── Security helpers ───────────────────────────────────────────────────
// URL-decode percent-encoded strings (%20 → space, etc.)
Expand Down Expand Up @@ -380,6 +440,8 @@ void HttpSession::process_request()
rest_result = mining_interface_->rest_v36_status();
else if (target == "/tracker_debug")
rest_result = mining_interface_->rest_tracker_debug();
else if (target == "/p2p_stats")
rest_result = build_p2p_stats_json();

// Merged mining endpoints
else if (target == "/merged_stats")
Expand Down
Loading
Loading