nix-bitcoin/modules/lnd.nix

295 lines
10 KiB
Nix
Raw Normal View History

2019-08-05 08:44:38 +00:00
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.lnd;
nbLib = config.nix-bitcoin.lib;
secretsDir = config.nix-bitcoin.secretsDir;
runAsUser = config.nix-bitcoin.runAsUserCmd;
2020-10-16 15:43:05 +00:00
bitcoind = config.services.bitcoind;
bitcoindRpcAddress = bitcoind.rpc.address;
2020-10-16 15:43:07 +00:00
networkDir = "${cfg.dataDir}/chain/bitcoin/${bitcoind.network}";
2019-08-05 08:44:38 +00:00
configFile = pkgs.writeText "lnd.conf" ''
datadir=${cfg.dataDir}
logdir=${cfg.dataDir}/logs
tlscertpath=${secretsDir}/lnd-cert
tlskeypath=${secretsDir}/lnd-key
2019-08-05 08:44:38 +00:00
listen=${toString cfg.address}:${toString cfg.port}
rpclisten=${cfg.rpcAddress}:${toString cfg.rpcPort}
restlisten=${cfg.restAddress}:${toString cfg.restPort}
2020-10-16 15:43:07 +00:00
bitcoin.${bitcoind.network}=1
2019-08-05 08:44:38 +00:00
bitcoin.active=1
bitcoin.node=bitcoind
${optionalString (cfg.enforceTor) "tor.active=true"}
${optionalString (cfg.tor-socks != null) "tor.socks=${cfg.tor-socks}"}
2019-08-05 08:44:38 +00:00
bitcoind.rpchost=${bitcoindRpcAddress}:${toString bitcoind.rpc.port}
2020-10-16 15:43:05 +00:00
bitcoind.rpcuser=${bitcoind.rpc.users.public.name}
bitcoind.zmqpubrawblock=${bitcoind.zmqpubrawblock}
bitcoind.zmqpubrawtx=${bitcoind.zmqpubrawtx}
2019-08-05 08:44:38 +00:00
${cfg.extraConfig}
'';
in {
options.services.lnd = {
2021-02-01 21:53:15 +00:00
enable = mkEnableOption "Lightning Network Daemon";
2019-08-05 08:44:38 +00:00
dataDir = mkOption {
type = types.path;
default = "/var/lib/lnd";
description = "The data directory for LND.";
};
2020-10-16 15:43:07 +00:00
networkDir = mkOption {
readOnly = true;
default = networkDir;
description = "The network data directory.";
};
address = mkOption {
type = types.str;
default = "localhost";
description = "Address to listen for peer connections";
};
port = mkOption {
2020-08-04 07:45:02 +00:00
type = types.port;
default = 9735;
description = "Port to listen for peer connections";
2020-08-04 07:45:02 +00:00
};
rpcAddress = mkOption {
type = types.str;
default = "localhost";
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 = "localhost";
description = ''
Address to listen for REST connections.
'';
};
restPort = mkOption {
type = types.port;
default = 8080;
description = "Port to listen for REST connections.";
};
tor-socks = mkOption {
type = types.nullOr types.str;
default = if cfg.enforceTor then config.services.tor.client.socksListenAddress 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.
'';
};
2019-08-05 08:44:38 +00:00
extraConfig = mkOption {
2020-03-09 08:22:00 +00:00
type = types.lines;
default = "";
example = ''
autopilot.active=1
'';
description = "Extra lines appended to <filename>lnd.conf</filename>.";
2020-03-09 08:22:00 +00:00
};
package = mkOption {
type = types.package;
default = config.nix-bitcoin.pkgs.lnd;
2020-03-09 08:22:00 +00:00
description = "The package providing lnd binaries.";
};
2019-11-27 13:04:34 +00:00
cli = mkOption {
default = pkgs.writeScriptBin "lncli"
# Switch user because lnd makes datadir contents readable by user only
''
2021-02-16 16:50:39 +00:00
${runAsUser} ${cfg.user} ${cfg.package}/bin/lncli \
--rpcserver ${cfg.rpcAddress}:${toString cfg.rpcPort} \
--tlscertpath '${secretsDir}/lnd-cert' \
2020-10-16 15:43:07 +00:00
--macaroonpath '${networkDir}/admin.macaroon' "$@"
2019-11-27 13:04:34 +00:00
'';
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.
'';
};
2021-02-16 16:50:39 +00:00
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.";
};
inherit (nbLib) enforceTor;
2019-08-05 08:44:38 +00:00
};
config = mkIf cfg.enable {
2020-06-15 10:34:11 +00:00
assertions = [
2020-10-16 15:43:05 +00:00
{ assertion = bitcoind.prune == 0;
2020-06-15 10:34:11 +00:00
message = "lnd does not support bitcoind pruning.";
}
];
services.bitcoind = {
enable = true;
# Increase rpc thread count due to reports that lightning implementations fail
# under high bitcoind rpc load
rpc.threads = 16;
zmqpubrawblock = "tcp://${bitcoindRpcAddress}:28332";
zmqpubrawtx = "tcp://${bitcoindRpcAddress}:28333";
};
2020-10-18 12:49:20 +00:00
environment.systemPackages = [ cfg.package (hiPrio cfg.cli) ];
systemd.tmpfiles.rules = [
2021-02-16 16:50:39 +00:00
"d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -"
];
2019-08-05 08:44:38 +00:00
systemd.services.lnd = {
wantedBy = [ "multi-user.target" ];
requires = [ "bitcoind.service" ];
after = [ "bitcoind.service" ];
2019-08-05 08:44:38 +00:00
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'
2019-08-05 08:44:38 +00:00
'';
serviceConfig = nbLib.defaultHardening // {
RuntimeDirectory = "lnd"; # Only used to store custom macaroons
RuntimeDirectoryMode = "711";
2020-03-09 08:22:00 +00:00
ExecStart = "${cfg.package}/bin/lnd --configfile=${cfg.dataDir}/lnd.conf";
2021-02-16 16:50:39 +00:00
User = cfg.user;
2019-08-05 08:44:38 +00:00
Restart = "on-failure";
RestartSec = "10s";
ReadWritePaths = cfg.dataDir;
ExecStartPost = let
# Retrying is necessary because it can happen that the lnd socket is
# existing, but the RPC service isn't yet, which results in error
# "waiting to start, RPC services not available".
curl = "${pkgs.curl}/bin/curl -s --show-error --retry 10";
restUrl = "https://${cfg.restAddress}:${toString cfg.restPort}/v1";
in [
(nbLib.script "lnd-create-wallet" ''
attempts=250
while ! { exec 3>/dev/tcp/${cfg.restAddress}/${toString cfg.restPort} && exec 3>&-; } &>/dev/null; do
((attempts-- == 0)) && { echo "lnd REST service unreachable"; exit 1; }
sleep 0.1
done
2020-10-16 15:43:07 +00:00
if [[ ! -f ${networkDir}/wallet.db ]]; then
mnemonic="${cfg.dataDir}/lnd-seed-mnemonic"
if [[ ! -f "$mnemonic" ]]; then
echo Create lnd seed
umask u=r,go=
2021-03-16 11:45:22 +00:00
${curl} \
--cacert ${secretsDir}/lnd-cert \
-X GET ${restUrl}/genseed | ${pkgs.jq}/bin/jq -c '.cipher_seed_mnemonic' > "$mnemonic"
fi
echo Create lnd wallet
2021-03-16 11:45:22 +00:00
${curl} --output /dev/null \
--cacert ${secretsDir}/lnd-cert \
-X POST -d "{\"wallet_password\": \"$(cat ${secretsDir}/lnd-wallet-password | tr -d '\n' | base64 -w0)\", \
\"cipher_seed_mnemonic\": $(cat "$mnemonic" | tr -d '\n')}" \
${restUrl}/initwallet
# Guarantees that RPC calls with cfg.cli succeed after the service is started
echo Wait until wallet is created
2020-10-16 15:43:07 +00:00
while [[ ! -f ${networkDir}/admin.macaroon ]]; do
sleep 0.1
done
else
echo Unlock lnd wallet
2021-03-16 11:45:22 +00:00
${curl} \
2020-10-16 15:43:07 +00:00
-H "Grpc-Metadata-macaroon: $(${pkgs.xxd}/bin/xxd -ps -u -c 99999 '${networkDir}/admin.macaroon')" \
--cacert ${secretsDir}/lnd-cert \
-X POST \
-d "{\"wallet_password\": \"$(cat ${secretsDir}/lnd-wallet-password | tr -d '\n' | base64 -w0)\"}" \
${restUrl}/unlockwallet
fi
state=""
while [ "$state" != "RPC_ACTIVE" ]; do
state=$(${curl} \
--cacert ${secretsDir}/lnd-cert \
-d '{}' \
-X POST \
${restUrl}/state |\
${pkgs.jq}/bin/jq -r '.state')
sleep 0.1
done
'')
# Setting macaroon permission for other users needs root permissions
(nbLib.privileged "lnd-create-macaroons" ''
umask ug=r,o=
${lib.concatMapStrings (macaroon: ''
echo "Create custom macaroon ${macaroon}"
macaroonPath="$RUNTIME_DIRECTORY/${macaroon}.macaroon"
2021-03-16 11:45:22 +00:00
${curl} \
2020-10-16 15:43:07 +00:00
-H "Grpc-Metadata-macaroon: $(${pkgs.xxd}/bin/xxd -ps -u -c 99999 '${networkDir}/admin.macaroon')" \
--cacert ${secretsDir}/lnd-cert \
-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)}
'')
];
} // nbLib.allowedIPAddresses cfg.enforceTor;
2019-08-05 08:44:38 +00:00
};
2020-09-28 11:09:03 +00:00
2021-02-16 16:50:39 +00:00
users.users.${cfg.user} = {
group = cfg.group;
extraGroups = [ "bitcoinrpc-public" ];
home = cfg.dataDir; # lnd creates .lnd dir in HOME
};
2021-02-16 16:50:39 +00:00
users.groups.${cfg.group} = {};
2020-09-28 11:09:03 +00:00
nix-bitcoin.operator = {
2021-02-16 16:50:39 +00:00
groups = [ cfg.group ];
allowRunAsUsers = [ cfg.user ];
2020-09-28 11:09:03 +00:00
};
nix-bitcoin.secrets = {
2021-02-16 16:50:39 +00:00
lnd-wallet-password.user = cfg.user;
lnd-key.user = cfg.user;
lnd-cert.user = cfg.user;
lnd-cert.permissions = "0444"; # world readable
};
2019-08-05 08:44:38 +00:00
};
}