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:27:34 -05:00
parent 1f2b109d95
commit 84aefb5475
9 changed files with 2 additions and 703 deletions

View File

@@ -7,11 +7,10 @@ bin_PROGRAMS += hush-gtest
# NOTE: the original test list used an invalid automake form (comment after a trailing
# backslash, and `zcash_gtest_SOURCES +=` with no prior `=`), which is why the whole
# gtest harness was disabled via a `#include`. Minimal valid set: the harness + the
# UTXO-snapshot round-trip test. Re-add other gtest sources here as they are revived.
# Re-add other gtest sources here as they are revived.
hush_gtest_SOURCES = \
gtest/main.cpp \
gtest/utils.cpp \
gtest/test_utxosnapshot.cpp \
gtest/test_randomx_preverify.cpp
hush_gtest_CPPFLAGS = $(AM_CPPFLAGS) -DMULTICORE -fopenmp -DBINARY_OUTPUT -DCURVE_ALT_BN128 -DSTATIC $(BITCOIN_INCLUDES)

View File

@@ -69,17 +69,6 @@ public:
double fTransactionsPerDay;
};
/** Trusted UTXO-snapshot (assumeutxo-style) anchor. When `hash` is set, a node loading a
* snapshot via -loadutxosnapshot must produce exactly this content hash at this height,
* otherwise the snapshot is refused. Null hash = not configured (loading requires the
* explicit -loadutxosnapshotunsafe override, e.g. for regtest/testing). Mirrors the
* hardcoded-checkpoint trust model. */
struct AssumeutxoData {
int height;
uint256 hash;
bool IsNull() const { return hash.IsNull(); }
};
enum Bech32Type {
SAPLING_PAYMENT_ADDRESS,
SAPLING_FULL_VIEWING_KEY,
@@ -116,7 +105,6 @@ public:
const std::string& Bech32HRP(Bech32Type type) const { return bech32HRPs[type]; }
const std::vector<uint8_t>& FixedSeeds() const { return vFixedSeeds; }
const CCheckpointData& Checkpoints() const { return checkpointData; }
const AssumeutxoData& Assumeutxo() const { return assumeutxoData; }
/** Return the founder's reward address and script for a given block height */
std::string GetFoundersRewardAddressAtHeight(int height) const;
CScript GetFoundersRewardScriptAtHeight(int height) const;
@@ -156,7 +144,6 @@ protected:
bool fMineBlocksOnDemand = false;
bool fTestnetToBeDeprecatedFieldRPC = false;
CCheckpointData checkpointData;
AssumeutxoData assumeutxoData; // null by default; set per-network in chainparams.cpp once a snapshot hash is published
std::vector<std::string> vFoundersRewardAddress;
};

View File

@@ -1,203 +0,0 @@
// Copyright (c) 2024-2026 The DragonX developers
// Distributed under the GPLv3 software license, see the accompanying
// file COPYING or https://www.gnu.org/licenses/gpl-3.0.en.html
//
// Round-trip tests for the trusted UTXO snapshot (assumeutxo-style) dump/load core
// (CCoinsViewDB::DumpSnapshot / LoadSnapshot). This exercises the highest-risk part of
// the feature in isolation: that coins, Sapling commitment trees, the nullifier set, the
// best block and the best Sapling anchor survive a serialize -> hash -> deserialize cycle
// exactly, and that integrity/trust verification rejects tampered or wrong-hash snapshots.
#include <gtest/gtest.h>
#include <boost/filesystem.hpp>
#include "chainparams.h"
#include "coins.h"
#include "txdb.h"
#include "script/script.h"
#include "uint256.h"
#include "zcash/IncrementalMerkleTree.hpp"
namespace {
// Populate an in-memory chainstate DB directly via BatchWrite (mirrors how blocks persist
// coins/anchors/nullifiers), so DumpSnapshot has a realistic mixed state to serialize.
void PopulateChainstate(CCoinsViewDB &db, const uint256 &bestBlock,
uint256 &anchorRootOut, const uint256 &nullifierIn)
{
// One unspent transparent output.
CCoinsMap mapCoins;
{
uint256 txid = uint256S("0xaa00000000000000000000000000000000000000000000000000000000000001");
CCoinsCacheEntry &e = mapCoins[txid];
e.coins.fCoinBase = false;
e.coins.nVersion = 1;
e.coins.nHeight = 100;
e.coins.vout.resize(1);
e.coins.vout[0].nValue = 12345;
e.coins.vout[0].scriptPubKey = CScript() << OP_TRUE;
e.flags = CCoinsCacheEntry::DIRTY;
}
// One Sapling commitment tree (anchor), keyed by its root.
SaplingMerkleTree tree;
tree.append(uint256S("0xbb00000000000000000000000000000000000000000000000000000000000002"));
anchorRootOut = tree.root();
CAnchorsSaplingMap mapSaplingAnchors;
{
CAnchorsSaplingCacheEntry &e = mapSaplingAnchors[anchorRootOut];
e.entered = true;
e.tree = tree;
e.flags = CAnchorsSaplingCacheEntry::DIRTY;
}
// One spent Sapling nullifier.
CNullifiersMap mapSaplingNullifiers;
{
CNullifiersCacheEntry &e = mapSaplingNullifiers[nullifierIn];
e.entered = true;
e.flags = CNullifiersCacheEntry::DIRTY;
}
CAnchorsSproutMap mapSproutAnchors; // empty
CNullifiersMap mapSproutNullifiers; // empty
ASSERT_TRUE(db.BatchWrite(mapCoins, bestBlock, uint256(), anchorRootOut,
mapSproutAnchors, mapSaplingAnchors, mapSproutNullifiers, mapSaplingNullifiers));
}
CUTXOSnapshotHeader MakeHeader(const uint256 &bestBlock, const uint256 &bestAnchor)
{
CUTXOSnapshotHeader h;
h.nMagic = UTXO_SNAPSHOT_MAGIC;
h.nVersion = UTXO_SNAPSHOT_VERSION;
memcpy(&h.nNetworkMagic, Params().MessageStart(), 4);
h.baseBlockHash = bestBlock;
h.nHeight = 100;
h.nChainTx = 1;
h.fHasChainSaplingValue = 1;
h.nChainSaplingValue = 999;
h.bestSaplingAnchor = bestAnchor;
return h;
}
} // namespace
TEST(UTXOSnapshot, RoundTripPreservesChainstate)
{
SelectParams(CBaseChainParams::REGTEST);
const uint256 bestBlock = uint256S("0xff00000000000000000000000000000000000000000000000000000000000009");
const uint256 nullifier = uint256S("0xcc00000000000000000000000000000000000000000000000000000000000003");
CCoinsViewDB src(1 << 20, true); // in-memory
uint256 anchorRoot;
PopulateChainstate(src, bestBlock, anchorRoot, nullifier);
boost::filesystem::path path = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path();
CUTXOSnapshotHeader header = MakeHeader(bestBlock, anchorRoot);
uint256 dumpHash; std::string err;
ASSERT_TRUE(src.DumpSnapshot(path.string(), header, dumpHash, err)) << err;
EXPECT_EQ(header.nCoins, 1u);
EXPECT_EQ(header.nSaplingAnchors, 1u);
EXPECT_EQ(header.nSaplingNullifiers, 1u);
// Load into a fresh in-memory DB (integrity check only, no trust hash).
CCoinsViewDB dst(1 << 20, true);
CUTXOSnapshotHeader loadedHeader; uint256 loadHash;
ASSERT_TRUE(dst.LoadSnapshot(path.string(), uint256(), /*fRequireExpected=*/false, loadedHeader, loadHash, err)) << err;
// Hash is deterministic across dump and load.
EXPECT_EQ(dumpHash, loadHash);
EXPECT_EQ(loadedHeader.nHeight, 100);
EXPECT_EQ(loadedHeader.baseBlockHash, bestBlock);
// Best block round-trips.
EXPECT_EQ(dst.GetBestBlock(), bestBlock);
// Coins round-trip: the stored UTXO must come back intact. (We check the specific coin
// directly rather than via GetStats(), which dereferences mapBlockIndex for the best block
// — not populated in this pure unit test.) The full-content equivalence is already proven
// by dumpHash == loadHash above.
const uint256 txid = uint256S("0xaa00000000000000000000000000000000000000000000000000000000000001");
CCoins c1, c2;
ASSERT_TRUE(src.GetCoins(txid, c1));
ASSERT_TRUE(dst.GetCoins(txid, c2));
ASSERT_EQ(c2.vout.size(), 1u);
EXPECT_EQ(c2.vout[0].nValue, c1.vout[0].nValue);
EXPECT_TRUE(c2.vout[0].scriptPubKey == c1.vout[0].scriptPubKey);
// Sapling anchor (commitment tree) round-trips byte-exactly: the recovered tree's root
// must equal the key it was stored under (this is the invariant ConnectBlock relies on).
SaplingMerkleTree recovered;
ASSERT_TRUE(dst.GetSaplingAnchorAt(anchorRoot, recovered));
EXPECT_EQ(recovered.root(), anchorRoot);
EXPECT_EQ(dst.GetBestAnchor(SAPLING), anchorRoot);
// Nullifier set round-trips.
EXPECT_TRUE(dst.GetNullifier(nullifier, SAPLING));
EXPECT_FALSE(dst.GetNullifier(uint256S("0xdead"), SAPLING));
boost::filesystem::remove(path);
}
TEST(UTXOSnapshot, RejectsTrustHashMismatch)
{
SelectParams(CBaseChainParams::REGTEST);
const uint256 bestBlock = uint256S("0xff0000000000000000000000000000000000000000000000000000000000000a");
const uint256 nullifier = uint256S("0xcc0000000000000000000000000000000000000000000000000000000000000b");
CCoinsViewDB src(1 << 20, true);
uint256 anchorRoot;
PopulateChainstate(src, bestBlock, anchorRoot, nullifier);
boost::filesystem::path path = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path();
CUTXOSnapshotHeader header = MakeHeader(bestBlock, anchorRoot);
uint256 dumpHash; std::string err;
ASSERT_TRUE(src.DumpSnapshot(path.string(), header, dumpHash, err)) << err;
// A wrong "trusted" hash must be refused.
CCoinsViewDB dst(1 << 20, true);
CUTXOSnapshotHeader h2; uint256 hh;
uint256 wrong = uint256S("0x1234");
EXPECT_FALSE(dst.LoadSnapshot(path.string(), wrong, /*fRequireExpected=*/true, h2, hh, err));
// The correct hash must pass.
EXPECT_TRUE(dst.LoadSnapshot(path.string(), dumpHash, /*fRequireExpected=*/true, h2, hh, err)) << err;
boost::filesystem::remove(path);
}
TEST(UTXOSnapshot, RejectsCorruptedFile)
{
SelectParams(CBaseChainParams::REGTEST);
const uint256 bestBlock = uint256S("0xff0000000000000000000000000000000000000000000000000000000000000c");
const uint256 nullifier = uint256S("0xcc0000000000000000000000000000000000000000000000000000000000000d");
CCoinsViewDB src(1 << 20, true);
uint256 anchorRoot;
PopulateChainstate(src, bestBlock, anchorRoot, nullifier);
boost::filesystem::path path = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path();
CUTXOSnapshotHeader header = MakeHeader(bestBlock, anchorRoot);
uint256 dumpHash; std::string err;
ASSERT_TRUE(src.DumpSnapshot(path.string(), header, dumpHash, err)) << err;
// Flip a byte near the end (inside the coins/anchor payload, before the trailing hash).
{
boost::filesystem::fstream f(path, std::ios::in | std::ios::out | std::ios::binary);
f.seekg(0, std::ios::end);
std::streamoff sz = f.tellg();
ASSERT_GT(sz, 40);
f.seekg(sz - 40);
char c; f.read(&c, 1);
f.seekp(sz - 40);
c = (char)(c ^ 0xff);
f.write(&c, 1);
}
CCoinsViewDB dst(1 << 20, true);
CUTXOSnapshotHeader h2; uint256 hh;
EXPECT_FALSE(dst.LoadSnapshot(path.string(), uint256(), /*fRequireExpected=*/false, h2, hh, err));
boost::filesystem::remove(path);
}

View File

@@ -390,8 +390,6 @@ std::string HelpMessage(HelpMessageMode mode)
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: 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"));
@@ -2099,44 +2097,6 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
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)) {

View File

@@ -85,7 +85,6 @@ 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;
@@ -4201,45 +4200,6 @@ 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.
@@ -4288,15 +4248,6 @@ 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,
@@ -6101,15 +6052,6 @@ 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");

View File

@@ -956,19 +956,10 @@ extern CChain chainActive;
/** Global variable that points to the active CCoinsView (protected by cs_main) */
extern CCoinsViewCache *pcoinsTip;
/** Global variable that points to the coins database (chainstate/, protected by cs_main).
* Exposed for the UTXO-snapshot (assumeutxo-style) dump/load paths. */
/** Global variable that points to the coins database (chainstate/, protected by cs_main). */
class CCoinsViewDB;
extern CCoinsViewDB *pcoinsdbview;
/** Activate a trusted UTXO snapshot (already written to the chainstate DB by LoadSnapshot) as the
* chain tip at its height H, without replaying blocks 0..H. Headers for H must already exist. */
struct CUTXOSnapshotHeader;
bool LoadSnapshotChainstate(const CUTXOSnapshotHeader& header, std::string& strError);
/** Height H of a loaded UTXO snapshot (assumeutxo). Reorgs whose fork point is below H are refused
* because the node has no block/undo data for 0..H. -1 means no snapshot is in effect. */
extern int nAssumeutxoSnapshotHeight;
/** Global variable that points to the active block tree (protected by cs_main) */
extern CBlockTreeDB *pblocktree;

View File

@@ -862,76 +862,6 @@ UniValue gettxoutsetinfo(const UniValue& params, bool fHelp, const CPubKey& mypk
return ret;
}
UniValue dumptxoutset(const UniValue& params, bool fHelp, const CPubKey& mypk)
{
if (fHelp || params.size() != 1)
throw runtime_error(
"dumptxoutset \"path\"\n"
"\nWrite a trusted snapshot of the current chainstate (UTXO set + Sapling commitment\n"
"trees, nullifier set and pool value) to disk. The snapshot can be loaded by a fresh\n"
"node with -loadutxosnapshot=<file> to skip replaying the chain from genesis.\n"
"\nThis is intended to be run at a final/checkpoint height; the node must be fully synced.\n"
"\nArguments:\n"
"1. \"path\" (string, required) path to write the snapshot file (must not already exist)\n"
"\nResult:\n"
"{\n"
" \"height\": n, (numeric) snapshot height H\n"
" \"base_hash\": \"hex\", (string) block hash at height H\n"
" \"snapshot_hash\": \"hex\", (string) content hash to hardcode for verification\n"
" \"coins\": n, (numeric) number of UTXO records\n"
" \"sapling_anchors\": n, (numeric) number of Sapling anchor records\n"
" \"sapling_nullifiers\": n, (numeric) number of Sapling nullifier records\n"
" \"path\": \"...\" (string) the file written\n"
"}\n"
"\nExamples:\n"
+ HelpExampleCli("dumptxoutset", "/path/to/dragonx-utxo.dat")
+ HelpExampleRpc("dumptxoutset", "\"/path/to/dragonx-utxo.dat\"")
);
boost::filesystem::path path = boost::filesystem::absolute(params[0].get_str());
if (boost::filesystem::exists(path))
throw JSONRPCError(RPC_INVALID_PARAMETER, "path already exists, refusing to overwrite: " + path.string());
LOCK(cs_main);
if (pcoinsdbview == nullptr || pcoinsTip == nullptr)
throw JSONRPCError(RPC_INTERNAL_ERROR, "chainstate not available");
// Flush so the on-disk chainstate matches the in-memory tip before we iterate it.
FlushStateToDisk();
CBlockIndex *tip = chainActive.Tip();
if (tip == nullptr)
throw JSONRPCError(RPC_INTERNAL_ERROR, "no chain tip");
CUTXOSnapshotHeader header;
header.nMagic = UTXO_SNAPSHOT_MAGIC;
header.nVersion = UTXO_SNAPSHOT_VERSION;
memcpy(&header.nNetworkMagic, Params().MessageStart(), 4);
header.baseBlockHash = tip->GetBlockHash();
header.nHeight = tip->GetHeight();
header.nChainTx = tip->nChainTx;
if (tip->nChainSaplingValue) {
header.fHasChainSaplingValue = 1;
header.nChainSaplingValue = *tip->nChainSaplingValue;
}
header.bestSaplingAnchor = pcoinsdbview->GetBestAnchor(SAPLING);
uint256 snapshotHash;
std::string strError;
if (!pcoinsdbview->DumpSnapshot(path.string(), header, snapshotHash, strError))
throw JSONRPCError(RPC_INTERNAL_ERROR, "dumptxoutset failed: " + strError);
UniValue ret(UniValue::VOBJ);
ret.push_back(Pair("height", (int64_t)header.nHeight));
ret.push_back(Pair("base_hash", header.baseBlockHash.GetHex()));
ret.push_back(Pair("snapshot_hash", snapshotHash.GetHex()));
ret.push_back(Pair("coins", (int64_t)header.nCoins));
ret.push_back(Pair("sapling_anchors", (int64_t)header.nSaplingAnchors));
ret.push_back(Pair("sapling_nullifiers", (int64_t)header.nSaplingNullifiers));
ret.push_back(Pair("path", path.string()));
return ret;
}
UniValue getblockmerkletree(const UniValue& params, bool fHelp, const CPubKey& mypk)
{
@@ -1924,7 +1854,6 @@ static const CRPCCommand commands[] =
{ "blockchain", "getrawmempool", &getrawmempool, true },
{ "blockchain", "gettxout", &gettxout, true },
{ "blockchain", "gettxoutsetinfo", &gettxoutsetinfo, true },
{ "blockchain", "dumptxoutset", &dumptxoutset, true },
{ "blockchain", "verifychain", &verifychain, true },
/* Not shown in help */

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) {

View File

@@ -56,61 +56,6 @@ static const int64_t nMaxDbCache = sizeof(void*) > 4 ? 16384 : 1024;
//! min. -dbcache in (MiB)
static const int64_t nMinDbCache = 4;
/** Magic + version for the trusted UTXO-snapshot (assumeutxo-style) file format. */
static const uint32_t UTXO_SNAPSHOT_MAGIC = 0x58535844; // 'DXSX'
static const uint8_t UTXO_SNAPSHOT_VERSION = 1;
/**
* Header of a trusted chainstate snapshot taken at a final height H. On this private
* chain the chainstate is more than transparent UTXOs, so the snapshot also carries the
* Sapling commitment trees, the nullifier set, the best Sapling anchor and the pool value.
*
* File layout: [CUTXOSnapshotHeader]
* nCoins × (uint256 txid, CCoins)
* nSaplingAnchors × (uint256 root, SaplingMerkleTree)
* nSaplingNullifiers × (uint256 nullifier)
* uint256 contentHash // hash over everything above (NOT itself)
*/
struct CUTXOSnapshotHeader
{
uint32_t nMagic;
uint8_t nVersion;
uint32_t nNetworkMagic; // Params().MessageStart() as uint32 — prevents cross-network use
uint256 baseBlockHash; // hash of block H (the snapshot tip)
int32_t nHeight; // H
uint64_t nChainTx; // cumulative tx count at H (needed for tip fix-up)
uint8_t fHasChainSaplingValue;
int64_t nChainSaplingValue; // cumulative Sapling pool value at H (valid iff fHasChainSaplingValue)
uint256 bestSaplingAnchor; // best Sapling anchor root at H
uint64_t nCoins;
uint64_t nSaplingAnchors;
uint64_t nSaplingNullifiers;
CUTXOSnapshotHeader() { SetNull(); }
void SetNull() {
nMagic = 0; nVersion = 0; nNetworkMagic = 0; baseBlockHash.SetNull();
nHeight = 0; nChainTx = 0; fHasChainSaplingValue = 0; nChainSaplingValue = 0;
bestSaplingAnchor.SetNull(); nCoins = 0; nSaplingAnchors = 0; nSaplingNullifiers = 0;
}
ADD_SERIALIZE_METHODS;
template <typename Stream, typename Operation>
inline void SerializationOp(Stream& s, Operation ser_action) {
READWRITE(nMagic);
READWRITE(nVersion);
READWRITE(nNetworkMagic);
READWRITE(baseBlockHash);
READWRITE(nHeight);
READWRITE(nChainTx);
READWRITE(fHasChainSaplingValue);
READWRITE(nChainSaplingValue);
READWRITE(bestSaplingAnchor);
READWRITE(nCoins);
READWRITE(nSaplingAnchors);
READWRITE(nSaplingNullifiers);
}
};
/** CCoinsView backed by the coin database (chainstate/) */
class CCoinsViewDB : public CCoinsView
{
@@ -136,19 +81,6 @@ public:
CNullifiersMap &mapSproutNullifiers,
CNullifiersMap &mapSaplingNullifiers);
bool GetStats(CCoinsStats &stats) const;
//! Stream the full chainstate at the current tip into a snapshot file (assumeutxo-style
//! producer). Caller fills the metadata fields of `header` (height, baseBlockHash, nChainTx,
//! pool value, bestSaplingAnchor); this fills the counts, writes the file, and returns the
//! content hash. Caller must hold cs_main and have flushed the cache to disk first.
bool DumpSnapshot(const std::string &path, CUTXOSnapshotHeader &header, uint256 &hashRet, std::string &strError) const;
//! Load a snapshot file produced by DumpSnapshot into the (empty) chainstate DB. Two passes:
//! pass 1 reads everything and verifies the internal content hash (and, if fRequireExpected,
//! that it equals expectedHash) WITHOUT touching the DB; pass 2 writes coins/anchors/nullifiers
//! plus the best-block / best-sapling-anchor pointers. Returns the header + computed hash.
bool LoadSnapshot(const std::string &path, const uint256 &expectedHash, bool fRequireExpected,
CUTXOSnapshotHeader &headerRet, uint256 &hashRet, std::string &strError);
};
/** Access to the block database (blocks/index/) */
@@ -185,9 +117,6 @@ public:
bool ReadTimestampBlockIndex(const uint256 &hash, unsigned int &logicalTS) const;
bool WriteFlag(const std::string &name, bool fValue);
bool ReadFlag(const std::string &name, bool &fValue) const;
//! Persist/restore the height of a loaded UTXO snapshot so the reorg-below-H guard survives restarts.
bool WriteAssumeutxoHeight(int nHeight);
bool ReadAssumeutxoHeight(int &nHeight) const;
bool LoadBlockIndexGuts();
bool blockOnchainActive(const uint256 &hash);
UniValue Snapshot(int top);