5 Commits

22 changed files with 186 additions and 953 deletions
+1 -159
View File
@@ -1,159 +1 @@
<div align="center">
<img src="iso/assets/sovran-hub-icon.svg" alt="Sovran Systems" width="160" />
# Sovran_SystemsOS
`Base Development` · NixOS Flake · AGPL-3.0
[Sovran Systems](https://sovransystems.com)
</div>
<div align="center">
<img src="assets/desktop-screenshot.png" alt="Sovran_SystemsOS desktop showing application dock and PRIVACY. SOVEREIGNTY. BITCOIN. tagline" width="800" />
*The Sovran_SystemsOS desktop — "Privacy. Sovereignty. Bitcoin."*
</div>
---
## Table of Contents
1. [What This Repo Is](#what-this-repo-is)
2. [Architecture](#architecture)
3. [Module Catalog](#module-catalog)
4. [The Three Modes (internal reference)](#the-three-modes-internal-reference)
5. [Build & Deploy Reference](#build--deploy-reference)
6. [Networking & Reverse Proxy](#networking--reverse-proxy)
7. [Security Posture](#security-posture)
8. [Backups & Recovery](#backups--recovery)
9. [License](#license)
---
## What This Repo Is
Sovran_SystemsOS is defined entirely as a **Nix flake** (`flake.nix`) and built from source. There is no pre-built binary — the System Installer is produced from this tree. Everything the system does is declared here.
The control center is the **Hub** — a built-in panel that lets the operator launch, monitor, and toggle services without touching a terminal. Under the hood, the Hub writes to `custom.nix`, which feeds back into the flake.
## Architecture
```
┌─────────────────────────┐
│ flake.nix │
│ inputs: nixpkgs, │
│ nix-bitcoin, nixvim, │
│ btc-clients │
└───────────┬─────────────┘
│ nixosModules.Sovran_SystemsOS
┌──────────────────────────┐ imports ┌──────────────────────────┐
│ configuration.nix │────────────▶│ modules/modules.nix │
│ boot / fs / users / │ │ core/* + services + opt │
│ desktop / nix settings │ │ features │
└──────────────────────────┘ └──────────┬───────────────┘
▲ │
│ ./role-state.nix (mode/role) ▼
│ ./custom.nix (user overrides) ┌────────────────────┐
│ │ modules/*.nix │
└───────── sovran-hub writes ───────▶│ synapse / wordpress│
│ nextcloud / etc. │
└────────────────────┘
```
- **`flake.nix`** declares two NixOS configurations:
- `nixosConfigurations.nixos` — the running system.
- `nixosConfigurations.sovran_systemsos-iso` — the System Installer.
- **`configuration.nix`** owns host concerns (boot, filesystems, users, desktop, locale, Nix settings, firewall, audio, backups).
- **`modules/modules.nix`** is the service router. Every other module is opt-in via flags read from `role-state.nix` and `custom.nix`.
## Module Catalog
Defaults follow the import order in `modules/modules.nix`. Toggles live in `custom.nix` (the Hub writes them) and `role-state.nix`.
| Module | Default | Purpose |
|---|---|---|
| `core/*` | **on** | Roles, Caddy, Njalla, Hub, desktop, perf, ssh-bootstrap |
| `php.nix`, `credentials.nix` | **on** | Required by web services & secrets |
| `synapse.nix` | **on** | Matrix homeserver |
| `wordpress.nix` | **on** | WordPress + PHP-FPM vhost |
| `nextcloud.nix` | **on** | Files / calendar / contacts |
| `vaultwarden.nix` | **on** | Bitwarden-compatible secrets vault |
| `bitcoinecosystem.nix` | **on** | bitcoind/electrs/LND/RTL/BTCPay (over Tor) |
| `wallet-autoconnect.nix` | **on** | Sparrow/Bisq ↔ node handshake |
| `haven.nix` | off | Nostr relay |
| `element-calling.nix` | off | LiveKit + JWT for E2E calling |
| `mempool.nix` | off | Mempool.space dashboard |
| `bitcoin-core.nix` | off | Switch node to Bitcoin Core (replaces default Bitcoin Knots + BIP110) |
| `rdp.nix` | off | xrdp remote desktop |
| `sshd.nix` | off | Public-facing OpenSSH |
> Tor is wired directly into the Bitcoin stack. In `modules/bitcoinecosystem.nix`, `bitcoind`, `electrs`, and `lnd` all set `tor.enforce = true` and `tor.proxy = true`, and onion services are exposed for them.
## The Three Modes (internal reference)
Selected by `role-state.nix`, resolved by `modules/core/role-logic.nix`. All three configurations are produced from this same flake.
| Mode | What's enabled on top of the base NixOS + GNOME |
|---|---|
| **Desktop** | Private daily-driver. Sparrow + Bisq included. |
| **Node** | Desktop + full Bitcoin stack (bitcoind/electrs/LND/RTL/BTCPay over Tor). |
| **Server+Desktop** | Node + self-hosting services (Synapse, Nextcloud, WordPress, Vaultwarden, Element Calling, etc.). |
## Build & Deploy Reference
Internal commands. Run from the flake root.
| Action | Command |
|---|---|
| Build the System Installer | `nix build .#nixosConfigurations.sovran_systemsos-iso.config.system.build.isoImage` |
| Switch now | `sudo nixos-rebuild switch --flake .#nixos` |
| Test in current boot only | `sudo nixos-rebuild test --flake .#nixos` |
| Stage for next boot | `sudo nixos-rebuild boot --flake .#nixos` |
| Build only (no activation) | `nixos-rebuild build --flake .#nixos` |
| Update pinned inputs | `nix flake update` (then rebuild) |
| Rollback last switch | `sudo nixos-rebuild switch --rollback` |
| Garbage-collect (>7 days) | Automatic weekly; manual: `sudo nix-collect-garbage -d` |
## Networking & Reverse Proxy
- **Firewall on by default** (`networking.firewall.enable = true`). Port are opened by the module that needs it.
- **Caddy** (`modules/core/caddy.nix`) terminates TLS for all HTTP services.
- **Njalla** dynamic DNS (`modules/core/njalla.nix`) keeps records in sync via a 15-minute cron job.
- **Tor** is enabled with `torsocks` available. The Bitcoin stack uses it directly — see [Security Posture](#security-posture).
- **SSH:** localhost-only by default (`core/sshd-localhost.nix`).
## Security Posture
Facts about the defaults, straight from `configuration.nix` and the modules:
- **Reproducible builds.** Every artifact derives from `flake.lock`. The same commit produces the same OS.
- **Bitcoin stack over Tor.** In `modules/bitcoinecosystem.nix`, `bitcoind`, `electrs`, and `lnd` all set `tor.enforce = true`, and onion services are exposed for `bitcoind`, `electrs`, `lnd`, and friends.
- **Firewall on, public sshd off, RDP off, auto-login off, fail2bain active**
- **Kernel surface trimmed.** `boot.blacklistedKernelModules = [ "rxrpc" ];`
- **Weekly garbage collection** with `--delete-older-than 7d`.
## Backups & Recovery
`services.rsnapshot` snapshots hourly and daily to `/run/media/Second_Drive/BTCEcoandBackup/NixOS_Snapshot_Backup`:
```
backup /home/ localhost/
backup /var/lib/ localhost/
backup /etc/nixos/ localhost/
backup /etc/nix-bitcoin-secrets/ localhost/
retain hourly 5
retain daily 5
cron hourly 0 * * * *
cron daily 50 21 * * *
```
The second drive is mounted by label (`BTCEcoandBackup`) with `nofail` so a missing drive doesn't block boot.
## License
Licensed under the **GNU Affero General Public License v3.0** — see [`LICENSE`](./LICENSE).
### Testing Branch
+43 -277
View File
@@ -223,15 +223,27 @@ FEATURE_REGISTRY = [
"port_requirements": [],
},
{
"id": "bitcoin-core",
"name": "Bitcoin Core",
"description": "Only one Bitcoin node implementation can be active: Bitcoin Knots + BIP110 (default) or Bitcoin Core. Enabling this replaces Knots + BIP110 with Bitcoin Core. Your timechain data is preserved.",
"id": "bip110",
"name": "Bitcoin Knots + BIP110",
"description": "Only one Bitcoin node implementation can be active at a time: Bitcoin Knots (default), Bitcoin Knots + BIP110, or Bitcoin Core. Enabling this option replaces the default Bitcoin Knots with Bitcoin Knots + BIP110 consensus changes. It will disable the currently active alternative.",
"category": "bitcoin",
"needs_domain": False,
"domain_name": None,
"needs_ddns": False,
"extra_fields": [],
"conflicts_with": [],
"conflicts_with": ["bitcoin-core"],
"port_requirements": [],
},
{
"id": "bitcoin-core",
"name": "Bitcoin Core",
"description": "Only one Bitcoin node implementation can be active at a time: Bitcoin Knots (default), Bitcoin Knots + BIP110, or Bitcoin Core. Enabling this option replaces the default Bitcoin Knots with Bitcoin Core. It will disable the currently active alternative.",
"category": "bitcoin",
"needs_domain": False,
"domain_name": None,
"needs_ddns": False,
"extra_fields": [],
"conflicts_with": ["bip110"],
"port_requirements": [],
},
{
@@ -265,16 +277,13 @@ FEATURE_REGISTRY = [
},
]
# Feature ids that have been removed/deprecated. The Hub must never write these
# back into custom.nix, and should strip any it finds (see startup migration).
DEPRECATED_FEATURE_IDS: set[str] = {"bip110"}
# Map feature IDs to their systemd units in config.json
FEATURE_SERVICE_MAP = {
"rdp": "gnome-remote-desktop.service",
"haven": "haven-relay.service",
"element-calling": "livekit.service",
"mempool": "mempool.service",
"bip110": None,
"bitcoin-core": None,
"btcpay-web": "btcpayserver.service",
"sshd": "sshd.service",
@@ -322,6 +331,7 @@ SERVICE_DOMAIN_MAP: dict[str, str] = {
# For features that share a unit, disambiguate by icon field
FEATURE_ICON_MAP = {
"bip110": "bip110",
"bitcoin-core": "bitcoin-core",
}
@@ -342,7 +352,7 @@ ROLE_CATEGORIES: dict[str, set[str] | None] = {
ROLE_FEATURES: dict[str, set[str] | None] = {
"server_plus_desktop": None,
"desktop": {"rdp", "sshd"},
"node": {"rdp", "bitcoin-core", "mempool", "btcpay-web", "sshd"},
"node": {"rdp", "bip110", "bitcoin-core", "mempool", "btcpay-web", "sshd"},
}
SERVICE_DESCRIPTIONS: dict[str, str] = {
@@ -642,37 +652,6 @@ templates = Jinja2Templates(directory=os.path.join(_BASE_DIR, "templates"))
# ── Static asset cache-busting ────────────────────────────────────
def _compute_asset_version() -> str:
"""Return a 16-char asset version from Nix store hash or static/template metadata."""
nix_match = re.search(r"/nix/store/([a-z0-9]{32})-", os.path.realpath(_BASE_DIR))
if nix_match:
return nix_match.group(1)[:16]
hasher = hashlib.sha256()
for root in (
os.path.join(_BASE_DIR, "static"),
os.path.join(_BASE_DIR, "templates"),
):
if not os.path.isdir(root):
continue
for dirpath, dirnames, filenames in os.walk(root):
dirnames.sort()
for filename in sorted(filenames):
path = os.path.join(dirpath, filename)
try:
stat = os.stat(path)
except OSError:
continue
hasher.update(path.encode())
hasher.update(b"\0")
hasher.update(f"{stat.st_mtime_ns}:{stat.st_size}".encode())
hasher.update(b"\0")
return hasher.hexdigest()[:16]
ASSET_VERSION = _compute_asset_version()
def _file_hash(filename: str) -> str:
"""Return first 8 chars of the MD5 hex digest for a static file."""
path = os.path.join(_BASE_DIR, "static", filename)
@@ -1509,9 +1488,7 @@ def _read_hub_overrides() -> tuple[dict, str | None, str | None, str | None]:
r'sovran_systemsOS\.features\.([a-zA-Z0-9_-]+)\s*=\s*(?:lib\.mkForce\s+)?(true|false)\s*;',
section,
):
feat_id = m.group(1)
if feat_id not in DEPRECATED_FEATURE_IDS:
features[feat_id] = m.group(2) == "true"
features[m.group(1)] = m.group(2) == "true"
for m in re.finditer(
r'sovran_systemsOS\.web\.btcpayserver\s*=\s*(?:lib\.mkForce\s+)?(true|false)\s*;',
section,
@@ -1544,8 +1521,6 @@ def _write_hub_overrides(features: dict, nostr_npub: str | None, timezone: str |
"""Write the Hub Managed section inside custom.nix."""
lines = []
for feat_id, enabled in features.items():
if feat_id in DEPRECATED_FEATURE_IDS:
continue
val = "true" if enabled else "false"
if feat_id == "btcpay-web":
lines.append(f" sovran_systemsOS.web.btcpayserver = lib.mkForce {val};")
@@ -1591,40 +1566,6 @@ def _write_hub_overrides(features: dict, nostr_npub: str | None, timezone: str |
f.write(content)
def _migrate_strip_deprecated_features() -> None:
"""One-time migration: remove deprecated feature lines from the Hub Managed
section of custom.nix. Any feature id in DEPRECATED_FEATURE_IDS is dropped
while all other Hub-managed settings (other features, nostr_npub, timezone,
locale) are preserved byte-for-byte in meaning.
This is a no-op (and never raises) if CUSTOM_NIX is missing, unreadable, or
contains no deprecated lines.
"""
try:
with open(CUSTOM_NIX, "r") as f:
content = f.read()
except (FileNotFoundError, OSError):
return
# Quick-exit: if none of the deprecated ids appear, nothing to do.
hub_begin = content.find(HUB_BEGIN)
hub_end = content.find(HUB_END)
if hub_begin == -1 or hub_end == -1:
return
section = content[hub_begin:hub_end]
if not any(f"features.{dep_id}" in section for dep_id in DEPRECATED_FEATURE_IDS):
return
try:
features, nostr_npub, timezone, locale = _read_hub_overrides()
# _read_hub_overrides already excludes DEPRECATED_FEATURE_IDS, so
# calling _write_hub_overrides with its output drops the stale lines.
_write_hub_overrides(features, nostr_npub, timezone, locale)
except Exception:
# Never let a migration failure break startup.
logger.exception("_migrate_strip_deprecated_features: unexpected error (non-fatal)")
# ── Feature status helpers ─────────────────────────────────────────
def _is_feature_enabled_in_config(feature_id: str) -> bool | None:
@@ -1634,7 +1575,7 @@ def _is_feature_enabled_in_config(feature_id: str) -> bool | None:
return False # Default off in Node role; only on via explicit hub toggle
unit = FEATURE_SERVICE_MAP.get(feature_id)
if unit is None:
return None # bitcoin-core — can't determine from config
return None # bip110, bitcoin-core — can't determine from config
cfg = load_config()
for svc in cfg.get("services", []):
if svc.get("unit") == unit:
@@ -1953,10 +1894,7 @@ def _verify_support_removed() -> bool:
@app.get("/login", response_class=HTMLResponse)
async def login_page(request: Request):
return templates.TemplateResponse("login.html", {
"request": request,
"asset_version": ASSET_VERSION,
})
return templates.TemplateResponse("login.html", {"request": request})
@app.get("/auto-login")
@@ -2023,7 +1961,6 @@ async def api_logout(request: Request):
async def index(request: Request):
return templates.TemplateResponse("index.html", {
"request": request,
"asset_version": ASSET_VERSION,
})
@@ -2032,7 +1969,6 @@ async def onboarding(request: Request):
_ensure_onboarding_reopened_for_migration()
return templates.TemplateResponse("onboarding.html", {
"request": request,
"asset_version": ASSET_VERSION,
"onboarding_js_hash": _ONBOARDING_JS_HASH,
})
@@ -2253,16 +2189,6 @@ _BTC_VERSION_CACHE_TTL = 60 # seconds — version doesn't change at runtime
# Cache for ``bitcoind --version`` output (available even before RPC is ready)
_btcd_version_cache: tuple[float, str | None] = (0.0, None)
# Cache for ``bitcoin-cli getdeploymentinfo`` output (BIP-110 live status)
_btc_deployment_cache: tuple[float, dict | None] = (0.0, None)
# Bitcoin Knots exposes BIP-110 as the `reduced_data` versionbits deployment
# (RDTS, bit 4) in getdeploymentinfo. See Knots src/deploymentinfo.cpp,
# src/kernel/chainparams.cpp, and doc/bips.md.
BIP110_DEPLOYMENT_NAMES = {"reduced_data", "rdts", "bip110", "uasf-bip110"}
BIP110_VERSIONBITS_BIT = 4
BIP110_SUBVERSION_MARKERS = {"bip110", "uasf-bip110", "reduced_data", "rdts"}
# ── Generic service version detection (NixOS store path) ─────────
@@ -2377,160 +2303,12 @@ def _get_bitcoin_version_info() -> dict | None:
return None
def _get_bitcoin_deployment_info() -> dict | None:
"""Call bitcoin-cli getdeploymentinfo and return parsed JSON, or None on error.
Results are cached for _BTC_VERSION_CACHE_TTL seconds. Never raises.
"""
global _btc_deployment_cache
now = time.monotonic()
cached_at, cached_val = _btc_deployment_cache
if now - cached_at < _BTC_VERSION_CACHE_TTL:
return cached_val
try:
result = subprocess.run(
["bitcoin-cli", f"-datadir={BITCOIN_DATADIR}", "getdeploymentinfo"],
capture_output=True,
text=True,
timeout=10,
)
if result.returncode != 0:
_btc_deployment_cache = (now, None)
return None
info = json.loads(result.stdout)
_btc_deployment_cache = (now, info)
return info
except Exception:
_btc_deployment_cache = (now, None)
return None
def _get_bip110_status() -> dict:
"""Return a dict describing the live BIP-110 deployment/signaling state.
The returned struct has four stable keys::
{
"supported": bool, # node build is BIP-110-capable
"signaling": bool, # node is actively signaling / locked-in / active
"state": str, # "active" | "locked_in" | "signaling" |
# "not_signaling" | "unsupported" | "unknown"
"source": str, # "getdeploymentinfo" | "subversion" | "none"
}
Resolution order (authoritative → fallback → honest unknown):
1. ``getdeploymentinfo`` (authoritative) — scan ``deployments`` for BIP-110.
Bitcoin Knots currently exposes BIP-110 as ``reduced_data`` (RDTS, bit 4;
see Knots deploymentinfo.cpp / chainparams.cpp / doc/bips.md), so matching
first uses known deployment names, then falls back to versionbits bit 4.
2. Subversion fallback — if getdeploymentinfo is unavailable or yields no
recognisable BIP-110 entry, inspect the ``subversion`` field from
``getnetworkinfo``. A case-insensitive match for known BIP-110 markers
(including "bip110", "uasf-bip110", "reduced_data", "rdts") is treated as
"signaling".
3. Unknown — if the node is entirely unreachable or neither source is
conclusive, return state="unknown", signaling=False, source="none".
"""
_unknown: dict = {"supported": False, "signaling": False, "state": "unknown", "source": "none"}
def _deployment_bit(entry: dict) -> int | None:
bip9 = entry.get("bip9", {}) or {}
bip8 = entry.get("bip8", {}) or {}
bit = bip9.get("bit")
if bit is None:
bit = bip8.get("bit")
if bit is None:
bit = entry.get("bit")
return bit
# ── 1. getdeploymentinfo (authoritative) ──────────────────────────
deploy_info = _get_bitcoin_deployment_info()
if deploy_info is not None:
deployments = deploy_info.get("deployments", {})
if isinstance(deployments, dict):
matched_entry: dict | None = None
# Primary match: known deployment names (case-insensitive exact match)
for key, entry in deployments.items():
if not isinstance(entry, dict):
continue
key_lower = key.lower()
if key_lower not in BIP110_DEPLOYMENT_NAMES:
continue
matched_entry = entry
break
# Secondary match: versionbits bit (fallback only)
if matched_entry is None:
for _, entry in deployments.items():
if not isinstance(entry, dict):
continue
if _deployment_bit(entry) != BIP110_VERSIONBITS_BIT:
continue
matched_entry = entry
break
if matched_entry is not None:
entry = matched_entry
# bip9 / bip8 status field
bip9 = entry.get("bip9", {}) or {}
bip8 = entry.get("bip8", {}) or {}
status = (
bip9.get("status")
or bip8.get("status")
or entry.get("status")
or ""
).lower()
active = entry.get("active", False)
if active or status == "active":
return {"supported": True, "signaling": True, "state": "active", "source": "getdeploymentinfo"}
if status == "locked_in":
return {"supported": True, "signaling": True, "state": "locked_in", "source": "getdeploymentinfo"}
if status in ("started", "defined"):
# Check whether deployment is currently signaling in this period.
stats = bip9.get("statistics") or bip8.get("statistics") or {}
# Some Knots outputs expose only ``count`` (not explicit signaling bool),
# so treat count>0 as a conservative signaling indicator for this period.
count = stats.get("count")
signaling = bool(
stats.get("signaling")
or stats.get("signalling")
or (isinstance(count, int) and count > 0)
)
if signaling:
return {"supported": True, "signaling": True, "state": "signaling", "source": "getdeploymentinfo"}
return {"supported": True, "signaling": False, "state": "not_signaling", "source": "getdeploymentinfo"}
if status == "failed":
return {"supported": True, "signaling": False, "state": "not_signaling", "source": "getdeploymentinfo"}
# Entry found but status unrecognised — node supports BIP-110 but state unclear
return {"supported": True, "signaling": False, "state": "unknown", "source": "getdeploymentinfo"}
# ── 2. Subversion fallback ─────────────────────────────────────────
net_info = _get_bitcoin_version_info()
if net_info is not None:
subversion = net_info.get("subversion", "") or ""
sv_lower = subversion.lower()
if any(marker in sv_lower for marker in BIP110_SUBVERSION_MARKERS):
return {"supported": True, "signaling": True, "state": "signaling", "source": "subversion"}
# Node is reachable via RPC but no BIP-110 marker found anywhere
return {"supported": False, "signaling": False, "state": "unsupported", "source": "subversion"}
# ── 3. Node unreachable / RPC not ready ───────────────────────────
return _unknown
def _get_bitcoind_version() -> str | None:
"""Run ``bitcoind --version`` and return the raw version string, or None on error.
Parses the first output line to extract the token after "version ".
For example: "Bitcoin Knots daemon version v29.3.knots20260508"
returns "v29.3.knots20260508".
For example: "Bitcoin Knots daemon version v29.3.knots20260210+bip110-v0.4.1"
returns "v29.3.knots20260210+bip110-v0.4.1".
Works regardless of whether the RPC server is ready (IBD, warmup, etc.).
Results are cached for 60 seconds (_BTC_VERSION_CACHE_TTL).
@@ -2565,13 +2343,26 @@ def _get_bitcoind_version() -> str | None:
def _format_bitcoin_version(raw_version: str, icon: str = "") -> str:
"""Format a raw version string from ``bitcoind --version`` for tile display.
For the BIP110 tile (icon == "bip110") a " (bip110)" tag is appended,
since mainline Bitcoin Knots (29.3.knots20260508+) now includes BIP-110
and no longer carries a separate ``+bip110-vX.Y.Z`` suffix.
Strips the ``+bip110-vX.Y.Z`` patch suffix so the base version is shown
cleanly (e.g. "v29.3.knots20260210+bip110-v0.4.1""v29.3.knots20260210").
For the BIP110 tile (icon == "bip110") a " (bip110 vX.Y.Z)" tag is appended
including the patch version.
"""
display = raw_version
if icon == "bip110" and "(bip110)" not in display.lower():
display += " (bip110)"
# Extract the BIP110 patch version before stripping the suffix
bip110_ver = ""
bip_match = re.search(r"\+bip110-v(\S+)", raw_version)
if bip_match:
bip110_ver = bip_match.group(1)
# Strip the +bip110... suffix for the base Knots version
display = re.sub(r"\+bip110\S*", "", raw_version)
# For BIP110 tile, append both the tag and the patch version
if icon == "bip110":
if bip110_ver:
display += f" (bip110 v{bip110_ver})"
elif "(bip110)" not in display.lower():
display += " (bip110)"
return display
@@ -2639,19 +2430,6 @@ async def api_bitcoin_version():
}
@app.get("/api/bitcoin/bip110")
async def api_bitcoin_bip110():
"""Return live BIP-110 deployment/signaling status from bitcoin-cli.
Always returns HTTP 200. When bitcoind is unreachable or the node is mid-IBD
the response will contain ``state = "unknown"`` so the UI can render a neutral
badge rather than an error toast.
"""
loop = asyncio.get_event_loop()
status = await loop.run_in_executor(None, _get_bip110_status)
return status
@app.get("/api/services")
async def api_services():
cfg = load_config()
@@ -2832,8 +2610,6 @@ async def api_services():
btc_ver = _format_bitcoin_version(raw_ver, icon=icon)
service_data["bitcoin_version"] = btc_ver # backwards compat
service_data["version"] = btc_ver
if icon == "bip110":
service_data["bip110"] = await loop.run_in_executor(None, _get_bip110_status)
return service_data
results = await asyncio.gather(*[get_status(s) for s in services])
@@ -3118,8 +2894,6 @@ async def api_service_detail(unit: str, icon: str | None = None):
btc_ver = _format_bitcoin_version(raw_ver, icon=icon)
service_detail["bitcoin_version"] = btc_ver # backwards compat
service_detail["version"] = btc_ver
if icon == "bip110":
service_detail["bip110"] = await loop.run_in_executor(None, _get_bip110_status)
return service_detail
@@ -4787,14 +4561,6 @@ async def _startup_recover_stale_status():
await loop.run_in_executor(None, _recover_stale_status, REBUILD_STATUS, REBUILD_LOG, REBUILD_UNIT)
@app.on_event("startup")
async def _startup_migrate_deprecated_features():
"""Strip deprecated feature lines (e.g. bip110) from the Hub Managed section
of custom.nix so they are never re-written and do not cause stale warnings."""
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, _migrate_strip_deprecated_features)
async def _background_domain_reachability_checker():
"""Periodically curl configured domains and cache reachability results."""
await asyncio.sleep(_DOMAIN_REACHABILITY_STARTUP_DELAY)
@@ -155,69 +155,6 @@
white-space: nowrap;
}
/* ── BIP-110 status badge (tile + detail modal) ───────────────────── */
.tile-bip110-badge {
display: inline-flex;
align-items: center;
gap: 3px;
font-size: 0.64rem;
font-weight: 600;
border-radius: 4px;
padding: 2px 6px;
margin-top: 4px;
white-space: nowrap;
letter-spacing: 0.02em;
}
.tile-bip110-badge--active {
background: rgba(109, 191, 139, 0.18);
color: var(--green);
border: 1px solid rgba(109, 191, 139, 0.3);
}
.tile-bip110-badge--locked_in {
background: rgba(94, 173, 138, 0.15);
color: var(--accent-color);
border: 1px solid rgba(94, 173, 138, 0.3);
}
.tile-bip110-badge--signaling {
background: rgba(94, 173, 138, 0.12);
color: var(--accent-color);
border: 1px solid rgba(94, 173, 138, 0.2);
}
.tile-bip110-badge--not_signaling {
background: rgba(229, 165, 10, 0.12);
color: var(--yellow);
border: 1px solid rgba(229, 165, 10, 0.25);
}
.tile-bip110-badge--unsupported {
background: rgba(94, 122, 106, 0.12);
color: var(--grey);
border: 1px solid rgba(94, 122, 106, 0.2);
}
.tile-bip110-badge--unknown {
background: transparent;
color: var(--text-dim);
border: 1px solid var(--border-color);
}
.bip110-status-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.bip110-source-label {
color: var(--text-dim);
font-size: 0.75rem;
}
/* ── Service detail modal sections ───────────────────────────────── */
.svc-detail-section {
@@ -413,11 +413,16 @@ function handleFeatureToggle(feat, newEnabled) {
});
}
if (feat.id === "bitcoin-core") {
var confirmMsg = "Only one Bitcoin node implementation can be active. Enabling Bitcoin Core will replace Bitcoin Knots + BIP110 as the active node. Your timechain data will be preserved — you will not need to re-download the timechain. Continue?";
if (conflictNames.length > 0) {
var confirmMsg;
if (feat.id === "bip110") {
confirmMsg = "Only one Bitcoin node implementation can be active. Enabling Bitcoin Knots + BIP110 will disable Bitcoin Core (if active). Your timechain data will be preserved — you will not need to re-download the timechain. Continue?";
} else if (feat.id === "bitcoin-core") {
confirmMsg = "Only one Bitcoin node implementation can be active. Enabling Bitcoin Core will disable Bitcoin Knots + BIP110 (if active). Your timechain data will be preserved — you will not need to re-download the timechain. Continue?";
} else {
confirmMsg = "This will disable " + conflictNames.join(", ") + ". Continue?";
}
openFeatureConfirm(confirmMsg, proceedAfterConflictCheck);
} else if (conflictNames.length > 0) {
openFeatureConfirm("This will disable " + conflictNames.join(", ") + ". Continue?", proceedAfterConflictCheck);
} else {
proceedAfterConflictCheck();
}
@@ -60,17 +60,3 @@ async function apiFetch(path, options) {
}
return res.json();
}
// ── BIP-110 badge state config ────────────────────────────────────
// Shared lookup used by tiles.js and service-detail.js.
// Keys match the "state" values returned by /api/bitcoin/bip110.
var BIP110_BADGE_CONFIG = {
active: { cls: 'tile-bip110-badge--active', label: 'Active', title: 'BIP-110 is active on this node' },
locked_in: { cls: 'tile-bip110-badge--locked_in', label: 'Locked In', title: 'BIP-110 is locked in and will activate shortly' },
signaling: { cls: 'tile-bip110-badge--signaling', label: 'Signaling', title: 'Node is signaling readiness for BIP-110' },
not_signaling: { cls: 'tile-bip110-badge--not_signaling',label: 'Not Signaling', title: 'Node supports BIP-110 but is not signaling this period' },
unsupported: { cls: 'tile-bip110-badge--unsupported', label: 'Not Supported', title: 'This node build does not include BIP-110' },
unknown: { cls: 'tile-bip110-badge--unknown', label: '\u2014', title: 'Status unavailable (node syncing or RPC not ready)' }
};
@@ -107,21 +107,6 @@ async function openServiceDetailModal(unit, name, icon) {
'</div>' +
'</div>';
// Section B2: BIP-110 live status (bip110 tile only)
if (icon === 'bip110' && data.bip110) {
var bip110 = data.bip110;
var bip110State = bip110.state || 'unknown';
var bip110Cfg = BIP110_BADGE_CONFIG[bip110State] || BIP110_BADGE_CONFIG.unknown;
var bip110Source = bip110.source ? ' <span class="bip110-source-label">(source: ' + escHtml(bip110.source) + ')</span>' : '';
html += '<div class="svc-detail-section">' +
'<div class="svc-detail-section-title">BIP-110 Deployment Status</div>' +
'<div class="bip110-status-row">' +
'<span class="tile-bip110-badge ' + bip110Cfg.cls + '" title="' + escHtml(bip110Cfg.title) + '">' + escHtml(bip110Cfg.label) + '</span>' +
bip110Source +
'</div>' +
'</div>';
}
// Section C: Domain diagnostics (domain services)
if (data.needs_domain) {
var steps = data.domain_check_steps || [];
@@ -257,7 +242,7 @@ async function openServiceDetailModal(unit, name, icon) {
var addonBtnCls = feat.enabled ? "btn btn-close-modal" : "btn btn-primary";
// Section title: use a more specific label for mutually-exclusive Bitcoin node features
var addonSectionTitle = (feat.id === "bitcoin-core")
var addonSectionTitle = (feat.id === "bip110" || feat.id === "bitcoin-core")
? "\u20BF Bitcoin Node Selection"
: "\uD83D\uDD27 Addon Feature";
+1 -34
View File
@@ -4,21 +4,6 @@
// Keyed by tileId: { progress: float, timestamp: ms }
var _btcSyncPrev = {};
// ── BIP-110 badge helper ──────────────────────────────────────────
function _renderBip110Badge(bip110) {
if (!bip110) return '';
var state = bip110.state || 'unknown';
var cfg = BIP110_BADGE_CONFIG[state] || BIP110_BADGE_CONFIG.unknown;
return '<div class="tile-bip110-badge ' + cfg.cls + '" title="' + escHtml(cfg.title) + '">' + escHtml(cfg.label) + '</div>';
}
function _firstElementFromHtml(html) {
var tmp = document.createElement("div");
tmp.innerHTML = html;
return tmp.firstElementChild || null;
}
// ── Render: initial build ─────────────────────────────────────────
function buildTiles(services, categoryLabels) {
@@ -180,8 +165,7 @@ function buildTile(svc) {
var ver = svc.version || svc.bitcoin_version || '';
var versionLabel = ver ? '<div class="tile-version">' + escHtml(ver) + '</div>' : '';
var bip110Badge = (svc.icon === 'bip110') ? _renderBip110Badge(svc.bip110) : '';
tile.innerHTML = '<img class="tile-icon" src="/static/icons/' + escHtml(svc.icon) + '.svg" alt="' + escHtml(svc.name) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'"><div class="tile-icon-fallback" style="display:none">?</div><div class="tile-name">' + escHtml(svc.name) + '</div>' + versionLabel + bip110Badge + '<div class="tile-status"><span class="status-dot ' + sc + '"></span><span class="status-text">' + st + '</span></div>';
tile.innerHTML = '<img class="tile-icon" src="/static/icons/' + escHtml(svc.icon) + '.svg" alt="' + escHtml(svc.name) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'"><div class="tile-icon-fallback" style="display:none">?</div><div class="tile-name">' + escHtml(svc.name) + '</div>' + versionLabel + '<div class="tile-status"><span class="status-dot ' + sc + '"></span><span class="status-text">' + st + '</span></div>';
tile.style.cursor = "pointer";
tile.addEventListener("click", function() {
@@ -281,23 +265,6 @@ function updateTiles(services) {
}
}
}
// Update BIP-110 badge for bip110 tiles
if (svc.icon === 'bip110') {
var badgeHtml = _renderBip110Badge(svc.bip110);
var badgeEl = tile.querySelector(".tile-bip110-badge");
if (badgeEl) {
// Replace existing badge in-place
var newBadge = _firstElementFromHtml(badgeHtml);
if (newBadge) { badgeEl.replaceWith(newBadge); } else { badgeEl.remove(); }
} else if (badgeHtml) {
// Insert badge after version label (or after tile-name if no version)
var anchorEl = tile.querySelector(".tile-version") || tile.querySelector(".tile-name");
if (anchorEl) {
var newBadgeEl = _firstElementFromHtml(badgeHtml);
if (newBadgeEl) anchorEl.insertAdjacentElement("afterend", newBadgeEl);
}
}
}
}
}
}
+22 -22
View File
@@ -4,17 +4,17 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sovran_SystemsOS Hub</title>
<link rel="stylesheet" href="/static/css/base.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/buttons.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/header.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/layout.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/tiles.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/modals.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/features.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/onboarding.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/support.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/domain-setup.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/security.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/base.css" />
<link rel="stylesheet" href="/static/css/buttons.css" />
<link rel="stylesheet" href="/static/css/header.css" />
<link rel="stylesheet" href="/static/css/layout.css" />
<link rel="stylesheet" href="/static/css/tiles.css" />
<link rel="stylesheet" href="/static/css/modals.css" />
<link rel="stylesheet" href="/static/css/features.css" />
<link rel="stylesheet" href="/static/css/onboarding.css" />
<link rel="stylesheet" href="/static/css/support.css" />
<link rel="stylesheet" href="/static/css/domain-setup.css" />
<link rel="stylesheet" href="/static/css/security.css" />
</head>
<body>
@@ -263,16 +263,16 @@
</div>
</div>
<script src="/static/js/constants.js?v={{ asset_version }}"></script>
<script src="/static/js/state.js?v={{ asset_version }}"></script>
<script src="/static/js/helpers.js?v={{ asset_version }}"></script>
<script src="/static/js/tiles.js?v={{ asset_version }}"></script>
<script src="/static/js/service-detail.js?v={{ asset_version }}"></script>
<script src="/static/js/support.js?v={{ asset_version }}"></script>
<script src="/static/js/update.js?v={{ asset_version }}"></script>
<script src="/static/js/rebuild.js?v={{ asset_version }}"></script>
<script src="/static/js/features.js?v={{ asset_version }}"></script>
<script src="/static/js/security.js?v={{ asset_version }}"></script>
<script src="/static/js/events.js?v={{ asset_version }}"></script>
<script src="/static/js/constants.js"></script>
<script src="/static/js/state.js"></script>
<script src="/static/js/helpers.js"></script>
<script src="/static/js/tiles.js"></script>
<script src="/static/js/service-detail.js"></script>
<script src="/static/js/support.js"></script>
<script src="/static/js/update.js"></script>
<script src="/static/js/rebuild.js"></script>
<script src="/static/js/features.js"></script>
<script src="/static/js/security.js"></script>
<script src="/static/js/events.js"></script>
</body>
</html>
@@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sovran Hub — Login</title>
<link rel="stylesheet" href="/static/css/base.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/buttons.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/base.css" />
<link rel="stylesheet" href="/static/css/buttons.css" />
</head>
<body>
<div class="login-wrapper">
-166
View File
@@ -1,166 +0,0 @@
import unittest
from unittest.mock import patch
from pathlib import Path
import sys
import types
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
def _install_web_stubs():
if "fastapi" in sys.modules:
return
class _HTTPException(Exception):
def __init__(self, status_code=None, detail=None):
super().__init__(detail)
self.status_code = status_code
self.detail = detail
class _FastAPI:
def __init__(self, *args, **kwargs):
pass
def mount(self, *args, **kwargs):
return None
def add_middleware(self, *args, **kwargs):
return None
def __getattr__(self, _name):
def _decorator_factory(*args, **kwargs):
def _decorator(func):
return func
return _decorator
return _decorator_factory
class _BaseModel:
pass
class _StaticFiles:
def __init__(self, *args, **kwargs):
pass
class _Jinja2Templates:
def __init__(self, *args, **kwargs):
pass
class _BaseHTTPMiddleware:
pass
fastapi_module = types.ModuleType("fastapi")
fastapi_module.FastAPI = _FastAPI
fastapi_module.HTTPException = _HTTPException
sys.modules["fastapi"] = fastapi_module
responses_module = types.ModuleType("fastapi.responses")
responses_module.HTMLResponse = object
responses_module.JSONResponse = object
responses_module.RedirectResponse = object
sys.modules["fastapi.responses"] = responses_module
staticfiles_module = types.ModuleType("fastapi.staticfiles")
staticfiles_module.StaticFiles = _StaticFiles
sys.modules["fastapi.staticfiles"] = staticfiles_module
templating_module = types.ModuleType("fastapi.templating")
templating_module.Jinja2Templates = _Jinja2Templates
sys.modules["fastapi.templating"] = templating_module
requests_module = types.ModuleType("fastapi.requests")
requests_module.Request = object
sys.modules["fastapi.requests"] = requests_module
pydantic_module = types.ModuleType("pydantic")
pydantic_module.BaseModel = _BaseModel
sys.modules["pydantic"] = pydantic_module
starlette_base_module = types.ModuleType("starlette.middleware.base")
starlette_base_module.BaseHTTPMiddleware = _BaseHTTPMiddleware
sys.modules["starlette.middleware.base"] = starlette_base_module
starlette_middleware_module = types.ModuleType("starlette.middleware")
starlette_middleware_module.base = starlette_base_module
sys.modules["starlette.middleware"] = starlette_middleware_module
starlette_module = types.ModuleType("starlette")
starlette_module.middleware = starlette_middleware_module
sys.modules["starlette"] = starlette_module
_install_web_stubs()
from sovran_systemsos_web import server
class Bip110StatusTests(unittest.TestCase):
def _status(self, deploy_info, net_info):
with patch.object(server, "_get_bitcoin_deployment_info", return_value=deploy_info), patch.object(
server, "_get_bitcoin_version_info", return_value=net_info
):
return server._get_bip110_status()
def test_started_reduced_data_reports_signaling(self):
deploy_info = {
"deployments": {
"reduced_data": {
"type": "bip9",
"active": False,
"bip9": {
"bit": 4,
"status": "started",
"statistics": {"elapsed": 833, "count": 4, "threshold": 1109},
"signalling": "--#--",
},
}
}
}
result = self._status(deploy_info, {"subversion": "/Satoshi:29.0.0/"})
self.assertEqual(
result,
{"supported": True, "signaling": True, "state": "signaling", "source": "getdeploymentinfo"},
)
def test_active_reduced_data_reports_active(self):
deploy_info = {
"deployments": {"reduced_data": {"active": True, "bip9": {"bit": 4, "status": "active"}}}
}
result = self._status(deploy_info, {"subversion": "/Satoshi:29.0.0/"})
self.assertEqual(result["state"], "active")
self.assertTrue(result["supported"])
self.assertTrue(result["signaling"])
self.assertEqual(result["source"], "getdeploymentinfo")
def test_locked_in_reduced_data_reports_locked_in(self):
deploy_info = {
"deployments": {"reduced_data": {"active": False, "bip9": {"bit": 4, "status": "locked_in"}}}
}
result = self._status(deploy_info, {"subversion": "/Satoshi:29.0.0/"})
self.assertEqual(result["state"], "locked_in")
self.assertTrue(result["supported"])
self.assertTrue(result["signaling"])
self.assertEqual(result["source"], "getdeploymentinfo")
def test_no_bip110_deployment_and_plain_subversion_reports_unsupported(self):
deploy_info = {
"deployments": {
"taproot": {"type": "bip9", "active": True, "bip9": {"bit": 2, "status": "active"}},
}
}
result = self._status(deploy_info, {"subversion": "/Satoshi:27.0.0/"})
self.assertEqual(
result,
{"supported": False, "signaling": False, "state": "unsupported", "source": "subversion"},
)
def test_node_unreachable_reports_unknown(self):
result = self._status(None, None)
self.assertEqual(result, {"supported": False, "signaling": False, "state": "unknown", "source": "none"})
if __name__ == "__main__":
unittest.main()
Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 KiB

-52
View File
@@ -1,52 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#153126"/>
<stop offset="55%" stop-color="#0F241B"/>
<stop offset="100%" stop-color="#091C14"/>
</linearGradient>
<linearGradient id="outerArc" x1="70" y1="40" x2="190" y2="210" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#42F39A"/>
<stop offset="45%" stop-color="#28D978"/>
<stop offset="100%" stop-color="#1AA45D"/>
</linearGradient>
<linearGradient id="innerArc" x1="90" y1="60" x2="180" y2="190" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#27C86F"/>
<stop offset="100%" stop-color="#157E49"/>
</linearGradient>
<filter id="innerShade" x="-10%" y="-10%" width="120%" height="120%">
<feOffset dx="0" dy="2"/>
<feGaussianBlur stdDeviation="5" result="blur"/>
<feComposite in="blur" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 .18 0"/>
</filter>
</defs>
<rect width="256" height="256" rx="48" ry="48" fill="url(#bg)"/>
<rect x="1.5" y="1.5" width="253" height="253" rx="46.5" ry="46.5"
fill="none" stroke="rgba(255,255,255,0.08)"/>
<rect x="6" y="6" width="244" height="244" rx="42" ry="42"
fill="none" filter="url(#innerShade)"/>
<path d="M128 32 A96 96 0 1 1 58 196"
fill="none"
stroke="url(#outerArc)"
stroke-width="12"
stroke-linecap="round"/>
<path d="M128 56 A72 72 0 1 1 76 178"
fill="none"
stroke="url(#innerArc)"
stroke-width="10"
stroke-linecap="round"/>
<circle cx="128" cy="128" r="8" fill="#F2FFF7"/>
<circle cx="128" cy="128" r="18" fill="none" stroke="#7BFFC0" stroke-opacity="0.14" stroke-width="4"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

+3 -9
View File
@@ -26,13 +26,6 @@
nix.settings = {
experimental-features = [ "nix-command" "flakes" ];
download-buffer-size = 524288000;
# Network resilience for cache.nixos.org (Fastly) flakiness.
connect-timeout = 10; # fail-fast on dead TCP connects (default: 0 = unlimited)
stalled-download-timeout = 90; # default 300s; retry sooner on stalled transfers
download-attempts = 7; # default 5
http-connections = 25; # cap concurrency (helps MTU/middlebox paths)
fallback = true; # build locally if a substitute can't be fetched
};
# ── Networking ──────────────────────────────────────────────
@@ -70,6 +63,7 @@
# ── Desktop ────────────────────────────────────────────────
services.displayManager.gdm.enable = true;
services.displayManager.gdm.autoSuspend = false;
services.displayManager.gdm.wayland = true;
services.desktopManager.gnome.enable = true;
services.printing.enable = true;
systemd.enableEmergencyMode = false;
@@ -145,10 +139,10 @@
ranger fastfetch gedit openssl pwgen
aspell aspellDicts.en lm_sensors
hunspell hunspellDicts.en_US
synadm brave dua
synadm brave dua bitwarden-desktop
gparted pv unzip parted screen zenity
libargon2 gnome-terminal libreoffice-fresh
dig firefox wp-cli axel
dig firefox element-desktop wp-cli axel
lk-jwt-service livekit-libwebrtc livekit-cli livekit
matrix-synapse age
];
Generated
+63 -29
View File
@@ -1,15 +1,33 @@
{
"nodes": {
"btc-clients": {
"bip110": {
"inputs": {
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1781013869,
"narHash": "sha256-XlEUtL+8M6kbPdmIh4sQQ7G02/1CwHQEk1RPvIMEWOs=",
"lastModified": 1778967282,
"narHash": "sha256-0g9RvVCD6zxY2vy54GhbB1OeeEZdKuxTr9r0whcpRjQ=",
"owner": "emmanuelrosa",
"repo": "bitcoin-knots-bip-110-nix",
"rev": "8d23ed98940d70e42ee870d719677a073a0a5920",
"type": "github"
},
"original": {
"owner": "emmanuelrosa",
"repo": "bitcoin-knots-bip-110-nix",
"type": "github"
}
},
"btc-clients": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1779373552,
"narHash": "sha256-9FCY5+WZmoZi4zN0xbdq3lNcPudvYjONpv21/9ddV/0=",
"owner": "emmanuelrosa",
"repo": "btc-clients-nix",
"rev": "9a6c78204dc8961840375b110bca595b1f6f084c",
"rev": "94797b5b75bbc021b0ceb83473110bcb58683542",
"type": "github"
},
"original": {
@@ -87,7 +105,7 @@
"inputs": {
"extra-container": "extra-container",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_2",
"nixpkgs": "nixpkgs_3",
"nixpkgs-25_05": "nixpkgs-25_05",
"nixpkgs-unstable": "nixpkgs-unstable"
},
@@ -108,15 +126,16 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1780218263,
"narHash": "sha256-T/f0pPDrH3Qc1VXyQXbK7yfHWRn90l3xwplc/nsxin4=",
"lastModified": 1777728799,
"narHash": "sha256-z7jjYQqhkFKab92VQ3duB7QVO7f7Y62qTFrJYXO/lyo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "7fc393d1b46fa000d48ff14e8b6a3c9985f03af0",
"rev": "4b2287113c2f9a2331c04899b2e2e5ab92dea9c5",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "master",
"repo": "nixpkgs",
"type": "github"
}
@@ -139,16 +158,16 @@
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1780902259,
"narHash": "sha256-q8yYEC5f1mFlQO9RGna4LTc9QrcvWunX6FYp83munkQ=",
"lastModified": 1751274312,
"narHash": "sha256-/bVBlRpECLVzjV19t5KMdMFWSwKLtb5RyXdjz3LJT+g=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "bd0ff2d3eac24699c3664d5966b9ef36f388e2ca",
"rev": "50ab793786d9de88ee30ec4e4c24fb4236fc2674",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-26.05",
"ref": "nixos-24.11",
"repo": "nixpkgs",
"type": "github"
}
@@ -170,6 +189,21 @@
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1777728799,
"narHash": "sha256-z7jjYQqhkFKab92VQ3duB7QVO7f7Y62qTFrJYXO/lyo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4b2287113c2f9a2331c04899b2e2e5ab92dea9c5",
"type": "github"
},
"original": {
"owner": "nixos",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1778737229,
"narHash": "sha256-6xWoytx8jFW4PF1GjRm/i/53trbpKGfz6zjzQGBr4cI=",
@@ -185,13 +219,13 @@
"type": "github"
}
},
"nixpkgs_3": {
"nixpkgs_4": {
"locked": {
"lastModified": 1780749050,
"narHash": "sha256-3av0pIjlOWQ6rDbNOmpUSvbNnJkGORQKKjb4LtCZsIY=",
"lastModified": 1778869304,
"narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a799d3e3886da994fa307f817a6bc705ae538eeb",
"rev": "d233902339c02a9c334e7e593de68855ad26c4cb",
"type": "github"
},
"original": {
@@ -201,13 +235,13 @@
"type": "github"
}
},
"nixpkgs_4": {
"nixpkgs_5": {
"locked": {
"lastModified": 1780336545,
"narHash": "sha256-vhVhuXzFrIOfcssC/9hDHx7MHzDKjF3keHuREOQqQiQ=",
"lastModified": 1779259093,
"narHash": "sha256-7DKWmH23hL2eYdkxCKeqj2i+yljTKuU+3Nk1UPHOnxc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4df1b885d76a54e1aa1a318f8d16fd6005b6401f",
"rev": "d99b013d5d1931ad77fe3912ed218170dec5d9a4",
"type": "github"
},
"original": {
@@ -220,15 +254,15 @@
"nixvim": {
"inputs": {
"flake-parts": "flake-parts",
"nixpkgs": "nixpkgs_4",
"nixpkgs": "nixpkgs_5",
"systems": "systems_2"
},
"locked": {
"lastModified": 1780995253,
"narHash": "sha256-6Lsoyw2XPvY8YNMCtPnsyw0JVVtHsXP2xtrFJBBTAOQ=",
"lastModified": 1779399125,
"narHash": "sha256-MWvSXTl1xUJsf74f1wD9mCWWsYN4uR1iAJNmUEnknqw=",
"owner": "nix-community",
"repo": "nixvim",
"rev": "43a7e6f82978ac975c3bba6728869b231e7a1ba0",
"rev": "7f6f92a3c9f5e1d61d417bf761b4934ac5acc401",
"type": "github"
},
"original": {
@@ -239,9 +273,10 @@
},
"root": {
"inputs": {
"bip110": "bip110",
"btc-clients": "btc-clients",
"nix-bitcoin": "nix-bitcoin",
"nixpkgs": "nixpkgs_3",
"nixpkgs": "nixpkgs_4",
"nixpkgs-stable": "nixpkgs-stable",
"nixvim": "nixvim"
}
@@ -263,16 +298,15 @@
},
"systems_2": {
"locked": {
"lastModified": 1774449309,
"narHash": "sha256-brhZ8DmuGtzkCYHJg4HEd602amKm89Y9ytsFZ5uWD1w=",
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "c29398b59d2048c4ab79345812849c9bd15e9150",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"ref": "future-26.11",
"repo": "default",
"type": "github"
}
+4 -2
View File
@@ -6,10 +6,11 @@
nix-bitcoin.url = "github:fort-nix/nix-bitcoin/release";
nixvim.url = "github:nix-community/nixvim";
btc-clients.url = "github:emmanuelrosa/btc-clients-nix";
nixpkgs-stable.url = "github:nixos/nixpkgs/nixos-26.05";
nixpkgs-stable.url = "github:nixos/nixpkgs/nixos-24.11";
bip110.url = "github:emmanuelrosa/bitcoin-knots-bip-110-nix";
};
outputs = { self, nixpkgs, nix-bitcoin, nixvim, btc-clients, nixpkgs-stable, ... }:
outputs = { self, nixpkgs, nix-bitcoin, nixvim, btc-clients, nixpkgs-stable, bip110, ... }:
let
overlay-stable = final: prev: {
@@ -55,6 +56,7 @@
btc-clients.packages.${pkgs.system}.bisq2
btc-clients.packages.${pkgs.system}.sparrow
];
sovran_systemsOS.packages.bip110 = bip110.packages.${pkgs.system}.bitcoind-knots-bip-110;
};
};
};
-52
View File
@@ -1,52 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#153126"/>
<stop offset="55%" stop-color="#0F241B"/>
<stop offset="100%" stop-color="#091C14"/>
</linearGradient>
<linearGradient id="outerArc" x1="70" y1="40" x2="190" y2="210" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#42F39A"/>
<stop offset="45%" stop-color="#28D978"/>
<stop offset="100%" stop-color="#1AA45D"/>
</linearGradient>
<linearGradient id="innerArc" x1="90" y1="60" x2="180" y2="190" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#27C86F"/>
<stop offset="100%" stop-color="#157E49"/>
</linearGradient>
<filter id="innerShade" x="-10%" y="-10%" width="120%" height="120%">
<feOffset dx="0" dy="2"/>
<feGaussianBlur stdDeviation="5" result="blur"/>
<feComposite in="blur" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 .18 0"/>
</filter>
</defs>
<rect width="256" height="256" rx="48" ry="48" fill="url(#bg)"/>
<rect x="1.5" y="1.5" width="253" height="253" rx="46.5" ry="46.5"
fill="none" stroke="rgba(255,255,255,0.08)"/>
<rect x="6" y="6" width="244" height="244" rx="42" ry="42"
fill="none" filter="url(#innerShade)"/>
<path d="M128 32 A96 96 0 1 1 58 196"
fill="none"
stroke="url(#outerArc)"
stroke-width="12"
stroke-linecap="round"/>
<path d="M128 56 A72 72 0 1 1 76 178"
fill="none"
stroke="url(#innerArc)"
stroke-width="10"
stroke-linecap="round"/>
<circle cx="128" cy="128" r="8" fill="#F2FFF7"/>
<circle cx="128" cy="128" r="18" fill="none" stroke="#7BFFC0" stroke-opacity="0.14" stroke-width="4"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

+23
View File
@@ -0,0 +1,23 @@
{ config, lib, pkgs, ... }:
let
cfg = config.sovran_systemsOS;
in
{
options.sovran_systemsOS.packages.bip110 = lib.mkOption {
type = lib.types.nullOr lib.types.package;
default = null;
description = "BIP110 Bitcoin package";
};
config = lib.mkIf (
cfg.features.bip110 &&
cfg.packages.bip110 != null
) {
services.bitcoind.package = lib.mkForce cfg.packages.bip110;
environment.systemPackages = [
cfg.packages.bip110
];
};
}
+1 -1
View File
@@ -4,7 +4,7 @@ lib.mkIf config.sovran_systemsOS.services.bitcoin {
services.bitcoind = {
enable = true;
package = pkgs.bitcoind-knots;
package = config.nix-bitcoin.pkgs.bitcoind-knots;
dataDir = "/run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node";
txindex = true;
tor.proxy = true;
+2 -1
View File
@@ -24,7 +24,7 @@
})
# ── Bitcoin Node Only Role ────────────────────────────────
# Bitcoin ecosystem + mempool, BTCPay runs but not exposed via Caddy
# Bitcoin ecosystem + mempool + bip110, BTCPay runs but not exposed via Caddy
(lib.mkIf config.sovran_systemsOS.roles.node {
sovran_systemsOS.services = {
bitcoin = lib.mkDefault true;
@@ -36,6 +36,7 @@
sovran_systemsOS.features = {
mempool = lib.mkDefault true;
bip110 = lib.mkDefault true;
};
sovran_systemsOS.web.btcpayserver = lib.mkDefault false;
+1 -24
View File
@@ -43,24 +43,12 @@
# ── Features (default OFF — user can enable in custom.nix) ──
features = {
haven = lib.mkEnableOption "Haven NOSTR relay";
bip110 = lib.mkEnableOption "BIP-110 Bitcoin Better Money";
mempool = lib.mkEnableOption "Bitcoin Mempool Explorer";
element-calling = lib.mkEnableOption "Element Video and Audio Calling";
bitcoin-core = lib.mkEnableOption "Bitcoin Core";
rdp = lib.mkEnableOption "Gnome Remote Desktop";
sshd = lib.mkEnableOption "SSH remote access";
# Deprecated: BIP-110 is now built into mainline Bitcoin Knots and is the
# default node. This option is retained ONLY so that existing machines with
# `sovran_systemsOS.features.bip110 = lib.mkForce true;` left in their local
# custom.nix continue to evaluate. It has no effect and will be removed in a
# future release once the Hub has cleaned up old custom.nix files.
bip110 = lib.mkOption {
type = lib.types.nullOr lib.types.bool;
default = null;
internal = true;
visible = false;
description = "(Deprecated, no-op) BIP-110 is now built into Bitcoin Knots.";
};
};
# ── Web exposure (controls Caddy vhosts) ──────────────────
@@ -101,15 +89,4 @@
description = "Nostr public key (npub1...) for Haven relay";
};
};
config = lib.mkIf (config.sovran_systemsOS.features.bip110 != null) {
warnings = [
''
sovran_systemsOS.features.bip110 is deprecated and has no effect:
BIP-110 is now built into mainline Bitcoin Knots, which is the default node.
You can safely remove the `sovran_systemsOS.features.bip110` line from
/etc/nixos/custom.nix. The Sovran Hub will also remove it automatically.
''
];
};
}
+9 -26
View File
@@ -29,7 +29,10 @@ let
]
# ── Bitcoin Base (node implementations) ────────────────────
++ lib.optionals cfg.services.bitcoin [
{ name = "Bitcoin Knots + BIP110"; unit = "bitcoind.service"; type = "system"; icon = "bip110"; enabled = cfg.services.bitcoin && !cfg.features.bitcoin-core; category = "bitcoin-base"; credentials = [
{ name = "Bitcoin Knots + BIP110"; unit = "bitcoind.service"; type = "system"; icon = "bip110"; enabled = cfg.features.bip110; category = "bitcoin-base"; credentials = [
{ label = "Tor Address Access from anywhere via Tor Browser"; file = "/var/lib/tor/onion/bitcoind/hostname"; prefix = "http://"; }
]; }
{ name = "Bitcoin Knots"; unit = "bitcoind.service"; type = "system"; icon = "bitcoind"; enabled = cfg.services.bitcoin && !cfg.features.bitcoin-core && !cfg.features.bip110; category = "bitcoin-base"; credentials = [
{ label = "Tor Address Access from anywhere via Tor Browser"; file = "/var/lib/tor/onion/bitcoind/hostname"; prefix = "http://"; }
]; }
{ name = "Bitcoin Core"; unit = "bitcoind.service"; type = "system"; icon = "bitcoin-core"; enabled = cfg.features.bitcoin-core; category = "bitcoin-base"; credentials = [
@@ -135,11 +138,7 @@ let
RC=0
echo " Step 1/3: nix flake update "
if ! nix flake update --flake /etc/nixos --print-build-logs \
--option connect-timeout 10 \
--option stalled-download-timeout 90 \
--option download-attempts 7 \
--option fallback true 2>&1; then
if ! nix flake update --flake /etc/nixos --print-build-logs 2>&1; then
echo "[ERROR] nix flake update failed"
RC=1
fi
@@ -147,11 +146,7 @@ let
if [ "$RC" -eq 0 ]; then
echo " Step 2/3: nixos-rebuild "
SWITCH_OUT=$(nixos-rebuild switch --flake /etc/nixos --print-build-logs \
--option connect-timeout 10 \
--option stalled-download-timeout 90 \
--option download-attempts 7 \
--option fallback true 2>&1)
SWITCH_OUT=$(nixos-rebuild switch --flake /etc/nixos --print-build-logs 2>&1)
SWITCH_RC=$?
echo "$SWITCH_OUT"
if [ "$SWITCH_RC" -eq 0 ]; then
@@ -160,11 +155,7 @@ let
echo ""
echo " Build succeeded a reboot is required to apply this update"
echo " (Critical system components changed; running nixos-rebuild boot instead)"
if nixos-rebuild boot --flake /etc/nixos --print-build-logs \
--option connect-timeout 10 \
--option stalled-download-timeout 90 \
--option download-attempts 7 \
--option fallback true 2>&1; then
if nixos-rebuild boot --flake /etc/nixos --print-build-logs 2>&1; then
echo "REBOOT_REQUIRED" > "$STATUS"
exit 0
else
@@ -218,11 +209,7 @@ let
echo ""
echo ""
echo " Rebuilding system configuration "
SWITCH_OUT=$(nixos-rebuild switch --flake /etc/nixos --print-build-logs \
--option connect-timeout 10 \
--option stalled-download-timeout 90 \
--option download-attempts 7 \
--option fallback true 2>&1)
SWITCH_OUT=$(nixos-rebuild switch --flake /etc/nixos --print-build-logs 2>&1)
SWITCH_RC=$?
echo "$SWITCH_OUT"
if [ "$SWITCH_RC" -eq 0 ]; then
@@ -235,11 +222,7 @@ let
echo ""
echo " Build succeeded a reboot is required to apply this rebuild"
echo " (Critical system components changed; running nixos-rebuild boot instead)"
if nixos-rebuild boot --flake /etc/nixos --print-build-logs \
--option connect-timeout 10 \
--option stalled-download-timeout 90 \
--option download-attempts 7 \
--option fallback true 2>&1; then
if nixos-rebuild boot --flake /etc/nixos --print-build-logs 2>&1; then
echo "REBOOT_REQUIRED" > "$STATUS"
else
echo "[ERROR] nixos-rebuild boot also failed"
+1
View File
@@ -31,6 +31,7 @@
# ── Features (default OFF — enable in custom.nix) ─────────
./haven.nix
./bip110.nix
./element-calling.nix
./mempool.nix
./bitcoin-core.nix