Files
ObsidianDragon/tools/fake_dragonxd/fake_dragonxd.cpp
T
DanSandClaude Opus 5.5 323143e111 perf(net): one scan budget for all wallet scans, gated on what changed
The balance, address scan, history and probe were throttled one by one at
10% each, which added up to 30-40% of dragonxd's cs_main with all of them
due. services::WalletScanGovernor runs one wallet scan at a time across
both RPC lanes and keeps one shared quiet clock at 15% of wall-clock (cut
when the node is behind). Waiters are served first in, first out, with a
head start for what the page shows and a starvation floor of max(30 s
visible / 900 s hidden, 15x the stream's cost). Explicit work (a dirty
set, the user's send, a first load) is never refused but is charged.

Routine scans run only when the tip, the mempool size (getmempoolinfo:
no cs_main) or a wallet-mutation epoch changed since their last good scan,
or a safety sweep is due. Pages that don't show the balance poll it at
most every max(10 min, 50x its cost) (2 min with client-side auto-shield);
coming back into view with an old value fetches it at once. The staleness
badge counts from the last poll that confirmed nothing changed, and hides
while the budget holds the balance within its guarantee.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 11:06:34 -05:00

1534 lines
60 KiB
C++

// fake_dragonxd — a deliberately slow, scriptable stand-in for dragonxd (test tooling only).
//
// Reproduces, in minutes and without a 13k-note wallet, the conditions behind the 2026-09 "GUI
// hard-kills a busy node" incident (see README.md):
// * dragonxd's HTTP-RPC queue: N workers + a bounded queue (8 + 16 by default, -rpcthreads /
// -rpcworkqueue honoured). A full queue gets an immediate plain-text HTTP 500 "Work queue depth
// exceeded" before any handler runs, and a request whose client gave up keeps its slot until a
// worker reaches it (libevent has no close callback);
// * a global cs_main that a "witness rebuild" holds for minutes, logging the real markers. Every
// method except the lock-free `stop` / `help` / `getrescaninfo` takes it;
// * dragonxd's shutdown: `stop` (or SIGTERM/SIGINT on POSIX; Windows has no signal channel, as in
// v1.3.0) unbinds the listener at once, answers new requests on open connections with 503, then
// waits for the rebuild (Phase 2 is not abortable) before "Shutdown: In progress..." -> flush ->
// "Shutdown: done" -> exit 0.
// Everything it does is appended to <datadir>/fake_dragonxd.events, so a test can tell a clean exit
// ("clean-exit") from a hard kill (no "clean-exit"). Knobs are FAKE_* environment variables; see
// README.md. Built only with -DDRAGONX_BUILD_FAKE_DAEMON=ON and never shipped.
#include <nlohmann/json.hpp>
#include <algorithm>
#include <atomic>
#include <cctype>
#include <chrono>
#include <condition_variable>
#include <csignal>
#include <cstdarg>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <deque>
#include <filesystem>
#include <fstream>
#include <map>
#include <memory>
#include <mutex>
#include <random>
#include <string>
#include <thread>
#include <vector>
#ifdef _WIN32
# ifndef WIN32_LEAN_AND_MEAN
# define WIN32_LEAN_AND_MEAN
# endif
# ifndef NOMINMAX
# define NOMINMAX
# endif
# include <winsock2.h>
# include <ws2tcpip.h>
# include <windows.h>
using socket_t = SOCKET;
static const socket_t kBadSocket = INVALID_SOCKET;
static void closeSocket(socket_t s) { closesocket(s); }
#else
# include <arpa/inet.h>
# include <fcntl.h>
# include <netinet/in.h>
# include <sys/select.h>
# include <sys/socket.h>
# include <unistd.h>
# ifdef __linux__
# include <sys/prctl.h>
# endif
using socket_t = int;
static const socket_t kBadSocket = -1;
static void closeSocket(socket_t s) { ::close(s); }
#endif
#ifndef MSG_NOSIGNAL
# define MSG_NOSIGNAL 0 // Windows/macOS: SIGPIPE is ignored process-wide instead (POSIX) or absent
#endif
namespace fs = std::filesystem;
using json = nlohmann::json;
using Clock = std::chrono::steady_clock;
using namespace std::chrono_literals;
namespace {
// =============================================================================================
// Configuration
// =============================================================================================
double envDouble(const char* name, double def)
{
const char* v = std::getenv(name);
if (!v || !*v) return def;
char* end = nullptr;
const double d = std::strtod(v, &end);
return (end && *end == '\0') ? d : def;
}
long envLong(const char* name, long def) { return static_cast<long>(envDouble(name, static_cast<double>(def))); }
struct Config {
fs::path datadir;
bool printToConsole = false;
int rpcPort = 21769;
int rpcThreads = 8; // DEFAULT_HTTP_THREADS
int rpcWorkQueue = 16; // DEFAULT_HTTP_WORKQUEUE
std::string rpcUser, rpcPassword;
// FAKE_* knobs. Times are seconds.
double warmupSecs = 3; // -28 "Loading block index..." before anything else
double replaySecs = 0; // -28 "Activating best chain..." with UpdateTip lines (startup replay)
double replayBlocksPerSec = 12;
double rebuildDelaySecs = 20; // after "Done Loading", so the GUI connects before the rebuild
double rebuildSecs = 300; // how long the rebuild holds cs_main; 0 = no rebuild
double phase1Secs = 2; // the abortable, logged part of the rebuild
double postRebuildLockSecs = 0; // cs_main held after the end line (periodic flush + SetBestChain)
double flushSecs = 3; // "Shutdown: In progress..." -> "Shutdown: done"
double updateTipSecs = 36; // block interval once loaded
double noiseSecs = 20; // an unrelated log line this often (defeats tail heuristics)
double serverTimeoutSecs = 30; // -rpcservertimeout: idle keep-alive connections are closed
long prefill = 0; // phantom abandoned requests injected when the rebuild starts
long height = 3327145;
long rebuildTotal = 62583;
long notes = 13632;
long version = 1030050; // getinfo "version" (real v1.3.0 reports 1030050)
bool stuckShutdown = false; // the Sep 29 orphan: datadir lock held, no listener, never exits
bool mining = false; // internal miner on: `stop` must wait for cs_main first
bool rebuildSilent = false; // hold cs_main like a rebuild but log no rebuild markers at all
};
Config g_cfg;
std::map<std::string, std::string> parseArgs(int argc, char** argv)
{
std::map<std::string, std::string> out;
for (int i = 1; i < argc; ++i) {
std::string a = argv[i];
const size_t start = a.find_first_not_of('-');
if (start == 0 || start == std::string::npos) continue;
a = a.substr(start);
const size_t eq = a.find('=');
if (eq == std::string::npos) out[a] = "1";
else out[a.substr(0, eq)] = a.substr(eq + 1);
}
return out;
}
std::map<std::string, std::string> parseConf(const fs::path& path)
{
std::map<std::string, std::string> out;
std::ifstream f(path);
std::string line;
while (std::getline(f, line)) {
if (!line.empty() && line.back() == '\r') line.pop_back();
const size_t hash = line.find('#');
if (hash != std::string::npos) line.erase(hash);
const size_t eq = line.find('=');
if (eq == std::string::npos) continue;
auto trim = [](std::string s) {
const size_t b = s.find_first_not_of(" \t");
const size_t e = s.find_last_not_of(" \t");
return b == std::string::npos ? std::string() : s.substr(b, e - b + 1);
};
out[trim(line.substr(0, eq))] = trim(line.substr(eq + 1));
}
return out;
}
fs::path defaultDatadir()
{
#ifdef _WIN32
const char* appdata = std::getenv("APPDATA");
return fs::path(appdata ? appdata : ".") / "Hush" / "DRAGONX";
#else
const char* home = std::getenv("HOME");
const fs::path h = home ? home : ".";
# ifdef __APPLE__
return h / "Library" / "Application Support" / "Hush" / "DRAGONX";
# else
return h / ".hush" / "DRAGONX";
# endif
#endif
}
// =============================================================================================
// Time, logging, events
// =============================================================================================
long long unixMs()
{
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
}
std::string utcStamp(std::time_t t)
{
std::tm tm{};
#ifdef _WIN32
gmtime_s(&tm, &t);
#else
gmtime_r(&t, &tm);
#endif
char buf[32];
std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm);
return buf;
}
std::string vformat(const char* fmt, va_list ap)
{
char buf[2048];
std::vsnprintf(buf, sizeof(buf), fmt, ap);
return buf;
}
// dragonxd's LogPrintf: with -printtoconsole every line goes to stdout INSTEAD of debug.log and
// without a timestamp; otherwise to <datadir>/debug.log with a "YYYY-MM-DD HH:MM:SS " UTC prefix.
class Logger {
public:
void open(const fs::path& datadir, bool console)
{
console_ = console;
if (!console_) file_ = std::fopen((datadir / "debug.log").string().c_str(), "ab");
}
void line(const std::string& msg)
{
std::lock_guard<std::mutex> lk(mx_);
if (console_) {
std::fputs((msg + "\n").c_str(), stdout); // EPIPE after the reader left is ignored, like dragonxd
std::fflush(stdout);
} else if (file_) {
std::fprintf(file_, "%s %s\n", utcStamp(std::time(nullptr)).c_str(), msg.c_str());
std::fflush(file_);
}
}
void close()
{
std::lock_guard<std::mutex> lk(mx_);
if (file_) std::fclose(file_);
file_ = nullptr;
}
private:
std::mutex mx_;
FILE* file_ = nullptr;
bool console_ = false;
};
Logger g_log;
void logf(const char* fmt, ...)
{
va_list ap;
va_start(ap, fmt);
const std::string s = vformat(fmt, ap);
va_end(ap);
g_log.line(s);
}
// <datadir>/fake_dragonxd.events: one "<name> t=<unix ms> [k=v ...]" line per event, for tests.
std::mutex g_eventsMx;
FILE* g_events = nullptr;
void event(const std::string& name, const std::string& extra = "")
{
std::lock_guard<std::mutex> lk(g_eventsMx);
if (!g_events) return;
std::fprintf(g_events, "%s t=%lld%s%s\n", name.c_str(), unixMs(), extra.empty() ? "" : " ", extra.c_str());
std::fflush(g_events);
}
// =============================================================================================
// Shared daemon state
// =============================================================================================
std::mutex g_csMain; // the daemon's cs_main
std::atomic<long> g_height{0};
std::atomic<bool> g_warmup{true};
std::mutex g_warmupMx;
std::string g_warmupStatus = "Loading block index...";
std::atomic<bool> g_minerOn{false};
enum ShutdownReason { kNoShutdown = 0, kRpcStop = 1, kSignal = 2 };
std::atomic<int> g_shutdownReason{kNoShutdown};
std::atomic<int> g_stopCount{0};
volatile std::sig_atomic_t g_signalled = 0;
extern "C" void onTermSignal(int) { g_signalled = 1; }
void requestShutdown(ShutdownReason reason)
{
if (reason == kRpcStop) {
const int n = ++g_stopCount;
event("stop-received", "n=" + std::to_string(n));
}
int expected = kNoShutdown;
if (g_shutdownReason.compare_exchange_strong(expected, reason) && reason == kSignal) event("sigterm");
}
bool shutdownRequested()
{
if (g_signalled && g_shutdownReason.load() == kNoShutdown) requestShutdown(kSignal);
return g_shutdownReason.load() != kNoShutdown;
}
// Sleeps up to `secs`; returns false (early) once a shutdown is requested.
bool sleepUnlessShutdown(double secs)
{
const auto end = Clock::now() + std::chrono::duration_cast<Clock::duration>(std::chrono::duration<double>(secs));
while (Clock::now() < end) {
if (shutdownRequested()) return false;
std::this_thread::sleep_for(50ms);
}
return !shutdownRequested();
}
void setInitMessage(const std::string& msg)
{
logf("init message: %s", msg.c_str());
std::lock_guard<std::mutex> lk(g_warmupMx);
g_warmupStatus = msg;
}
std::string warmupStatus()
{
std::lock_guard<std::mutex> lk(g_warmupMx);
return g_warmupStatus;
}
struct Stats {
std::atomic<long> requests{0}, queueFull{0}, unavailable{0}, unhandled{0}, unauthorized{0};
std::atomic<int> abandonedNow{0}, maxAbandoned{0}, maxSlots{0};
};
Stats g_stats;
// Requests per JSON-RPC method, for "what is the GUI polling" (the `methods` event).
std::mutex g_methodsMx;
std::map<std::string, long> g_methodCounts;
std::string topMethodsLine(size_t n)
{
std::vector<std::pair<long, std::string>> v;
{
std::lock_guard<std::mutex> lk(g_methodsMx);
for (const auto& kv : g_methodCounts) v.push_back({kv.second, kv.first});
}
std::sort(v.rbegin(), v.rend());
std::string out;
for (size_t i = 0; i < v.size() && i < n; ++i) out += (i ? " " : "") + v[i].second + "=" + std::to_string(v[i].first);
return out;
}
bool raiseMax(std::atomic<int>& max, int value)
{
int cur = max.load();
while (value > cur) {
if (max.compare_exchange_weak(cur, value)) return true;
}
return false;
}
void noteAbandoned(int delta)
{
const int now = (g_stats.abandonedNow += delta);
if (delta > 0 && raiseMax(g_stats.maxAbandoned, now)) event("max-abandoned", "n=" + std::to_string(now));
}
std::string statsLine()
{
return "requests=" + std::to_string(g_stats.requests.load()) +
" queue_full_500=" + std::to_string(g_stats.queueFull.load()) +
" unavailable_503=" + std::to_string(g_stats.unavailable.load()) +
" unhandled_500=" + std::to_string(g_stats.unhandled.load()) +
" unauthorized_401=" + std::to_string(g_stats.unauthorized.load()) +
" max_slots=" + std::to_string(g_stats.maxSlots.load()) +
" max_abandoned=" + std::to_string(g_stats.maxAbandoned.load()) +
" abandoned_now=" + std::to_string(g_stats.abandonedNow.load()) +
" stops=" + std::to_string(g_stopCount.load());
}
// =============================================================================================
// Canned RPC results
// =============================================================================================
std::uint64_t splitmix64(std::uint64_t x)
{
x += 0x9E3779B97F4A7C15ULL;
x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9ULL;
x = (x ^ (x >> 27)) * 0x94D049BB133111EBULL;
return x ^ (x >> 31);
}
std::string fakeHash(long h)
{
char buf[80];
std::snprintf(buf, sizeof(buf), "%016llx%016llx%016llx%016llx",
(unsigned long long)splitmix64(h * 4 + 0), (unsigned long long)splitmix64(h * 4 + 1),
(unsigned long long)splitmix64(h * 4 + 2), (unsigned long long)splitmix64(h * 4 + 3));
return buf;
}
const char* const kFakeZAddr =
"zs1fakedragonxd0000000000000000000000000000000000000000000000000000000000000";
const char* const kFakeTAddr = "RFakeDragonxdTransparent0000000000";
struct Canned {
bool known = true;
json result;
int errCode = 0;
std::string errMsg;
};
// ---------------------------------------------------------------------------------------------
// Optional note wallet (FAKE_WALLET_NOTES="addr:count:amount,..."): shielded notes per address, plus
// z_mergetoaddress and its async operations with dragonxd's rules — confirmed, unlocked notes only;
// a batch's notes are locked when the call returns; "only source == destination with one note" is
// refused; the merged note appears one block later. Everything here runs under g_csMain (execOne).
// ---------------------------------------------------------------------------------------------
struct FakeNote { long id; std::string address; double amount; long height; bool locked; };
struct FakeOp {
std::string id;
std::vector<long> noteIds;
std::string to;
double fee = 0.0;
double finishAt = 0.0; // unixMs()/1000.0
std::string status = "executing";
long long created = 0;
};
bool g_noteWallet = false;
std::vector<FakeNote> g_notes;
std::vector<FakeOp> g_ops;
long g_noteSeq = 0;
long g_opSeq = 0;
double g_mergeSecs = 6.0;
void loadNoteWallet()
{
const char* spec = std::getenv("FAKE_WALLET_NOTES");
if (!spec || !*spec) return;
g_noteWallet = true;
g_mergeSecs = envDouble("FAKE_MERGE_SECS", 6.0);
std::string s(spec);
size_t pos = 0;
while (pos <= s.size()) {
size_t comma = s.find(',', pos);
if (comma == std::string::npos) comma = s.size();
const std::string item = s.substr(pos, comma - pos);
const size_t c1 = item.find(':'), c2 = item.find(':', c1 == std::string::npos ? 0 : c1 + 1);
if (c1 != std::string::npos && c2 != std::string::npos) {
const std::string addr = item.substr(0, c1);
const long count = std::strtol(item.c_str() + c1 + 1, nullptr, 10);
const double amount = std::strtod(item.c_str() + c2 + 1, nullptr);
for (long i = 0; i < count; ++i)
g_notes.push_back({++g_noteSeq, addr, amount, g_height.load() - 10 - i, false});
}
pos = comma + 1;
}
}
std::vector<std::string> noteAddresses()
{
std::vector<std::string> out;
for (const auto& n : g_notes)
if (std::find(out.begin(), out.end(), n.address) == out.end()) out.push_back(n.address);
return out;
}
// Operations past their run time succeed: their notes are spent, one merged note lands next block.
void settleOps()
{
const double now = unixMs() / 1000.0;
for (auto& op : g_ops) {
if (op.status != "executing" || now < op.finishAt) continue;
double sum = 0.0;
for (long id : op.noteIds) {
auto it = std::find_if(g_notes.begin(), g_notes.end(), [&](const FakeNote& n) { return n.id == id; });
if (it != g_notes.end()) { sum += it->amount; g_notes.erase(it); }
}
g_notes.push_back({++g_noteSeq, op.to, sum - op.fee, g_height.load() + 1, false});
op.status = "success";
event("merge-done", op.id + " notes=" + std::to_string(op.noteIds.size()) + " to=" + op.to);
}
}
json opStatus(const FakeOp& op)
{
json o = {{"id", op.id}, {"status", op.status}, {"creation_time", op.created}, {"method", "z_mergetoaddress"}};
if (op.status == "success") o["result"] = {{"txid", fakeHash(1000000 + std::stol(op.id.substr(5)))}};
return o;
}
// The note-wallet answer for `m`, or c.known = false when it isn't a note-wallet method.
Canned noteWalletReply(const std::string& m, const json& params)
{
Canned c;
settleOps();
const long h = g_height.load();
if (m == "z_listunspent") {
json arr = json::array();
for (const auto& n : g_notes) {
const long conf = std::max(0L, h - n.height + 1);
arr.push_back({{"txid", fakeHash(5000000 + n.id)}, {"outindex", 0}, {"confirmations", conf},
{"spendable", true}, {"address", n.address}, {"amount", n.amount}, {"memo", "f6"},
{"change", false}});
}
c.result = arr;
} else if (m == "z_listaddresses") {
json arr = json::array({kFakeZAddr});
for (const auto& a : noteAddresses()) arr.push_back(a);
c.result = arr;
} else if (m == "z_gettotalbalance" || m == "z_getbalance") {
const std::string only = (m == "z_getbalance" && !params.empty() && params[0].is_string())
? params[0].get<std::string>() : std::string();
double sum = 0.0;
for (const auto& n : g_notes)
if (only.empty() || n.address == only) sum += n.amount;
if (m == "z_getbalance") c.result = sum;
else {
char buf[64];
std::snprintf(buf, sizeof(buf), "%.8f", sum);
c.result = {{"transparent", "0.00"}, {"private", buf}, {"total", buf}};
}
} else if (m == "z_mergetoaddress") {
if (params.size() < 2 || !params[0].is_array() || !params[1].is_string()) {
c.errCode = -8; c.errMsg = "Invalid parameter";
return c;
}
std::vector<std::string> from;
bool anySapling = false;
for (const auto& a : params[0]) {
if (!a.is_string()) continue;
if (a.get<std::string>() == "ANY_SAPLING" || a.get<std::string>() == "*") anySapling = true;
else from.push_back(a.get<std::string>());
}
const std::string to = params[1].get<std::string>();
const double fee = params.size() > 2 && params[2].is_number() ? params[2].get<double>() : 0.0001;
long limit = params.size() > 4 && params[4].is_number() ? params[4].get<long>() : 90;
if (limit <= 0) limit = 1000000;
std::vector<long> picked;
double merging = 0.0, remainingValue = 0.0;
long counter = 0;
for (const auto& n : g_notes) {
if (n.locked || h - n.height + 1 < 1) continue;
if (!anySapling && std::find(from.begin(), from.end(), n.address) == from.end()) continue;
++counter;
if (static_cast<long>(picked.size()) < limit) { picked.push_back(n.id); merging += n.amount; }
else remainingValue += n.amount;
}
if (picked.empty()) { c.errCode = -6; c.errMsg = "Could not find any funds to merge."; return c; }
if (from.size() == 1 && !anySapling && from[0] == to && picked.size() == 1) {
c.errCode = -8;
c.errMsg = "Destination address is also the only source address, and all its funds are already merged.";
return c;
}
if (merging - fee < fee) {
c.errCode = -6; c.errMsg = "Insufficient funds to pay the fee"; return c;
}
for (auto& n : g_notes)
if (std::find(picked.begin(), picked.end(), n.id) != picked.end()) n.locked = true;
FakeOp op;
op.id = "opid-" + std::to_string(++g_opSeq);
op.noteIds = picked;
op.to = to;
op.fee = fee;
op.created = unixMs() / 1000;
op.finishAt = unixMs() / 1000.0 + g_mergeSecs;
g_ops.push_back(op);
event("merge", op.id + " notes=" + std::to_string(picked.size()) + " to=" + to);
c.result = {{"remainingUTXOs", 0}, {"remainingTransparentValue", 0.0},
{"remainingNotes", counter - static_cast<long>(picked.size())},
{"remainingShieldedValue", remainingValue}, {"mergingUTXOs", 0},
{"mergingTransparentValue", 0.0}, {"mergingNotes", static_cast<long>(picked.size())},
{"mergingShieldedValue", merging}, {"opid", op.id}};
} else if (m == "z_getoperationstatus" || m == "z_getoperationresult") {
std::vector<std::string> filter;
if (!params.empty() && params[0].is_array())
for (const auto& id : params[0]) if (id.is_string()) filter.push_back(id.get<std::string>());
json arr = json::array();
std::vector<FakeOp> keep;
for (const auto& op : g_ops) {
const bool match = filter.empty() || std::find(filter.begin(), filter.end(), op.id) != filter.end();
const bool finished = op.status != "executing";
if (match && (m == "z_getoperationstatus" || finished)) arr.push_back(opStatus(op));
if (!(m == "z_getoperationresult" && match && finished)) keep.push_back(op);
}
g_ops.swap(keep);
c.result = arr;
} else {
c.known = false;
}
return c;
}
Canned cannedReply(const std::string& m, const json& params)
{
Canned c;
const long h = g_height.load();
const long long now = unixMs() / 1000;
const double difficulty = 152341.734;
const std::string sub = "/DragonX:" + std::to_string(g_cfg.version / 1000000) + "." +
std::to_string(g_cfg.version / 10000 % 100) + "." +
std::to_string(g_cfg.version / 100 % 100) + "/";
auto param0 = [&]() -> std::string {
if (params.empty()) return "";
return params[0].is_string() ? params[0].get<std::string>() : params[0].dump();
};
if (m == "getinfo") {
c.result = {{"version", g_cfg.version}, {"protocolversion", 170012}, {"walletversion", 60000},
{"balance", 0.0}, {"blocks", h}, {"longestchain", h}, {"notarized", 0},
{"tiptime", now}, {"difficulty", difficulty}, {"keypoololdest", now - 86400},
{"keypoolsize", 101}, {"paytxfee", 0.0}, {"timeoffset", 0}, {"connections", 2},
{"tls_connections", 2}, {"proxy", ""}, {"testnet", false}, {"relayfee", 0.000001},
{"errors", ""}, {"name", "DRAGONX"}, {"p2pport", 21768}, {"rpcport", g_cfg.rpcPort}};
} else if (m == "getblockchaininfo") {
c.result = {{"chain", "main"}, {"blocks", h}, {"synced", true}, {"headers", h},
{"bestblockhash", fakeHash(h)}, {"difficulty", difficulty}, {"verificationprogress", 1.0},
{"chainwork", std::string(48, '0') + "1f3a5c7e9b2d4f60"}, {"pruned", false},
{"valuePools", json::array()}, {"softforks", json::array()}, {"upgrades", json::object()},
{"longestchain", h}, {"notarized", 0}};
} else if (m == "getblockcount") {
c.result = h;
} else if (m == "getbestblockhash") {
c.result = fakeHash(h);
} else if (m == "getblockhash") {
c.result = fakeHash(params.empty() || !params[0].is_number() ? h : params[0].get<long>());
} else if (m == "getblock") {
long bh = h;
const std::string p = param0();
if (!p.empty() && p.size() < 12) bh = std::strtol(p.c_str(), nullptr, 10);
c.result = {{"hash", fakeHash(bh)}, {"confirmations", std::max(1L, h - bh + 1)}, {"height", bh},
{"version", 4}, {"time", now - (h - bh) * 36}, {"difficulty", difficulty},
{"previousblockhash", fakeHash(bh - 1)}, {"tx", json::array({fakeHash(bh + 7777777)})},
{"size", 1765}};
} else if (m == "getwalletinfo") {
c.result = {{"walletversion", 60000}, {"balance", 0.0}, {"unconfirmed_balance", 0.0},
{"immature_balance", 0.0}, {"txcount", 42}, {"keypoololdest", now - 86400},
{"keypoolsize", 101}, {"paytxfee", 0.0}};
} else if (m == "z_gettotalbalance") {
c.result = {{"transparent", "0.00"}, {"private", "1234.5678"}, {"total", "1234.5678"}};
} else if (m == "z_getbalance") {
c.result = 1234.5678;
} else if (m == "getbalance") {
c.result = 0.0;
} else if (m == "getmininginfo") {
c.result = {{"blocks", h}, {"currentblocksize", 0}, {"currentblocktx", 0}, {"difficulty", difficulty},
{"errors", ""}, {"genproclimit", -1}, {"localsolps", 0.0}, {"networksolps", 182345},
{"networkhashps", 182345}, {"pooledtx", 0}, {"testnet", false}, {"chain", "main"},
{"generate", g_minerOn.load()}, {"numthreads", -1}};
} else if (m == "getlocalsolps") {
c.result = 0.0;
} else if (m == "getnetworkhashps" || m == "getnetworksolps") {
c.result = 182345;
} else if (m == "setgenerate") {
c.result = nullptr; // the miner flag is flipped in execOne: this table stays side-effect free
} else if (m == "getgenerate") {
c.result = g_minerOn.load();
} else if (m == "getpeerinfo") {
// Documentation-range (TEST-NET) addresses only.
json peers = json::array();
const char* addrs[] = {"203.0.113.10:21768", "198.51.100.23:21768"};
for (int i = 0; i < 2; ++i) {
peers.push_back({{"id", i + 1}, {"addr", addrs[i]}, {"services", "0000000000000005"},
{"tls_verified", false}, {"lastsend", now}, {"lastrecv", now},
{"bytessent", 123456}, {"bytesrecv", 654321}, {"conntime", now - 3600},
{"timeoffset", 0}, {"pingtime", 0.052}, {"version", 170012}, {"subver", sub},
{"inbound", false}, {"startingheight", h - 100}, {"banscore", 0},
{"synced_headers", h}, {"synced_blocks", h}, {"inflight", json::array()},
{"whitelisted", false}});
}
c.result = peers;
} else if (m == "getnetworkinfo") {
c.result = {{"version", g_cfg.version}, {"subversion", sub}, {"protocolversion", 170012},
{"localservices", "0000000000000005"}, {"timeoffset", 0}, {"connections", 2},
{"tls_connections", 2}, {"networks", json::array()}, {"relayfee", 0.000001},
{"localaddresses", json::array()}, {"warnings", ""}};
} else if (m == "getconnectioncount") {
c.result = 2;
} else if (m == "getmempoolinfo") {
c.result = {{"size", 0}, {"bytes", 0}, {"usage", 0}};
} else if (m == "listbanned" || m == "listunspent" || m == "z_listunspent" || m == "listtransactions" ||
m == "z_listreceivedbyaddress" || m == "z_listsentbyaddress" || m == "z_getoperationstatus" ||
m == "z_getoperationresult") {
c.result = json::array();
} else if (m == "setban" || m == "clearbanned") {
c.result = nullptr;
} else if (m == "z_mergetoaddress") {
c.errCode = -8; // only the note wallet (FAKE_WALLET_NOTES) can merge; execOne answers it there
c.errMsg = "fake_dragonxd: set FAKE_WALLET_NOTES to merge notes";
} else if (m == "z_listaddresses") {
c.result = json::array({kFakeZAddr});
} else if (m == "getaddressesbyaccount" || m == "listaddresses") {
c.result = json::array({kFakeTAddr});
} else if (m == "getrescaninfo") {
c.result = {{"rescanning", false}};
} else if (m == "validateaddress") {
c.result = {{"isvalid", true}, {"address", param0()}, {"ismine", false}};
} else if (m == "z_validateaddress") {
c.result = {{"isvalid", true}, {"address", param0()}, {"type", "sapling"}, {"ismine", false}};
} else if (m == "z_exportkey" || m == "z_exportmnemonic") {
c.errCode = -4; // RPC_WALLET_ERROR: keeps seed-derived features (chat) quietly off
c.errMsg = "fake_dragonxd holds no keys";
} else {
c.known = false;
}
return c;
}
// dragonxd's warmup allow-list (rpc/server.cpp CRPCTable::execute).
bool allowedInWarmup(const std::string& m)
{
static const char* const kAllowed[] = {
"stop", "help", "z_listaddresses", "z_exportkey", "getNotarizationsForBlock", "scanNotarizationsDB",
"getaddressesbyaccount", "listaddresses", "z_exportwallet", "notaries", "signmessage",
"decoderawtransaction", "dumpprivkey", "getpeerinfo", "getnetworkinfo", "abortrescan"};
for (const char* a : kAllowed)
if (m == a) return true;
return false;
}
std::string helpText(const json& params)
{
if (params.empty() || !params[0].is_string())
return "== Control ==\nhelp ( \"command\" )\nstop\n\n(fake_dragonxd: canned responses only)";
const std::string cmd = params[0].get<std::string>();
if (cmd == "stop") return "stop\n\nStop Hush server.";
if (cmd == "help") return "help ( \"command\" )\n\nList all commands, or get help for a specified command.";
if (cannedReply(cmd, json::array()).known) return cmd + "\n\n(fake_dragonxd canned method)";
return "help: unknown command: " + cmd;
}
json rpcReply(const json& result, const json& error, const json& id)
{
return {{"result", result}, {"error", error}, {"id", id}};
}
// One JSON-RPC call. `status` gets the HTTP status dragonxd would use (httprpc.cpp JSONErrorReply).
json execOne(const json& req, int& status)
{
const json id = req.is_object() && req.contains("id") ? req["id"] : json(nullptr);
auto fail = [&](int code, const std::string& msg) {
status = code == -32600 ? 400 : code == -32601 ? 404 : 500;
return rpcReply(nullptr, {{"code", code}, {"message", msg}}, id);
};
if (!req.is_object() || !req.contains("method") || !req["method"].is_string())
return fail(-32600, "Missing method");
const std::string m = req["method"].get<std::string>();
{
std::lock_guard<std::mutex> lk(g_methodsMx);
++g_methodCounts[m];
}
json params = req.contains("params") ? req["params"] : json::array();
if (!params.is_array()) params = json::array();
status = 200;
// FAKE_METHODS_ALL=1: one "call" event per request (method + params), to see the GUI's RPC cadence.
static const bool logAll = [] { const char* v = std::getenv("FAKE_METHODS_ALL"); return v && std::string(v) == "1"; }();
if (logAll) {
std::string p = params.dump();
if (p.size() > 160) p = p.substr(0, 160) + "...";
event("call", "m=" + m + " p=" + p);
}
// Like dragonxd: stop and help take no lock, nor does getrescaninfo (it only reads wallet fields).
const bool lockFree = (m == "stop" || m == "help" || m == "getrescaninfo");
if (!lockFree && !cannedReply(m, params).known) return fail(-32601, "Method not found");
if (g_warmup.load() && !allowedInWarmup(m)) return fail(-28, warmupStatus());
if (m == "help") return rpcReply(helpText(params), nullptr, id);
if (m == "stop") {
// stop's GenerateBitcoins(false) joins the miner threads, which may be waiting on cs_main.
if (g_minerOn.load()) {
std::lock_guard<std::mutex> miners(g_csMain);
g_minerOn = false;
}
requestShutdown(kRpcStop);
return rpcReply("DRAGONX server stopping...", nullptr, id);
}
Canned c;
if (lockFree) {
c = cannedReply(m, params);
} else {
std::lock_guard<std::mutex> lk(g_csMain);
c = cannedReply(m, params);
if (m == "setgenerate") g_minerOn = !params.empty() && params[0].is_boolean() && params[0].get<bool>();
if (g_noteWallet) {
Canned w = noteWalletReply(m, params);
if (w.known) c = w;
}
}
if (c.errCode != 0) return fail(c.errCode, c.errMsg);
return rpcReply(c.result, nullptr, id);
}
// =============================================================================================
// HTTP server: N workers + a bounded queue, like dragonxd's httpserver.cpp WorkQueue
// =============================================================================================
const char* const kTextType = "text/html; charset=ISO-8859-1"; // libevent's default for a bare reply
std::string g_expectedAuth; // "Basic <base64(user:pass)>"
std::atomic<bool> g_httpInterrupted{false};
std::string base64(const std::string& in)
{
static const char* const t = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string out;
std::uint32_t val = 0;
int bits = -6;
for (unsigned char c : in) {
val = (val << 8) | c;
bits += 8;
while (bits >= 0) {
out.push_back(t[(val >> bits) & 0x3F]);
bits -= 6;
}
}
if (bits > -6) out.push_back(t[((val << 8) >> (bits + 8)) & 0x3F]);
while (out.size() % 4) out.push_back('=');
return out;
}
struct Item {
bool isPost = true;
bool prefill = false;
std::string body, auth;
std::mutex mx;
std::condition_variable cv;
bool done = false;
int status = 0;
std::string contentType, payload;
};
using ItemPtr = std::shared_ptr<Item>;
void complete(Item& it, int status, const std::string& contentType, const std::string& payload)
{
{
std::lock_guard<std::mutex> lk(it.mx);
it.status = status;
it.contentType = contentType;
it.payload = payload;
it.done = true;
}
it.cv.notify_all();
if (it.prefill) noteAbandoned(-1);
}
void processItem(Item& it)
{
if (!it.isPost) {
complete(it, 405, kTextType, "JSONRPC server handles only POST requests");
return;
}
if (it.auth != g_expectedAuth) {
std::this_thread::sleep_for(250ms); // httprpc.cpp: MilliSleep(250) to deter brute forcing
++g_stats.unauthorized;
complete(it, 401, "", "");
return;
}
const json req = json::parse(it.body, nullptr, false);
int status = 200;
std::string payload;
if (req.is_discarded()) {
status = 500;
payload = rpcReply(nullptr, {{"code", -32700}, {"message", "Parse error"}}, nullptr).dump() + "\n";
} else if (req.is_array()) {
json out = json::array();
for (const auto& r : req) {
int ignored = 200;
out.push_back(execOne(r, ignored));
}
payload = out.dump() + "\n";
} else {
payload = execOne(req, status).dump() + "\n";
}
complete(it, status, "application/json", payload);
}
class WorkQueue {
public:
void configure(size_t maxDepth, int threads)
{
maxDepth_ = maxDepth;
capacity_ = maxDepth + static_cast<size_t>(threads);
}
bool enqueue(const ItemPtr& item)
{
std::lock_guard<std::mutex> lk(mx_);
if (queue_.size() >= maxDepth_) return false;
queue_.push_back(item);
raiseMax(g_stats.maxSlots, static_cast<int>(queue_.size()) + inProgress_);
cv_.notify_one();
return true;
}
size_t slotsInUse()
{
std::lock_guard<std::mutex> lk(mx_);
return queue_.size() + static_cast<size_t>(inProgress_);
}
size_t capacity() const { return capacity_; }
void run()
{
for (;;) {
ItemPtr it;
{
std::unique_lock<std::mutex> lk(mx_);
cv_.wait(lk, [&] { return !running_ || !queue_.empty(); });
if (!running_) return; // queued items are left for drain(), as in dragonxd
it = queue_.front();
queue_.pop_front();
++inProgress_;
}
processItem(*it);
std::lock_guard<std::mutex> lk(mx_);
--inProgress_;
}
}
void interrupt()
{
std::lock_guard<std::mutex> lk(mx_);
running_ = false;
cv_.notify_all();
}
std::deque<ItemPtr> drain()
{
std::lock_guard<std::mutex> lk(mx_);
std::deque<ItemPtr> out;
out.swap(queue_);
return out;
}
private:
std::mutex mx_;
std::condition_variable cv_;
std::deque<ItemPtr> queue_;
bool running_ = true;
int inProgress_ = 0;
size_t maxDepth_ = 16;
size_t capacity_ = 24;
};
WorkQueue g_queue;
bool waitReadable(socket_t s, int timeoutMs)
{
fd_set rf;
FD_ZERO(&rf);
FD_SET(s, &rf);
timeval tv{timeoutMs / 1000, (timeoutMs % 1000) * 1000};
return select(static_cast<int>(s) + 1, &rf, nullptr, nullptr, &tv) > 0;
}
bool sendAll(socket_t s, const std::string& data)
{
size_t off = 0;
while (off < data.size()) {
const int n = send(s, data.data() + off, static_cast<int>(std::min<size_t>(data.size() - off, 1 << 20)), MSG_NOSIGNAL);
if (n <= 0) return false;
off += static_cast<size_t>(n);
}
return true;
}
// The client hung up (e.g. its curl timeout fired) while its request still holds a slot.
bool peerClosed(socket_t s)
{
if (!waitReadable(s, 0)) return false;
char c;
return recv(s, &c, 1, MSG_PEEK) <= 0;
}
const char* reasonPhrase(int status)
{
switch (status) {
case 200: return "OK";
case 400: return "Bad Request";
case 401: return "Unauthorized";
case 404: return "Not Found";
case 405: return "Method Not Allowed";
case 500: return "Internal Server Error";
case 503: return "Service Unavailable";
default: return "Unknown";
}
}
std::string httpReply(int status, const std::string& contentType, const std::string& body, bool close)
{
std::string r = "HTTP/1.1 " + std::to_string(status) + " " + reasonPhrase(status) + "\r\n";
if (!contentType.empty()) r += "Content-Type: " + contentType + "\r\n";
if (status == 401) r += "WWW-Authenticate: Basic realm=\"jsonrpc\"\r\n";
r += "Content-Length: " + std::to_string(body.size()) + "\r\n";
if (close) r += "Connection: close\r\n";
r += "\r\n";
r += body;
return r;
}
struct HttpRequest {
std::string method, body, auth;
bool close = false;
};
enum class ReadResult { Ok, Closed, Timeout, Bad };
ReadResult recvMore(socket_t s, std::string& buf, Clock::time_point deadline)
{
for (;;) {
const auto left = std::chrono::duration_cast<std::chrono::milliseconds>(deadline - Clock::now()).count();
if (left <= 0) return ReadResult::Timeout;
if (!waitReadable(s, static_cast<int>(std::min<long long>(left, 500)))) continue;
char tmp[8192];
const int n = recv(s, tmp, sizeof(tmp), 0);
if (n <= 0) return ReadResult::Closed;
buf.append(tmp, static_cast<size_t>(n));
return ReadResult::Ok;
}
}
std::string lower(std::string s)
{
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return s;
}
ReadResult readRequest(socket_t s, std::string& buf, HttpRequest& out)
{
const auto idle = std::chrono::duration_cast<Clock::duration>(std::chrono::duration<double>(g_cfg.serverTimeoutSecs));
auto deadline = Clock::now() + idle;
size_t headerEnd;
while ((headerEnd = buf.find("\r\n\r\n")) == std::string::npos) {
if (buf.size() > 64 * 1024) return ReadResult::Bad;
const ReadResult r = recvMore(s, buf, deadline);
if (r != ReadResult::Ok) return r;
}
const std::string head = buf.substr(0, headerEnd);
size_t lineEnd = head.find("\r\n");
const std::string requestLine = head.substr(0, lineEnd);
out = HttpRequest{};
out.method = requestLine.substr(0, requestLine.find(' '));
const bool http10 = requestLine.find("HTTP/1.0") != std::string::npos;
size_t contentLength = 0;
bool keepAlive = false;
while (lineEnd != std::string::npos) {
const size_t next = head.find("\r\n", lineEnd + 2);
const std::string line = head.substr(lineEnd + 2, next == std::string::npos ? std::string::npos : next - lineEnd - 2);
lineEnd = next;
const size_t colon = line.find(':');
if (colon == std::string::npos) continue;
const std::string name = lower(line.substr(0, colon));
std::string value = line.substr(colon + 1);
value.erase(0, value.find_first_not_of(" \t"));
while (!value.empty() && (value.back() == ' ' || value.back() == '\t')) value.pop_back();
if (name == "content-length") contentLength = static_cast<size_t>(std::strtoul(value.c_str(), nullptr, 10));
else if (name == "authorization") out.auth = value;
else if (name == "connection") {
out.close = lower(value) == "close";
keepAlive = lower(value) == "keep-alive";
}
}
if (http10 && !keepAlive) out.close = true;
if (contentLength > 16 * 1024 * 1024) return ReadResult::Bad;
const size_t total = headerEnd + 4 + contentLength;
deadline = Clock::now() + idle;
while (buf.size() < total) {
const ReadResult r = recvMore(s, buf, deadline);
if (r != ReadResult::Ok) return r;
}
out.body = buf.substr(headerEnd + 4, contentLength);
buf.erase(0, total);
return ReadResult::Ok;
}
void serveConnection(socket_t s)
{
std::string buf;
for (;;) {
HttpRequest req;
if (readRequest(s, buf, req) != ReadResult::Ok) break;
++g_stats.requests;
const bool isStop = req.body.find("\"stop\"") != std::string::npos;
// After InterruptHTTPServer, requests on already-open connections get evhttp_send_error(503).
if (g_httpInterrupted.load()) {
++g_stats.unavailable;
if (isStop) event("stop-rejected", "status=503");
sendAll(s, httpReply(503, "text/html",
"<HTML><HEAD>\n<TITLE>503 Service Unavailable</TITLE>\n</HEAD><BODY>\n"
"<H1>Service Unavailable</H1>\n</BODY></HTML>\n",
true));
break;
}
auto item = std::make_shared<Item>();
item->isPost = req.method == "POST";
item->body = std::move(req.body);
item->auth = std::move(req.auth);
if (!g_queue.enqueue(item)) {
if (++g_stats.queueFull == 1) event("queue-full", "slots=" + std::to_string(g_queue.slotsInUse()));
if (isStop) event("stop-rejected", "status=500-queue-full");
if (!sendAll(s, httpReply(500, kTextType, "Work queue depth exceeded", req.close)) || req.close) break;
continue;
}
bool gone = false;
for (;;) {
{
std::unique_lock<std::mutex> lk(item->mx);
if (item->cv.wait_for(lk, 200ms, [&] { return item->done; })) break;
}
if (!gone && peerClosed(s)) {
gone = true; // the slot stays taken until a worker reaches it
noteAbandoned(+1);
}
}
if (gone) {
noteAbandoned(-1);
break;
}
if (!sendAll(s, httpReply(item->status, item->contentType, item->payload, req.close)) || req.close) break;
}
closeSocket(s);
}
void acceptLoop(socket_t listener)
{
while (!g_httpInterrupted.load()) {
if (!waitReadable(listener, 200)) continue;
sockaddr_in addr{};
socklen_t len = sizeof(addr);
const socket_t c = accept(listener, reinterpret_cast<sockaddr*>(&addr), &len);
if (c == kBadSocket) continue;
std::thread(serveConnection, c).detach();
}
closeSocket(listener); // unbound: new connects are refused (or time out, on Windows)
}
socket_t bindListener(int port)
{
const socket_t s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (s == kBadSocket) return kBadSocket;
#ifdef _WIN32
BOOL exclusive = TRUE;
setsockopt(s, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, reinterpret_cast<const char*>(&exclusive), sizeof(exclusive));
#else
int one = 1;
setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
#endif
sockaddr_in a{};
a.sin_family = AF_INET;
a.sin_port = htons(static_cast<unsigned short>(port));
a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
if (bind(s, reinterpret_cast<sockaddr*>(&a), sizeof(a)) != 0 || listen(s, 64) != 0) {
closeSocket(s);
return kBadSocket;
}
return s;
}
// =============================================================================================
// Background "node" activity
// =============================================================================================
void logUpdateTip(long h)
{
const std::time_t blockTime = std::time(nullptr) - 20;
logf("UpdateTip: new best=%s height=%ld log2_work=56.82813377 tx=%ld date=%s progress=1.000000 cache=1.2MiB(345tx)",
fakeHash(h).c_str(), h, h * 3 + 12345, utcStamp(blockTime).c_str());
}
// A new block every FAKE_UPDATETIP_SECS; blocks on cs_main like ConnectTip, so none during a rebuild.
void updateTipLoop()
{
while (sleepUnlessShutdown(g_cfg.updateTipSecs)) {
std::lock_guard<std::mutex> lk(g_csMain);
if (shutdownRequested()) return; // net threads are interrupted: no more blocks connect
logUpdateTip(++g_height);
}
}
// Unrelated lines from other threads, as on the real node (they defeat "newest line" heuristics).
void noiseLoop()
{
static const char* const kLines[] = {
"socket sending timeout: 1201s",
"TLS: connection from 203.0.113.10:21768 has been established (initiated)",
"receive version message: /DragonX:1.3.0/: version 170012, blocks=3327101, us=0.0.0.0:0, peer=17",
"Using /16 prefix for IP bucketing",
};
size_t i = 0;
while (sleepUnlessShutdown(g_cfg.noiseSecs)) logf("%s", kLines[i++ % (sizeof(kLines) / sizeof(kLines[0]))]);
}
void prefillAbandoned(long n)
{
long injected = 0;
for (; injected < n; ++injected) {
auto item = std::make_shared<Item>();
item->prefill = true;
item->auth = g_expectedAuth;
item->body = R"({"jsonrpc":"1.0","id":"prefill","method":"getinfo","params":[]})";
noteAbandoned(+1);
// Give the workers a moment to take the first items, so the queue depth alone isn't the limit;
// stop once every slot (workers + queue) is taken.
bool queued = false;
while (!(queued = g_queue.enqueue(item)) && g_queue.slotsInUse() < g_queue.capacity())
std::this_thread::sleep_for(5ms);
if (!queued) {
noteAbandoned(-1);
break;
}
}
event("prefill", "n=" + std::to_string(injected));
}
// BuildWitnessCache: Phase 1 logs "Reading blocks for witness rebuild: <offset> / <total>" whenever
// the ABSOLUTE height is a multiple of 5000, and aborts on shutdown. Phase 2 logs nothing and ignores
// shutdown. Both hold cs_main.
void rebuildLoop()
{
if (!sleepUnlessShutdown(g_cfg.rebuildDelaySecs)) return;
std::unique_lock<std::mutex> lk(g_csMain);
if (shutdownRequested()) return;
const auto started = Clock::now();
const long endHeight = g_height.load();
const long startHeight = endHeight - g_cfg.rebuildTotal;
event("rebuild-start", "total=" + std::to_string(g_cfg.rebuildTotal) + " secs=" + std::to_string(g_cfg.rebuildSecs));
if (g_cfg.prefill > 0) std::thread(prefillAbandoned, g_cfg.prefill).detach();
std::vector<long> offsets;
for (long h = (startHeight / 5000 + 1) * 5000; h <= endHeight; h += 5000) offsets.push_back(h - startHeight);
const double step = offsets.empty() ? 0 : g_cfg.phase1Secs / static_cast<double>(offsets.size());
for (long off : offsets) {
if (shutdownRequested()) {
logf("BuildWitnessCache: shutdown requested, aborting witness rebuild");
event("rebuild-aborted");
return;
}
if (!g_cfg.rebuildSilent) logf("Reading blocks for witness rebuild: %ld / %ld", off, g_cfg.rebuildTotal);
std::this_thread::sleep_for(std::chrono::duration<double>(step));
}
const auto phase2End = started + std::chrono::duration_cast<Clock::duration>(std::chrono::duration<double>(g_cfg.rebuildSecs));
std::this_thread::sleep_until(phase2End); // Phase 2: silent, and deaf to shutdown
const long long ms = std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now() - started).count();
if (!g_cfg.rebuildSilent)
logf("BuildWitnessCache: rebuilt %ld note witness cache(s) to height %ld in %lldms using %d thread(s)",
g_cfg.notes, endHeight, ms, 8);
event("rebuild-end", "ms=" + std::to_string(ms));
if (g_cfg.postRebuildLockSecs > 0)
std::this_thread::sleep_for(std::chrono::duration<double>(g_cfg.postRebuildLockSecs));
}
// =============================================================================================
// Datadir lock, pid file, cookie
// =============================================================================================
#ifdef _WIN32
HANDLE g_lockHandle = INVALID_HANDLE_VALUE;
#else
int g_lockFd = -1;
#endif
bool lockDatadir(const fs::path& datadir)
{
const std::string path = (datadir / ".lock").string();
#ifdef _WIN32
g_lockHandle = CreateFileA(path.c_str(), GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL, nullptr);
if (g_lockHandle == INVALID_HANDLE_VALUE) return false;
OVERLAPPED ov{};
return LockFileEx(g_lockHandle, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY, 0, MAXDWORD, MAXDWORD, &ov) != 0;
#else
g_lockFd = ::open(path.c_str(), O_RDWR | O_CREAT, 0644);
if (g_lockFd < 0) return false;
struct flock fl{};
fl.l_type = F_WRLCK;
fl.l_whence = SEEK_SET;
return fcntl(g_lockFd, F_SETLK, &fl) == 0; // what boost::interprocess::file_lock uses
#endif
}
std::string randomHex(size_t bytes)
{
std::random_device rd;
std::string out;
char b[3];
for (size_t i = 0; i < bytes; ++i) {
std::snprintf(b, sizeof(b), "%02x", rd() & 0xFF);
out += b;
}
return out;
}
[[noreturn]] void exitNow(int code)
{
g_log.close();
{
std::lock_guard<std::mutex> lk(g_eventsMx);
if (g_events) std::fclose(g_events);
g_events = nullptr;
}
std::fflush(stdout);
std::_Exit(code);
}
[[noreturn]] void initError(const std::string& msg)
{
logf("Error: %s", msg.c_str());
std::fprintf(stderr, "Error: %s\n", msg.c_str());
event("init-error", "msg=\"" + msg + "\"");
exitNow(1);
}
void setThreadName(const char* name)
{
#ifdef __linux__
prctl(PR_SET_NAME, name, 0, 0, 0); // the main thread's name is /proc/<pid>/comm
#else
(void)name;
#endif
}
void loadConfig(int argc, char** argv)
{
const auto args = parseArgs(argc, argv);
g_cfg.datadir = args.count("datadir") ? fs::path(args.at("datadir")) : defaultDatadir();
std::error_code ec;
fs::create_directories(g_cfg.datadir, ec);
const auto conf = parseConf(g_cfg.datadir / "DRAGONX.conf");
auto get = [&](const std::string& key, const std::string& def) {
if (args.count(key)) return args.at(key);
if (conf.count(key)) return conf.at(key);
return def;
};
g_cfg.printToConsole = get("printtoconsole", "0") != "0";
g_cfg.rpcPort = std::atoi(get("rpcport", "21769").c_str());
g_cfg.rpcThreads = std::max(1, std::atoi(get("rpcthreads", "8").c_str()));
g_cfg.rpcWorkQueue = std::max(1, std::atoi(get("rpcworkqueue", "16").c_str()));
g_cfg.rpcUser = get("rpcuser", "");
g_cfg.rpcPassword = get("rpcpassword", "");
g_cfg.warmupSecs = envDouble("FAKE_WARMUP_SECS", g_cfg.warmupSecs);
g_cfg.replaySecs = envDouble("FAKE_REPLAY_SECS", g_cfg.replaySecs);
g_cfg.replayBlocksPerSec = std::max(0.1, envDouble("FAKE_REPLAY_BPS", g_cfg.replayBlocksPerSec));
g_cfg.rebuildDelaySecs = envDouble("FAKE_REBUILD_DELAY_SECS", g_cfg.rebuildDelaySecs);
g_cfg.rebuildSecs = envDouble("FAKE_REBUILD_SECS", g_cfg.rebuildSecs);
g_cfg.phase1Secs = std::min(envDouble("FAKE_PHASE1_SECS", g_cfg.phase1Secs), g_cfg.rebuildSecs);
g_cfg.postRebuildLockSecs = envDouble("FAKE_POST_REBUILD_LOCK_SECS", g_cfg.postRebuildLockSecs);
g_cfg.flushSecs = envDouble("FAKE_FLUSH_SECS", g_cfg.flushSecs);
g_cfg.updateTipSecs = std::max(1.0, envDouble("FAKE_UPDATETIP_SECS", g_cfg.updateTipSecs));
g_cfg.noiseSecs = std::max(1.0, envDouble("FAKE_NOISE_SECS", g_cfg.noiseSecs));
g_cfg.serverTimeoutSecs = std::max(1.0, envDouble("FAKE_SERVER_TIMEOUT_SECS", g_cfg.serverTimeoutSecs));
g_cfg.prefill = envLong("FAKE_PREFILL", g_cfg.prefill);
g_cfg.height = envLong("FAKE_HEIGHT", g_cfg.height);
g_cfg.rebuildTotal = std::max(1L, envLong("FAKE_REBUILD_TOTAL", g_cfg.rebuildTotal));
g_cfg.notes = envLong("FAKE_NOTES", g_cfg.notes);
g_cfg.version = envLong("FAKE_VERSION", g_cfg.version);
g_cfg.stuckShutdown = envLong("FAKE_STUCK_SHUTDOWN", 0) != 0;
g_cfg.mining = envLong("FAKE_MINING", 0) != 0;
g_cfg.rebuildSilent = envLong("FAKE_REBUILD_SILENT", 0) != 0;
}
std::string knobsLine()
{
char buf[512];
std::snprintf(buf, sizeof(buf),
"warmup=%gs replay=%gs rebuild=%gs after=%gs phase1=%gs post_lock=%gs flush=%gs tip=%gs "
"prefill=%ld version=%ld workers=%d depth=%d port=%d console=%d mining=%d silent=%d",
g_cfg.warmupSecs, g_cfg.replaySecs, g_cfg.rebuildSecs, g_cfg.rebuildDelaySecs, g_cfg.phase1Secs,
g_cfg.postRebuildLockSecs, g_cfg.flushSecs, g_cfg.updateTipSecs, g_cfg.prefill, g_cfg.version,
g_cfg.rpcThreads, g_cfg.rpcWorkQueue, g_cfg.rpcPort, g_cfg.printToConsole ? 1 : 0,
g_cfg.mining ? 1 : 0, g_cfg.rebuildSilent ? 1 : 0);
return buf;
}
} // namespace
int main(int argc, char** argv)
{
#ifdef _WIN32
WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);
#else
std::signal(SIGPIPE, SIG_IGN); // dragonxd ignores SIGPIPE too (init.cpp)
std::signal(SIGHUP, SIG_IGN);
#endif
setThreadName("dragonxd");
loadConfig(argc, argv);
g_log.open(g_cfg.datadir, g_cfg.printToConsole);
g_events = std::fopen((g_cfg.datadir / "fake_dragonxd.events").string().c_str(), "ab");
#ifdef _WIN32
const long pid = static_cast<long>(GetCurrentProcessId());
#else
const long pid = static_cast<long>(getpid());
#endif
const std::string ver = "v" + std::to_string(g_cfg.version / 1000000) + "." +
std::to_string(g_cfg.version / 10000 % 100) + "." +
std::to_string(g_cfg.version / 100 % 100);
logf("DragonX version %s-fakestub", ver.c_str());
logf("fake_dragonxd: %s", knobsLine().c_str());
logf("Using data directory %s", g_cfg.datadir.string().c_str());
event("start", "pid=" + std::to_string(pid) + " " + knobsLine());
if (!lockDatadir(g_cfg.datadir))
initError("Cannot obtain a lock on data directory " + g_cfg.datadir.string() +
". Full node is probably already running.");
if (g_cfg.stuckShutdown) {
// The Sep 29 orphan: an old node wedged in shutdown. It holds the datadir lock, has already
// unbound RPC, and nothing short of a hard kill ends it (on POSIX SIGTERM just re-sets
// fRequestShutdown, which changes nothing).
#ifndef _WIN32
std::signal(SIGTERM, SIG_IGN);
std::signal(SIGINT, SIG_IGN);
#endif
logf("fake_dragonxd: FAKE_STUCK_SHUTDOWN=1, holding the datadir lock with no RPC listener until killed");
event("stuck");
for (;;) std::this_thread::sleep_for(1h);
}
#ifndef _WIN32
std::signal(SIGTERM, onTermSignal);
std::signal(SIGINT, onTermSignal);
const fs::path pidFile = g_cfg.datadir / "hushd.pid";
{
std::ofstream(pidFile) << pid << "\n";
}
#endif
fs::path cookieFile;
if (g_cfg.rpcPassword.empty()) {
g_cfg.rpcUser = "__cookie__";
g_cfg.rpcPassword = randomHex(32);
cookieFile = g_cfg.datadir / ".cookie";
std::ofstream(cookieFile) << g_cfg.rpcUser << ":" << g_cfg.rpcPassword;
logf("Generated RPC authentication cookie %s", cookieFile.string().c_str());
}
g_expectedAuth = "Basic " + base64(g_cfg.rpcUser + ":" + g_cfg.rpcPassword);
g_height = g_cfg.height;
loadNoteWallet(); // FAKE_WALLET_NOTES (optional)
g_minerOn = g_cfg.mining;
const socket_t listener = bindListener(g_cfg.rpcPort);
if (listener == kBadSocket) initError("Unable to bind any endpoint for RPC server");
g_queue.configure(static_cast<size_t>(g_cfg.rpcWorkQueue), g_cfg.rpcThreads);
logf("HTTP: creating work queue of depth %d", g_cfg.rpcWorkQueue);
logf("HTTP: starting %d worker threads", g_cfg.rpcThreads);
std::vector<std::thread> workers;
for (int i = 0; i < g_cfg.rpcThreads; ++i) workers.emplace_back([] { g_queue.run(); });
std::thread acceptThread(acceptLoop, listener);
event("listening", "port=" + std::to_string(g_cfg.rpcPort));
// ---- Init: warmup, optional startup replay, then "Done Loading" -----------------------------
std::thread tipThread, noiseThread, rebuildThread;
setInitMessage("Loading block index...");
bool initOk = sleepUnlessShutdown(g_cfg.warmupSecs);
if (initOk && g_cfg.replaySecs > 0) {
// ActivateBestChain: cs_main per step, checks ShutdownRequested between steps.
setInitMessage("Activating best chain...");
event("replay-start");
const auto end = Clock::now() + std::chrono::duration_cast<Clock::duration>(std::chrono::duration<double>(g_cfg.replaySecs));
const auto stepDur = std::chrono::duration<double>(1.0 / g_cfg.replayBlocksPerSec);
while (initOk && Clock::now() < end) {
{
std::lock_guard<std::mutex> lk(g_csMain);
logUpdateTip(++g_height);
}
initOk = sleepUnlessShutdown(stepDur.count());
}
event(initOk ? "replay-end" : "replay-interrupted");
}
if (initOk) {
setInitMessage("Full Node Done Loading! :)");
g_warmup = false;
event("warmup-end");
tipThread = std::thread(updateTipLoop);
noiseThread = std::thread(noiseLoop);
if (g_cfg.rebuildSecs > 0) rebuildThread = std::thread(rebuildLoop);
}
// ---- Run until `stop` / SIGTERM --------------------------------------------------------------
auto lastStats = Clock::now();
std::string lastStatsLine;
while (!shutdownRequested()) {
std::this_thread::sleep_for(50ms);
if (Clock::now() - lastStats >= 30s) {
lastStats = Clock::now();
const std::string s = statsLine();
if (s != lastStatsLine) {
event("stats", s);
event("methods", topMethodsLine(10));
}
lastStatsLine = s;
}
}
// ---- Shutdown ----------------------------------------------------------------------------------
// Interrupt(): unbind RPC and reject new requests on open connections; workers finish only the
// item they hold. Then the thread-group join waits for the rebuild (Phase 2 can't be aborted),
// and only then does Shutdown() log "In progress...".
g_httpInterrupted = true;
g_queue.interrupt();
acceptThread.join();
event("http-interrupted");
logf("tor: Thread interrupt");
logf("scheduler thread interrupt");
logf("net thread interrupt");
if (noiseThread.joinable()) noiseThread.join();
if (tipThread.joinable()) tipThread.join();
if (rebuildThread.joinable()) rebuildThread.join();
logf("Shutdown: In progress...");
setThreadName("hush-shutoff");
event("shutdown-begin");
for (auto& w : workers) w.join();
for (const auto& it : g_queue.drain()) { // StopHTTPServer: ~HTTPRequest on never-run items
logf("~HTTPRequest: Unhandled request");
++g_stats.unhandled;
complete(*it, 500, kTextType, "Unhandled request");
}
std::this_thread::sleep_for(std::chrono::duration<double>(g_cfg.flushSecs));
logf("Shutdown: done");
#ifndef _WIN32
std::error_code ec;
fs::remove(pidFile, ec);
#endif
if (!cookieFile.empty()) {
std::error_code ec2;
fs::remove(cookieFile, ec2);
}
std::this_thread::sleep_for(200ms); // let connection threads flush their last replies
event("methods", topMethodsLine(10));
event("clean-exit", statsLine());
exitNow(0);
}