Files
ObsidianDragon/src/wallet/lite_rollout_policy.h
DanS b3c2282b53 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>
2026-06-06 12:01:08 -05:00

86 lines
3.8 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// Lite-wallet rollout / kill-switch policy.
//
// A pure decision core that gates whether the lite wallet may run, given a local override, an
// emergency env kill-switch, an optional rollout manifest, the running app version, and a stable
// per-install rollout bucket. Privacy posture (this is a privacy coin): the manifest is read from a
// LOCAL cache file only — there is NO runtime network fetch (a signed remote fetcher can populate
// that cache later without touching this policy), it is consulted FAIL-OPEN (absent/invalid →
// allowed, so a missing file never bricks a working install), and the install bucket is derived
// from a locally-generated random id that is never transmitted and carries no PII.
#pragma once
#include <string>
#include <vector>
namespace dragonx::wallet {
// User/support local override (persisted in settings as "lite_rollout").
enum class LiteRolloutOverride {
Auto, // honor the manifest / staged rollout
ForceOn, // always enable; bypasses the manifest + staged rollout, but NOT the env kill-switch
ForceOff // always disable
};
enum class LiteRolloutStatus {
Allowed,
DisabledByKillSwitchEnv, // DRAGONX_LITE_KILL_SWITCH=1 (emergency, absolute)
DisabledByLocalOverride, // lite_rollout=force_off
DisabledByManifest, // manifest global_enabled=false
DisabledUnsupportedVersion, // version below min / above max supported
DisabledBlockedVersion, // version explicitly blocked
DisabledByStagedRollout // install bucket above the rollout threshold
};
// Rollout manifest. Loaded from a local cache file; present=false means no manifest (fail-open).
struct LiteRolloutManifest {
bool present = false; // a manifest file was found
bool valid = false; // it parsed and validated
bool globalEnabled = true; // master enable switch
std::string minVersion; // inclusive floor (empty = none)
std::string maxVersion; // inclusive ceiling (empty = none)
std::vector<std::string> blockedVersions;
int rolloutPermille = 1000; // 0..1000; install allowed when bucket < permille (1000 = 100%)
std::string message; // user-facing reason shown when disabled
};
struct LiteRolloutInputs {
LiteRolloutOverride override = LiteRolloutOverride::Auto;
bool killSwitchEnv = false;
LiteRolloutManifest manifest;
std::string appVersion;
int installBucket = 0; // 0..999, stable per install
};
struct LiteRolloutDecision {
bool allowed = true;
LiteRolloutStatus status = LiteRolloutStatus::Allowed;
std::string message; // user-facing (empty when allowed)
};
// The pure decision. No I/O. Evaluation order: env kill-switch (absolute) → local override →
// manifest gates (global / version / blocked / staged rollout) → fail-open allow.
LiteRolloutDecision evaluateLiteRollout(const LiteRolloutInputs& inputs);
// Stable bucket 0..999 from an install id (FNV-1a). Deterministic; empty id -> 0.
int liteRolloutBucketFromInstallId(const std::string& installId);
// Compare dotted version cores, ignoring any "-suffix"/"+build". Returns -1 / 0 / 1.
int compareLiteVersions(const std::string& a, const std::string& b);
// Override <-> string ("auto"/"force_on"/"force_off"); unknown parses to Auto.
LiteRolloutOverride liteRolloutOverrideFromString(const std::string& value);
const char* liteRolloutOverrideToString(LiteRolloutOverride override);
const char* liteRolloutStatusName(LiteRolloutStatus status);
// Load + validate a manifest from a JSON file. Missing file -> {present=false}; parse error ->
// {present=true, valid=false}. Both are fail-open at evaluation time.
LiteRolloutManifest loadLiteRolloutManifestFromFile(const std::string& path);
} // namespace dragonx::wallet