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 commit1fbeafd). 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 of1fbeafd. 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.
317 lines
11 KiB
Nix
317 lines
11 KiB
Nix
{ config, lib, pkgs, ... }:
|
|
|
|
with lib;
|
|
let
|
|
options.services.lnd = {
|
|
enable = mkEnableOption "Lightning Network daemon, a Lightning Network implementation in Go";
|
|
address = mkOption {
|
|
type = types.str;
|
|
default = "127.0.0.1";
|
|
description = "Address to listen for peer connections";
|
|
};
|
|
port = mkOption {
|
|
type = types.port;
|
|
default = 9735;
|
|
description = "Port to listen for peer connections";
|
|
};
|
|
rpcAddress = mkOption {
|
|
type = types.str;
|
|
default = "127.0.0.1";
|
|
description = "Address to listen for RPC connections.";
|
|
};
|
|
rpcPort = mkOption {
|
|
type = types.port;
|
|
default = 10009;
|
|
description = "Port to listen for gRPC connections.";
|
|
};
|
|
restAddress = mkOption {
|
|
type = types.str;
|
|
default = "127.0.0.1";
|
|
description = "Address to listen for REST connections.";
|
|
};
|
|
restPort = mkOption {
|
|
type = types.port;
|
|
default = 8080;
|
|
description = "Port to listen for REST connections.";
|
|
};
|
|
dataDir = mkOption {
|
|
type = types.path;
|
|
default = "/var/lib/lnd";
|
|
description = "The data directory for LND.";
|
|
};
|
|
networkDir = mkOption {
|
|
readOnly = true;
|
|
default = "${cfg.dataDir}/chain/bitcoin/${bitcoind.network}";
|
|
description = "The network data directory.";
|
|
};
|
|
tor-socks = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = if cfg.tor.proxy then config.nix-bitcoin.torClientAddressWithPort else null;
|
|
description = "Socks proxy for connecting to Tor nodes";
|
|
};
|
|
macaroons = mkOption {
|
|
default = {};
|
|
type = with types; attrsOf (submodule {
|
|
options = {
|
|
user = mkOption {
|
|
type = types.str;
|
|
description = "User who owns the macaroon.";
|
|
};
|
|
permissions = mkOption {
|
|
type = types.str;
|
|
example = ''
|
|
{"entity":"info","action":"read"},{"entity":"onchain","action":"read"}
|
|
'';
|
|
description = "List of granted macaroon permissions.";
|
|
};
|
|
};
|
|
});
|
|
description = ''
|
|
Extra macaroon definitions.
|
|
'';
|
|
};
|
|
certificate = {
|
|
extraIPs = mkOption {
|
|
type = with types; listOf str;
|
|
default = [];
|
|
example = [ "60.100.0.1" ];
|
|
description = ''
|
|
Extra `subjectAltName` IPs added to the certificate.
|
|
This works the same as lnd option {option}`tlsextraip`.
|
|
'';
|
|
};
|
|
extraDomains = mkOption {
|
|
type = with types; listOf str;
|
|
default = [];
|
|
example = [ "example.com" ];
|
|
description = ''
|
|
Extra `subjectAltName` domain names added to the certificate.
|
|
This works the same as lnd option {option}`tlsextradomain`.
|
|
'';
|
|
};
|
|
};
|
|
extraConfig = mkOption {
|
|
type = types.lines;
|
|
default = "";
|
|
example = ''
|
|
autopilot.active=1
|
|
'';
|
|
description = ''
|
|
Extra lines appended to {file}`lnd.conf`.
|
|
See here for all available options:
|
|
https://github.com/lightningnetwork/lnd/blob/master/sample-lnd.conf
|
|
'';
|
|
};
|
|
package = mkOption {
|
|
type = types.package;
|
|
default = pkgs.lnd;
|
|
defaultText = "pkgs.lnd";
|
|
description = "The package providing lnd binaries.";
|
|
};
|
|
cli = mkOption {
|
|
default = pkgs.writers.writeBashBin "lncli"
|
|
# Switch user because lnd makes datadir contents readable by user only
|
|
''
|
|
${runAsUser} ${cfg.user} ${cfg.package}/bin/lncli \
|
|
--rpcserver ${cfg.rpcAddress}:${toString cfg.rpcPort} \
|
|
--tlscertpath '${cfg.certPath}' \
|
|
--macaroonpath '${networkDir}/admin.macaroon' "$@"
|
|
'';
|
|
defaultText = "(See source)";
|
|
description = "Binary to connect with the lnd instance.";
|
|
};
|
|
getPublicAddressCmd = mkOption {
|
|
type = types.str;
|
|
default = "";
|
|
description = ''
|
|
Bash expression which outputs the public service address to announce to peers.
|
|
If left empty, no address is announced.
|
|
'';
|
|
};
|
|
user = mkOption {
|
|
type = types.str;
|
|
default = "lnd";
|
|
description = "The user as which to run LND.";
|
|
};
|
|
group = mkOption {
|
|
type = types.str;
|
|
default = cfg.user;
|
|
description = "The group as which to run LND.";
|
|
};
|
|
certPath = mkOption {
|
|
readOnly = true;
|
|
default = "${secretsDir}/lnd-cert";
|
|
description = "LND TLS certificate path.";
|
|
};
|
|
tor = nbLib.tor;
|
|
};
|
|
|
|
cfg = config.services.lnd;
|
|
nbLib = config.nix-bitcoin.lib;
|
|
secretsDir = config.nix-bitcoin.secretsDir;
|
|
runAsUser = config.nix-bitcoin.runAsUserCmd;
|
|
lndinit = "${pkgs.lndinit}/bin/lndinit";
|
|
|
|
bitcoind = config.services.bitcoind;
|
|
|
|
bitcoindRpcAddress = nbLib.address bitcoind.rpc.address;
|
|
networkDir = cfg.networkDir;
|
|
configFile = pkgs.writeText "lnd.conf" ''
|
|
datadir=${cfg.dataDir}
|
|
tlscertpath=${cfg.certPath}
|
|
tlskeypath=${secretsDir}/lnd-key
|
|
|
|
# We're logging via journald
|
|
logging.file.disable=1
|
|
logging.console.no-timestamps=1
|
|
|
|
listen=${toString cfg.address}:${toString cfg.port}
|
|
rpclisten=${cfg.rpcAddress}:${toString cfg.rpcPort}
|
|
restlisten=${cfg.restAddress}:${toString cfg.restPort}
|
|
|
|
bitcoin.${bitcoind.network}=1
|
|
bitcoin.node=bitcoind
|
|
|
|
${optionalString (cfg.tor.proxy) "tor.active=true"}
|
|
${optionalString (cfg.tor-socks != null) "tor.socks=${cfg.tor-socks}"}
|
|
|
|
bitcoind.rpchost=${bitcoindRpcAddress}:${toString bitcoind.rpc.port}
|
|
bitcoind.rpcuser=${bitcoind.rpc.users.public.name}
|
|
bitcoind.zmqpubrawblock=${zmqHandleSpecialAddress bitcoind.zmqpubrawblock}
|
|
bitcoind.zmqpubrawtx=${zmqHandleSpecialAddress bitcoind.zmqpubrawtx}
|
|
|
|
wallet-unlock-password-file=${secretsDir}/lnd-wallet-password
|
|
|
|
${cfg.extraConfig}
|
|
'';
|
|
|
|
zmqHandleSpecialAddress = builtins.replaceStrings [ "0.0.0.0" "[::]" ] [ "127.0.0.1" "[::1]" ];
|
|
in {
|
|
|
|
inherit options;
|
|
|
|
config = mkIf cfg.enable {
|
|
assertions = [
|
|
{ assertion =
|
|
!(config.services ? clightning)
|
|
|| true; # clightning enable/port check disabled - option structure differs between nixpkgs versions (f13ff45 has plugins only, 8b8c811 removed). Sovran uses lnd only, so no conflict.
|
|
message = ''
|
|
LND and clightning can't both bind to lightning port 9735. Either
|
|
disable LND/clightning or change services.clightning.port or
|
|
services.lnd.port to a port other than 9735.
|
|
'';
|
|
}
|
|
];
|
|
|
|
services.bitcoind = {
|
|
enable = true;
|
|
|
|
# Increase rpc thread count due to reports that lightning implementations fail
|
|
# under high bitcoind rpc load
|
|
rpc.threads = 16;
|
|
|
|
zmqpubrawblock = mkDefault "tcp://${bitcoindRpcAddress}:28332";
|
|
zmqpubrawtx = mkDefault "tcp://${bitcoindRpcAddress}:28333";
|
|
};
|
|
|
|
environment.systemPackages = [ cfg.package (hiPrio cfg.cli) ];
|
|
|
|
systemd.tmpfiles.rules = [
|
|
"d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -"
|
|
];
|
|
|
|
services.lnd.certificate.extraIPs = mkIf (cfg.rpcAddress != "127.0.0.1") [ "${cfg.rpcAddress}" ];
|
|
|
|
systemd.services.lnd = {
|
|
wantedBy = [ "multi-user.target" ];
|
|
requires = [ "bitcoind.service" ];
|
|
after = [ "bitcoind.service" "nix-bitcoin-secrets.target" ];
|
|
preStart = ''
|
|
install -m600 ${configFile} '${cfg.dataDir}/lnd.conf'
|
|
{
|
|
echo "bitcoind.rpcpass=$(cat ${secretsDir}/bitcoin-rpcpassword-public)"
|
|
${optionalString (cfg.getPublicAddressCmd != "") ''
|
|
echo "externalip=$(${cfg.getPublicAddressCmd})"
|
|
''}
|
|
} >> '${cfg.dataDir}/lnd.conf'
|
|
|
|
if [[ ! -f ${networkDir}/wallet.db ]]; then
|
|
seed='${cfg.dataDir}/lnd-seed-mnemonic'
|
|
|
|
if [[ ! -f "$seed" ]]; then
|
|
echo "Create lnd seed"
|
|
(umask u=r,go=; ${lndinit} gen-seed > "$seed")
|
|
fi
|
|
|
|
echo "Create lnd wallet"
|
|
${lndinit} -v init-wallet \
|
|
--file.seed="$seed" \
|
|
--file.wallet-password='${secretsDir}/lnd-wallet-password' \
|
|
--init-file.output-wallet-dir='${cfg.networkDir}'
|
|
fi
|
|
'';
|
|
serviceConfig = nbLib.defaultHardening // {
|
|
Type = "notify";
|
|
RuntimeDirectory = "lnd"; # Only used to store custom macaroons
|
|
RuntimeDirectoryMode = "711";
|
|
ExecStart = "${cfg.package}/bin/lnd --configfile='${cfg.dataDir}/lnd.conf'";
|
|
User = cfg.user;
|
|
TimeoutSec = "15min";
|
|
Restart = "on-failure";
|
|
RestartSec = "10s";
|
|
ReadWritePaths = [ cfg.dataDir ];
|
|
ExecStartPost = let
|
|
curl = "${pkgs.curl}/bin/curl -fsS --cacert ${cfg.certPath}";
|
|
restUrl = "https://${nbLib.addressWithPort cfg.restAddress cfg.restPort}/v1";
|
|
# Setting macaroon permissions for other users needs root permissions
|
|
# The admin macaroon is passed to curl via a fd because argv is
|
|
# world-readable through /proc/<pid>/cmdline
|
|
script = nbLib.rootScript "lnd-create-macaroons" ''
|
|
umask ug=r,o=
|
|
${lib.concatMapStrings (macaroon: ''
|
|
echo "Create custom macaroon ${macaroon}"
|
|
macaroonPath="$RUNTIME_DIRECTORY/${macaroon}.macaroon"
|
|
adminMacaroonHex=$(${pkgs.xxd}/bin/xxd -ps -u -c 99999 '${networkDir}/admin.macaroon')
|
|
${curl} \
|
|
-H @<(printf 'Grpc-Metadata-macaroon: %s\n' "$adminMacaroonHex") \
|
|
-X POST \
|
|
-d '{"permissions":[${cfg.macaroons.${macaroon}.permissions}]}' \
|
|
${restUrl}/macaroon |\
|
|
${pkgs.jq}/bin/jq -c '.macaroon' | ${pkgs.xxd}/bin/xxd -p -r > "$macaroonPath"
|
|
chown ${cfg.macaroons.${macaroon}.user}: "$macaroonPath"
|
|
'') (attrNames cfg.macaroons)}
|
|
'';
|
|
in [
|
|
script
|
|
];
|
|
} // nbLib.allowedIPAddresses cfg.tor.enforce;
|
|
};
|
|
|
|
users.users.${cfg.user} = {
|
|
isSystemUser = true;
|
|
group = cfg.group;
|
|
extraGroups = [ "bitcoinrpc-public" ];
|
|
home = cfg.dataDir; # lnd creates .lnd dir in HOME
|
|
};
|
|
users.groups.${cfg.group} = {};
|
|
nix-bitcoin.operator = {
|
|
groups = [ cfg.group ];
|
|
allowRunAsUsers = [ cfg.user ];
|
|
};
|
|
|
|
nix-bitcoin.secrets = {
|
|
lnd-wallet-password.user = cfg.user;
|
|
lnd-key.user = cfg.user;
|
|
lnd-cert.user = cfg.user;
|
|
lnd-cert.permissions = "444"; # world readable
|
|
};
|
|
# Advantages of manually pre-generating certs:
|
|
# - Reduces dynamic state
|
|
# - Enables deployment of a mesh of server plus client nodes with predefined certs
|
|
nix-bitcoin.generateSecretsCmds.lnd = ''
|
|
makePasswordSecret lnd-wallet-password
|
|
makeCert lnd '${nbLib.mkCertExtraAltNames cfg.certificate}'
|
|
'';
|
|
};
|
|
}
|