IBD/sync speedups: parallel RandomX pre-verify, adaptive dbcache, P2P download fixes

- Parallel RandomX PoW pre-verification pool (CCheckQueue) run ahead of the serial
  connect; consensus-neutral (inline CheckRandomXSolution fallback still verifies
  anything not pre-verified). New -randomxverifythreads (default = -par).
- Adaptive dbcache: default sizes the UTXO/coins cache to most of RAM and shrinks
  under memory pressure, always leaving a reserve free; -dbcache pins a fixed value.
- P2P block download: bounded socket recv-drain loop (tlsmanager); frontier-block
  reassignment to break head-of-line stalls (-blockreassigntimeout); ProcessGetData
  serves a bounded batch of blocks per pass instead of one (fixes the serve-side
  one-block-per-tick throttle that caps download network-wide).
- assumeutxo: dumptxoutset RPC + LoadSnapshot machinery + AssumeutxoData chainparams.
- Signed bootstrap verification (util/bootstrap-dragonx.sh, util/sign-bootstrap.md).
- gtest: RandomX pre-verify consensus-equivalence test + UTXO-snapshot round-trip;
  revived the gtest harness (Makefile.am include fix, Makefile.gtest.include).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-19 12:30:10 -05:00
parent 2b011d6ee2
commit 1673cfb6dc
18 changed files with 1599 additions and 154 deletions

View File

@@ -43,6 +43,7 @@
#endif
#include "main.h"
#include "metrics.h"
#include "pow.h"
#include "miner.h"
#include "net.h"
#include "rpc/server.h"
@@ -176,7 +177,7 @@ public:
// Writes do not need similar protection, as failure to write is handled by the caller.
};
static CCoinsViewDB *pcoinsdbview = NULL;
CCoinsViewDB *pcoinsdbview = NULL; // global (declared extern in main.h) for UTXO-snapshot dump/load
static CCoinsViewErrorCatcher *pcoinscatcher = NULL;
static boost::scoped_ptr<ECCVerifyHandle> globalVerifyHandle;
@@ -387,14 +388,17 @@ std::string HelpMessage(HelpMessageMode mode)
}
strUsage += HelpMessageOpt("-datadir=<dir>", _("Specify data directory (this path cannot use '~')"));
strUsage += HelpMessageOpt("-exportdir=<dir>", _("Specify directory to be used when exporting data"));
strUsage += HelpMessageOpt("-dbcache=<n>", strprintf(_("Set database cache size in megabytes (%d to %d, default: %d)"), nMinDbCache, nMaxDbCache, nDefaultDbCache));
strUsage += HelpMessageOpt("-dbcache=<n>", strprintf(_("Set database cache size in megabytes (%d to %d). Default: adaptive - uses most free RAM to speed up initial block download (far fewer UTXO flushes to disk) and automatically shrinks if other applications need memory, always leaving a reserve free. Setting a fixed value disables adaptive sizing."), nMinDbCache, nMaxDbCache));
strUsage += HelpMessageOpt("-loadblock=<file>", _("Imports blocks from external blk000??.dat file") + " " + _("on startup"));
strUsage += HelpMessageOpt("-loadutxosnapshot=<file>", _("On a fresh node (empty chainstate), load a trusted UTXO snapshot produced by 'dumptxoutset' and fast-forward the tip to its height, skipping replay of earlier blocks. Block headers up to that height must already be present (e.g. via header sync or bootstrap). Blocks above the snapshot are still fully validated."));
strUsage += HelpMessageOpt("-loadutxosnapshotunsafe", _("Allow -loadutxosnapshot even when no trusted snapshot hash is hardcoded for this network (verifies file integrity only, not authenticity). Testing/regtest only."));
strUsage += HelpMessageOpt("-maxdebugfilesize=<n>", strprintf(_("Set the max size of the debug.log file (default: %u)"), 15));
strUsage += HelpMessageOpt("-maxorphantx=<n>", strprintf(_("Keep at most <n> unconnectable transactions in memory (default: %u)"), DEFAULT_MAX_ORPHAN_TRANSACTIONS));
strUsage += HelpMessageOpt("-maxreorg=<n>", _("Specify the maximum length of a blockchain re-organization"));
strUsage += HelpMessageOpt("-mempooltxinputlimit=<n>", _("[DEPRECATED/IGNORED] Set the maximum number of transparent inputs in a transaction that the mempool will accept (default: 0 = no limit applied)"));
strUsage += HelpMessageOpt("-par=<n>", strprintf(_("Set the number of script verification threads (%u to %d, 0 = auto, <0 = leave that many cores free, default: %d)"),
-(int)boost::thread::hardware_concurrency(), MAX_SCRIPTCHECK_THREADS, DEFAULT_SCRIPTCHECK_THREADS));
strUsage += HelpMessageOpt("-randomxverifythreads=<n>", strprintf(_("Number of threads for parallel RandomX PoW pre-verification of post-checkpoint blocks during sync (0 = inline only, max %d, default: same as -par)"), MAX_SCRIPTCHECK_THREADS));
#ifndef _WIN32
strUsage += HelpMessageOpt("-pid=<file>", strprintf(_("Specify pid file (default: %s)"), "hushd.pid"));
#endif
@@ -987,6 +991,123 @@ bool AppInitServers(boost::thread_group& threadGroup)
*/
extern int32_t HUSH_REWIND;
// --- Adaptive coins-cache sizing -------------------------------------------------------------
// The in-memory UTXO/coins cache (nCoinCacheUsage) is the biggest lever on IBD speed: a bigger
// cache means far fewer chainstate flushes to disk. We size it to use most of RAM, but a scheduled
// background task (AdjustCoinCacheForMemoryPressure, registered in AppInit2) shrinks the target when
// free system memory runs low — e.g. the user opens other apps — and grows it back when memory frees
// up, always leaving a reserve free for the rest of the system. The existing per-block flush
// (FlushStateToDisk, FLUSH_STATE_IF_NEEDED, which fires when cacheSize > nCoinCacheUsage) enforces
// whatever target is current, so the task only moves the threshold: it never touches cs_main or the
// flush path. NOTE: the coins cache is application heap, not OS file cache — "freeing" it means an
// early flush that clears the map; on Linux the allocator returns the pages, on Windows the heap
// returns them best-effort (RSS may lag), but either way the node stops growing past the target.
// windows.h / <unistd.h> arrive via compat.h (net.h). Memory helpers return 0 if undeterminable.
static int64_t GetPhysicalMemoryMB()
{
#ifdef WIN32
MEMORYSTATUSEX status;
status.dwLength = sizeof(status);
if (GlobalMemoryStatusEx(&status))
return (int64_t)(status.ullTotalPhys / (1024 * 1024));
return 0;
#elif defined(_SC_PHYS_PAGES) && defined(_SC_PAGESIZE)
long pages = sysconf(_SC_PHYS_PAGES);
long pageSize = sysconf(_SC_PAGESIZE);
if (pages > 0 && pageSize > 0)
return (int64_t)((int64_t)pages * (int64_t)pageSize / (1024 * 1024));
return 0;
#else
return 0;
#endif
}
// Currently-available (allocatable) physical RAM in MiB. On Linux uses MemAvailable (counts
// reclaimable page cache), falling back to truly-free pages.
static int64_t GetAvailableMemoryMB()
{
#ifdef WIN32
MEMORYSTATUSEX status;
status.dwLength = sizeof(status);
if (GlobalMemoryStatusEx(&status))
return (int64_t)(status.ullAvailPhys / (1024 * 1024));
return 0;
#else
FILE* f = fopen("/proc/meminfo", "r");
if (f) {
char line[256];
long long availKB = -1;
while (fgets(line, sizeof(line), f)) {
if (sscanf(line, "MemAvailable: %lld kB", &availKB) == 1)
break;
}
fclose(f);
if (availKB >= 0)
return (int64_t)(availKB / 1024);
}
#if defined(_SC_AVPHYS_PAGES) && defined(_SC_PAGESIZE)
long pages = sysconf(_SC_AVPHYS_PAGES);
long pageSize = sysconf(_SC_PAGESIZE);
if (pages > 0 && pageSize > 0)
return (int64_t)((int64_t)pages * (int64_t)pageSize / (1024 * 1024));
#endif
return 0;
#endif
}
// RAM (MiB) to always keep free for the OS and other applications: 20% of total, at least 2 GiB.
static int64_t GetMemoryReserveMB()
{
int64_t ramMB = GetPhysicalMemoryMB();
int64_t reserve = (ramMB > 0) ? ramMB / 5 : 2048; // 20%
if (reserve < 2048) reserve = 2048;
return reserve;
}
// Startup -dbcache default: use most of RAM (total minus the reserve), clamped to
// [nDefaultDbCache, nMaxDbCache] MiB. Falls back to the fixed default if RAM can't be detected.
static int64_t GetDefaultDbCacheMB()
{
int64_t ramMB = GetPhysicalMemoryMB();
if (ramMB <= 0)
return nDefaultDbCache;
int64_t cacheMB = ramMB - GetMemoryReserveMB();
if (cacheMB < nDefaultDbCache) cacheMB = nDefaultDbCache;
if (cacheMB > nMaxDbCache) cacheMB = nMaxDbCache;
return cacheMB;
}
// Ceiling (bytes) the adaptive task may grow the coins cache back up to (the startup nCoinCacheUsage).
static size_t g_nMaxCoinCacheUsage = 0;
static const int64_t g_nMinCoinCacheMB = 256; // never thrash below this working set
// Scheduled task: nudge nCoinCacheUsage toward "use all RAM except the reserve". If free RAM is below
// the reserve we shrink the target (the next per-block flush releases the excess); if there is spare
// RAM we grow it back toward the startup ceiling. Lock-free: it only reads system memory and writes
// the aligned size_t threshold that the flush path reads.
static void AdjustCoinCacheForMemoryPressure()
{
if (g_nMaxCoinCacheUsage == 0)
return; // adaptive sizing disabled (user pinned -dbcache) or RAM undetectable
int64_t availMB = GetAvailableMemoryMB();
if (availMB <= 0)
return; // can't measure pressure; leave the target untouched
int64_t reserveMB = GetMemoryReserveMB();
// Error term: free RAM beyond the reserve. >0 => spare, grow; <0 => pressure, shrink.
int64_t errMB = availMB - reserveMB;
// Deadband: ignore small fluctuations so the target settles instead of oscillating.
if (errMB > -256 && errMB < 256)
return;
int64_t curTargetMB = (int64_t)(nCoinCacheUsage >> 20);
// Damped proportional step (gain 1/4) toward "free RAM == reserve"; the clamps bound it and the
// per-block flush (FLUSH_STATE_IF_NEEDED) enforces a lowered target within ~one block during IBD.
int64_t newTargetMB = curTargetMB + errMB / 4;
int64_t ceilMB = (int64_t)(g_nMaxCoinCacheUsage >> 20);
if (newTargetMB > ceilMB) newTargetMB = ceilMB;
if (newTargetMB < g_nMinCoinCacheMB) newTargetMB = g_nMinCoinCacheMB;
nCoinCacheUsage = (size_t)(newTargetMB << 20);
}
bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
{
//fprintf(stderr,"%s start\n", __FUNCTION__);
@@ -1309,6 +1430,15 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
else if (nScriptCheckThreads > MAX_SCRIPTCHECK_THREADS)
nScriptCheckThreads = MAX_SCRIPTCHECK_THREADS;
// Parallel RandomX pre-verification threads (speeds up post-checkpoint sync). Defaults to the
// script-check thread count — RandomX pre-verify and script checks do not run simultaneously
// within a single connect, so they can share the same budget. 0 disables (inline-only).
nRandomXVerifyThreads = GetArg("-randomxverifythreads", nScriptCheckThreads);
if (nRandomXVerifyThreads < 0)
nRandomXVerifyThreads = 0;
else if (nRandomXVerifyThreads > MAX_SCRIPTCHECK_THREADS)
nRandomXVerifyThreads = MAX_SCRIPTCHECK_THREADS;
fServer = GetBoolArg("-server", false);
//fprintf(stderr,"%s tik6\n", __FUNCTION__);
@@ -1545,6 +1675,14 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
threadGroup.create_thread(&ThreadScriptCheck);
}
// Spawn the parallel RandomX pre-verification worker pool (the connect thread joins as the Nth
// worker via CCheckQueueControl::Wait, so spawn N-1 here, mirroring ThreadScriptCheck).
if (ASSETCHAINS_ALGO == ASSETCHAINS_RANDOMX && nRandomXVerifyThreads > 0) {
LogPrintf("Using %u threads for parallel RandomX pre-verification\n", nRandomXVerifyThreads);
for (int i = 0; i < nRandomXVerifyThreads - 1; i++)
threadGroup.create_thread(&ThreadRandomXVerify);
}
//fprintf(stderr,"%s tik13\n", __FUNCTION__);
// Start the lightweight task scheduler thread
@@ -1840,7 +1978,7 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
LogPrintf("* Compression is %s\n", dbCompression ? "enabled" : "disabled");
// cache size calculations
int64_t nTotalCache = (GetArg("-dbcache", nDefaultDbCache) << 20);
int64_t nTotalCache = (GetArg("-dbcache", GetDefaultDbCacheMB()) << 20);
nTotalCache = std::max(nTotalCache, nMinDbCache << 20); // total cache cannot be less than nMinDbCache
nTotalCache = std::min(nTotalCache, nMaxDbCache << 20); // total cache cannot be greated than nMaxDbcache
int64_t nBlockTreeDBCache = nTotalCache / 8;
@@ -1857,6 +1995,14 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
int64_t nCoinDBCache = std::min(nTotalCache / 2, (nTotalCache / 4) + (1 << 23)); // use 25%-50% of the remainder for disk cache
nTotalCache -= nCoinDBCache;
nCoinCacheUsage = nTotalCache; // the rest goes to in-memory cache
// Adaptive sizing: unless the user pinned -dbcache, grow/shrink the coins cache with free system
// memory (AdjustCoinCacheForMemoryPressure), using the startup size as the ceiling.
if (!mapArgs.count("-dbcache")) {
g_nMaxCoinCacheUsage = nCoinCacheUsage;
scheduler.scheduleEvery(&AdjustCoinCacheForMemoryPressure, 5);
LogPrintf("* Adaptive dbcache enabled: ceiling %.0fMiB, keeping >= %lldMiB RAM free for the system\n",
nCoinCacheUsage * (1.0 / 1024 / 1024), (long long)GetMemoryReserveMB());
}
LogPrintf("Cache configuration:\n");
LogPrintf("* Max cache setting possible %.1fMiB\n", nMaxDbCache);
LogPrintf("* Using %.1fMiB for block index database\n", nBlockTreeDBCache * (1.0 / 1024 / 1024));
@@ -1938,6 +2084,45 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
strLoadError = _("Error initializing block database");
break;
}
// Trusted UTXO snapshot fast-sync (assumeutxo-style). If -loadutxosnapshot is given
// and the chainstate is still empty, load the verified snapshot and fast-forward the
// tip to height H; blocks above H then sync with full PoW/script/Sapling validation.
{
std::string snapPath = GetArg("-loadutxosnapshot", "");
if (!snapPath.empty()) {
if (!pcoinsdbview->GetBestBlock().IsNull()) {
LogPrintf("%s: -loadutxosnapshot ignored, chainstate is not empty\n", __func__);
} else {
const CChainParams::AssumeutxoData& au = chainparams.Assumeutxo();
bool unsafe = GetBoolArg("-loadutxosnapshotunsafe", false);
if (au.IsNull() && !unsafe) {
strLoadError = _("-loadutxosnapshot: no trusted snapshot hash is configured for this network; refusing (use -loadutxosnapshotunsafe for testing only)");
break;
}
CUTXOSnapshotHeader hdr; uint256 gotHash; std::string snapErr;
bool requireExpected = !au.IsNull() && !unsafe;
if (!pcoinsdbview->LoadSnapshot(snapPath, au.hash, requireExpected, hdr, gotHash, snapErr)) {
strLoadError = strprintf(_("Failed to load UTXO snapshot: %s"), snapErr);
break;
}
if (!au.IsNull() && hdr.nHeight != au.height) {
strLoadError = _("UTXO snapshot height does not match the trusted value for this network");
break;
}
pcoinsTip->SetBestBlock(hdr.baseBlockHash); // refresh cache view of the freshly-written chainstate
std::string fixErr;
if (!LoadSnapshotChainstate(hdr, fixErr)) {
strLoadError = strprintf(_("Failed to activate UTXO snapshot tip: %s"), fixErr);
break;
}
pblocktree->WriteAssumeutxoHeight(hdr.nHeight); // persist reorg-below-H guard across restarts
LogPrintf("%s: loaded trusted UTXO snapshot at height %d (hash %s); syncing forward with full validation\n",
__func__, hdr.nHeight, gotHash.GetHex());
}
}
}
HUSH_LOADINGBLOCKS = 0;
// Check for changed -txindex state
if (fTxIndex != GetBoolArg("-txindex", true)) {