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:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user