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:
138
src/main.cpp
138
src/main.cpp
@@ -85,10 +85,12 @@ void hush_pricesupdate(int32_t height,CBlock *pblock);
|
||||
BlockMap mapBlockIndex;
|
||||
CChain chainActive;
|
||||
CBlockIndex *pindexBestHeader = NULL;
|
||||
int nAssumeutxoSnapshotHeight = -1; // height H of a loaded UTXO snapshot; reorgs below H are refused (-1 = none)
|
||||
static int64_t nTimeBestReceived = 0;
|
||||
CWaitableCriticalSection csBestBlock;
|
||||
CConditionVariable cvBlockChange;
|
||||
int nScriptCheckThreads = 0;
|
||||
int nRandomXVerifyThreads = 0; // parallel RandomX pre-verification worker count (0 = inline only)
|
||||
bool fExperimentalMode = true;
|
||||
bool fImporting = false;
|
||||
bool fReindex = false;
|
||||
@@ -485,7 +487,7 @@ namespace {
|
||||
|
||||
/** Update pindexLastCommonBlock and add not-in-flight missing successors to vBlocks, until it has
|
||||
* at most count entries. */
|
||||
void FindNextBlocksToDownload(NodeId nodeid, unsigned int count, std::vector<CBlockIndex*>& vBlocks, NodeId& nodeStaller) {
|
||||
void FindNextBlocksToDownload(NodeId nodeid, unsigned int count, std::vector<CBlockIndex*>& vBlocks, NodeId& nodeStaller, CBlockIndex** pFrontierStuck = NULL) {
|
||||
if (count == 0)
|
||||
return;
|
||||
|
||||
@@ -562,8 +564,9 @@ namespace {
|
||||
return;
|
||||
}
|
||||
} else if (waitingfor == -1) {
|
||||
// This is the first already-in-flight block.
|
||||
// This is the first already-in-flight block (the download frontier).
|
||||
waitingfor = mapBlocksInFlight[pindex->GetBlockHash()].first;
|
||||
if (pFrontierStuck) *pFrontierStuck = pindex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4140,6 +4143,45 @@ static void PruneBlockIndexCandidates() {
|
||||
assert(!setBlockIndexCandidates.empty());
|
||||
}
|
||||
|
||||
// Activate a trusted UTXO snapshot (assumeutxo-style) as the chain tip WITHOUT replaying blocks
|
||||
// 0..H. The chainstate has already been populated by CCoinsViewDB::LoadSnapshot(); here we mark the
|
||||
// snapshot's base block (height H) as fully validated and set it as the active tip. Blocks above H
|
||||
// then connect normally with full PoW + script + Sapling-proof validation. Requires that the block
|
||||
// HEADERS for height H are already present in mapBlockIndex (from prior header sync or bootstrap).
|
||||
// NOTE: below-H blocks have no body/undo data, so reorgs below H are impossible (see Stage D guard).
|
||||
bool LoadSnapshotChainstate(const CUTXOSnapshotHeader& header, std::string& strError)
|
||||
{
|
||||
LOCK(cs_main);
|
||||
BlockMap::iterator it = mapBlockIndex.find(header.baseBlockHash);
|
||||
if (it == mapBlockIndex.end() || it->second == NULL) {
|
||||
strError = "block header for the snapshot height is not present; sync headers (or use the bootstrap) before loading a UTXO snapshot";
|
||||
return false;
|
||||
}
|
||||
CBlockIndex* pindexH = it->second;
|
||||
if (pindexH->GetHeight() != header.nHeight) {
|
||||
strError = "snapshot base block height does not match its header index";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only nChainTx is consensus-relevant for tip selection; nTx must merely be non-zero so the
|
||||
// (nChainTx != 0) candidate-eligibility checks hold. Ancestors legitimately have nTx==0 here
|
||||
// because we never received their bodies — this is the assumeutxo trust assumption.
|
||||
if (pindexH->nTx == 0)
|
||||
pindexH->nTx = (header.nChainTx > 0 ? (unsigned int)header.nChainTx : 1);
|
||||
pindexH->nChainTx = (unsigned int)header.nChainTx;
|
||||
if (header.fHasChainSaplingValue)
|
||||
pindexH->nChainSaplingValue = header.nChainSaplingValue;
|
||||
|
||||
pindexH->RaiseValidity(BLOCK_VALID_SCRIPTS);
|
||||
nAssumeutxoSnapshotHeight = pindexH->GetHeight(); // arm the reorg-below-H guard (Stage D)
|
||||
setBlockIndexCandidates.insert(pindexH);
|
||||
chainActive.SetTip(pindexH);
|
||||
if (pindexBestHeader == NULL || pindexBestHeader->GetHeight() < pindexH->GetHeight())
|
||||
pindexBestHeader = pindexH;
|
||||
PruneBlockIndexCandidates();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to make some progress towards making pindexMostWork the active block.
|
||||
* pblock is either NULL or a pointer to a CBlock corresponding to pindexMostWork.
|
||||
@@ -4188,6 +4230,16 @@ static bool ActivateBestChainStep(bool fSkipdpow, CValidationState &state, CBloc
|
||||
return state.DoS(100, error("ActivateBestChainStep(): pindexOldTip->GetHeight().%d > notarizedht %d && pindexFork->GetHeight().%d is < notarizedht %d, so ignore it",(int32_t)pindexOldTip->GetHeight(),notarizedht,(int32_t)pindexFork->GetHeight(),notarizedht),
|
||||
REJECT_INVALID, "past-notarized-height");
|
||||
}
|
||||
// Refuse reorgs whose fork point is below a loaded UTXO snapshot height (Stage D): the node has
|
||||
// no block/undo data for 0..H, so disconnecting below H is impossible. Belt-and-suspenders on top
|
||||
// of checkpoint fork-rejection (H sits at/below the last hardcoded checkpoint).
|
||||
if ( nAssumeutxoSnapshotHeight >= 0 && pindexFork != 0 && pindexFork->GetHeight() < nAssumeutxoSnapshotHeight )
|
||||
{
|
||||
return state.DoS(100, error("ActivateBestChainStep(): reorg fork height %d is below the loaded UTXO snapshot height %d; refusing",
|
||||
(int32_t)pindexFork->GetHeight(), nAssumeutxoSnapshotHeight),
|
||||
REJECT_INVALID, "below-assumeutxo-snapshot");
|
||||
}
|
||||
|
||||
// - On ChainDB initialization, pindexOldTip will be null, so there are no removable blocks.
|
||||
// - If pindexMostWork is in a chain that doesn't have the same genesis block as our chain,
|
||||
// then pindexFork will be null, and we would need to remove the entire chain including
|
||||
@@ -4258,6 +4310,36 @@ static bool ActivateBestChainStep(bool fSkipdpow, CValidationState &state, CBloc
|
||||
}
|
||||
nHeight = nTargetHeight;
|
||||
|
||||
// Parallel RandomX pre-verification (Stage 4): verify this about-to-be-connected window's
|
||||
// PoW on the worker pool BEFORE the serial connect, so ConnectBlock rarely pays the
|
||||
// ~tens-of-ms light-mode hash. Pure optimization — CheckBlockHeader's inline
|
||||
// CheckRandomXSolution still verifies anything not pre-verified, so consensus is unchanged.
|
||||
// We hold cs_main; key derivation + the disk reads happen here on the main thread, and the
|
||||
// pool workers receive only value-type work items (no cs_main, no chainstate pointers).
|
||||
if (nRandomXVerifyThreads > 0 && rxCheckQueue.IsIdle()) {
|
||||
std::map<std::string, std::vector<CRandomXCheck> > rxGroups; // grouped by RandomX key
|
||||
BOOST_FOREACH(CBlockIndex *pidx, vpindexToConnect) {
|
||||
if (pidx->fRandomXVerified || !RandomXValidationRequired(pidx->GetHeight()))
|
||||
continue;
|
||||
std::string rxKey = GetRandomXKey(pidx->GetHeight());
|
||||
if (rxKey.empty())
|
||||
continue; // can't derive key -> inline fallback
|
||||
CBlock blk;
|
||||
if (!ReadBlockFromDisk(blk, pidx, false))
|
||||
continue; // -> inline fallback
|
||||
if (blk.nSolution.size() != 32) // RANDOMX_HASH_SIZE; wrong size -> inline (will error)
|
||||
continue;
|
||||
rxGroups[rxKey].push_back(CRandomXCheck(rxKey, GetRandomXInput(blk), blk.nSolution.data(), &pidx->fRandomXVerified));
|
||||
}
|
||||
for (std::map<std::string, std::vector<CRandomXCheck> >::iterator it = rxGroups.begin(); it != rxGroups.end(); ++it) {
|
||||
if (!RandomXValidatorPrepareKey(it->first))
|
||||
break; // cache alloc failed -> leave the rest for the inline fallback
|
||||
CCheckQueueControl<CRandomXCheck> control(&rxCheckQueue);
|
||||
control.Add(it->second);
|
||||
control.Wait();
|
||||
}
|
||||
}
|
||||
|
||||
// Connect new blocks.
|
||||
BOOST_REVERSE_FOREACH(CBlockIndex *pindexConnect, vpindexToConnect) {
|
||||
if (!ConnectTip(state, pindexConnect, pindexConnect == pindexMostWork ? pblock : NULL)) {
|
||||
@@ -4993,7 +5075,11 @@ bool CheckBlockHeader(int32_t *futureblockp,int32_t height,CBlockIndex *pindex,
|
||||
{
|
||||
if ( !CheckEquihashSolution(&blockhdr, Params()) )
|
||||
return state.DoS(100, error("CheckBlockHeader(): Equihash solution invalid"),REJECT_INVALID, "invalid-solution");
|
||||
if ( !CheckRandomXSolution(&blockhdr, height) )
|
||||
// Skip the inline RandomX recompute only if the parallel pre-verify pool already verified
|
||||
// THIS block (fRandomXVerified set 1:1 on a real hash match). Every other case — pool miss,
|
||||
// straggler, disabled pool, or any pindex==NULL caller (TestBlockValidity/VerifyDB/header
|
||||
// accept) — falls through to the inline check, so consensus is unchanged.
|
||||
if ( !(pindex && pindex->fRandomXVerified) && !CheckRandomXSolution(&blockhdr, height) )
|
||||
return state.DoS(100, error("CheckBlockHeader(): RandomX solution invalid"),REJECT_INVALID, "invalid-randomx-solution");
|
||||
}
|
||||
// Check proof of work matches claimed amount
|
||||
@@ -5957,6 +6043,15 @@ bool static LoadBlockIndexDB()
|
||||
pblocktree->ReadReindexing(fReindexing);
|
||||
fReindex |= fReindexing;
|
||||
|
||||
// Restore the loaded-UTXO-snapshot height so the reorg-below-H guard survives restarts.
|
||||
{
|
||||
int snapHeight = -1;
|
||||
if (pblocktree->ReadAssumeutxoHeight(snapHeight) && snapHeight >= 0) {
|
||||
nAssumeutxoSnapshotHeight = snapHeight;
|
||||
LogPrintf("%s: loaded-from-UTXO-snapshot height is %d; reorgs below it are refused\n", __func__, snapHeight);
|
||||
}
|
||||
}
|
||||
|
||||
// Check whether we have a transaction index
|
||||
pblocktree->ReadFlag("txindex", fTxIndex);
|
||||
LogPrintf("%s: transaction index %s\n", __func__, fTxIndex ? "enabled" : "disabled");
|
||||
@@ -6746,6 +6841,13 @@ void static ProcessGetData(CNode* pfrom)
|
||||
std::deque<CInv>::iterator it = pfrom->vRecvGetData.begin();
|
||||
|
||||
vector<CInv> vNotFound;
|
||||
// Serve up to this many blocks per ProcessGetData pass. The old code broke after a SINGLE block,
|
||||
// so a 16-block getdata was dribbled out one block per message-handler tick (~100ms), throttling
|
||||
// block download for every peer fetching from us. Bound the per-pass work (cs_main is held while
|
||||
// reading blocks from disk); any remainder is served on the next pass (the message handler keeps
|
||||
// fSleep=false while vRecvGetData is non-empty, so there is no 100ms park between passes).
|
||||
const unsigned int nMaxBlocksServedPerPass = 16;
|
||||
unsigned int nBlocksServed = 0;
|
||||
|
||||
LOCK(cs_main);
|
||||
|
||||
@@ -6863,7 +6965,10 @@ void static ProcessGetData(CNode* pfrom)
|
||||
}
|
||||
}
|
||||
|
||||
if (inv.type == MSG_BLOCK || inv.type == MSG_FILTERED_BLOCK)
|
||||
// Serve a bounded batch of blocks per pass rather than one (see nMaxBlocksServedPerPass
|
||||
// above). The send-buffer gate at the top of the loop still pauses us if the buffer fills;
|
||||
// this counter bounds the cs_main hold for a (possibly malicious) large getdata.
|
||||
if ((inv.type == MSG_BLOCK || inv.type == MSG_FILTERED_BLOCK) && ++nBlocksServed >= nMaxBlocksServedPerPass)
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -8145,12 +8250,35 @@ bool SendMessages(CNode* pto, bool fSendTrickle)
|
||||
if (!pto->fDisconnect && !pto->fClient && (fFetch || !IsInitialBlockDownload()) && state.nBlocksInFlight < MAX_BLOCKS_IN_TRANSIT_PER_PEER) {
|
||||
vector<CBlockIndex*> vToDownload;
|
||||
NodeId staller = -1;
|
||||
FindNextBlocksToDownload(pto->GetId(), MAX_BLOCKS_IN_TRANSIT_PER_PEER - state.nBlocksInFlight, vToDownload, staller);
|
||||
CBlockIndex *pFrontierStuck = NULL;
|
||||
FindNextBlocksToDownload(pto->GetId(), MAX_BLOCKS_IN_TRANSIT_PER_PEER - state.nBlocksInFlight, vToDownload, staller, &pFrontierStuck);
|
||||
BOOST_FOREACH(CBlockIndex *pindex, vToDownload) {
|
||||
vGetData.push_back(CInv(MSG_BLOCK, pindex->GetBlockHash()));
|
||||
MarkBlockAsInFlight(pto->GetId(), pindex->GetBlockHash(), consensusParams, pindex);
|
||||
LogPrint("net", "Requesting block %s (%d) peer=%d\n", pindex->GetBlockHash().ToString(), pindex->GetHeight(), pto->id);
|
||||
}
|
||||
// Frontier reassignment: when this peer has nothing new to fetch because the next-needed
|
||||
// (frontier) block is in flight from another, slow peer and has been stuck beyond a short
|
||||
// threshold, re-request it from THIS (responsive) peer instead of waiting out the long
|
||||
// (~72s) timeout or disconnecting the slow peer. This breaks the head-of-line stall that
|
||||
// throttles IBD when downloading from few, distant peers. Trustless: the block is still
|
||||
// fully validated on arrival - we only change which peer serves it. -blockreassigntimeout
|
||||
// = seconds (0 disables; default 5).
|
||||
static const int64_t nReassignUs = GetArg("-blockreassigntimeout", 5) * 1000000LL;
|
||||
if (nReassignUs > 0 && vToDownload.empty() && pFrontierStuck != NULL &&
|
||||
staller != -1 && staller != pto->GetId()) {
|
||||
map<uint256, pair<NodeId, list<QueuedBlock>::iterator> >::iterator itF =
|
||||
mapBlocksInFlight.find(pFrontierStuck->GetBlockHash());
|
||||
if (itF != mapBlocksInFlight.end() && itF->second.first == staller &&
|
||||
itF->second.second->nTime < nNow - nReassignUs) {
|
||||
uint256 hReassign = pFrontierStuck->GetBlockHash();
|
||||
LogPrint("net", "Reassigning stalled frontier block %s (%d) from peer=%d to peer=%d\n",
|
||||
hReassign.ToString(), pFrontierStuck->GetHeight(), staller, pto->id);
|
||||
MarkBlockAsReceived(hReassign); // free from slow peer (no disconnect)
|
||||
vGetData.push_back(CInv(MSG_BLOCK, hReassign));
|
||||
MarkBlockAsInFlight(pto->GetId(), hReassign, consensusParams, pFrontierStuck); // re-request from this peer
|
||||
}
|
||||
}
|
||||
if (state.nBlocksInFlight == 0 && staller != -1) {
|
||||
if (State(staller)->nStallingSince == 0) {
|
||||
State(staller)->nStallingSince = nNow;
|
||||
|
||||
Reference in New Issue
Block a user