Remove assumeutxo / UTXO-snapshot feature

Removes the dumptxoutset RPC, -loadutxosnapshot / -loadutxosnapshotunsafe,
the CCoinsViewDB Dump/LoadSnapshot machinery + CUTXOSnapshotHeader, the
AssumeutxoData chainparams anchor, the LoadSnapshotChainstate activation +
reorg-below-H guard, the persisted assumeutxo-height flag, and the gtest.

Rationale: it duplicated the existing bootstrap (same skip-the-genesis-grind
fast-sync, no speed advantage), its only real edge was a trust model we don't
need for this chain, and it was inert anyway (no published snapshot hash in
chainparams). The -loadutxosnapshot load path adopted an external UTXO set and
bypassed genesis validation, so removing it also drops that attack surface.
Builds clean (no dangling references); the kept IBD speedups (RandomX
pre-verify, adaptive dbcache, tlsmanager) are untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-30 16:42:37 -05:00
co-authored by Claude Opus 4.8
parent 1f2b109d95
commit 84aefb5475
9 changed files with 2 additions and 703 deletions
-235
View File
@@ -271,233 +271,6 @@ bool CCoinsViewDB::GetStats(CCoinsStats &stats) const {
return true;
}
// Helper: count entries in the coins DB whose key prefix matches `prefix`.
// LevelDB returns keys in sorted order, so iteration is deterministic across nodes.
static uint64_t CountByPrefix(CDBWrapper &db, char prefix)
{
boost::scoped_ptr<CDBIterator> pcursor(db.NewIterator());
uint64_t n = 0;
for (pcursor->Seek(prefix); pcursor->Valid(); pcursor->Next()) {
boost::this_thread::interruption_point();
std::pair<char, uint256> key;
if (pcursor->GetKey(key) && key.first == prefix) n++;
else break;
}
return n;
}
bool CCoinsViewDB::DumpSnapshot(const std::string &path, CUTXOSnapshotHeader &header, uint256 &hashRet, std::string &strError) const
{
CDBWrapper *pdb = const_cast<CDBWrapper*>(&db);
// Counting pass (caller holds cs_main and has flushed, so the set is stable).
header.nCoins = CountByPrefix(*pdb, DB_COINS);
header.nSaplingAnchors = CountByPrefix(*pdb, DB_SAPLING_ANCHOR);
header.nSaplingNullifiers = CountByPrefix(*pdb, DB_SAPLING_NULLIFIER);
FILE *f = fopen(path.c_str(), "wb");
if (f == nullptr) { strError = "cannot open snapshot file for writing: " + path; return false; }
CAutoFile fileout(f, SER_DISK, CLIENT_VERSION);
// The content hash is computed over the same logical object stream the loader will
// reconstruct, so producer and consumer agree regardless of on-disk encoding.
CHashWriter hasher(SER_GETHASH, PROTOCOL_VERSION);
fileout << header;
hasher << header;
// Coins ('c')
{
boost::scoped_ptr<CDBIterator> pcursor(pdb->NewIterator());
uint64_t n = 0;
for (pcursor->Seek(DB_COINS); pcursor->Valid(); pcursor->Next()) {
boost::this_thread::interruption_point();
std::pair<char, uint256> key;
CCoins coins;
if (pcursor->GetKey(key) && key.first == DB_COINS) {
if (!pcursor->GetValue(coins)) { strError = "failed reading coins record"; return false; }
fileout << key.second; hasher << key.second;
fileout << coins; hasher << coins;
n++;
} else break;
}
if (n != header.nCoins) { strError = "coin count changed during dump"; return false; }
}
// Sapling anchors ('Z') — the commitment trees referenced by spends above H.
{
boost::scoped_ptr<CDBIterator> pcursor(pdb->NewIterator());
uint64_t n = 0;
for (pcursor->Seek(DB_SAPLING_ANCHOR); pcursor->Valid(); pcursor->Next()) {
boost::this_thread::interruption_point();
std::pair<char, uint256> key;
SaplingMerkleTree tree;
if (pcursor->GetKey(key) && key.first == DB_SAPLING_ANCHOR) {
if (!pcursor->GetValue(tree)) { strError = "failed reading sapling anchor"; return false; }
fileout << key.second; hasher << key.second;
fileout << tree; hasher << tree;
n++;
} else break;
}
if (n != header.nSaplingAnchors) { strError = "sapling anchor count changed during dump"; return false; }
}
// Sapling nullifiers ('S') — spent markers; value is always true, so only the key matters.
{
boost::scoped_ptr<CDBIterator> pcursor(pdb->NewIterator());
uint64_t n = 0;
for (pcursor->Seek(DB_SAPLING_NULLIFIER); pcursor->Valid(); pcursor->Next()) {
boost::this_thread::interruption_point();
std::pair<char, uint256> key;
if (pcursor->GetKey(key) && key.first == DB_SAPLING_NULLIFIER) {
fileout << key.second; hasher << key.second;
n++;
} else break;
}
if (n != header.nSaplingNullifiers) { strError = "sapling nullifier count changed during dump"; return false; }
}
hashRet = hasher.GetHash();
fileout << hashRet; // trailing content hash (not fed into the hasher)
return true;
}
bool CCoinsViewDB::LoadSnapshot(const std::string &path, const uint256 &expectedHash, bool fRequireExpected,
CUTXOSnapshotHeader &headerRet, uint256 &hashRet, std::string &strError)
{
uint32_t netmagic = 0;
memcpy(&netmagic, Params().MessageStart(), 4);
// ---- Pass 1: read + verify integrity (and the trusted hash) WITHOUT writing to the DB ----
CUTXOSnapshotHeader header;
uint256 computed;
{
FILE *f = fopen(path.c_str(), "rb");
if (f == nullptr) { strError = "cannot open snapshot file: " + path; return false; }
CAutoFile filein(f, SER_DISK, CLIENT_VERSION);
CHashWriter hasher(SER_GETHASH, PROTOCOL_VERSION);
try {
filein >> header; hasher << header;
if (header.nMagic != UTXO_SNAPSHOT_MAGIC) { strError = "not a DragonX UTXO snapshot (bad magic)"; return false; }
if (header.nVersion != UTXO_SNAPSHOT_VERSION) { strError = "unsupported snapshot version"; return false; }
if (header.nNetworkMagic != netmagic) { strError = "snapshot is for a different network"; return false; }
for (uint64_t i = 0; i < header.nCoins; i++) {
boost::this_thread::interruption_point();
uint256 txid; CCoins coins;
filein >> txid; filein >> coins;
hasher << txid; hasher << coins;
}
for (uint64_t i = 0; i < header.nSaplingAnchors; i++) {
boost::this_thread::interruption_point();
uint256 root; SaplingMerkleTree tree;
filein >> root; filein >> tree;
hasher << root; hasher << tree;
}
for (uint64_t i = 0; i < header.nSaplingNullifiers; i++) {
boost::this_thread::interruption_point();
uint256 nf;
filein >> nf;
hasher << nf;
}
uint256 stored;
filein >> stored;
computed = hasher.GetHash();
if (computed != stored) { strError = "snapshot content hash mismatch (corrupt or truncated)"; return false; }
} catch (const std::exception &e) {
strError = std::string("error reading snapshot: ") + e.what();
return false;
}
}
if (fRequireExpected && computed != expectedHash) {
strError = "snapshot hash does not match the trusted value hardcoded for this network";
return false;
}
hashRet = computed;
headerRet = header;
// ---- Pass 2: apply to the (empty) chainstate DB in bounded batches ----
const size_t CHUNK = 100000;
CCoinsMap mapCoins;
CAnchorsSproutMap mapSproutAnchors; // unused on this chain, always empty
CAnchorsSaplingMap mapSaplingAnchors;
CNullifiersMap mapSproutNullifiers; // unused, always empty
CNullifiersMap mapSaplingNullifiers;
{
FILE *f = fopen(path.c_str(), "rb");
if (f == nullptr) { strError = "cannot reopen snapshot file: " + path; return false; }
CAutoFile filein(f, SER_DISK, CLIENT_VERSION);
try {
CUTXOSnapshotHeader hdr2;
filein >> hdr2; // header already validated in pass 1
for (uint64_t i = 0; i < header.nCoins; i++) {
boost::this_thread::interruption_point();
uint256 txid; CCoins coins;
filein >> txid; filein >> coins;
CCoinsCacheEntry &e = mapCoins[txid];
e.coins = coins;
e.flags = CCoinsCacheEntry::DIRTY;
if (mapCoins.size() >= CHUNK) {
if (!BatchWrite(mapCoins, uint256(), uint256(), uint256(), mapSproutAnchors, mapSaplingAnchors, mapSproutNullifiers, mapSaplingNullifiers))
{ strError = "batch write failed (coins)"; return false; }
mapCoins.clear();
}
}
if (!mapCoins.empty()) {
if (!BatchWrite(mapCoins, uint256(), uint256(), uint256(), mapSproutAnchors, mapSaplingAnchors, mapSproutNullifiers, mapSaplingNullifiers))
{ strError = "batch write failed (coins remainder)"; return false; }
mapCoins.clear();
}
for (uint64_t i = 0; i < header.nSaplingAnchors; i++) {
boost::this_thread::interruption_point();
uint256 root; SaplingMerkleTree tree;
filein >> root; filein >> tree;
CAnchorsSaplingCacheEntry &e = mapSaplingAnchors[root];
e.entered = true;
e.tree = tree;
e.flags = CAnchorsSaplingCacheEntry::DIRTY;
if (mapSaplingAnchors.size() >= CHUNK) {
if (!BatchWrite(mapCoins, uint256(), uint256(), uint256(), mapSproutAnchors, mapSaplingAnchors, mapSproutNullifiers, mapSaplingNullifiers))
{ strError = "batch write failed (anchors)"; return false; }
mapSaplingAnchors.clear();
}
}
if (!mapSaplingAnchors.empty()) {
if (!BatchWrite(mapCoins, uint256(), uint256(), uint256(), mapSproutAnchors, mapSaplingAnchors, mapSproutNullifiers, mapSaplingNullifiers))
{ strError = "batch write failed (anchors remainder)"; return false; }
mapSaplingAnchors.clear();
}
for (uint64_t i = 0; i < header.nSaplingNullifiers; i++) {
boost::this_thread::interruption_point();
uint256 nf;
filein >> nf;
CNullifiersCacheEntry &e = mapSaplingNullifiers[nf];
e.entered = true;
e.flags = CNullifiersCacheEntry::DIRTY;
if (mapSaplingNullifiers.size() >= CHUNK) {
if (!BatchWrite(mapCoins, uint256(), uint256(), uint256(), mapSproutAnchors, mapSaplingAnchors, mapSproutNullifiers, mapSaplingNullifiers))
{ strError = "batch write failed (nullifiers)"; return false; }
mapSaplingNullifiers.clear();
}
}
} catch (const std::exception &e) {
strError = std::string("error applying snapshot: ") + e.what();
return false;
}
}
// Final write: flush any remaining nullifiers AND set the best-block / best-sapling-anchor
// pointers, so GetBestBlock()==H and GetBestAnchor(SAPLING) resolve after load.
if (!BatchWrite(mapCoins, header.baseBlockHash, uint256(), header.bestSaplingAnchor,
mapSproutAnchors, mapSaplingAnchors, mapSproutNullifiers, mapSaplingNullifiers))
{ strError = "final batch write failed"; return false; }
return true;
}
bool CBlockTreeDB::WriteBatchSync(const std::vector<std::pair<int, const CBlockFileInfo*> >& fileInfo, int nLastFile, const std::vector<CBlockIndex*>& blockinfo) {
CDBBatch batch(*this);
if (fDebug)
@@ -884,14 +657,6 @@ bool CBlockTreeDB::ReadFlag(const std::string &name, bool &fValue) const {
return true;
}
bool CBlockTreeDB::WriteAssumeutxoHeight(int nHeight) {
return Write(std::make_pair(DB_FLAG, std::string("assumeutxoheight")), nHeight);
}
bool CBlockTreeDB::ReadAssumeutxoHeight(int &nHeight) const {
return Read(std::make_pair(DB_FLAG, std::string("assumeutxoheight")), nHeight);
}
void hush_index2pubkey33(uint8_t *pubkey33,CBlockIndex *pindex,int32_t height);
bool CBlockTreeDB::blockOnchainActive(const uint256 &hash) {