fix(bootstrap): keep the archive unless a checksum truly mismatches

Any verification failure deleted the downloaded archive and forced a
full multi-GB re-download on the next attempt. But most failures are not
a corrupt archive: a missing or unreadable checksum sidecar, or a fetch
blip while pulling the checksum, would throw away a perfectly good file.
On top of that the checksum parser took the first token of whatever came
back, so an HTTP error page could be mistaken for a digest.

- The archive is now removed only on a genuine digest mismatch. A
  missing sidecar, an unusable one, or a transient fetch error keeps the
  file so the next attempt can reuse or resume it; the no-checksum case
  still fails closed and keeps the archive.
- The checksum parser now accepts a token only if it is exactly the
  expected length and all hex, lowercasing it; anything else (an error
  page, a truncated or wrong-width digest) is rejected.
- The small-file fetch moved onto the shared capped HTTP helper, and MD5
  is consulted only as a fallback when no SHA-256 is published.

Tests cover the checksum parser across good, wrong-length, non-hex, and
error-page inputs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 20:57:21 -05:00
co-authored by Claude Opus 4.8
parent b3a1a9e654
commit 4d9d528566
3 changed files with 68 additions and 118 deletions
+8 -6
View File
@@ -137,6 +137,10 @@ public:
/// stripped from each component, so "./wallet.dat" and "DRAGONX.conf." are protected too.
static bool isProtectedEntry(const std::string& name);
/// First whitespace-delimited token of a checksum file, lowercased, iff it is exactly
/// expectedHexLen hex chars; "" otherwise (an HTTP error page is never a checksum).
static std::string parseChecksumFile(const std::string& content, std::size_t expectedHexLen);
private:
mutable std::mutex mutex_;
Progress progress_;
@@ -158,9 +162,6 @@ private:
bool cleanChainData(const std::string& dataDir);
void setProgress(State state, const std::string& text, double downloaded = 0, double total = 0);
/// Download a small text file (checksum) into a string. Returns empty on failure.
std::string downloadSmallFile(const std::string& url);
/// Compute SHA-256 of a file, return lowercase hex digest.
/// pctBase/pctRange map file progress onto a portion of the overall percent.
std::string computeSHA256(const std::string& filePath, float pctBase = 0.0f, float pctRange = 100.0f);
@@ -169,9 +170,6 @@ private:
/// pctBase/pctRange map file progress onto a portion of the overall percent.
std::string computeMD5(const std::string& filePath, float pctBase = 0.0f, float pctRange = 100.0f);
/// Parse the first hex token from a checksum file (handles "<hash> <filename>" format).
static std::string parseChecksumFile(const std::string& content);
/// Verify downloaded zip against remote checksums. Returns true if valid.
bool verifyChecksums(const std::string& zipPath, const std::string& baseUrl);
@@ -188,6 +186,10 @@ private:
int last_curl_{0};
std::string download_error_;
/// Set by verifyChecksums (worker thread only): true only on a real digest MISMATCH, so the caller
/// deletes the archive only then - never for a missing/unusable sidecar or a transient fetch error.
bool last_verify_mismatch_{false};
// libcurl progress callback (static → calls into instance via clientp)
static int progressCallback(void* clientp, long long dltotal, long long dlnow,
long long ultotal, long long ulnow);