Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9531a2efdd | |||
| 24be048f98 | |||
| 467990b527 | |||
| 426451b9b8 | |||
| 20ccde21fe | |||
| a7931e674a | |||
| 51609a1217 | |||
| 629f5f76c2 | |||
| b6e9dea268 |
@@ -10,14 +10,6 @@
|
||||
|
||||
</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
|
||||
@@ -47,7 +39,7 @@ The control center is the **Hub** — a built-in panel that lets the operator la
|
||||
│ flake.nix │
|
||||
│ inputs: nixpkgs, │
|
||||
│ nix-bitcoin, nixvim, │
|
||||
│ btc-clients │
|
||||
│ btc-clients, bip110 │
|
||||
└───────────┬─────────────┘
|
||||
│ nixosModules.Sovran_SystemsOS
|
||||
▼
|
||||
@@ -86,9 +78,10 @@ Defaults follow the import order in `modules/modules.nix`. Toggles live in `cust
|
||||
| `bitcoinecosystem.nix` | **on** | bitcoind/electrs/LND/RTL/BTCPay (over Tor) |
|
||||
| `wallet-autoconnect.nix` | **on** | Sparrow/Bisq ↔ node handshake |
|
||||
| `haven.nix` | off | Nostr relay |
|
||||
| `bip110.nix` | off | Bitcoin Knots BIP-110 |
|
||||
| `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) |
|
||||
| `bitcoin-core.nix` | off | Standalone bitcoind |
|
||||
| `rdp.nix` | off | xrdp remote desktop |
|
||||
| `sshd.nix` | off | Public-facing OpenSSH |
|
||||
|
||||
@@ -121,19 +114,21 @@ Internal commands. Run from the flake root.
|
||||
|
||||
## 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.
|
||||
- **Firewall on by default** (`networking.firewall.enable = true`). The only port opened at host level is **UDP 5353** for mDNS (Avahi). Every other port is opened by the module that needs it.
|
||||
- **Caddy** (`modules/core/caddy.nix`) terminates TLS for all HTTP services. Operator vhosts go through `sovran_systemsOS.caddy.extraVirtualHosts`.
|
||||
- **Njalla** dynamic DNS (`modules/core/njalla.nix`) keeps records in sync via a 15-minute cron job.
|
||||
- **Avahi** publishes `sovransystemsos.local` on the LAN.
|
||||
- **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`).
|
||||
|
||||
- **SSH:** localhost-only by default (`core/sshd-localhost.nix`). Public OpenSSH is opt-in (`modules/sshd.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**
|
||||
- **Firewall on, public sshd off, RDP off, auto-login off.**
|
||||
- **EFI** is mounted with `umask=0077`.
|
||||
- **Kernel surface trimmed.** `boot.blacklistedKernelModules = [ "rxrpc" ];`
|
||||
- **Weekly garbage collection** with `--delete-older-than 7d`.
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 |
+3
-9
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
Executable
+23
@@ -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
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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.
|
||||
''
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
|
||||
# ── Features (default OFF — enable in custom.nix) ─────────
|
||||
./haven.nix
|
||||
./bip110.nix
|
||||
./element-calling.nix
|
||||
./mempool.nix
|
||||
./bitcoin-core.nix
|
||||
|
||||
Reference in New Issue
Block a user