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:
@@ -91,6 +91,8 @@ const char* liteWalletLifecycleAvailabilityName(LiteWalletLifecycleAvailability
|
||||
return "BridgeCallsDisabled";
|
||||
case LiteWalletLifecycleAvailability::NoUsableServer:
|
||||
return "NoUsableServer";
|
||||
case LiteWalletLifecycleAvailability::RolloutDisabled:
|
||||
return "RolloutDisabled";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
@@ -131,6 +133,9 @@ LiteWalletLifecycleAvailability LiteWalletLifecycleService::availability() const
|
||||
if (!isLiteBuild(capabilities_)) return LiteWalletLifecycleAvailability::UnsupportedBuild;
|
||||
if (!supportsLiteBackend(capabilities_)) return LiteWalletLifecycleAvailability::BackendUnavailable;
|
||||
if (!bridge_ || !bridge_->available()) return LiteWalletLifecycleAvailability::BridgeUnavailable;
|
||||
// Runtime kill-switch / staged-rollout gate: a structural readiness check, applied before
|
||||
// server selection so a gated-off wallet reports the rollout reason rather than a server error.
|
||||
if (options_.rolloutBlocked) return LiteWalletLifecycleAvailability::RolloutDisabled;
|
||||
if (!selectLiteServer(connectionSettings_).ok) return LiteWalletLifecycleAvailability::NoUsableServer;
|
||||
if (!options_.allowBridgeCalls) return LiteWalletLifecycleAvailability::BridgeCallsDisabled;
|
||||
return LiteWalletLifecycleAvailability::Ready;
|
||||
@@ -142,6 +147,9 @@ WalletBackendStatus LiteWalletLifecycleService::status() const
|
||||
if (currentAvailability == LiteWalletLifecycleAvailability::NoUsableServer) {
|
||||
return statusFor(currentAvailability, selectLiteServer(connectionSettings_).error);
|
||||
}
|
||||
if (currentAvailability == LiteWalletLifecycleAvailability::RolloutDisabled) {
|
||||
return statusFor(currentAvailability, options_.rolloutMessage);
|
||||
}
|
||||
return statusFor(currentAvailability);
|
||||
}
|
||||
|
||||
@@ -310,6 +318,14 @@ WalletBackendStatus LiteWalletLifecycleService::statusFor(
|
||||
{},
|
||||
0.0
|
||||
};
|
||||
case LiteWalletLifecycleAvailability::RolloutDisabled:
|
||||
return WalletBackendStatus{
|
||||
WalletBackendState::Unavailable,
|
||||
detail.empty() ? "the lite wallet is disabled by the rollout policy" : detail,
|
||||
{},
|
||||
{},
|
||||
0.0
|
||||
};
|
||||
}
|
||||
|
||||
return WalletBackendStatus{WalletBackendState::Unavailable, "unknown lite wallet lifecycle state", {}, {}, 0.0};
|
||||
|
||||
Reference in New Issue
Block a user