fix(startup): surface filesystem failures and verify Sapling param integrity
Three verified daemon-startup edge-case fixes centered on the config/params filesystem path: - F7: new non-throwing Platform::ensureDirectory(dir, outError) with one consistent "Cannot create <dir>: <reason>. Check permissions / free space." message. Replaces the unchecked/throwing create_directories sites at main.cpp (pre-init: log + Windows MessageBox + return 1), connection.cpp's autoDetectConfig (was the *throwing* overload -- could raise an uncaught filesystem_error through its callers; now sets the new ConnectionConfig::dir_error), and both app.cpp daemon-dir sites (surface via daemon_status_ + return false). The primary connect path (app_network.cpp) checks dir_error and shows it instead of mislabelling it "waiting for config". embedded_resources.cpp already checked its error_code, so it is left as-is. - F6: verifySaplingParams() now hash-verifies each param against its pinned canonical SHA-256 (source of truth: scripts/build-lite-backend-artifact.sh) instead of only checking existence, so a truncated / corrupt-but-present param is rejected up front rather than failing later on a shielded operation. A <params_dir>/.sapling_verified marker keyed on size:mtime avoids re-hashing ~48MB on every startup. Logic extracted to the injectable, unit-testable verifySaplingParamsIn(dir, digests); reuses util::sha256Hex (no new hash impl). - F5: startEmbeddedDaemon() now checks extractEmbeddedResources()'s return and the previously-dropped copy_file error_code in the daemon-binary fallback loop, aborting with a clear status (sb_daemon_extract_failed / sb_daemon_files_failed) instead of failing opaquely at spawn. An absent source file stays non-fatal. Adds testPlatformEnsureDirectory and testVerifySaplingParams to test_phase4.cpp. i18n keys added to i18n.cpp (English source of truth); the res/lang/*.json back-fill via add_missing_translations.py is deferred to a single run at the end of the batch. Progress tracked in docs/daemon-startup-hardening.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2478,6 +2478,105 @@ void testDaemonShutdownPolicy()
|
||||
EXPECT_TRUE(bootstrap.disconnectRpc);
|
||||
}
|
||||
|
||||
void testVerifySaplingParams()
|
||||
{
|
||||
using dragonx::rpc::Connection;
|
||||
namespace fsn = std::filesystem;
|
||||
|
||||
fsn::path dir = fsn::temp_directory_path() / "od_sapling_test";
|
||||
std::error_code rmec;
|
||||
fsn::remove_all(dir, rmec);
|
||||
fsn::create_directories(dir);
|
||||
|
||||
auto writeFile = [](const fsn::path& p, const std::string& content) {
|
||||
std::ofstream(p.string(), std::ios::binary) << content;
|
||||
};
|
||||
const std::string spendContent = "fake-spend-params-contents";
|
||||
const std::string outputContent = "fake-output-params-contents";
|
||||
writeFile(dir / "sapling-spend.params", spendContent);
|
||||
writeFile(dir / "sapling-output.params", outputContent);
|
||||
|
||||
const std::string spendHash = dragonx::util::sha256Hex(spendContent.data(), spendContent.size());
|
||||
const std::string outputHash = dragonx::util::sha256Hex(outputContent.data(), outputContent.size());
|
||||
const std::vector<std::pair<std::string, std::string>> good = {
|
||||
{ "sapling-spend.params", spendHash },
|
||||
{ "sapling-output.params", outputHash },
|
||||
};
|
||||
|
||||
// Valid params → pass, and a verification marker is written.
|
||||
EXPECT_TRUE(Connection::verifySaplingParamsIn(dir.string(), good));
|
||||
EXPECT_TRUE(fsn::exists(dir / ".sapling_verified"));
|
||||
|
||||
// Second call → marker fast-path, still true (round-trips the cache).
|
||||
EXPECT_TRUE(Connection::verifySaplingParamsIn(dir.string(), good));
|
||||
|
||||
// Wrong expected hash → integrity failure (fresh dir so no marker can short-circuit it).
|
||||
fsn::path dir2 = fsn::temp_directory_path() / "od_sapling_test2";
|
||||
fsn::remove_all(dir2, rmec);
|
||||
fsn::create_directories(dir2);
|
||||
writeFile(dir2 / "sapling-spend.params", spendContent);
|
||||
writeFile(dir2 / "sapling-output.params", outputContent);
|
||||
const std::vector<std::pair<std::string, std::string>> wrong = {
|
||||
{ "sapling-spend.params", std::string(64, 'a') },
|
||||
{ "sapling-output.params", outputHash },
|
||||
};
|
||||
EXPECT_TRUE(!Connection::verifySaplingParamsIn(dir2.string(), wrong));
|
||||
|
||||
// Truncated content (size change) invalidates the marker AND fails the hash.
|
||||
writeFile(dir / "sapling-spend.params", std::string("x"));
|
||||
EXPECT_TRUE(!Connection::verifySaplingParamsIn(dir.string(), good));
|
||||
|
||||
// A missing param → fail.
|
||||
fsn::remove(dir / "sapling-output.params", rmec);
|
||||
EXPECT_TRUE(!Connection::verifySaplingParamsIn(dir.string(), good));
|
||||
|
||||
fsn::remove_all(dir, rmec);
|
||||
fsn::remove_all(dir2, rmec);
|
||||
}
|
||||
|
||||
void testPlatformEnsureDirectory()
|
||||
{
|
||||
using dragonx::util::Platform;
|
||||
|
||||
// An existing directory → true (temp_directory_path always exists).
|
||||
{
|
||||
std::string err = "sentinel";
|
||||
EXPECT_TRUE(Platform::ensureDirectory(std::filesystem::temp_directory_path().string(), &err));
|
||||
}
|
||||
|
||||
// A fresh nested path → created, no error.
|
||||
{
|
||||
std::filesystem::path base = std::filesystem::temp_directory_path() / "od_ensuredir_test";
|
||||
std::error_code rmec; std::filesystem::remove_all(base, rmec);
|
||||
std::filesystem::path nested = base / "a" / "b" / "c";
|
||||
std::string err;
|
||||
EXPECT_TRUE(Platform::ensureDirectory(nested.string(), &err));
|
||||
EXPECT_TRUE(std::filesystem::is_directory(nested));
|
||||
EXPECT_TRUE(err.empty());
|
||||
std::filesystem::remove_all(base, rmec);
|
||||
}
|
||||
|
||||
// Empty path → false with a message.
|
||||
{
|
||||
std::string err;
|
||||
EXPECT_TRUE(!Platform::ensureDirectory("", &err));
|
||||
EXPECT_TRUE(!err.empty());
|
||||
}
|
||||
|
||||
// A path whose parent component is a regular file cannot be created. This fails the
|
||||
// same way for root and non-root, so it's a stable negative case across environments.
|
||||
{
|
||||
std::filesystem::path f = std::filesystem::temp_directory_path() / "od_ensuredir_file";
|
||||
std::error_code rmec; std::filesystem::remove_all(f, rmec);
|
||||
{ std::ofstream(f.string()) << "x"; }
|
||||
std::string err;
|
||||
bool ok = Platform::ensureDirectory((f / "child").string(), &err);
|
||||
std::filesystem::remove_all(f, rmec);
|
||||
EXPECT_TRUE(!ok);
|
||||
EXPECT_TRUE(err.find("Cannot create") != std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
void testDatadirLockGate()
|
||||
{
|
||||
using dragonx::daemon::EmbeddedDaemon;
|
||||
@@ -6643,6 +6742,8 @@ int main()
|
||||
testWalletSecurityWorkflowExecutor();
|
||||
testDaemonShutdownPolicy();
|
||||
testDatadirLockGate();
|
||||
testPlatformEnsureDirectory();
|
||||
testVerifySaplingParams();
|
||||
testDaemonLifecycleExecution();
|
||||
testDaemonLifecycleAdapters();
|
||||
testConsoleTextLayout();
|
||||
|
||||
Reference in New Issue
Block a user