fix(bootstrap): check archive and free space before deleting chain data
The bootstrap removed blocks/, chainstate/ and notarizations/ first and only then opened the archive. An unsafe entry, a full disk or a corrupt member therefore left no chain, or a half-written one for dragonxd to start on. - Preflight before anything is deleted: every archive entry must be safe (no absolute path, no "..") and readable, and the disk must hold the extracted size plus max(256 MiB, 5%), counting what removing the old chain frees (a symlinked chain dir counts 0). Free space counts as unknown only when the probe errors, the capacity is 0 or the available size is -1; a known disk with 0 bytes free is a refusal. The old chain is kept on every preflight failure. - Protected members are skipped, compared case-insensitively: any path component starting with "wallet", any "database" component, a .cookie or *.conf file, and top-level *.dat files other than fee_estimates, peers and banlist. The number of non-critical member failures is logged. - The removal of the old chain is verified (a locked file fails the run instead of mixing old and new chain files). If the extraction fails or is aborted, the partial chain is removed again before the failure is published; isDone() turns true only after the worker's last filesystem change. - Cancel is disabled while the old chain data is being replaced, and quit aborts a running bootstrap. - A complete zip from an earlier attempt is reused on Retry; its checksum is re-verified. A download that cannot fit fails early, and a disk-full write error says so. Tests: the space rule (boundaries, unknown vs. full disk, saturation); planArchive sizes, zip-slip, garbage and truncated zips; the protected-name list; reclaimableBytes with symlinks; each preflight refusal keeps the old chain and wallet.dat byte-identical; a corrupt critical member removes the partial chain before the failure is published. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -80,6 +80,7 @@
|
||||
#include "util/pool_registry.h"
|
||||
#include "util/daemon_updater.h"
|
||||
#include "util/http_download.h"
|
||||
#include "util/bootstrap.h"
|
||||
#include "util/background_sampler.h"
|
||||
#include "util/lite_server_probe.h"
|
||||
#include "util/wallet_file_probe.h"
|
||||
@@ -126,6 +127,8 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <sodium.h>
|
||||
#define MINIZ_NO_ZLIB_COMPATIBLE_NAMES // only the mz_zip_* API is used; keep zlib-style names out of this file
|
||||
#include <miniz.h>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
@@ -3550,6 +3553,351 @@ void testResourceNeedsWrite()
|
||||
removeTree(dir);
|
||||
}
|
||||
|
||||
// ── C222: bootstrap preflight (util/bootstrap.h) ──
|
||||
// Writes a zip with miniz. level 0 stores members uncompressed, so a test can corrupt member data in place.
|
||||
static bool writeBootstrapTestZip(const fs::path& zipPath,
|
||||
const std::vector<std::pair<std::string, std::string>>& entries,
|
||||
mz_uint level = MZ_DEFAULT_LEVEL)
|
||||
{
|
||||
mz_zip_archive zip;
|
||||
std::memset(&zip, 0, sizeof(zip));
|
||||
if (!mz_zip_writer_init_file(&zip, zipPath.string().c_str(), 0)) return false;
|
||||
bool ok = true;
|
||||
for (const auto& e : entries)
|
||||
ok = ok && mz_zip_writer_add_mem(&zip, e.first.c_str(), e.second.data(), e.second.size(), level);
|
||||
ok = mz_zip_writer_finalize_archive(&zip) && ok;
|
||||
mz_zip_writer_end(&zip);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static fs::space_info bootstrapTestSpace(std::uintmax_t avail, std::uintmax_t capacity)
|
||||
{
|
||||
fs::space_info si{};
|
||||
si.capacity = capacity;
|
||||
si.free = avail;
|
||||
si.available = avail;
|
||||
return si;
|
||||
}
|
||||
|
||||
static bool bootstrapPathAbsent(const fs::path& p)
|
||||
{
|
||||
std::error_code ec;
|
||||
return fs::symlink_status(p, ec).type() == fs::file_type::not_found;
|
||||
}
|
||||
|
||||
static bool waitBootstrapDone(const dragonx::util::Bootstrap& b, double timeoutSec)
|
||||
{
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
while (secondsSince(t0) < timeoutSec) {
|
||||
if (b.isDone()) return true;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||||
}
|
||||
return b.isDone();
|
||||
}
|
||||
|
||||
// An old chain plus a wallet in <data>: blocks/old, chainstate/old, wallet.dat.
|
||||
static void plantOldBootstrapChain(const fs::path& data, const std::string& walletBytes)
|
||||
{
|
||||
fs::create_directories(data / "blocks");
|
||||
fs::create_directories(data / "chainstate");
|
||||
{ std::ofstream(data / "blocks" / "old", std::ios::binary) << "OLD-BLOCKS"; }
|
||||
{ std::ofstream(data / "chainstate" / "old", std::ios::binary) << "OLD-CHAINSTATE"; }
|
||||
{ std::ofstream f(data / "wallet.dat", std::ios::binary); f.write(walletBytes.data(), (std::streamsize)walletBytes.size()); }
|
||||
}
|
||||
|
||||
void testBootstrapSpaceSufficient()
|
||||
{
|
||||
using dragonx::util::Bootstrap;
|
||||
const std::uint64_t MiB = 1024ull * 1024ull;
|
||||
const std::uint64_t GiB = 1024ull * MiB;
|
||||
const std::uint64_t kMax = std::numeric_limits<std::uint64_t>::max();
|
||||
std::string msg;
|
||||
|
||||
// margin = max(256 MiB, 5%): 5 GiB needs 5 GiB + 256 MiB
|
||||
EXPECT_TRUE(!Bootstrap::spaceSufficient(5 * GiB, 1 * GiB, 100 * GiB, (36 * GiB) / 10, &msg));
|
||||
EXPECT_TRUE(msg.find("need") != std::string::npos);
|
||||
EXPECT_TRUE(Bootstrap::spaceSufficient(5 * GiB, 6 * GiB, 100 * GiB, 0, nullptr));
|
||||
// A genuinely full disk (known capacity, nothing available) is a refusal, not "unknown".
|
||||
msg.clear();
|
||||
EXPECT_TRUE(!Bootstrap::spaceSufficient(5 * GiB, 0, 100 * GiB, 0, &msg));
|
||||
EXPECT_TRUE(msg.find("need") != std::string::npos);
|
||||
// Unknown: capacity 0 or avail == (uintmax_t)-1 -> fail open.
|
||||
EXPECT_TRUE(Bootstrap::spaceSufficient(5 * GiB, 0, 0, 0, nullptr));
|
||||
EXPECT_TRUE(Bootstrap::spaceSufficient(5 * GiB, kMax, 100 * GiB, 0, nullptr));
|
||||
// Exact boundary.
|
||||
EXPECT_TRUE(Bootstrap::spaceSufficient(5 * GiB, 5 * GiB + 256 * MiB, 100 * GiB, 0, nullptr));
|
||||
EXPECT_TRUE(!Bootstrap::spaceSufficient(5 * GiB, 5 * GiB + 256 * MiB - 1, 100 * GiB, 0, nullptr));
|
||||
// The old chain's bytes count toward it.
|
||||
EXPECT_TRUE(Bootstrap::spaceSufficient(5 * GiB, 2 * GiB, 100 * GiB, 3 * GiB + 256 * MiB, nullptr));
|
||||
EXPECT_TRUE(!Bootstrap::spaceSufficient(5 * GiB, 2 * GiB, 100 * GiB, 3 * GiB + 256 * MiB - 1, nullptr));
|
||||
// 256 MiB floor for a small archive; 5% above it.
|
||||
EXPECT_TRUE(!Bootstrap::spaceSufficient(1 * MiB, 256 * MiB, 100 * GiB, 0, nullptr));
|
||||
EXPECT_TRUE(Bootstrap::spaceSufficient(1 * MiB, 257 * MiB, 100 * GiB, 0, nullptr));
|
||||
EXPECT_TRUE(!Bootstrap::spaceSufficient(100 * GiB, 105 * GiB - 1, 1000 * GiB, 0, nullptr));
|
||||
EXPECT_TRUE(Bootstrap::spaceSufficient(100 * GiB, 105 * GiB, 1000 * GiB, 0, nullptr));
|
||||
// Huge values saturate instead of wrapping around to a small "need".
|
||||
EXPECT_TRUE(!Bootstrap::spaceSufficient(kMax - 1, 10 * GiB, 100 * GiB, 0, nullptr));
|
||||
EXPECT_TRUE(!Bootstrap::spaceSufficient(kMax, kMax - 1, kMax, 0, nullptr));
|
||||
EXPECT_TRUE(Bootstrap::spaceSufficient(5 * GiB, 1 * GiB, 100 * GiB, kMax, nullptr));
|
||||
}
|
||||
|
||||
void testBootstrapPlanArchive()
|
||||
{
|
||||
using dragonx::util::Bootstrap;
|
||||
const fs::path dir = makeTempDir();
|
||||
|
||||
const fs::path good = dir / "good.zip";
|
||||
EXPECT_TRUE(writeBootstrapTestZip(good, {
|
||||
{"blocks/", ""},
|
||||
{"blocks/a", std::string(1000, 'b')},
|
||||
{"chainstate/b", std::string(500, 'c')},
|
||||
{"peers.dat", std::string(19, 'p')},
|
||||
{"wallet.dat", std::string(300, 'w')},
|
||||
{"wallet-cold.dat", "cold"},
|
||||
{"blocks/sub/wallet.dat", "nested"},
|
||||
{"DRAGONX.conf", "rpcpassword=x"},
|
||||
{"database/", ""},
|
||||
{"database/log.0000000001", "bdb-log"},
|
||||
{".cookie", "__cookie__:x"},
|
||||
{"cold.dat", "another wallet file"},
|
||||
}));
|
||||
Bootstrap::ArchivePlan plan = Bootstrap::planArchive(good.string());
|
||||
EXPECT_TRUE(plan.ok);
|
||||
EXPECT_EQ(plan.bytes, (std::uint64_t)(1000 + 500 + 19)); // wallet*, *.conf, database/, .cookie, cold.dat excluded
|
||||
|
||||
const fs::path evil = dir / "evil.zip";
|
||||
EXPECT_TRUE(writeBootstrapTestZip(evil, {{"blocks/a", std::string(10, 'b')}, {"../evil", "x"}}));
|
||||
plan = Bootstrap::planArchive(evil.string());
|
||||
EXPECT_TRUE(!plan.ok);
|
||||
EXPECT_TRUE(plan.error.find("unsafe") != std::string::npos);
|
||||
|
||||
const fs::path garbage = dir / "garbage.zip";
|
||||
{ std::ofstream(garbage, std::ios::binary) << "this is not a zip archive, just some bytes"; }
|
||||
plan = Bootstrap::planArchive(garbage.string());
|
||||
EXPECT_TRUE(!plan.ok);
|
||||
EXPECT_TRUE(!plan.error.empty());
|
||||
|
||||
const fs::path truncated = dir / "truncated.zip"; // the first half: the central directory is gone
|
||||
{
|
||||
const std::string bytes = readProvisionFile(good);
|
||||
std::ofstream f(truncated, std::ios::binary);
|
||||
f.write(bytes.data(), (std::streamsize)(bytes.size() / 2));
|
||||
}
|
||||
plan = Bootstrap::planArchive(truncated.string());
|
||||
EXPECT_TRUE(!plan.ok);
|
||||
EXPECT_TRUE(!Bootstrap::planArchive((dir / "missing.zip").string()).ok);
|
||||
|
||||
for (const char* n : {"wallet.dat", "Wallet.DAT", "wallet-ip-1.dat", "wallets/w.dat", "blocks/wallet.dat",
|
||||
"database/", "database/log.1", "DRAGONX.conf", "sub/x.conf", ".cookie", "cold.dat",
|
||||
"sub\\wallet.dat"})
|
||||
EXPECT_TRUE(Bootstrap::isProtectedEntry(n) || (std::cerr << " not protected: " << n << "\n", false));
|
||||
for (const char* n : {"blocks/", "blocks/blk00000.dat", "blocks/index/000001.ldb", "chainstate/000002.ldb",
|
||||
"notarizations/x", "peers.dat", "fee_estimates.dat", "banlist.dat"})
|
||||
EXPECT_TRUE(!Bootstrap::isProtectedEntry(n) || (std::cerr << " protected: " << n << "\n", false));
|
||||
removeTree(dir);
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
void testBootstrapReclaimable()
|
||||
{
|
||||
using dragonx::util::Bootstrap;
|
||||
const fs::path dir = makeTempDir();
|
||||
fs::create_directories(dir / "blocks");
|
||||
{ std::ofstream(dir / "blocks" / "f", std::ios::binary) << std::string(1000, 'x'); }
|
||||
fs::create_directories(dir / "elsewhere");
|
||||
{ std::ofstream(dir / "elsewhere" / "g", std::ios::binary) << std::string(5000, 'y'); }
|
||||
// chainstate is a symlink to a 5000 B dir: removing it frees nothing on this filesystem.
|
||||
fs::create_directory_symlink(dir / "elsewhere", dir / "chainstate");
|
||||
// A file symlink inside blocks/ frees nothing either.
|
||||
fs::create_symlink(dir / "elsewhere" / "g", dir / "blocks" / "link");
|
||||
EXPECT_EQ(Bootstrap::reclaimableBytes(dir.string()), (std::uint64_t)1000);
|
||||
fs::create_directories(dir / "notarizations" / "sub");
|
||||
{ std::ofstream(dir / "notarizations" / "sub" / "n", std::ios::binary) << std::string(24, 'n'); }
|
||||
EXPECT_EQ(Bootstrap::reclaimableBytes(dir.string()), (std::uint64_t)1024);
|
||||
EXPECT_EQ(Bootstrap::reclaimableBytes((dir / "missing").string()), (std::uint64_t)0);
|
||||
removeTree(dir);
|
||||
}
|
||||
#endif
|
||||
|
||||
void testBootstrapPreflightKeepsOldChain()
|
||||
{
|
||||
using dragonx::util::Bootstrap;
|
||||
const auto noDaemon = [] { return false; };
|
||||
const Bootstrap::SpaceFn plenty = [](const std::string&, std::error_code& ec) {
|
||||
ec.clear();
|
||||
return bootstrapTestSpace(1ull << 40, 1ull << 41);
|
||||
};
|
||||
std::string walletBytes = "WALLET-bytes-must-survive";
|
||||
walletBytes.push_back('\0');
|
||||
walletBytes += "\x01\x02 tail";
|
||||
|
||||
// (1) An unsafe entry: refused before anything is deleted.
|
||||
{
|
||||
const fs::path dir = makeTempDir();
|
||||
const fs::path data = dir / "DRAGONX";
|
||||
plantOldBootstrapChain(data, walletBytes);
|
||||
const fs::path zip = dir / "unsafe.zip";
|
||||
EXPECT_TRUE(writeBootstrapTestZip(zip, {{"blocks/new", "NEW"}, {"../evil", "x"}}));
|
||||
Bootstrap b;
|
||||
b.startFromZip(data.string(), zip.string(), noDaemon, plenty);
|
||||
EXPECT_TRUE(waitBootstrapDone(b, 20.0));
|
||||
const Bootstrap::Progress p = b.getProgress();
|
||||
EXPECT_TRUE(p.state == Bootstrap::State::Failed);
|
||||
EXPECT_TRUE(p.error.find("unsafe") != std::string::npos);
|
||||
EXPECT_TRUE(p.cancellable);
|
||||
EXPECT_TRUE(fs::exists(data / "blocks" / "old"));
|
||||
EXPECT_TRUE(fs::exists(data / "chainstate" / "old"));
|
||||
EXPECT_TRUE(!fs::exists(data / "blocks" / "new"));
|
||||
EXPECT_TRUE(!fs::exists(dir / "evil"));
|
||||
EXPECT_TRUE(fs::exists(zip)); // the download is kept
|
||||
EXPECT_EQ(readProvisionFile(data / "wallet.dat"), walletBytes);
|
||||
removeTree(dir);
|
||||
}
|
||||
|
||||
// (2) A full disk (avail 0, capacity 100 GiB): refused, the old chain untouched.
|
||||
{
|
||||
const fs::path dir = makeTempDir();
|
||||
const fs::path data = dir / "DRAGONX";
|
||||
plantOldBootstrapChain(data, walletBytes);
|
||||
const fs::path zip = dir / "full.zip";
|
||||
EXPECT_TRUE(writeBootstrapTestZip(zip, {{"blocks/new", std::string(4096, 'n')},
|
||||
{"chainstate/new", std::string(1024, 'c')}}));
|
||||
std::atomic<int> spaceCalls{0};
|
||||
Bootstrap b;
|
||||
b.startFromZip(data.string(), zip.string(), noDaemon,
|
||||
[&spaceCalls](const std::string&, std::error_code& ec) {
|
||||
++spaceCalls;
|
||||
ec.clear();
|
||||
return bootstrapTestSpace(0, 100ull * 1024ull * 1024ull * 1024ull);
|
||||
});
|
||||
EXPECT_TRUE(waitBootstrapDone(b, 20.0));
|
||||
const Bootstrap::Progress p = b.getProgress();
|
||||
EXPECT_TRUE(p.state == Bootstrap::State::Failed);
|
||||
EXPECT_TRUE(p.error.find("need") != std::string::npos);
|
||||
EXPECT_TRUE(p.cancellable);
|
||||
EXPECT_EQ(spaceCalls.load(), 1);
|
||||
EXPECT_TRUE(fs::exists(data / "blocks" / "old"));
|
||||
EXPECT_TRUE(fs::exists(data / "chainstate" / "old"));
|
||||
EXPECT_TRUE(!fs::exists(data / "blocks" / "new"));
|
||||
EXPECT_TRUE(fs::exists(zip));
|
||||
EXPECT_EQ(readProvisionFile(data / "wallet.dat"), walletBytes);
|
||||
removeTree(dir);
|
||||
}
|
||||
|
||||
// (3) A corrupt critical member: the partial chain is removed BEFORE the failure is published.
|
||||
{
|
||||
const fs::path dir = makeTempDir();
|
||||
const fs::path data = dir / "DRAGONX";
|
||||
plantOldBootstrapChain(data, walletBytes);
|
||||
const fs::path zip = dir / "corrupt.zip";
|
||||
EXPECT_TRUE(writeBootstrapTestZip(zip, {{"chainstate/y", std::string(2048, 'y')},
|
||||
{"blocks/x", std::string(4096, 'x')},
|
||||
{"blocks/z", std::string(64, 'z')}},
|
||||
MZ_NO_COMPRESSION));
|
||||
// Flip one byte of blocks/x's stored data (its CRC no longer matches).
|
||||
std::uint64_t headerOfs = 0;
|
||||
{
|
||||
mz_zip_archive zr;
|
||||
std::memset(&zr, 0, sizeof(zr));
|
||||
EXPECT_TRUE(mz_zip_reader_init_file(&zr, zip.string().c_str(), 0));
|
||||
const int idx = mz_zip_reader_locate_file(&zr, "blocks/x", nullptr, 0);
|
||||
mz_zip_archive_file_stat st;
|
||||
EXPECT_TRUE(idx >= 0 && mz_zip_reader_file_stat(&zr, (mz_uint)idx, &st));
|
||||
if (idx >= 0) headerOfs = st.m_local_header_ofs;
|
||||
mz_zip_reader_end(&zr);
|
||||
}
|
||||
{
|
||||
std::fstream f(zip, std::ios::in | std::ios::out | std::ios::binary);
|
||||
unsigned char lh[30] = {};
|
||||
f.seekg((std::streamoff)headerOfs);
|
||||
f.read(reinterpret_cast<char*>(lh), sizeof(lh));
|
||||
const std::uint64_t nameLen = lh[26] | (lh[27] << 8);
|
||||
const std::uint64_t extraLen = lh[28] | (lh[29] << 8);
|
||||
const std::uint64_t dataOfs = headerOfs + 30 + nameLen + extraLen + 100;
|
||||
f.seekp((std::streamoff)dataOfs);
|
||||
f.put('X');
|
||||
}
|
||||
Bootstrap b;
|
||||
b.startFromZip(data.string(), zip.string(), noDaemon, plenty);
|
||||
bool sawDone = false, chainGoneAtDone = false;
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
while (secondsSince(t0) < 20.0) { // tight poll: the first "done" must already see the clean slate
|
||||
if (b.isDone()) {
|
||||
chainGoneAtDone = bootstrapPathAbsent(data / "blocks") && bootstrapPathAbsent(data / "chainstate");
|
||||
sawDone = true;
|
||||
break;
|
||||
}
|
||||
std::this_thread::yield();
|
||||
}
|
||||
EXPECT_TRUE(sawDone);
|
||||
EXPECT_TRUE(chainGoneAtDone);
|
||||
const Bootstrap::Progress p = b.getProgress();
|
||||
EXPECT_TRUE(p.state == Bootstrap::State::Failed);
|
||||
EXPECT_EQ(p.error, std::string("Failed to extract critical file: blocks/x"));
|
||||
EXPECT_TRUE(!p.cancellable);
|
||||
EXPECT_TRUE(bootstrapPathAbsent(data / "blocks"));
|
||||
EXPECT_TRUE(bootstrapPathAbsent(data / "chainstate"));
|
||||
EXPECT_TRUE(fs::exists(zip)); // kept for a retry
|
||||
EXPECT_EQ(readProvisionFile(data / "wallet.dat"), walletBytes);
|
||||
removeTree(dir);
|
||||
}
|
||||
|
||||
// (4) Success: not cancellable at the end, wallet.dat byte-identical, protected members never written.
|
||||
{
|
||||
const fs::path dir = makeTempDir();
|
||||
const fs::path data = dir / "DRAGONX";
|
||||
plantOldBootstrapChain(data, walletBytes);
|
||||
const fs::path zip = dir / "good.zip";
|
||||
EXPECT_TRUE(writeBootstrapTestZip(zip, {{"blocks/", ""},
|
||||
{"blocks/blk00000.dat", std::string(3000, 'B')},
|
||||
{"chainstate/000001.ldb", std::string(1500, 'C')},
|
||||
{"peers.dat", "peers"},
|
||||
{"wallet.dat", "EVIL-WALLET"},
|
||||
{"wallet-cold.dat", "EVIL-COLD"},
|
||||
{"DRAGONX.conf", "rpcpassword=evil"},
|
||||
{"database/log.0000000001", "evil"},
|
||||
{".cookie", "evil"}}));
|
||||
Bootstrap b;
|
||||
EXPECT_TRUE(b.getProgress().cancellable);
|
||||
b.startFromZip(data.string(), zip.string(), noDaemon, plenty);
|
||||
EXPECT_TRUE(waitBootstrapDone(b, 20.0));
|
||||
const Bootstrap::Progress p = b.getProgress();
|
||||
EXPECT_TRUE(p.state == Bootstrap::State::Completed);
|
||||
EXPECT_TRUE(!p.cancellable);
|
||||
EXPECT_TRUE(!fs::exists(data / "blocks" / "old"));
|
||||
EXPECT_TRUE(!fs::exists(data / "chainstate" / "old"));
|
||||
EXPECT_EQ(readProvisionFile(data / "blocks" / "blk00000.dat"), std::string(3000, 'B'));
|
||||
EXPECT_TRUE(fs::exists(data / "chainstate" / "000001.ldb"));
|
||||
EXPECT_TRUE(fs::exists(data / "peers.dat"));
|
||||
EXPECT_EQ(readProvisionFile(data / "wallet.dat"), walletBytes);
|
||||
EXPECT_TRUE(!fs::exists(data / "wallet-cold.dat"));
|
||||
EXPECT_TRUE(!fs::exists(data / "DRAGONX.conf"));
|
||||
EXPECT_TRUE(!fs::exists(data / "database"));
|
||||
EXPECT_TRUE(!fs::exists(data / ".cookie"));
|
||||
EXPECT_TRUE(!fs::exists(zip)); // removed after a successful extraction
|
||||
removeTree(dir);
|
||||
}
|
||||
|
||||
// (5) Unknown free space (the probe fails): fails open, as before.
|
||||
{
|
||||
const fs::path dir = makeTempDir();
|
||||
const fs::path data = dir / "DRAGONX";
|
||||
plantOldBootstrapChain(data, walletBytes);
|
||||
const fs::path zip = dir / "unknown.zip";
|
||||
EXPECT_TRUE(writeBootstrapTestZip(zip, {{"blocks/new", "NEW"}, {"chainstate/new", "NEW"}}));
|
||||
Bootstrap b;
|
||||
b.startFromZip(data.string(), zip.string(), noDaemon, [](const std::string&, std::error_code& ec) {
|
||||
ec = std::make_error_code(std::errc::io_error);
|
||||
return bootstrapTestSpace(static_cast<std::uintmax_t>(-1), static_cast<std::uintmax_t>(-1));
|
||||
});
|
||||
EXPECT_TRUE(waitBootstrapDone(b, 20.0));
|
||||
EXPECT_TRUE(b.getProgress().state == Bootstrap::State::Completed);
|
||||
EXPECT_TRUE(fs::exists(data / "blocks" / "new"));
|
||||
EXPECT_EQ(readProvisionFile(data / "wallet.dat"), walletBytes);
|
||||
removeTree(dir);
|
||||
}
|
||||
}
|
||||
|
||||
void testWriteFileAtomic()
|
||||
{
|
||||
using namespace dragonx::resources;
|
||||
@@ -16315,6 +16663,12 @@ int main()
|
||||
testVerifySaplingParams();
|
||||
testStartupProvisionState();
|
||||
testResourceNeedsWrite();
|
||||
testBootstrapSpaceSufficient();
|
||||
testBootstrapPlanArchive();
|
||||
#ifndef _WIN32
|
||||
testBootstrapReclaimable();
|
||||
#endif
|
||||
testBootstrapPreflightKeepsOldChain();
|
||||
testWriteFileAtomic();
|
||||
testCopyFileAtomic();
|
||||
testBundleCopy();
|
||||
|
||||
Reference in New Issue
Block a user