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>
86 lines
3.8 KiB
C++
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
|