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:
2026-08-02 11:07:32 -05:00
parent b3444e0a89
commit 2675b8ab93
10 changed files with 340 additions and 35 deletions

View File

@@ -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();