NixOS already knows whether a reboot is pending: /nix/var/nix/profiles/
system vs /run/current-system. Marker files only the Hub's own updater
wrote desynced for terminal-updated machines (and markers from older
updaters could never clear), pinning the badge on forever. Reconcile
REBOOT_REQUIRED against live state on every read; the stale marker
self-heals to IDLE. The .generation marker write is now informational.
The local packages/lndinit/default.nix is a verbatim copy of the
upstream Nixpkgs expression, frozen at v0.1.3-beta (the version that
was vendored in from nix-bitcoin before the Aug 10 2026 refactor in
commit 1fbeafd). It carries no Sovran-specific patches, no local
overrides, and no behavioral modifications - it is byte-for-byte
identical to what Nixpkgs ships, except ~19 minor versions older
(Nixpkgs currently ships 0.1.22-beta; the developer upstream
lightninglabs/lndinit is at v0.1.36-beta as of June 10 2026).
Why this matters
----------------
The Aug 10 2026 refactor (1fbeafd, "refactor: move vendor/nix-bitcoin
to modules/bitcoin, remove overlays") stated the new convention:
No more random vendor/ or pkgs/ dirs - follows Sovran convention:
modules/ for NixOS modules, packages/ for packages
That refactor successfully removed:
* pkgs/sovran-overlay.nix
* pkgs/nbxplorer.nix
* pkgs/README.md
* modules/vendor/ (entire directory)
* overlay-sovran from flake.nix
It moved the lndinit expression into packages/lndinit/default.nix as
an intermediate step, but the file is still a verbatim upstream copy
and therefore still incurs the maintenance burden the refactor was
meant to eliminate: manual version bumps, manual hash refreshes, and
no upstream security or bug-fix flow. Removing it completes the
intent of 1fbeafd.
The change
----------
modules/bitcoin/lnd.nix (line 153):
- lndinit = "${(pkgs.callPackage ../../packages/lndinit {})}/bin/lndinit";
+ lndinit = "${pkgs.lndinit}/bin/lndinit";
The two later uses of `lndinit` in the same file (lines 243 and 247,
inside the systemd.services.lnd.preStart block that calls
`lndinit gen-seed` and `lndinit init-wallet`) are unchanged because
they reference the let-bound `lndinit` value, not the callPackage
expression. They continue to work with the new pkgs.lndinit binary
path transparently.
Removed:
* packages/lndinit/default.nix
* packages/lndinit/ (now empty directory)
No other files in the repository reference packages/lndinit. Verified
by:
* Git tree search for "packages/lndinit" -> only the file and its
parent directory match
* Content grep of flake.nix, configuration.nix,
modules/bitcoin/default.nix, modules/bitcoin/common.nix, and
iso/common.nix -> zero matches
Why this is safe
----------------
1. CLI compatibility. The preStart script only invokes two
lndinit subcommands:
* `lndinit gen-seed`
* `lndinit -v init-wallet --file.seed=... --file.wallet-password=... --init-file.output-wallet-dir=...`
Both subcommands and all four flags have been stable since the
0.1.x line. The Nixpkgs 0.1.22-beta binary produces a wallet.db
and admin.macaroon in the same on-disk format that 0.1.3-beta did
for the same LND version (LND is pinned separately by pkgs.lnd
from Nixpkgs and is unaffected by this change).
2. No coupled Go modules or shared vendor tree. The local
packages/lndinit/default.nix is a self-contained buildGoModule
derivation; it has no shared state with any other Sovran package.
3. Nixpkgs pin is current. flake.nix pins
github:NixOS/nixpkgs/nixos-unstable, which has shipped pkgs.lndinit
since 2022 and is currently at 0.1.22-beta. There is no
"missing attribute" risk.
4. Wallet data is forward-compatible. The wallet.db format is owned
by LND, not lndinit. lndinit is only used at first boot to create
the seed and initialize the wallet; subsequent LND restarts do
not invoke lndinit. So even if a user already initialized a
wallet with 0.1.3-beta, the binary being upgraded to 0.1.22-beta
is irrelevant - LND owns the wallet from that point on.
5. Single call site. Only modules/bitcoin/lnd.nix references
lndinit. No other modules, scripts, or tests need to change.
Operational notes
-----------------
* After this commit, lndinit updates flow through the normal
`nix flake update` workflow (or whatever automated dependency
tooling is already in use, e.g. for the recent "chore(deps):
update RTL to 0.15.10" commits). No Sovran-side action is needed
to pick up future lndinit versions.
* If a future LND version requires a specific lndinit version, the
pin can be done in flake.nix via a one-line overlay:
nixpkgs.overlays = [ (final: prev: {
lndinit = prev.lndinit.overrideAttrs (o: {
version = "X.Y.Z-beta";
src = prev.fetchFromGitHub { ... };
vendorHash = "...";
});
}) ];
This keeps the upgrade path explicit without bringing the entire
expression back into the Sovran tree.
* This drops ~20 lines of frozen derivation code, eliminates one
source of upstream drift, and reduces the surface area of what
Sovran needs to keep current.
The full-system updater runs as a detached systemd service and can finish
successfully even when the browser loses its status connection. In that
case the update log and status file correctly report REBOOT_REQUIRED, but
the Hub modal can remain on "Updating..." with its controls disabled.
There were four independent ways for the frontend to get stuck:
* update status fetches had no deadline, so a request that stayed pending
never rejected and never advanced the existing failure counter;
* setInterval started async polls without waiting for the previous poll,
allowing slow requests to overlap and responses to arrive out of order;
* each log chunk used textContent +=, replacing the complete and growing
Nix build log every two seconds, which could stall browser rendering and
was especially visible over RDP; and
* page reload, tab resume, and RDP reconnect did not reattach the modal to
the update status persisted by the backend.
This produced a dangerous UX mismatch: the machine had a fully staged
NixOS generation and was ready to reboot, while the Hub continued telling
the user that the update was still running.
Bound status requests with AbortController, prevent overlapping polls, and
replace the endless spinner after sustained failures with an explicit
"Update status unavailable" state and Retry Status action. Reconcile state
immediately on focus, visibility, online, page startup, and before starting
a new update. Use no-store requests and render verbose logs incrementally
with a bounded visible tail while retaining the complete report in memory.
Apply the same timeout and single-flight protection to rebuild polling.
Record the exact generation produced by `nixos-rebuild boot`. The Hub now
keeps REBOOT_REQUIRED visible until that generation matches
/run/current-system, then clears the marker after reboot. For an update
started by an older updater that did not write the marker, recover the
staged generation from the final nixos-rebuild log line. The dashboard
sidebar also distinguishes update-in-progress and restart-required states.
Regression coverage verifies generation marker/log recovery, pre- versus
post-reboot detection, request timeout wiring, single-flight polling,
connection-loss UX, RDP/tab resume reconciliation, bounded log rendering,
page-reload recovery, and JavaScript syntax.
Validation:
* python3 -m unittest discover -s tests -p 'test_*.py' -v (170 passed)
* node --check app/sovran_systemsos_web/static/js/*.js
* python3 -m py_compile for changed Python modules
* git diff --check
A Nix evaluation was not available in the development sandbox; the NixOS
module should still be evaluated and built in CI or on a test machine before
release.
nixpkgs commit c8f9654 refactored the services.i2pd module to use
an RFC42-style settings attribute set and removed services.i2pd.proto.
After updating the root nixpkgs input from f13ff45 to ec2d622, the
vendored bitcoind module failed evaluation on the obsolete
services.i2pd.proto.sam.enable definition.
The error occurred even with services.bitcoind.i2p at its false default:
bitcoind was enabled, so NixOS still validated the obsolete option path
inside the conditional i2pd integration.
Read the SAM endpoint from services.i2pd.settings.sam and configure its
new upstream-style fields explicitly. Keep 127.0.0.1:7656, matching the
old typed option defaults that bitcoind uses to generate its i2psam
setting.
This preserves optional I2P support without activating it by default.
i2pd remains disabled until services.bitcoind.i2p is set to true or
"only-outgoing".
Nixpkgs migration: https://github.com/NixOS/nixpkgs/commit/c8f965411e812060a9377fa4c2d7d0f84e8b10e0
livekit-turn-setup.service detects the primary interface from the IPv4
default route, but had no ordering against network-online.target. With
NetworkManager+DHCP the default route is applied late at boot, so the
oneshot could run before it existed, exit 1, and — being a hard
dependency of livekit.service — take livekit down with it. The Hub then
showed a 'failed' red dot until livekit was restarted manually.
Order both livekit.service and livekit-turn-setup.service after
network-online.target. Also add a bounded retry when copying Caddy's ACME
cert so we never write an empty turn.crt/turn.key on a fresh boot.
The LND-only rewrite of lndconnect.nix shipped a wrapper Zeus cannot
use: unknown flags (--cert/--macaroon), a non-existent onion path
(free/lnd.onion), and a REST hidden service that collided with LND's
P2P onion. Restore the nix-bitcoin contract — dedicated lnd-rest
onion on port 8080, --nocert over Tor, admin macaroon in the URI —
and only persist a valid lndconnect:// URI for the Hub QR.
The Hub launcher used an ephemeral /tmp profile deleted on exit, which
wiped the hub_manual_logout marker cookie. On reopen, /auto-login minted a
new session and logged the user straight back in without a password.
Use a persistent per-user profile under XDG_STATE_HOME and drop the
deletion trap so the logout marker survives close/reopen. Keep
--skip-origin-startup-dialog. Adds regression tests.