feat(lite): runtime kill-switch + staged-rollout gate (M5b)
Adds a fail-open, local-only gate that decides whether the lite wallet may run,
so a post-release issue can disable it and rollout can be staged — without any
phone-home (privacy posture: no runtime network fetch; the per-install rollout
bucket is a hashed, never-transmitted local id).
- wallet/lite_rollout_policy.{h,cpp}: a pure decision core. Order — emergency env
kill-switch (absolute) -> local override -> manifest gates (global enable /
version floor-ceiling / blocklist / staged-rollout permille) -> fail-open allow.
Plus a JSON manifest loader (missing/invalid -> fail-open) and FNV-1a bucketing.
- Threads the decision through LiteWalletController -> LiteWalletLifecycleService:
new availability() reason RolloutDisabled blocks create/open/restore and surfaces
the gate's user-facing message via the lifecycle status.
- App::rebuildLiteWallet() resolves it from: DRAGONX_LITE_KILL_SWITCH (env), the
lite_rollout setting (auto/force_on/force_off), and a locally-cached manifest at
<config-dir>/lite_rollout.json. install id generated once via libsodium.
- Settings: persist lite_rollout override + the install id.
A signed remote fetcher can populate the manifest cache later without touching the
policy. Unit-tested (version compare, bucketing, override/env precedence, manifest
gates, staged rollout, loader fail-open, controller integration) and runtime-verified
on Linux (env kill-switch, manifest disable, control sync). Both variants build;
full suite passes; hygiene clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
61
src/app.cpp
61
src/app.cpp
@@ -13,7 +13,9 @@
|
||||
#include "config/settings.h"
|
||||
#include "wallet/lite_wallet_controller.h"
|
||||
#include "wallet/lite_wallet_server_selection_adapter.h"
|
||||
#include "wallet/lite_rollout_policy.h"
|
||||
|
||||
#include <cstdlib> // std::getenv for the lite kill-switch env var
|
||||
#include <sodium.h> // sodium_memzero for the lite unlock-passphrase buffer
|
||||
#include "daemon/daemon_controller.h"
|
||||
#include "daemon/embedded_daemon.h"
|
||||
@@ -370,6 +372,62 @@ void App::preFrame()
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Resolve the lite-wallet rollout / kill-switch decision (see wallet/lite_rollout_policy.h).
|
||||
// Local-only: reads the override + a stable per-install id from settings, an emergency env var,
|
||||
// and a locally-cached manifest file (NO network fetch). Fail-open: a missing/invalid manifest
|
||||
// leaves the wallet enabled.
|
||||
wallet::LiteRolloutDecision resolveLiteRolloutDecision(config::Settings& settings)
|
||||
{
|
||||
using namespace dragonx::wallet;
|
||||
LiteRolloutInputs inputs;
|
||||
inputs.appVersion = DRAGONX_VERSION;
|
||||
|
||||
// Emergency kill-switch env var: any value other than empty/"0"/"false" disables.
|
||||
if (const char* env = std::getenv("DRAGONX_LITE_KILL_SWITCH")) {
|
||||
const std::string v = env;
|
||||
inputs.killSwitchEnv = !v.empty() && v != "0" && v != "false";
|
||||
}
|
||||
|
||||
inputs.override = liteRolloutOverrideFromString(settings.getLiteRolloutOverride());
|
||||
|
||||
// Stable per-install bucket source — generated once, persisted, never transmitted, no PII.
|
||||
std::string installId = settings.getLiteInstallId();
|
||||
if (installId.empty()) {
|
||||
if (sodium_init() >= 0) {
|
||||
unsigned char buf[16];
|
||||
randombytes_buf(buf, sizeof(buf));
|
||||
static const char kHex[] = "0123456789abcdef";
|
||||
installId.reserve(sizeof(buf) * 2);
|
||||
for (unsigned char c : buf) {
|
||||
installId.push_back(kHex[c >> 4]);
|
||||
installId.push_back(kHex[c & 0x0F]);
|
||||
}
|
||||
}
|
||||
settings.setLiteInstallId(installId);
|
||||
settings.save();
|
||||
}
|
||||
inputs.installBucket = liteRolloutBucketFromInstallId(installId);
|
||||
|
||||
// Local manifest cache next to settings.json — no network fetch (a signed remote fetcher can
|
||||
// populate this later). Absent/unreadable -> fail-open.
|
||||
try {
|
||||
const std::filesystem::path cfgDir =
|
||||
std::filesystem::path(config::Settings::getDefaultPath()).parent_path();
|
||||
inputs.manifest = loadLiteRolloutManifestFromFile((cfgDir / "lite_rollout.json").string());
|
||||
} catch (...) {
|
||||
// leave manifest absent -> fail-open
|
||||
}
|
||||
|
||||
const LiteRolloutDecision decision = evaluateLiteRollout(inputs);
|
||||
if (!decision.allowed) {
|
||||
DEBUG_LOGF("[lite-rollout] lite wallet gated OFF: %s — %s\n",
|
||||
liteRolloutStatusName(decision.status), decision.message.c_str());
|
||||
}
|
||||
return decision;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void App::rebuildLiteWallet()
|
||||
{
|
||||
if (!supportsLiteBackend() || !settings_) return;
|
||||
@@ -381,7 +439,8 @@ void App::rebuildLiteWallet()
|
||||
|
||||
lite_wallet_ = wallet::LiteWalletController::createLinked(
|
||||
walletCapabilities(),
|
||||
wallet::liteConnectionSettingsFromAppSettings(*settings_));
|
||||
wallet::liteConnectionSettingsFromAppSettings(*settings_),
|
||||
resolveLiteRolloutDecision(*settings_));
|
||||
lite_wallet_->setPersistCallback([this]() { settings_->save(); });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user