Skip to content

Commit b5812f8

Browse files
chopperbrianoclaude
andcommitted
Dashboard Serving/Asset index rows, getnodestats RPC, FIX RPC actually listens (win.31)
Three features plus a discovery of a massive pre-existing bug. FEATURES - Part A: new "Serving:" dashboard row polls IPFS bitswap stats every 30s (POST /api/v0/stats/bitswap) and displays BlocksSent total, rate per minute, and DataSent as a human-readable byte count. Proves to the operator that their node is actually serving content out to IPFS peers instead of just pinning it. Wired through a new NodeStats singleton so the getnodestats RPC method can report the same numbers without doing its own HTTP call. - Part B: new "Asset index:" dashboard row cross-checks the local assets table against mctrivia's /permanent/<page>.json list (every asset mctrivia's permanent storage pool tracks). Reports local count + tracked count + coverage percentage. A 100% result is strong evidence the chain analyzer is not missing any issuances, because PSP-enrolled and non-PSP-enrolled assets go through the same parse path. Missing assetIds are logged at WARNING level so they're visible without flipping to DEBUG. - Part C: new getnodestats RPC method returns a snapshot of buildVersion, syncHeight, assetCount, bitswap stats, and permanent-list coverage as JSON. Intended for two-node side-by-side comparison: DigiAssetCore-cli.exe getnodestats Run it on two nodes; if assetCount differs at the same syncHeight, one chain analyzer has a bug. - Expanded [H] info key: pressing H now prints a multi-section plain-English explanation of every dashboard row, what the numbers mean, and what the node is doing. Readable by a first-time operator with no DigiAsset background. FIX: RPC server actually listens now. The big one. src/boost/asio.hpp in this fork was a hand-written NO-OP STUB that silently emulated boost::asio's API with empty implementations: class acceptor { void open(int) {} void set_option(const reuse_address&) {} void bind(const endpoint&) {} void listen() {} void accept(socket&) { std::this_thread::sleep_for(std::chrono::seconds(1)); } }; Because <boost/asio.hpp> at src/ was shadowed by this stub (src/ is on the include path), RPC::Server compiled against it. Every bind/listen/ accept call was a no-op. The "RPC Server listening on port 14024" log line was logged after the no-op listen() returned. The "RPC call # received" DEBUG lines we had been staring at for hours were the stub's accept() loop spinning once per second, NOT real inbound connections. netstat never showed port 14024 bound because no socket was ever created. DigiAssetCore-cli.exe has never worked on this Windows fork for any RPC command. Not version, not getblockcount, not listassets, nothing. It always failed with a libcurl-like error because there was no server. Upstream CMakeLists.txt explicitly hacked around this for WebServer.cpp with a per-source COMPILE_FLAGS /I to the real boost path, acknowledging that "WebServer.cpp needs real Boost Beast headers (not the stub in src/boost/)" -- but nothing else got the fix. The fix, minimal but enough: - include_directories(BEFORE ...) in src/CMakeLists.txt puts the real boost 1.82 at packages/boost.1.82.0/lib/native/include AT THE FRONT of the include path, so <boost/asio.hpp> resolves to the real header for every source file globally. The stub at src/boost/asio.hpp becomes dead. - RPC/Server.h and RPC/Server.cpp switch from <boost/asio.hpp> to the specific sub-headers <boost/asio/io_context.hpp>, <boost/asio/ip/tcp.hpp>, <boost/asio/executor_work_guard.hpp>, <boost/asio/post.hpp>, and <boost/asio/write.hpp>. Sub-headers aren't shadowed by the stub so this also works independently. - cli/main.cpp drops its unused #include "RPC/Server.h" so the cli build doesn't need real boost headers at all. With real boost in the build, two latent bugs in Server::Server surfaced and needed fixing to compile: - _workGuard must be a MEMBER of Server, not a local in the ctor body. Previously it was `auto work = boost::asio::make_work_guard(_io);` which went out of scope the moment the ctor returned, leaving _io with no outstanding work and the 16 thread-pool threads exiting immediately. (This was invisible against the stub because the stub's run() was a no-op anyway; against real boost the thread pool would have been empty for the entire lifetime of Server.) Now a member initialized in the init list. - _acceptor must be initialized with an executor in the init list. Real boost::asio::basic_socket_acceptor has no default constructor. Plus defensive improvements in case accept() ever does exit in the future: - Server::start() now logs CRITICAL and calls AppMain::GetInstance()->setRpcServer(nullptr) when accept() returns, so the dashboard's RPC probe reflects reality instead of a dangling raw pointer from a destroyed Server object. - DigiByteCore::makeConnection() optionally prints the RPC URL to stderr when DGBCORE_DEBUG_URL=1 is set, for debugging cases where libcurl reports "couldn't resolve host" but the URL is valid. - ConsoleDashboard's spawn-race guard for the bitswap poll and the coverage scan no longer ORs against _probed / _checked. The _lastBitswapPoll / _lastCoverageCheck fields are epoch-initialized, so the first render fires immediately because `elapsed > threshold` is trivially true, and subsequent renders skip until the next interval elapses. Previously the `|| !_bitswapProbed` fallback caused every render during the first probe's in-flight window (~500ms) to spawn a duplicate probe thread. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7d2812d commit b5812f8

13 files changed

Lines changed: 704 additions & 24 deletions

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ SET(PATCH_VERSION 0)
4040
SET(SO_VERSION 0)
4141

4242
# Windows port build number (increment for each Windows-specific release)
43-
SET(WIN_BUILD 30)
43+
SET(WIN_BUILD 31)
4444

4545
# Add source directory
4646
include_directories(src)

cli/main.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
#include "Config.h"
22
#include "Database.h"
33
#include "DigiByteCore.h"
4-
#include "RPC/Server.h"
54
#include <iostream>
65
#include <jsonrpccpp/client.h>
76
#include <regex>

src/CMakeLists.txt

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,15 @@ find_package(CURL)
6565
include_directories("${CMAKE_SOURCE_DIR}/jsoncpp/install/include")
6666
include_directories("${CMAKE_SOURCE_DIR}/libjson-rpc-cpp/install/include")
6767

68+
# Real boost headers. Without this, any file that includes <boost/asio/*.hpp>
69+
# (Server.cpp, WebServer.cpp, etc.) can't find the headers at all, and files
70+
# that include <boost/asio.hpp> silently resolve to the no-op stub at
71+
# src/boost/asio.hpp — which for the RPC server silently no-op'd every
72+
# socket operation and meant the server never actually listened.
73+
if(WIN32 AND MSVC)
74+
include_directories(BEFORE "${CMAKE_SOURCE_DIR}/packages/boost.1.82.0/lib/native/include")
75+
endif()
76+
6877
#add_library(ldigibyteapi STATIC IMPORTED)
6978
add_library(lsqlite3 STATIC IMPORTED)
7079
add_library(llibcurl STATIC IMPORTED)
@@ -105,6 +114,7 @@ set(HEADER_FILES
105114
AppMain.h
106115
CurlHandler.h
107116
ConsoleDashboard.h
117+
NodeStats.h
108118
WebServer.h
109119
Version.h
110120
OldStream.h
@@ -148,6 +158,7 @@ set(SOURCE_FILES
148158
AppMain.cpp
149159
CurlHandler.cpp
150160
ConsoleDashboard.cpp
161+
NodeStats.cpp
151162
WebServer.cpp
152163
OldStream.cpp
153164
UniqueTaskQueue.cpp
@@ -168,9 +179,12 @@ set(SOURCE_FILES
168179

169180
add_executable(DigiAssetCore main.cpp ${SOURCE_FILES} ${HEADER_FILES} ${RPC_METHODS_FILES})
170181

171-
# WebServer.cpp needs real Boost Beast headers (not the stub in src/boost/)
182+
# WebServer.cpp AND RPC/Server.cpp both need real Boost Beast / boost::asio
183+
# headers, not the no-op stub in src/boost/. The stub makes everything compile
184+
# but silently never binds any sockets — which is how RPC on the Windows port
185+
# looked green on the dashboard but never actually listened on 14024.
172186
if(WIN32 AND MSVC)
173-
set_source_files_properties(WebServer.cpp PROPERTIES
187+
set_source_files_properties(WebServer.cpp RPC/Server.cpp PROPERTIES
174188
COMPILE_FLAGS "/I\"${CMAKE_SOURCE_DIR}/packages/boost.1.82.0/lib/native/include\"")
175189
endif()
176190

src/ConsoleDashboard.cpp

Lines changed: 405 additions & 10 deletions
Large diffs are not rendered by default.

src/ConsoleDashboard.h

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,33 @@ class ConsoleDashboard {
8383
std::chrono::steady_clock::time_point _lastPspCheck;
8484
void checkPspRegistration();
8585

86+
// IPFS bitswap stats: are we actually serving content out to peers?
87+
// pollBitswapStats() POSTs to <ipfspath>stats/bitswap every ~30s from a
88+
// detached thread. BlocksSent is a monotonic counter — we diff against
89+
// the previous reading to compute the rate.
90+
std::mutex _bitswapMutex;
91+
bool _bitswapProbed = false;
92+
bool _bitswapAvailable = false; // false if IPFS API unreachable
93+
uint64_t _bitswapBlocksSent = 0;
94+
uint64_t _bitswapDataSent = 0;
95+
uint64_t _bitswapBlocksSentPrev = 0;
96+
std::chrono::steady_clock::time_point _bitswapPrevTime;
97+
double _bitswapBlocksPerMin = 0.0;
98+
std::chrono::steady_clock::time_point _lastBitswapPoll;
99+
void pollBitswapStats();
100+
101+
// Permanent-list coverage check: download mctrivia's /permanent/<page>.json
102+
// pages 0..N, extract every assetId mctrivia tracks, and query the local
103+
// assets table to count how many we have. A healthy node should have
104+
// 100% coverage — every PSP-enrolled issuance on-chain should appear in
105+
// both lists. Missing assetIds indicate a chain-analyzer bug or lost sync.
106+
std::mutex _coverageMutex;
107+
bool _coverageChecked = false;
108+
unsigned int _coverageTrackedCount = 0; // total assetIds in mctrivia's permanent pages
109+
unsigned int _coverageHaveCount = 0; // of those, how many are in our local db
110+
std::chrono::steady_clock::time_point _lastCoverageCheck;
111+
void checkPermanentCoverage();
112+
86113
// Cached asset count (refreshed every 5 seconds)
87114
uint64_t _assetCount = 0;
88115
std::chrono::steady_clock::time_point _lastAssetCountTime;

src/DigiByteCore.cpp

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,25 @@ void DigiByteCore::makeConnection() {
9090

9191
//see if core is online and config if valid
9292
try {
93-
httpClient.reset(new jsonrpc::HttpClient(
93+
// Build the JSON-RPC URL separately so we can log it at DEBUG, and so
94+
// the cli can `-v` itself by enabling DEBUG-level output. libcurl
95+
// error 6 ("couldn't resolve host") against this URL usually means
96+
// rpcbind, rpcuser, or rpcpassword has a character that breaks the
97+
// libcurl URL parser — worth seeing the exact string in the log.
98+
std::string rpcUrl =
9499
"http://" + config.getString("rpcuser") + ":" +
95100
config.getString("rpcpassword") + "@" +
96101
config.getString("rpcbind", "127.0.0.1") + ":" +
97-
std::to_string(_useAssetPort ? config.getInteger("rpcassetport", 14024) : config.getInteger("rpcport", 14022))));
102+
std::to_string(_useAssetPort ? config.getInteger("rpcassetport", 14024) : config.getInteger("rpcport", 14022));
103+
// Diagnostic hook: set DGBCORE_DEBUG_URL=1 to print the JSON-RPC URL
104+
// to stderr on connect. Useful when debugging libcurl errors like
105+
// "couldn't resolve host" that usually mean a character in
106+
// rpcuser/rpcpassword/rpcbind broke the URL parser. Using cerr
107+
// directly (not Log) because the cli doesn't link Log.cpp.
108+
if (std::getenv("DGBCORE_DEBUG_URL")) {
109+
std::cerr << "DGBCORE_URL=" << rpcUrl << std::endl;
110+
}
111+
httpClient.reset(new jsonrpc::HttpClient(rpcUrl));
98112
client.reset(new jsonrpc::Client(*httpClient, jsonrpc::JSONRPC_CLIENT_V1));
99113
httpClient->SetTimeout(config.getInteger("rpctimeout", 50000));
100114
if (!_useAssetPort) getblockcount();

src/NodeStats.cpp

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#include "NodeStats.h"
2+
3+
NodeStats& NodeStats::instance() {
4+
static NodeStats s;
5+
return s;
6+
}
7+
8+
void NodeStats::setBitswap(bool available, uint64_t blocksSent, uint64_t dataSent, double blocksPerMin) {
9+
std::lock_guard<std::mutex> lk(_m);
10+
_bitswapProbed = true;
11+
_bitswapAvailable = available;
12+
if (available) {
13+
_blocksSent = blocksSent;
14+
_dataSent = dataSent;
15+
_blocksPerMin = blocksPerMin;
16+
}
17+
}
18+
19+
void NodeStats::setCoverage(unsigned int tracked, unsigned int have) {
20+
std::lock_guard<std::mutex> lk(_m);
21+
_coverageChecked = true;
22+
_coverageTracked = tracked;
23+
_coverageHave = have;
24+
}
25+
26+
NodeStats::Snapshot NodeStats::snapshot() {
27+
std::lock_guard<std::mutex> lk(_m);
28+
Snapshot s;
29+
s.bitswapProbed = _bitswapProbed;
30+
s.bitswapAvailable = _bitswapAvailable;
31+
s.blocksSent = _blocksSent;
32+
s.dataSent = _dataSent;
33+
s.blocksPerMin = _blocksPerMin;
34+
s.coverageChecked = _coverageChecked;
35+
s.coverageTracked = _coverageTracked;
36+
s.coverageHave = _coverageHave;
37+
return s;
38+
}

src/NodeStats.h

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//
2+
// NodeStats - shared process-wide cache of slow-to-compute stats so that
3+
// the dashboard and the RPC method getnodestats can both report the same
4+
// numbers without either one duplicating the HTTP calls or holding up the
5+
// RPC thread on slow network fetches.
6+
//
7+
// The dashboard is the writer: pollBitswapStats() and checkPermanentCoverage()
8+
// in ConsoleDashboard.cpp populate these fields as they complete. The RPC
9+
// method getnodestats.cpp is the reader. All accessors are mutex-guarded.
10+
//
11+
12+
#ifndef DIGIASSET_CORE_NODESTATS_H
13+
#define DIGIASSET_CORE_NODESTATS_H
14+
15+
#include <cstdint>
16+
#include <mutex>
17+
#include <string>
18+
19+
class NodeStats {
20+
std::mutex _m;
21+
bool _bitswapProbed = false;
22+
bool _bitswapAvailable = false;
23+
uint64_t _blocksSent = 0;
24+
uint64_t _dataSent = 0;
25+
double _blocksPerMin = 0.0;
26+
27+
bool _coverageChecked = false;
28+
unsigned int _coverageTracked = 0;
29+
unsigned int _coverageHave = 0;
30+
31+
NodeStats() = default;
32+
33+
public:
34+
NodeStats(const NodeStats&) = delete;
35+
NodeStats& operator=(const NodeStats&) = delete;
36+
static NodeStats& instance();
37+
38+
void setBitswap(bool available, uint64_t blocksSent, uint64_t dataSent, double blocksPerMin);
39+
void setCoverage(unsigned int tracked, unsigned int have);
40+
41+
struct Snapshot {
42+
bool bitswapProbed;
43+
bool bitswapAvailable;
44+
uint64_t blocksSent;
45+
uint64_t dataSent;
46+
double blocksPerMin;
47+
bool coverageChecked;
48+
unsigned int coverageTracked;
49+
unsigned int coverageHave;
50+
};
51+
Snapshot snapshot();
52+
};
53+
54+
#endif // DIGIASSET_CORE_NODESTATS_H

src/RPC/MethodList.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ namespace RPC {
2020
{"getencryptedkey", Methods::getencryptedkey},
2121
{"getexchangerates", Methods::getexchangerates},
2222
{"getipfscount", Methods::getipfscount},
23+
{"getnodestats", Methods::getnodestats},
2324
{"getoldstreamkey", Methods::getoldstreamkey},
2425
{"getpsp", Methods::getpsp},
2526
{"getrandom", Methods::getrandom},

src/RPC/MethodList.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ namespace RPC {
2626
extern const Response getencryptedkey(const Json::Value& params);
2727
extern const Response getexchangerates(const Json::Value& params);
2828
extern const Response getipfscount(const Json::Value& params);
29+
extern const Response getnodestats(const Json::Value& params);
2930
extern const Response getoldstreamkey(const Json::Value& params);
3031
extern const Response getpsp(const Json::Value& params);
3132
extern const Response getrandom(const Json::Value& params);

0 commit comments

Comments
 (0)