Skip to content

Commit c0df013

Browse files
committed
core: p2p message counters, tx-pool gauges and embedded-timestamp diagnostics
Not one of the canonical p2pool pool-protocol message types emitted anything at any verbosity, and m_known_txs.size() was exposed nowhere -- no log, no dashboard field, no JSON. A live investigation had to guess, and a log-grep returning zero was misread as "not implemented" for code that was fully wired and running. A grep of zero is not evidence when nothing ever logs. Adds core/p2p_message_stats.hpp: a process-wide struct of relaxed atomics, incremented at the two choke points every pool message must pass through -- pool::NodeBridge::handle() inbound and pool::Peer::write() outbound. Both are shared code, so every coin lane is instrumented without touching a single per-coin protocol file, and the numbers mean the same thing across coins. Read back through a new read-only endpoint, GET /p2p_stats: messages.<type>.{in,out} per-type wire counters for version, verack, ping, pong, addrme, addrs, getaddrs, shares, sharereq, sharereply, have_tx, losing_tx, remember_tx, forget_tx, bestblock, plus an unknown bucket totals.{in,out} txpool.known_txs_size m_known_txs.size() txpool.known_txs_order_size m_known_txs_order.size(), null where the lane has no recency deque (DASH) txpool.last_have_tx_advert_size size of the last have_tx put on the wire txpool.last_losing_tx_advert_size txpool.have_tx_adverts_sent sharechain_timestamps.tip_lag_seconds now - tip share_data.timestamp sharechain_timestamps.saturation_fraction share of the last 100 embedded deltas pinned to the clip bound sharechain_timestamps.clip_upper_bound 2*SHARE_PERIOD-1 (39 s on DASH) sharechain_timestamps.{delta_samples,delta_saturated,tip_embedded_timestamp} The txpool block settles whether TXPOOL=0 on a peer dashboard means our pool is genuinely empty or the advert is being suppressed. The timestamp block exists because the aggregate gauges proved untrustworthy. Upstream p2pool clips embedded share timestamps to [prev+1, prev+2*SHARE_PERIOD-1] (data.py:239-242); once real cadence outruns that bound the embedded clock saturates, the difficulty retarget goes blind to hashrate and the floor decays. A 6.81 hour embedded lag was measured live. pool_hash_rate and min_difficulty are computed from that same saturated history, so they report floor decay rather than the pool -- tip lag and saturation fraction are the honest early-warning pair. DASH publishes the timestamp diagnostics from publish_snapshot() (already under the exclusive tracker lock, bounded at 101 chain lookups per think cycle) and the tx-pool gauges from the 10 s advert sweep; btc/ltc/dgb publish their tx-pool gauges from prune_shares(). Observability only: counters and read-only fields. No consensus, minting, share bytes or peer behaviour changes. Per-message logging stays off by default behind a flag nothing in-tree enables. Tests folded into the EXISTING allowlisted core_test target (never a new add_executable -- that is the "Not Run" trap): 12 cases over the command mapping including the NUL-padded wire form, the per-direction counters, and the saturation detector.
1 parent 0170f4c commit c0df013

12 files changed

Lines changed: 800 additions & 5 deletions

File tree

docs/DASHBOARD_INTEGRATION.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,64 @@ custom dashboard, or scraping pool metrics.
104104
| `/ban_stats` | P2P ban statistics |
105105
| `/api/coin_peers` | Parent-coin daemon peer info |
106106
| `/api/node_topology` | Sharechain topology graph |
107+
| `/p2p_stats` | Per-message-type p2p wire counters + tx-pool and embedded-timestamp gauges |
108+
109+
### `/p2p_stats`
110+
111+
Read-only observability for the pool p2p protocol. Before it existed, none of
112+
the pool message types emitted anything at any verbosity, so "is this message
113+
type actually moving on the wire?" had no observable answer and a log-grep
114+
returning zero was easy to misread as "not implemented".
115+
116+
```json
117+
{
118+
"messages": { "shares": { "in": 412, "out": 388 }, "remember_tx": { "in": 9, "out": 14 }, "...": {} },
119+
"totals": { "in": 1204, "out": 1189 },
120+
"trace_enabled": false,
121+
"txpool": {
122+
"known_txs_size": 12043,
123+
"known_txs_order_size": 12043,
124+
"last_have_tx_advert_size": 500,
125+
"last_losing_tx_advert_size": 0,
126+
"have_tx_adverts_sent": 37,
127+
"updated_at": 1785049468
128+
},
129+
"sharechain_timestamps": {
130+
"tip_embedded_timestamp": 1785024952,
131+
"tip_lag_seconds": 24516,
132+
"clip_upper_bound": 39,
133+
"delta_samples": 100,
134+
"delta_saturated": 97,
135+
"saturation_fraction": 0.97,
136+
"updated_at": 1785049468
137+
}
138+
}
139+
```
140+
141+
* `messages` — one `in`/`out` pair per canonical p2pool message type, counted at
142+
the two shared choke points every pool message passes through, so the numbers
143+
are identical in meaning across every coin. `unknown` buckets anything that
144+
did not match a known command. `verack`/`pong` belong to the coin-daemon p2p
145+
layer, not the pool protocol, and read 0 on a healthy pool node — that zero is
146+
an answer, not a gap.
147+
* `txpool` — settles whether a peer dashboard showing `TXPOOL=0` for this node
148+
means the pool is genuinely empty (`known_txs_size` 0) or the advert is being
149+
suppressed (`known_txs_size` > 0 with no adverts sent).
150+
`known_txs_order_size` is `null` on lanes with no recency deque (DASH), which
151+
is deliberately distinct from `0`.
152+
* `sharechain_timestamps``tip_lag_seconds` is wall-clock now minus the chain
153+
tip's **embedded** `share_data.timestamp`; `saturation_fraction` is the share
154+
of the last 100 embedded deltas pinned exactly to the clip upper bound
155+
(`2*SHARE_PERIOD - 1`; 39 s on DASH). Upstream p2pool clips embedded
156+
timestamps to `[prev+1, prev+2*SHARE_PERIOD-1]`, so once real cadence outruns
157+
that bound the embedded clock saturates, the difficulty retarget goes blind to
158+
hashrate, and the floor decays. **These two fields are the honest
159+
early-warning signal; `pool_hash_rate` and `min_difficulty` are not** — under
160+
saturation they are computed from that same saturated history and report floor
161+
decay rather than the pool.
162+
163+
`trace_enabled` reports the per-message debug log, which is off by default and
164+
is not switched on by any code path in-tree; the counters are the mechanism.
107165

108166
---
109167

src/core/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ set(source
2828
opscript.hpp
2929
random.hpp random.cpp
3030
addr_store.hpp addr_store.cpp
31+
p2p_message_stats.hpp
3132
web_server.hpp web_server.cpp
3233
http_session.cpp
3334
stratum_server.hpp stratum_server.cpp

src/core/http_session.cpp

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
#include "web_server.hpp"
1818
#include "filesystem.hpp"
19+
#include "p2p_message_stats.hpp"
1920

2021
#include <filesystem>
2122
#include <fstream>
@@ -27,6 +28,65 @@
2728

2829
namespace core {
2930

31+
// ── /p2p_stats — read-only p2p wire observability ──────────────────────
32+
//
33+
// Serialises core::obs::p2p_stats() (src/core/p2p_message_stats.hpp). Pure
34+
// reader: it loads relaxed atomics and formats them. It calls NOTHING on the
35+
// node, takes no lock, and can never block the IO or compute thread — which is
36+
// also why it lives here rather than behind a MiningInterface accessor.
37+
//
38+
// Answers, per message type and direction, "did this actually move on the
39+
// wire?" — a question that previously had NO observable answer at any
40+
// verbosity, so a log-grep returning zero was misread as "not implemented".
41+
static nlohmann::json build_p2p_stats_json()
42+
{
43+
const auto& s = obs::p2p_stats();
44+
45+
nlohmann::json messages = nlohmann::json::object();
46+
for (std::size_t i = 0; i < obs::P2P_MESSAGE_COUNT; ++i) {
47+
const auto m = static_cast<obs::P2PMessage>(i);
48+
messages[std::string(obs::p2p_message_name(m))] = {
49+
{"in", s.get_in(m)},
50+
{"out", s.get_out(m)}
51+
};
52+
}
53+
54+
const auto samples = s.ts_delta_samples.load(std::memory_order_relaxed);
55+
const auto saturated = s.ts_delta_saturated.load(std::memory_order_relaxed);
56+
const auto order_size = s.known_txs_order_size.load(std::memory_order_relaxed);
57+
58+
nlohmann::json out;
59+
out["messages"] = std::move(messages);
60+
out["totals"] = {{"in", s.total_in()}, {"out", s.total_out()}};
61+
out["trace_enabled"] = s.trace_enabled.load(std::memory_order_relaxed);
62+
63+
// Tx-pool visibility. known_txs_order_size is null on lanes with no
64+
// recency deque (DASH), NOT 0 — an absent sidecar is not an empty one.
65+
out["txpool"] = {
66+
{"known_txs_size", s.known_txs_size.load(std::memory_order_relaxed)},
67+
{"known_txs_order_size", order_size < 0 ? nlohmann::json(nullptr)
68+
: nlohmann::json(order_size)},
69+
{"last_have_tx_advert_size", s.last_have_tx_advert_size.load(std::memory_order_relaxed)},
70+
{"last_losing_tx_advert_size", s.last_losing_tx_advert_size.load(std::memory_order_relaxed)},
71+
{"have_tx_adverts_sent", s.have_tx_adverts_sent.load(std::memory_order_relaxed)},
72+
{"updated_at", s.known_txs_updated_at.load(std::memory_order_relaxed)}
73+
};
74+
75+
// Sharechain embedded-timestamp health. tip_lag_seconds and
76+
// saturation_fraction are the honest early-warning pair; pool_hash_rate /
77+
// min_difficulty are NOT (under saturation they measure floor history).
78+
out["sharechain_timestamps"] = {
79+
{"tip_embedded_timestamp", s.tip_embedded_timestamp.load(std::memory_order_relaxed)},
80+
{"tip_lag_seconds", s.tip_lag_seconds.load(std::memory_order_relaxed)},
81+
{"clip_upper_bound", s.ts_clip_upper_bound.load(std::memory_order_relaxed)},
82+
{"delta_samples", samples},
83+
{"delta_saturated", saturated},
84+
{"saturation_fraction", samples ? static_cast<double>(saturated) / samples : 0.0},
85+
{"updated_at", s.sharechain_updated_at.load(std::memory_order_relaxed)}
86+
};
87+
return out;
88+
}
89+
3090

3191
// ── Security helpers ───────────────────────────────────────────────────
3292
// URL-decode percent-encoded strings (%20 → space, etc.)
@@ -380,6 +440,8 @@ void HttpSession::process_request()
380440
rest_result = mining_interface_->rest_v36_status();
381441
else if (target == "/tracker_debug")
382442
rest_result = mining_interface_->rest_tracker_debug();
443+
else if (target == "/p2p_stats")
444+
rest_result = build_p2p_stats_json();
383445

384446
// Merged mining endpoints
385447
else if (target == "/merged_stats")

0 commit comments

Comments
 (0)