upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBitflash <btf01@proton.me>2026-07-31 08:53:27 -0300
committerGitHub <noreply@github.com>2026-07-31 08:53:27 -0300
commit3bbfb50ef85feeddad936eb1fe8cce232410b8a6 (patch)
treea3a8b628489f81159aa417bbe44650b9c9eaf5c4 /src
parent50b0a91b5dddd38f3094b9e97db77773d8299442 (diff)
db: verify the tail of the chain on startup and back out of a bad tip (#61)
A node that accepted blocks its current rules would reject -- because it was running an older build, or because the tip it landed on was never valid -- had no way to notice. It kept the branch, kept building on it, and kept mining coins the rest of the network throws away. The only cure was deleting blk*.dat by hand, which requires knowing something is wrong. A node here once mined a 323-block fork for five days that way. On load, walk back from the tip running CheckBlock on each block. If one fails, move the best chain back to its parent, disconnect what came after, and let the node re-download from peers. Satoshi added this after the block 74638 incident for the same reason: so it would not have needed anyone to delete files. Only the last 288 blocks are checked by default -- about ten hours at a two-minute target -- because every check costs a RandomX hash. /checkblocks=N changes it, 0 does the whole chain. The repair deliberately runs in LoadBlockIndex() in main.cpp rather than in CTxDB::LoadBlockIndex where the detection happens. CTxDB::LoadBlockIndex walks the index with a cursor it never closes, so it still holds read locks when it returns, and a write transaction opened alongside it waits on them forever. That is measured, not guessed: the first version came up, printed the diagnosis, and hung with nothing in any log. Closing that cursor is a separate fix worth making on its own. A failed rollback reports and starts anyway. Refusing to start would leave the operator with no node to read the message from. What this catches is what CheckBlock catches: proof of work, merkle root, duplicate transactions, sigops, timestamps. It does not re-verify signatures or spends, which would mean rebuilding the transaction index. Verified by forcing a block at tip-5 to fail: the node reported the bad block, moved back six, stayed up, and re-synced past its old tip. Without the forced failure it verifies 288 and says so.
Diffstat (limited to 'src')
-rw-r--r--src/db.cpp41
-rw-r--r--src/main.cpp75
-rw-r--r--src/main.h2
-rw-r--r--src/main_gui.cpp5
4 files changed, 123 insertions, 0 deletions
diff --git a/src/db.cpp b/src/db.cpp
index 4cc54ac..ce7b875 100644
--- a/src/db.cpp
+++ b/src/db.cpp
@@ -435,6 +435,47 @@ bool CTxDB::LoadBlockIndex()
435 nBestHeight = pindexBest->nHeight; 435 nBestHeight = pindexBest->nHeight;
436 printf("LoadBlockIndex(): hashBestChain=%s height=%d\n", hashBestChain.ToString().substr(0,14).c_str(), nBestHeight); 436 printf("LoadBlockIndex(): hashBestChain=%s height=%d\n", hashBestChain.ToString().substr(0,14).c_str(), nBestHeight);
437 437
438 // Verify the tail of the best chain, and fall back to the last good block
439 // if any of it fails.
440 //
441 // A node that accepted blocks its current rules would reject -- because it
442 // was running an older build, or because the tip it landed on was never
443 // valid -- has no way to notice on its own. It keeps the branch, keeps
444 // building on it, and keeps mining coins that the rest of the network
445 // throws away. The only cure used to be deleting blk*.dat by hand, which
446 // requires knowing something is wrong in the first place. That is how a
447 // node here once mined a 323-block fork for five days.
448 //
449 // Checking every block on every start would mean a RandomX hash per block,
450 // so only the tail is checked by default. /checkblocks=0 does the lot.
451 int nCheckBlocks = nCheckBlocksOnLoad;
452 CBlockIndex* pindexFork = NULL;
453 int nChecked = 0;
454 for (CBlockIndex* pindex = pindexBest; pindex && pindex->pprev; pindex = pindex->pprev)
455 {
456 if (nCheckBlocks > 0 && nChecked >= nCheckBlocks)
457 break;
458 nChecked++;
459 CBlock block;
460 if (!block.ReadFromDisk(pindex->nFile, pindex->nBlockPos, true))
461 return error("LoadBlockIndex() : ReadFromDisk failed at height %d", pindex->nHeight);
462 if (!block.CheckBlock())
463 {
464 printf("LoadBlockIndex() : *** bad block at height %d, hash=%s\n",
465 pindex->nHeight, pindex->GetBlockHash().ToString().substr(0,14).c_str());
466 // Keep going: an older bad block further back wins, because
467 // everything after it has to come off too.
468 pindexFork = pindex->pprev;
469 }
470 }
471 printf("LoadBlockIndex(): verified %d block(s)\n", nChecked);
472
473 // The repair itself happens in the caller, after this handle is closed.
474 // The cursor above is still open and holds read locks, so a write
475 // transaction opened here waits on it forever -- measured: the node came
476 // up, printed the diagnosis, and hung with no error anywhere.
477 pindexBadChainFork = pindexFork;
478
438 return true; 479 return true;
439} 480}
440 481
diff --git a/src/main.cpp b/src/main.cpp
index 15805ca..dbb2c41 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -49,6 +49,15 @@ uint256 hashGenesisMerkleRoot("0x1a45b4482532abb29b10e234d3f13132230525a339ecea9
49CBlockIndex* pindexGenesisBlock = NULL; 49CBlockIndex* pindexGenesisBlock = NULL;
50int nBestHeight = -1; 50int nBestHeight = -1;
51uint256 hashBestChain = 0; 51uint256 hashBestChain = 0;
52// Blocks at the tip to re-verify on startup. 0 means the whole chain. 288 is
53// about ten hours at a two-minute target -- far enough back to catch a bad tip,
54// short enough that startup does not pay a RandomX hash per block for the whole
55// history. /checkblocks=N overrides it.
56int nCheckBlocksOnLoad = 288;
57// Set by CTxDB::LoadBlockIndex when the tail of the chain fails verification.
58// The repair runs in LoadBlockIndex() below, once that read-only handle and its
59// cursor are closed.
60CBlockIndex* pindexBadChainFork = NULL;
52CBlockIndex* pindexBest = NULL; 61CBlockIndex* pindexBest = NULL;
53 62
54map<uint256, CBlock*> mapOrphanBlocks; 63map<uint256, CBlock*> mapOrphanBlocks;
@@ -1577,6 +1586,72 @@ bool LoadBlockIndex(bool fAllowNew)
1577 return false; 1586 return false;
1578 txdb.Close(); 1587 txdb.Close();
1579 1588
1589 // Repair a bad tip found during verification.
1590 //
1591 // This has to happen after the handle above is closed. CTxDB::LoadBlockIndex
1592 // walks the index with a cursor it never closes, so it is still holding read
1593 // locks -- a write transaction opened alongside it waits on them forever.
1594 // Measured, not guessed: the node came up, printed the diagnosis, and hung
1595 // there with nothing in any log.
1596 if (pindexBadChainFork)
1597 {
1598 CBlockIndex* pindexFork = pindexBadChainFork;
1599 pindexBadChainFork = NULL;
1600
1601 printf("LoadBlockIndex() : *** moving best chain back to height %d, discarding %d block(s)\n",
1602 pindexFork->nHeight, nBestHeight - pindexFork->nHeight);
1603
1604 vector<CBlockIndex*> vDisconnect;
1605 for (CBlockIndex* pindex = pindexBest; pindex && pindex != pindexFork; pindex = pindex->pprev)
1606 vDisconnect.push_back(pindex);
1607
1608 bool fRolledBack = false;
1609 try
1610 {
1611 CTxDB txdbWrite;
1612 txdbWrite.TxnBegin();
1613 foreach(CBlockIndex* pindex, vDisconnect)
1614 {
1615 CBlock block;
1616 if (!block.ReadFromDisk(pindex->nFile, pindex->nBlockPos, true))
1617 throw runtime_error("ReadFromDisk for disconnect failed");
1618 if (!block.DisconnectBlock(txdbWrite, pindex))
1619 throw runtime_error("DisconnectBlock failed");
1620 }
1621 if (!txdbWrite.WriteHashBestChain(pindexFork->GetBlockHash()))
1622 throw runtime_error("WriteHashBestChain failed");
1623 txdbWrite.TxnCommit();
1624 txdbWrite.Close();
1625 fRolledBack = true;
1626 }
1627 catch (const std::exception& e)
1628 {
1629 printf("LoadBlockIndex() : rollback failed: %s\n", e.what());
1630 }
1631 catch (...)
1632 {
1633 printf("LoadBlockIndex() : rollback failed\n");
1634 }
1635
1636 if (fRolledBack)
1637 {
1638 foreach(CBlockIndex* pindex, vDisconnect)
1639 if (pindex->pprev)
1640 pindex->pprev->pnext = NULL;
1641
1642 pindexBest = pindexFork;
1643 hashBestChain = pindexBest->GetBlockHash();
1644 nBestHeight = pindexBest->nHeight;
1645 printf("LoadBlockIndex() : now at height %d, will re-download from peers\n", nBestHeight);
1646 }
1647 else
1648 {
1649 // Start anyway. The operator needs a running node to read this from.
1650 printf("LoadBlockIndex() : *** still on the bad chain at height %d -- back up the data directory and report this\n",
1651 nBestHeight);
1652 }
1653 }
1654
1580 // 1655 //
1581 // Init with genesis block 1656 // Init with genesis block
1582 // 1657 //
diff --git a/src/main.h b/src/main.h
index 750d3f1..eea10b1 100644
--- a/src/main.h
+++ b/src/main.h
@@ -52,6 +52,8 @@ extern unsigned int GENESIS_NONCE;
52extern CBlockIndex* pindexGenesisBlock; 52extern CBlockIndex* pindexGenesisBlock;
53extern int nBestHeight; 53extern int nBestHeight;
54extern uint256 hashBestChain; 54extern uint256 hashBestChain;
55extern int nCheckBlocksOnLoad;
56extern CBlockIndex* pindexBadChainFork;
55extern CBlockIndex* pindexBest; 57extern CBlockIndex* pindexBest;
56extern unsigned int nTransactionsUpdated; 58extern unsigned int nTransactionsUpdated;
57extern string strSetDataDir; 59extern string strSetDataDir;
diff --git a/src/main_gui.cpp b/src/main_gui.cpp
index 852ad4d..99bcb6f 100644
--- a/src/main_gui.cpp
+++ b/src/main_gui.cpp
@@ -54,6 +54,7 @@ static void PrintUsage()
54 printf(" /participant=POOL_BTF_ADDRESS\n"); 54 printf(" /participant=POOL_BTF_ADDRESS\n");
55 printf(" /solomine\n"); 55 printf(" /solomine\n");
56 printf(" /genproclimit=N (mining threads; 0 or absent = every core but one)\n"); 56 printf(" /genproclimit=N (mining threads; 0 or absent = every core but one)\n");
57 printf(" /checkblocks=N (blocks re-verified at startup, default 288, 0 = all)\n");
57 printf("\n"); 58 printf("\n");
58 printf("Pool operator announcement:\n"); 59 printf("Pool operator announcement:\n");
59 printf(" /poolname=NAME\n"); 60 printf(" /poolname=NAME\n");
@@ -101,6 +102,10 @@ static void ParseStartupArguments(int argc, char* argv[])
101 if (arg(argc,argv,"/solomine") || arg(argc,argv,"-solomine")) 102 if (arg(argc,argv,"/solomine") || arg(argc,argv,"-solomine"))
102 fSoloMineTest = true; 103 fSoloMineTest = true;
103 104
105 string strCheckBlocks = argval2(argc, argv, "/checkblocks", "-checkblocks");
106 if (!strCheckBlocks.empty())
107 nCheckBlocksOnLoad = atoi(strCheckBlocks.c_str());
108
104 // /genproclimit=N -- threads to hash with. 0 or absent means automatic, 109 // /genproclimit=N -- threads to hash with. 0 or absent means automatic,
105 // which is every core but one. Named after Bitcoin's own option so it reads 110 // which is every core but one. Named after Bitcoin's own option so it reads
106 // familiarly to anyone who has run one of these before. 111 // familiarly to anyone who has run one of these before.