fake_dragonxd now answers rescan, importprivkey, z_importkey and z_importviewingkey, so the GUI's long wallet RPCs can be reproduced without a real node. - Each one holds cs_main for FAKE_RESCAN_SECS (default 0). The lock-free getrescaninfo reports the scan while it runs, and, like v1.3.0, the scan stops early when a shutdown is requested. - Argument checks, error codes and return values follow dragonxd (z_importkey returns null, importprivkey the t-address, the default "whenkeyisnew" skips a known key, z_importviewingkey needs its zaddr). Keys are only prefix-checked and never stored or logged; an imported spending key gets a made-up address that the address lists then show. - FAKE_ENCRYPTED=1 / FAKE_WALLET_PASSPHRASE: an encrypted wallet that starts locked, with walletpassphrase (-14 on a wrong passphrase), walletpassphrasechange, walletlock and getwalletinfo unlocked_until; the imports answer -13 while it is locked. As in dragonxd, the right passphrase with a non-integer timeout unlocks first and only then fails (-1): the wallet stays unlocked with no relock timer of its own. A timer pending from an earlier unlock keeps running; otherwise only walletlock locks it again. Without FAKE_ENCRYPTED, those three methods answer -15 like dragonxd. - FAKE_METHODS_ALL call events show keys and passphrases as <redacted>. A plain run is unchanged: the same 63 GUI calls before and after, and identical replies to every one of them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1908 lines
80 KiB
C++
1908 lines
80 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
|
|
double tipUnknownSecs = 0; // longestchain reads 0 this long after "Done Loading" (no UpdateTip seen yet)
|
|
long longestAhead = 0; // the network tip is this many blocks ahead: the node is behind...
|
|
double catchupSecs = 0; // ...for this long after "Done Loading" (0 = for good)
|
|
long txcount = 42; // getwalletinfo txcount
|
|
};
|
|
|
|
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::atomic<long long> g_loadedAtMs{0}; // when warmup ended ("Done Loading")
|
|
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 startAt = 0.0; // FAKE_MERGE_SERIAL: queued until the previous op finished
|
|
double finishAt = 0.0; // unixMs()/1000.0
|
|
std::string status = "executing";
|
|
std::string error; // a failed op's message (FAKE_MERGE_FAIL=opfail:N)
|
|
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;
|
|
// Consolidation-run knobs (C210 manual checks).
|
|
double g_scanSecs = 0.0; // FAKE_SCAN_SECS: a wallet scan holds cs_main this long
|
|
bool g_mergeSerial = false; // FAKE_MERGE_SERIAL=1: one op at a time, like dragonxd's single async worker
|
|
std::string g_mergeFailKind; // FAKE_MERGE_FAIL=syncing:N | lockedkey:N | opfail:N
|
|
long g_mergeFailFrom = 0;
|
|
long g_mergeCalls = 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);
|
|
g_mergeSerial = envLong("FAKE_MERGE_SERIAL", 0) == 1;
|
|
if (const char* f = std::getenv("FAKE_MERGE_FAIL")) {
|
|
const std::string spec(f);
|
|
const size_t colon = spec.find(':');
|
|
g_mergeFailKind = spec.substr(0, colon);
|
|
g_mergeFailFrom = colon == std::string::npos ? 1 : std::max(1L, std::strtol(spec.c_str() + colon + 1, nullptr, 10));
|
|
}
|
|
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 == "queued" && now >= op.startAt) op.status = "executing";
|
|
if (op.status != "executing" || now < op.finishAt) continue;
|
|
if (!op.error.empty()) { // FAKE_MERGE_FAIL=opfail: the op fails and its notes are unlocked again
|
|
for (auto& n : g_notes)
|
|
if (std::find(op.noteIds.begin(), op.noteIds.end(), n.id) != op.noteIds.end()) n.locked = false;
|
|
op.status = "failed";
|
|
event("merge-failed", op.id);
|
|
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)))}};
|
|
if (op.status == "failed") o["error"] = {{"code", -6}, {"message", op.error}};
|
|
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}, {"locked", n.locked}});
|
|
}
|
|
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>();
|
|
++g_mergeCalls;
|
|
const bool failNow = !g_mergeFailKind.empty() && g_mergeCalls >= g_mergeFailFrom;
|
|
if (failNow && g_mergeFailKind == "syncing") { // THROW_IF_SYNCING runs before the scan (cheap)
|
|
c.errCode = -1;
|
|
c.errMsg = "z_mergetoaddress: Extreme Privacy! Chain still syncing at height " + std::to_string(h) +
|
|
", aborting to prevent linkability analysis. Please wait until FULLY SYNCED and try again.";
|
|
return c;
|
|
}
|
|
if (g_scanSecs > 0.0) { // GetFilteredNotes: a whole-wallet scan under cs_main
|
|
event("scan-begin", "m=z_mergetoaddress");
|
|
std::this_thread::sleep_for(std::chrono::duration<double>(g_scanSecs));
|
|
event("scan-end", "m=z_mergetoaddress");
|
|
}
|
|
if (failNow && g_mergeFailKind == "lockedkey") {
|
|
c.errCode = -8;
|
|
c.errMsg = "Could not find spending key for payment address.";
|
|
return c;
|
|
}
|
|
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;
|
|
const double nowSec = unixMs() / 1000.0;
|
|
double prevFinish = nowSec;
|
|
if (g_mergeSerial) {
|
|
for (const auto& o : g_ops)
|
|
if (o.status == "queued" || o.status == "executing") prevFinish = std::max(prevFinish, o.finishAt);
|
|
}
|
|
op.startAt = prevFinish;
|
|
op.finishAt = op.startAt + g_mergeSecs;
|
|
op.status = op.startAt > nowSec ? "queued" : "executing";
|
|
if (failNow && g_mergeFailKind == "opfail") op.error = "Proof failed (FAKE_MERGE_FAIL)";
|
|
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 == "success" || op.status == "failed";
|
|
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;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------------
|
|
// Rescan, key imports and wallet encryption (dragonxd's rpcdump.cpp / rpcwallet.cpp). walletReply runs
|
|
// under g_csMain (execOne); getrescaninfo and getwalletinfo read only the atomics.
|
|
// * rescan, importprivkey, z_importkey and z_importviewingkey: dragonxd's argument checks, then a
|
|
// ScanForWalletTransactions that holds cs_main for FAKE_RESCAN_SECS (0 = instant). The lock-free
|
|
// getrescaninfo reports it, it logs "Still rescanning" once a minute and, like v1.3.0, it stops
|
|
// between blocks once a shutdown is requested. Keys are only prefix-checked, never decoded, stored
|
|
// or logged; an imported key gets a derived fake address, and a key already imported isn't added again.
|
|
// * FAKE_ENCRYPTED=1: an encrypted wallet, locked at start, with the passphrase FAKE_WALLET_PASSPHRASE.
|
|
// walletpassphrase (-14 when wrong), walletpassphrasechange and walletlock work, getwalletinfo carries
|
|
// unlocked_until and the imports need the wallet unlocked (-13). Unencrypted, those three answer -15.
|
|
// Like dragonxd, the right passphrase with a non-integer timeout unlocks first and only then fails
|
|
// (-1), leaving the wallet unlocked with no relock timer of its own.
|
|
// ---------------------------------------------------------------------------------------------
|
|
double g_rescanSecs = 0.0; // FAKE_RESCAN_SECS
|
|
std::atomic<bool> g_rescanning{false};
|
|
std::atomic<long> g_rescanStart{0}, g_rescanHeight{0};
|
|
std::vector<std::uint64_t> g_importedKeys; // fingerprints of the imported keys
|
|
std::vector<std::string> g_importedZ, g_importedT; // their addresses, listed with the wallet's own
|
|
bool g_walletEncrypted = false; // FAKE_ENCRYPTED=1 (set before any thread starts)
|
|
std::string g_walletPassphrase; // FAKE_WALLET_PASSPHRASE
|
|
std::atomic<long long> g_unlockedUntil{0}; // unix seconds; 0 = locked
|
|
std::atomic<bool> g_unlockedNoTimer{false}; // unlocked by a walletpassphrase whose timeout didn't parse
|
|
|
|
const char* const kNotString = "JSON value is not a string as expected";
|
|
const char* const kNotInteger = "JSON value is not an integer as expected";
|
|
const char* const kBase58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
|
|
bool isWalletMethod(const std::string& m)
|
|
{
|
|
return m == "rescan" || m == "importprivkey" || m == "z_importkey" || m == "z_importviewingkey" ||
|
|
m == "walletpassphrase" || m == "walletpassphrasechange" || m == "walletlock";
|
|
}
|
|
|
|
// The wallet relocks when walletpassphrase's timeout runs out (dragonxd's RPCRunLater("lockwallet")).
|
|
long long unlockedUntilNow()
|
|
{
|
|
const long long until = g_unlockedUntil.load();
|
|
return (until != 0 && until <= unixMs() / 1000) ? 0 : until;
|
|
}
|
|
|
|
// CWallet::IsLocked() == false. Separate from unlocked_until (nWalletUnlockTime), which an unlock with an
|
|
// unparsable timeout leaves as it was.
|
|
bool walletUnlockedNow()
|
|
{
|
|
return unlockedUntilNow() != 0 || g_unlockedNoTimer.load();
|
|
}
|
|
|
|
std::uint64_t fingerprint(const std::string& s) // FNV-1a
|
|
{
|
|
std::uint64_t h = 1469598103934665603ULL;
|
|
for (unsigned char ch : s) h = (h ^ ch) * 1099511628211ULL;
|
|
return h;
|
|
}
|
|
|
|
std::string derivedAddress(const std::string& prefix, size_t length, std::uint64_t fp, const char* alphabet)
|
|
{
|
|
std::string out = prefix;
|
|
const size_t n = std::strlen(alphabet);
|
|
for (std::uint64_t i = 0; out.size() < length; ++i) out.push_back(alphabet[splitmix64(fp + i) % n]);
|
|
return out;
|
|
}
|
|
|
|
// ScanForWalletTransactions from height `from`, under the caller's cs_main.
|
|
void walletScan(const std::string& m, long from)
|
|
{
|
|
const long tip = g_height.load();
|
|
g_rescanStart = from;
|
|
g_rescanHeight = from;
|
|
g_rescanning = true;
|
|
const auto started = Clock::now();
|
|
event("rescan-begin", "m=" + m + " from=" + std::to_string(from));
|
|
auto lastLog = started;
|
|
bool interrupted = false;
|
|
for (;;) {
|
|
const double secs = std::chrono::duration<double>(Clock::now() - started).count();
|
|
const double frac = g_rescanSecs > 0.0 ? std::min(1.0, secs / g_rescanSecs) : 1.0;
|
|
g_rescanHeight = from + static_cast<long>(static_cast<double>(tip - from) * frac);
|
|
if (frac >= 1.0) break;
|
|
if (shutdownRequested()) {
|
|
interrupted = true;
|
|
break;
|
|
}
|
|
if (Clock::now() - lastLog >= 60s) {
|
|
lastLog = Clock::now();
|
|
logf("Still rescanning. At block %ld. Progress=%f", g_rescanHeight.load(),
|
|
tip > 0 ? static_cast<double>(g_rescanHeight.load()) / static_cast<double>(tip) : 1.0);
|
|
}
|
|
std::this_thread::sleep_for(50ms);
|
|
}
|
|
const long long ms = std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now() - started).count();
|
|
if (interrupted) {
|
|
logf("ScanForWalletTransactions: Rescan interrupted by shutdown request at block %ld", g_rescanHeight.load());
|
|
event("rescan-interrupted", "m=" + m + " at=" + std::to_string(g_rescanHeight.load()) + " ms=" + std::to_string(ms));
|
|
} else {
|
|
event("rescan-end", "m=" + m + " ms=" + std::to_string(ms));
|
|
}
|
|
g_rescanning = false;
|
|
}
|
|
|
|
// The answer for isWalletMethod(m) (the checks in dragonxd's order), or c.known = false.
|
|
Canned walletReply(const std::string& m, const json& params)
|
|
{
|
|
Canned c;
|
|
if (!isWalletMethod(m)) {
|
|
c.known = false;
|
|
return c;
|
|
}
|
|
auto fail = [&c](int code, const std::string& msg) {
|
|
c.errCode = code;
|
|
c.errMsg = msg;
|
|
return c;
|
|
};
|
|
auto str = [&](size_t i) { return params[i].get<std::string>(); };
|
|
const long tip = g_height.load();
|
|
const bool locked = g_walletEncrypted && !walletUnlockedNow();
|
|
const char* const kUnlockNeeded = "Error: Please enter the wallet passphrase with walletpassphrase first.";
|
|
c.result = nullptr;
|
|
|
|
if (m == "walletpassphrase" || m == "walletpassphrasechange" || m == "walletlock") {
|
|
const size_t argc = m == "walletlock" ? 0 : 2;
|
|
const std::string usage = m == "walletlock" ? "walletlock" : m == "walletpassphrase"
|
|
? "walletpassphrase \"passphrase\" timeout"
|
|
: "walletpassphrasechange \"oldpassphrase\" \"newpassphrase\"";
|
|
if (g_walletEncrypted && params.size() != argc) return fail(-1, usage);
|
|
if (!g_walletEncrypted) return fail(-15, "Error: running with an unencrypted wallet, but " + m + " was called.");
|
|
if (m == "walletlock") {
|
|
g_unlockedUntil = 0;
|
|
g_unlockedNoTimer = false;
|
|
event("walletlock", "result=ok");
|
|
return c;
|
|
}
|
|
if (!params[0].is_string()) return fail(-1, kNotString);
|
|
if (m == "walletpassphrase") {
|
|
if (str(0).empty()) return fail(-1, usage);
|
|
if (str(0) != g_walletPassphrase) {
|
|
event("walletpassphrase", "result=-14");
|
|
return fail(-14, "Error: The wallet passphrase entered was incorrect.");
|
|
}
|
|
// dragonxd unlocks before it reads the timeout (get_int64), so a bad one leaves the wallet
|
|
// unlocked with no new relock timer; a timer still pending from an earlier unlock keeps running.
|
|
if (!params[1].is_number_integer()) {
|
|
if (unlockedUntilNow() == 0) g_unlockedNoTimer = true;
|
|
return fail(-1, kNotInteger);
|
|
}
|
|
const long long timeout = params[1].get<long long>();
|
|
g_unlockedUntil = unixMs() / 1000 + timeout;
|
|
g_unlockedNoTimer = false; // the new timer relocks it
|
|
event("walletpassphrase", "result=ok timeout=" + std::to_string(timeout));
|
|
return c;
|
|
}
|
|
if (!params[1].is_string()) return fail(-1, kNotString);
|
|
if (str(0).empty() || str(1).empty()) return fail(-1, usage);
|
|
if (str(0) != g_walletPassphrase) {
|
|
event("walletpassphrasechange", "result=-14");
|
|
return fail(-14, "Error: The wallet passphrase entered was incorrect.");
|
|
}
|
|
g_walletPassphrase = str(1);
|
|
event("walletpassphrasechange", "result=ok");
|
|
return c;
|
|
}
|
|
|
|
if (m == "rescan") {
|
|
if (params.size() > 1) return fail(-1, "rescan \"height\"");
|
|
long from = 0;
|
|
if (!params.empty()) {
|
|
if (!params[0].is_number_integer()) return fail(-1, kNotInteger);
|
|
from = params[0].get<long>();
|
|
}
|
|
if (from < 0 || from > tip) return fail(-8, "Block height out of range");
|
|
logf("Rescanning from height=%ld", from);
|
|
walletScan(m, from);
|
|
logf("Done rescanning from height=%ld", from);
|
|
return c;
|
|
}
|
|
|
|
if (m == "importprivkey") {
|
|
if (params.empty() || params.size() > 5)
|
|
return fail(-1, "importprivkey \"dragonxprivkey\" ( \"label\" rescan height secret_key)");
|
|
if (locked) return fail(-13, kUnlockNeeded);
|
|
if (!params[0].is_string() || (params.size() > 1 && !params[1].is_string())) return fail(-1, kNotString);
|
|
bool rescan = true;
|
|
if (params.size() > 2) {
|
|
if (!params[2].is_boolean()) return fail(-1, "JSON value is not a boolean as expected");
|
|
rescan = params[2].get<bool>();
|
|
}
|
|
long from = 0;
|
|
if (rescan && params.size() == 4) {
|
|
if (!params[3].is_number_integer()) return fail(-1, kNotInteger);
|
|
from = params[3].get<long>();
|
|
}
|
|
if (from < 0 || from > tip) return fail(-4, "Rescan height is out of range.");
|
|
const std::string& key = params[0].get_ref<const std::string&>();
|
|
if ((key.size() != 51 && key.size() != 52) || key.find_first_not_of(kBase58) != std::string::npos)
|
|
return fail(-5, "Invalid private key encoding");
|
|
const std::uint64_t fp = fingerprint("t:" + key);
|
|
const std::string addr = derivedAddress("RFakeImport", 34, fp, kBase58);
|
|
c.result = addr;
|
|
if (std::find(g_importedKeys.begin(), g_importedKeys.end(), fp) != g_importedKeys.end()) {
|
|
event("import", "m=" + m + " known=1 addr=" + addr);
|
|
return c; // "Don't throw error in case a key is already there": no rescan either
|
|
}
|
|
g_importedKeys.push_back(fp);
|
|
g_importedT.push_back(addr);
|
|
event("import", "m=" + m + " addr=" + addr + " rescan=" + (rescan ? "1" : "0"));
|
|
if (rescan) walletScan(m, from);
|
|
return c;
|
|
}
|
|
|
|
// z_importkey "zkey" ( rescan startHeight ) / z_importviewingkey "vkey" ( rescan startHeight zaddr )
|
|
const bool viewing = m == "z_importviewingkey";
|
|
if (params.empty() || params.size() > (viewing ? 4u : 3u))
|
|
return fail(-1, m + (viewing ? " \"vkey\" ( rescan startHeight zaddr )" : " \"zkey\" ( rescan startHeight )"));
|
|
if (locked) return fail(-13, kUnlockNeeded);
|
|
bool rescan = true;
|
|
bool ignoreExisting = true; // the default "whenkeyisnew"
|
|
if (params.size() > 1) {
|
|
if (!params[1].is_string()) return fail(-1, kNotString);
|
|
const std::string r = str(1);
|
|
if (r != "whenkeyisnew") {
|
|
ignoreExisting = false;
|
|
if (r == "yes" || r == "no") rescan = r == "yes";
|
|
else if (!viewing && (r == "true" || r == "false")) rescan = r == "true"; // z_importkey's older API
|
|
else return fail(-8, "rescan must be \"yes\", \"no\" or \"whenkeyisnew\"");
|
|
}
|
|
}
|
|
long from = 0;
|
|
if (params.size() > 2) {
|
|
if (!params[2].is_number_integer()) return fail(-1, kNotInteger);
|
|
from = params[2].get<long>();
|
|
}
|
|
if (from < 0 || from > tip) return fail(-8, "Block height out of range");
|
|
if (!params[0].is_string()) return fail(-1, kNotString);
|
|
const std::string& key = params[0].get_ref<const std::string&>();
|
|
std::uint64_t fp = 0;
|
|
std::string addr;
|
|
if (viewing) {
|
|
if (key.rfind("zivks1", 0) != 0) return fail(-5, "Invalid viewing key");
|
|
if (params.size() < 4) return fail(-5, "Missing zaddr for Sapling viewing key.");
|
|
if (!params[3].is_string()) return fail(-1, kNotString);
|
|
addr = str(3);
|
|
if (addr.rfind("zs1", 0) != 0) return fail(-5, "Invalid zaddr");
|
|
fp = fingerprint("v:" + addr); // dragonxd keeps one incoming viewing key per address
|
|
} else {
|
|
if (key.rfind("secret-extended-key-main1", 0) != 0) return fail(-5, "Invalid spending key");
|
|
fp = fingerprint("z:" + key);
|
|
addr = derivedAddress("zs1fakeimport", std::strlen(kFakeZAddr), fp, "0123456789abcdef");
|
|
}
|
|
const bool known = std::find(g_importedKeys.begin(), g_importedKeys.end(), fp) != g_importedKeys.end();
|
|
if (known && ignoreExisting) {
|
|
event("import", "m=" + m + " known=1 addr=" + addr);
|
|
return c;
|
|
}
|
|
if (!known) {
|
|
g_importedKeys.push_back(fp);
|
|
if (!viewing) g_importedZ.push_back(addr); // watch-only addresses aren't in z_listaddresses
|
|
}
|
|
event("import", "m=" + m + (known ? " known=1" : "") + " addr=" + addr + " rescan=" + (rescan ? "1" : "0"));
|
|
if (rescan) walletScan(m, from);
|
|
return c;
|
|
}
|
|
|
|
// Keys and passphrases never reach the events file (FAKE_METHODS_ALL).
|
|
json redactedParams(const std::string& m, json params)
|
|
{
|
|
size_t secrets = 0;
|
|
if (m == "importprivkey" || m == "z_importkey" || m == "z_importviewingkey" || m == "walletpassphrase" ||
|
|
m == "encryptwallet")
|
|
secrets = 1;
|
|
else if (m == "walletpassphrasechange")
|
|
secrets = 2;
|
|
for (size_t i = 0; i < secrets && i < params.size(); ++i) params[i] = "<redacted>";
|
|
return params;
|
|
}
|
|
|
|
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();
|
|
};
|
|
|
|
// FAKE_TIP_UNKNOWN_SECS / FAKE_LONGEST_AHEAD / FAKE_CATCHUP_SECS: what the node knows of the network tip.
|
|
const double sinceLoaded = g_loadedAtMs.load() > 0 ? (unixMs() - g_loadedAtMs.load()) / 1000.0 : 0.0;
|
|
const bool tipUnknown = g_loadedAtMs.load() == 0 || sinceLoaded < g_cfg.tipUnknownSecs;
|
|
const long ahead = (g_cfg.catchupSecs > 0 && sinceLoaded >= g_cfg.catchupSecs) ? 0 : g_cfg.longestAhead;
|
|
const long longest = tipUnknown ? 0 : h + ahead;
|
|
const long headers = h + ahead;
|
|
|
|
if (m == "getinfo") {
|
|
c.result = {{"version", g_cfg.version}, {"protocolversion", 170012}, {"walletversion", 60000},
|
|
{"balance", 0.0}, {"blocks", h}, {"longestchain", longest}, {"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", ahead == 0}, {"headers", headers},
|
|
{"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", longest}, {"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", g_cfg.txcount}, {"keypoololdest", now - 86400},
|
|
{"keypoolsize", 101}, {"paytxfee", 0.0}};
|
|
if (g_walletEncrypted) c.result["unlocked_until"] = unlockedUntilNow();
|
|
} 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", g_rescanning.load()}};
|
|
if (g_rescanning.load()) {
|
|
if (h != 0) {
|
|
char progress[16];
|
|
std::snprintf(progress, sizeof(progress), "%.4f",
|
|
static_cast<double>(g_rescanHeight.load()) / static_cast<double>(h));
|
|
c.result["rescan_progress"] = progress; // a string, like dragonxd
|
|
}
|
|
c.result["rescan_start_height"] = g_rescanStart.load();
|
|
c.result["rescan_height"] = g_rescanHeight.load();
|
|
}
|
|
} else if (isWalletMethod(m)) {
|
|
c.result = nullptr; // stateful: walletReply answers it under cs_main (execOne)
|
|
} 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 = redactedParams(m, 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>();
|
|
// FAKE_SCAN_SECS: the GUI's wallet-wide scans hold cs_main (z_mergetoaddress sleeps in its own handler).
|
|
if (g_scanSecs > 0.0 && (m == "z_listunspent" || m == "listunspent" || m == "z_gettotalbalance" ||
|
|
m == "z_listreceivedbyaddress")) {
|
|
event("scan-begin", "m=" + m);
|
|
std::this_thread::sleep_for(std::chrono::duration<double>(g_scanSecs));
|
|
event("scan-end", "m=" + m);
|
|
}
|
|
if (g_noteWallet) {
|
|
Canned w = noteWalletReply(m, params);
|
|
if (w.known) c = w;
|
|
}
|
|
Canned w = walletReply(m, params); // rescan, key imports, FAKE_ENCRYPTED
|
|
if (w.known) c = w;
|
|
// An imported key's address is listed with the wallet's own from then on.
|
|
const auto& imported = m == "z_listaddresses" ? g_importedZ : g_importedT;
|
|
if ((m == "z_listaddresses" || m == "getaddressesbyaccount" || m == "listaddresses") && c.result.is_array())
|
|
for (const auto& a : imported) c.result.push_back(a);
|
|
}
|
|
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.tipUnknownSecs = envDouble("FAKE_TIP_UNKNOWN_SECS", g_cfg.tipUnknownSecs);
|
|
g_cfg.longestAhead = std::max(0L, envLong("FAKE_LONGEST_AHEAD", g_cfg.longestAhead));
|
|
g_cfg.catchupSecs = envDouble("FAKE_CATCHUP_SECS", g_cfg.catchupSecs);
|
|
g_scanSecs = envDouble("FAKE_SCAN_SECS", 0.0);
|
|
g_rescanSecs = std::max(0.0, envDouble("FAKE_RESCAN_SECS", 0.0));
|
|
g_walletEncrypted = envLong("FAKE_ENCRYPTED", 0) == 1;
|
|
const char* pass = std::getenv("FAKE_WALLET_PASSPHRASE");
|
|
g_walletPassphrase = (pass && *pass) ? pass : "fake passphrase one";
|
|
g_cfg.txcount = envLong("FAKE_TXCOUNT", g_cfg.txcount);
|
|
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_loadedAtMs = unixMs();
|
|
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);
|
|
}
|