// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "lite_rollout_policy.h" #include #include #include #include #include namespace dragonx { namespace wallet { namespace { using json = nlohmann::json; // Numeric dotted core of a version, dropping any "-prerelease" / "+build" suffix. std::vector parseVersionCore(const std::string& v) { const std::string core = v.substr(0, v.find_first_of("-+")); std::vector parts; std::stringstream ss(core); std::string token; while (std::getline(ss, token, '.')) { if (token.empty()) { parts.push_back(0); continue; } try { parts.push_back(std::stoll(token)); } catch (...) { parts.push_back(0); } } return parts; } const char* defaultMessageFor(LiteRolloutStatus s) { switch (s) { case LiteRolloutStatus::DisabledByKillSwitchEnv: return "The lite wallet is disabled by an emergency kill-switch on this machine."; case LiteRolloutStatus::DisabledByLocalOverride: return "The lite wallet is turned off in this app's settings."; case LiteRolloutStatus::DisabledByManifest: return "The lite wallet is currently disabled by the rollout policy."; case LiteRolloutStatus::DisabledUnsupportedVersion: return "This app version is not supported by the lite wallet; please update."; case LiteRolloutStatus::DisabledBlockedVersion: return "This app version has a known issue and the lite wallet is disabled; please update."; case LiteRolloutStatus::DisabledByStagedRollout: return "The lite wallet is rolling out gradually and is not yet enabled for this install."; case LiteRolloutStatus::Allowed: default: return ""; } } LiteRolloutDecision disabledDecision(LiteRolloutStatus s, const std::string& manifestMessage) { LiteRolloutDecision d; d.allowed = false; d.status = s; d.message = !manifestMessage.empty() ? manifestMessage : defaultMessageFor(s); return d; } } // namespace int compareLiteVersions(const std::string& a, const std::string& b) { const auto pa = parseVersionCore(a); const auto pb = parseVersionCore(b); const std::size_t n = std::max(pa.size(), pb.size()); for (std::size_t i = 0; i < n; ++i) { const long long va = i < pa.size() ? pa[i] : 0; const long long vb = i < pb.size() ? pb[i] : 0; if (va < vb) return -1; if (va > vb) return 1; } return 0; } int liteRolloutBucketFromInstallId(const std::string& installId) { if (installId.empty()) return 0; // FNV-1a (64-bit) → 0..999. Stable across runs for a given id. std::uint64_t h = 1469598103934665603ULL; for (unsigned char c : installId) { h ^= static_cast(c); h *= 1099511628211ULL; } return static_cast(h % 1000ULL); } LiteRolloutOverride liteRolloutOverrideFromString(const std::string& value) { if (value == "force_on") return LiteRolloutOverride::ForceOn; if (value == "force_off") return LiteRolloutOverride::ForceOff; return LiteRolloutOverride::Auto; } const char* liteRolloutOverrideToString(LiteRolloutOverride o) { switch (o) { case LiteRolloutOverride::ForceOn: return "force_on"; case LiteRolloutOverride::ForceOff: return "force_off"; case LiteRolloutOverride::Auto: default: return "auto"; } } const char* liteRolloutStatusName(LiteRolloutStatus s) { switch (s) { case LiteRolloutStatus::Allowed: return "Allowed"; case LiteRolloutStatus::DisabledByKillSwitchEnv: return "DisabledByKillSwitchEnv"; case LiteRolloutStatus::DisabledByLocalOverride: return "DisabledByLocalOverride"; case LiteRolloutStatus::DisabledByManifest: return "DisabledByManifest"; case LiteRolloutStatus::DisabledUnsupportedVersion: return "DisabledUnsupportedVersion"; case LiteRolloutStatus::DisabledBlockedVersion: return "DisabledBlockedVersion"; case LiteRolloutStatus::DisabledByStagedRollout: return "DisabledByStagedRollout"; } return "Unknown"; } LiteRolloutDecision evaluateLiteRollout(const LiteRolloutInputs& in) { // 1. Emergency env kill-switch is absolute — not even ForceOn bypasses it. if (in.killSwitchEnv) return disabledDecision(LiteRolloutStatus::DisabledByKillSwitchEnv, {}); // 2/3. Local override. if (in.override == LiteRolloutOverride::ForceOff) return disabledDecision(LiteRolloutStatus::DisabledByLocalOverride, {}); if (in.override == LiteRolloutOverride::ForceOn) return LiteRolloutDecision{true, LiteRolloutStatus::Allowed, {}}; // 4. Manifest gates — fail-open: only enforced when a manifest is present AND valid. const LiteRolloutManifest& m = in.manifest; if (m.present && m.valid) { if (!m.globalEnabled) return disabledDecision(LiteRolloutStatus::DisabledByManifest, m.message); if (!m.minVersion.empty() && compareLiteVersions(in.appVersion, m.minVersion) < 0) return disabledDecision(LiteRolloutStatus::DisabledUnsupportedVersion, m.message); if (!m.maxVersion.empty() && compareLiteVersions(in.appVersion, m.maxVersion) > 0) return disabledDecision(LiteRolloutStatus::DisabledUnsupportedVersion, m.message); for (const auto& bad : m.blockedVersions) if (compareLiteVersions(in.appVersion, bad) == 0) return disabledDecision(LiteRolloutStatus::DisabledBlockedVersion, m.message); if (in.installBucket >= m.rolloutPermille) return disabledDecision(LiteRolloutStatus::DisabledByStagedRollout, m.message); } // 5. Fail-open default. return LiteRolloutDecision{true, LiteRolloutStatus::Allowed, {}}; } LiteRolloutManifest loadLiteRolloutManifestFromFile(const std::string& path) { LiteRolloutManifest m; // present=false, valid=false (fail-open) std::ifstream in(path); if (!in.is_open()) return m; // no file -> no manifest m.present = true; try { json j; in >> j; if (j.contains("global_enabled")) m.globalEnabled = j["global_enabled"].get(); if (j.contains("min_version")) m.minVersion = j["min_version"].get(); if (j.contains("max_version")) m.maxVersion = j["max_version"].get(); if (j.contains("blocked_versions")) m.blockedVersions = j["blocked_versions"].get>(); if (j.contains("rollout_permille")) { const int p = j["rollout_permille"].get(); m.rolloutPermille = std::max(0, std::min(1000, p)); } if (j.contains("message")) m.message = j["message"].get(); m.valid = true; } catch (...) { m.valid = false; // parse error -> fail-open } return m; } } // namespace wallet } // namespace dragonx