From e15de97be3545374351547f028f81a8e24e42ecd Mon Sep 17 00:00:00 2001 From: hermes Date: Thu, 18 Jun 2026 02:34:51 -0700 Subject: [PATCH] utxosnapshot: gate requireCheckpoint on trust source Local file snapshots (init.cpp) skip the known-checkpoint gate; P2P-delivered snapshots (bootstrap.cpp) keep it. Rationale: the checkpoint gate exists to prevent malicious peers from injecting fake UTXO sets. Local file loads come from operator-trusted sources (filesystem access already grants equal power), so the gate is unnecessary friction. --- src/bootstrap.cpp | 7 +++++-- src/init.cpp | 7 ++++++- src/utxosnapshot.cpp | 16 +++++++++++++--- src/utxosnapshot.h | 6 +++++- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/bootstrap.cpp b/src/bootstrap.cpp index 0c7bab5..188fc31 100644 --- a/src/bootstrap.cpp +++ b/src/bootstrap.cpp @@ -792,8 +792,11 @@ bool DownloadUtxoSnapshot(const std::string& host, printf("Bootstrap: UTXO snapshot downloaded, loading into database...\n"); - // Load the snapshot into a fresh active chain DB - if (!UtxoSnapshot::LoadSnapshot(tmpPath, dataDir, strError)) { + // Load the snapshot into a fresh active chain DB. P2P-delivered + // snapshots keep the checkpoint gate on (requireCheckpoint=true) — + // the manifest height+hash already passed IsKnownCheckpoint above, + // and we re-check here as defense in depth. + if (!UtxoSnapshot::LoadSnapshot(tmpPath, dataDir, strError, /*requireCheckpoint=*/true)) { fs::remove(tmpPath); return false; } diff --git a/src/init.cpp b/src/init.cpp index 83077f5..2ad9e65 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -1025,8 +1025,13 @@ bool AppInit2() printf("Found utxo-snapshot.bin — loading UTXO snapshot...\n"); uiInterface.InitMessage(_("Loading UTXO snapshot...")); + // Local file load: skip the checkpoint gate. The operator has + // filesystem access, so the trust model is already equivalent + // to direct chain state modification — a malicious local file + // is no worse than a malicious chain DB. P2P-delivered + // snapshots (SnapshotNet) keep the checkpoint gate on. std::string strError; - if (UtxoSnapshot::LoadSnapshot(snapshotFile, dataPath, strError)) { + if (UtxoSnapshot::LoadSnapshot(snapshotFile, dataPath, strError, /*requireCheckpoint=*/false)) { printf("UTXO snapshot loaded successfully.\n"); } else { printf("UTXO snapshot load failed: %s\n", strError.c_str()); diff --git a/src/utxosnapshot.cpp b/src/utxosnapshot.cpp index f931361..5f1cada 100644 --- a/src/utxosnapshot.cpp +++ b/src/utxosnapshot.cpp @@ -214,11 +214,20 @@ static bool SnapAddressFromScript(const CScript& script, int& nType, uint160& ha // --------------------------------------------------------------------------- // LoadSnapshot - load a UTXO snapshot into a fresh LevelDB +// +// `requireCheckpoint` controls whether the snapshot's tip block must be a +// known checkpoint. This gate exists to prevent malicious P2P peers from +// tricking the daemon into accepting a fake UTXO set at an arbitrary +// height on an alternate chain. Local file loads (operator already has +// filesystem access, so the trust model is the same as editing the chain +// state directly) skip the gate via requireCheckpoint=false. P2P-delivered +// snapshots (SnapshotNet) keep the gate on. // --------------------------------------------------------------------------- bool LoadSnapshot(const fs::path& snapshotPath, const fs::path& /*dataDir — unused; resolved per-backend via GetChainDataDir()*/, - std::string& strError) + std::string& strError, + bool requireCheckpoint) { FILE* file = fopen(snapshotPath.string().c_str(), "rb"); if (!file) { @@ -274,8 +283,9 @@ bool LoadSnapshot(const fs::path& snapshotPath, return false; } - // Verify snapshot block is a known checkpoint - if (!Checkpoints::IsKnownCheckpoint(height, blockHash)) { + // Verify snapshot block is a known checkpoint (only for P2P-delivered + // snapshots — local files are operator-trusted and can be at any height) + if (requireCheckpoint && !Checkpoints::IsKnownCheckpoint(height, blockHash)) { fclose(file); strError = "Snapshot block " + blockHash.ToString() + " at height " + std::to_string(height) + " is not a known checkpoint"; diff --git a/src/utxosnapshot.h b/src/utxosnapshot.h index 5fb7a1b..81b8ef7 100644 --- a/src/utxosnapshot.h +++ b/src/utxosnapshot.h @@ -29,10 +29,14 @@ namespace UtxoSnapshot { // Load a UTXO snapshot from a file into a fresh LevelDB. // Writes block index entries, UTXOs, hashBestChain, and dbformat. // The LevelDB must NOT be open yet (call before LoadBlockIndex). + // `requireCheckpoint` enforces that the snapshot tip is a known + // checkpoint (for P2P-delivered snapshots). Local loads from a + // trusted operator pass false. // Returns true on success, sets strError on failure. bool LoadSnapshot(const std::filesystem::path& snapshotPath, const std::filesystem::path& dataDir, - std::string& strError); + std::string& strError, + bool requireCheckpoint); } // namespace UtxoSnapshot