diff --git a/examples/README.md b/examples/README.md index b9bcfeb..445f846 100644 --- a/examples/README.md +++ b/examples/README.md @@ -68,3 +68,10 @@ c systemctl status bitcoind }' container --run c nodeinfo ``` See [`run-tests.sh`](../test/run-tests.sh) for a complete documentation. + + +### Real-world example +Check the [server repo](https://github.com/fort-nix/nixbitcoin.org) for https://nixbitcoin.org +to see the configuration of a nix-bitcoin node that's used in production. + +The commands in `shell.nix` allow you to locally run the node in a VM or container. diff --git a/examples/configuration.nix b/examples/configuration.nix index bf5cd84..edc0fa6 100644 --- a/examples/configuration.nix +++ b/examples/configuration.nix @@ -24,9 +24,9 @@ # modules by commenting out their respective line. ### BITCOIND - # Bitcoind is enabled by default if nix-bitcoin is enabled + # Bitcoind is enabled by default. # - # Enable this option to set pruning to a specified MiB value. + # Set this option to enable pruning with a specified MiB value. # clightning is compatible with pruning. See # https://github.com/ElementsProject/lightning/#pruning for more information. # LND and electrs are not compatible with pruning. @@ -42,8 +42,7 @@ # ''; ### CLIGHTNING - # Enable this module to use clightning, a Lightning Network implementation - # in C. + # Enable clightning, a Lightning Network implementation in C. services.clightning.enable = true; # # Set this to create an onion service by which clightning can accept incoming connections @@ -56,12 +55,13 @@ # services.clightning.plugins.prometheus.enable = true; ### LND - # Uncomment the following line in order to enable lnd, a lightning - # implementation written in Go. In order to avoid collisions with clightning - # you must disable clightning or change the services.clightning.port or - # services.lnd.port to a port other than 9735. + # Set this to enable lnd, a lightning implementation written in Go. # services.lnd.enable = true; # + # NOTE: In order to avoid collisions with clightning you must disable clightning or + # change the services.clightning.port or services.lnd.port to a port other than + # 9735. + # # Set this to create an onion service by which lnd can accept incoming connections # via Tor. # The onion service is automatically announced to peers. @@ -85,33 +85,37 @@ # scp bitcoin-node:/var/lib/lnd/chain/bitcoin/mainnet/channel.backup ./backups/lnd/ ### SPARK WALLET - # Enable this module to use spark-wallet, a minimalistic wallet GUI for + # Set this to enable spark-wallet, a minimalistic wallet GUI for # c-lightning, accessible over the web or through mobile and desktop apps. # Automatically enables clightning. # services.spark-wallet.enable = true; ### ELECTRS - # Enable this module to use electrs, an efficient re-implementation of - # Electrum Server in Rust. + # Set this to enable electrs, an efficient Electrum server implemented in Rust. # services.electrs.enable = true; + # # If you have more than 8GB memory, enable this option so electrs will # sync faster. Only available if hardware wallets are disabled. # services.electrs.high-memory = true; ### BTCPayServer - # Enable this module to use BTCPayServer, a self-hosted, open-source + # Set this to enable BTCPayServer, a self-hosted, open-source # cryptocurrency payment processor. + # services.btcpayserver.enable = true; + # # Privacy Warning: BTCPayServer currently looks up price rates without # proxying them through Tor. This means an outside observer can correlate # your BTCPayServer usage, like invoice creation times, with your IP address. - # services.btcpayserver.enable = true; + # # Enable this option to connect BTCPayServer to clightning. # services.btcpayserver.lightningBackend = "clightning"; + # # Enable this option to connect BTCPayServert to lnd. # services.btcpayserver.lightningBackend = "lnd"; - # The lightning backend service automatically enabled. + # + # The lightning backend service is automatically enabled. # Afterwards you need to go into Store > General Settings > Lightning Nodes - # and click to use "the internal lightning node of this BTCPay Server". + # and select "the internal lightning node of this BTCPay Server". # # Set this to create an onion service to make the btcpayserver web interface # accessible via Tor. @@ -122,16 +126,18 @@ ### LIQUIDD # Enable this module to use Liquid, a sidechain for an inter-exchange # settlement network linking together cryptocurrency exchanges and - # institutions around the world. Liquid is accessed with the elements-cli - # tool run as user operator. + # institutions around the world. # services.liquidd.enable = true; + # + # Liquid can be controlled with command 'elements-cli'. ### RECURRING-DONATIONS - # Enable this module to send recurring donations. This is EXPERIMENTAL; it's + # Set this to enable recurring donations. This is EXPERIMENTAL; it's # not guaranteed that payments are succeeding or that you will notice payment # failure. - # Automatically enables clightning. # services.recurring-donations.enable = true; + # This automatically enables clightning. + # # Specify the receivers of the donations. By default donations are every # Monday at a randomized time. Check `journalctl -eu recurring-donations` or # `lightning-cli listpayments` for successful lightning donations. @@ -142,54 +148,68 @@ # }; ### Hardware wallets - # Enable this module to allow using hardware wallets. See https://github.com/bitcoin-core/HWI - # for more information. Only available if electrs.high-memory is disabled. + # Enable the following to allow using hardware wallets. + # See https://github.com/bitcoin-core/HWI for more information. + # Only available if electrs.high-memory is disabled. + # # Ledger must be initialized through the official ledger live app and the Bitcoin app must # be installed and running on the device. # services.hardware-wallets.ledger = true; + # # Trezor can be initialized with the trezorctl command in nix-bitcoin. More information in # `docs/usage.md`. # services.hardware-wallets.trezor = true; - ### netns-isolation (EXPERIMENTAL) - # Enable this module to use Network Namespace Isolation. This feature places - # every service in its own network namespace and only allows truly necessary - # connections between network namespaces, making sure services are isolated on - # a network-level as much as possible. - # nix-bitcoin.netns-isolation.enable = true; - ### lightning-loop - # Enable this module to use lightninglab's non-custodial off/on chain bridge. + # Set this to enable lightninglab's non-custodial off/on chain bridge. + # services.lightning-loop.enable = true; + # # loopd (lightning-loop daemon) will be started automatically. Users can # interact with off/on chain bridge using `loop in` and `loop out`. # Automatically enables lnd. - # services.lightning-loop.enable = true; ### lightning-pool - # Enable this module to use Lightning Lab's non-custodial batched uniform + # Set this to enable Lightning Lab's non-custodial batched uniform # clearing-price auction for Lightning Channel Leases. + # services.lightning-pool.enable = true; + # # Use the `pool` command to interact with the lightning-pool service. # Automatically enables lnd. - # services.lightning-pool.enable = true; # # lightning-pool requires that lnd has a publicly reachable address. # Set this to create a public onion service for lnd. # nix-bitcoin.onionServices.lnd.public = true; ### charge-lnd - # Enable this module to use charge-lnd, a simple policy based fee manager for + # Set this to enable charge-lnd, a simple policy based fee manager for # LND. With this tool you can set fees to autobalance, recover channel open # costs, use on-chain fees as reference, or just use static fees. You decide. # services.charge-lnd.enable = true; + # # Define policies as outlined in the project documentation. # services.charge-lnd.policies = '' # ''; + ### JOINMARKET + # Set this to enable the JoinMarket service, including its command-line scripts. + # These scripts have prefix 'jm-', like 'jm-tumbler'. + # Note: JoinMarket has full access to bitcoind, including its wallet functionality. + # services.joinmarket.enable = true; + # + # Set this to enable the JoinMarket Yield Generator Bot. You will be able to + # earn sats by providing CoinJoin liquidity. This makes it impossible to use other + # scripts that access your wallet. + # services.joinmarket.yieldgenerator.enable = true; + # + # Set this to enable the JoinMarket order book watcher. + # services.joinmarket-ob-watcher.enable = true; + ### Backups - # Enable this module to use nix-bitcoin's own backups module. By default, it + # Set this to enable nix-bitcoin's own backup service. By default, it # uses duplicity to incrementally back up all important files in /var/lib to # /var/lib/localBackups once a day. # services.backups.enable = true; + # # You can pull the localBackups folder with # `scp bitcoin-node:/var/lib/localBackups /my-backup-path/` # Alternatively, you can also set a remote target url, for example @@ -206,17 +226,12 @@ # and electrs data directory, enable # services.backups.with-bulk-data = true; - ### JOINMARKET - # Enable this module to allow using JoinMarket's user interactive scripts (including - # tumbler.py). - # Note: JoinMarket has full access to bitcoind, including its wallet functionality. - # services.joinmarket.enable = true; - # Enable this option to enable the JoinMarket Yield Generator Bot. You will be able to - # earn sats by providing CoinJoin liquidity. This makes it impossible to use other - # scripts that access your wallet. - # services.joinmarket.yieldgenerator.enable = true; - # Enable this option to enable the JoinMarket order book watcher. - # services.joinmarket-ob-watcher.enable = true; + ### netns-isolation (EXPERIMENTAL) + # Enable this module to use Network Namespace Isolation. This feature places + # every service in its own network namespace and only allows truly necessary + # connections between network namespaces, making sure services are isolated on + # a network-level as much as possible. + # nix-bitcoin.netns-isolation.enable = true; # FIXME: Define your hostname. networking.hostName = "host"; diff --git a/examples/minimal-configuration.nix b/examples/minimal-configuration.nix index e7c2757..71dd4a5 100644 --- a/examples/minimal-configuration.nix +++ b/examples/minimal-configuration.nix @@ -1,6 +1,6 @@ { config, pkgs, lib, ... }: { imports = [ - + ]; nix-bitcoin.generateSecrets = true; @@ -15,7 +15,8 @@ name = "main"; # Set this to your system's main user }; - # The system's main unprivileged user + # The system's main unprivileged user. This setting is usually part of your + # existing NixOS configuration. users.users.main = { isNormalUser = true; password = "a"; diff --git a/modules/backups.nix b/modules/backups.nix index 946f526..a7747c6 100644 --- a/modules/backups.nix +++ b/modules/backups.nix @@ -21,6 +21,8 @@ let ${config.services.postgresqlBackup.location}/btcpaydb.sql.gz ${optionalString config.nix-bitcoin.generateSecrets "${config.nix-bitcoin.secretsDir}"} /var/lib/tor + /var/lib/nixos + # Extra files ${cfg.extraFiles} diff --git a/modules/bitcoind.nix b/modules/bitcoind.nix index 3ca5c2a..e2365ab 100644 --- a/modules/bitcoind.nix +++ b/modules/bitcoind.nix @@ -374,11 +374,11 @@ in { cd ${cfg.cli}/bin echo "Importing node banlist..." cat ${./banlist.cli.txt} | while read line; do - if ! err=$(eval "$line" 2>&1) && [[ $err != *already\ banned* ]]; then - # unexpected error - echo "$err" - exit 1 - fi + if ! err=$(eval "$line" 2>&1) && [[ $err != *already\ banned* ]]; then + # unexpected error + echo "$err" + exit 1 + fi done ''; serviceConfig = nbLib.defaultHardening // { diff --git a/modules/clightning.nix b/modules/clightning.nix index f415a30..93a3b5a 100644 --- a/modules/clightning.nix +++ b/modules/clightning.nix @@ -111,7 +111,6 @@ in { requires = [ "bitcoind.service" ]; after = [ "bitcoind.service" ]; preStart = '' - chown -R '${cfg.user}:${cfg.group}' '${cfg.dataDir}' # The RPC socket has to be removed otherwise we might have stale sockets rm -f ${cfg.networkDir}/lightning-rpc install -m 640 ${configFile} '${cfg.dataDir}/config' diff --git a/modules/joinmarket-ob-watcher.nix b/modules/joinmarket-ob-watcher.nix index 82499ef..2f4fdc0 100644 --- a/modules/joinmarket-ob-watcher.nix +++ b/modules/joinmarket-ob-watcher.nix @@ -74,7 +74,7 @@ in { serviceConfig = nbLib.defaultHardening // rec { DynamicUser = true; StateDirectory = "joinmarket-ob-watcher"; - StateDirectoryMode = "0770"; + StateDirectoryMode = "770"; WorkingDirectory = cfg.dataDir; # The service creates dir 'logs' in the working dir ExecStart = '' ${nbPkgs.joinmarket}/bin/ob-watcher --datadir=${cfg.dataDir} \ diff --git a/modules/joinmarket.nix b/modules/joinmarket.nix index 2d27094..289bf2e 100644 --- a/modules/joinmarket.nix +++ b/modules/joinmarket.nix @@ -282,7 +282,7 @@ in { users.groups.${cfg.group} = {}; nix-bitcoin.operator = { groups = [ cfg.group ]; - allowRunAsUsers = [ cfg.group ]; + allowRunAsUsers = [ cfg.user ]; }; nix-bitcoin.secrets.jm-wallet-password.user = cfg.user; diff --git a/modules/liquid.nix b/modules/liquid.nix index 28f2e4f..7804ba0 100644 --- a/modules/liquid.nix +++ b/modules/liquid.nix @@ -221,7 +221,6 @@ in { after = [ "bitcoind.service" ]; wantedBy = [ "multi-user.target" ]; preStart = '' - chown -R '${cfg.user}:${cfg.group}' '${cfg.dataDir}' install -m 640 ${configFile} '${cfg.dataDir}/elements.conf' { echo "rpcpassword=$(cat ${secretsDir}/liquid-rpcpassword)" diff --git a/modules/lnd.nix b/modules/lnd.nix index d5c29e1..fcadc5c 100644 --- a/modules/lnd.nix +++ b/modules/lnd.nix @@ -157,6 +157,16 @@ in { { assertion = bitcoind.prune == 0; message = "lnd does not support bitcoind pruning."; } + { assertion = + !(config.services ? clightning) + || !config.services.clightning.enable + || config.services.clightning.port != cfg.port; + 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 = { @@ -207,31 +217,31 @@ 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 + ((attempts-- == 0)) && { echo "lnd REST service unreachable"; exit 1; } + sleep 0.1 done if [[ ! -f ${networkDir}/wallet.db ]]; then mnemonic="${cfg.dataDir}/lnd-seed-mnemonic" if [[ ! -f "$mnemonic" ]]; then - echo Create lnd seed + echo "Create lnd seed" umask u=r,go= ${curl} -X GET ${restUrl}/genseed | ${pkgs.jq}/bin/jq -c '.cipher_seed_mnemonic' > "$mnemonic" fi - echo Create lnd wallet + echo "Create lnd wallet" ${curl} --output /dev/null \ -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 + echo "Wait until wallet is created" while [[ ! -f ${networkDir}/admin.macaroon ]]; do sleep 0.1 done else - echo Unlock lnd wallet + echo "Unlock lnd wallet" ${curl} \ -H "Grpc-Metadata-macaroon: $(${pkgs.xxd}/bin/xxd -ps -u -c 99999 '${networkDir}/admin.macaroon')" \ -X POST \ @@ -244,7 +254,7 @@ in { done '') # Setting macaroon permission for other users needs root permissions - (nbLib.privileged "lnd-create-macaroons" '' + (nbLib.rootScript "lnd-create-macaroons" '' umask ug=r,o= ${lib.concatMapStrings (macaroon: '' echo "Create custom macaroon ${macaroon}" diff --git a/modules/modules.nix b/modules/modules.nix index c32bdf0..0e0ac5a 100644 --- a/modules/modules.nix +++ b/modules/modules.nix @@ -1,9 +1,10 @@ -{ config, pkgs, lib, ... }: - -with lib; { + # The modules are topologically sorted by their dependencies. + # This means that modules only depend on modules higher in the list + # (unless otherwise noted). imports = [ # Core modules + ./nix-bitcoin.nix ./secrets/secrets.nix ./operator.nix @@ -13,7 +14,7 @@ with lib; ./clightning-plugins ./spark-wallet.nix ./lnd.nix - ./lnd-rest-onion-service.nix + ./lnd-rest-onion-service.nix # Requires onion-addresses.nix ./lightning-loop.nix ./lightning-pool.nix ./charge-lnd.nix @@ -36,56 +37,4 @@ with lib; ]; disabledModules = [ "services/networking/bitcoind.nix" ]; - - options = { - nix-bitcoin = { - pkgs = mkOption { - type = types.attrs; - default = (import ../pkgs { inherit pkgs; }).modulesPkgs; - }; - - lib = mkOption { - readOnly = true; - default = import ../pkgs/lib.nix lib pkgs; - }; - - torClientAddressWithPort = mkOption { - readOnly = true; - default = with config.services.tor.client.socksListenAddress; - "${addr}:${toString port}"; - }; - - # Torify binary that works with custom Tor SOCKS addresses - # Related issue: https://github.com/NixOS/nixpkgs/issues/94236 - torify = mkOption { - readOnly = true; - default = pkgs.writeScriptBin "torify" '' - ${pkgs.tor}/bin/torify \ - --address ${config.services.tor.client.socksListenAddress.addr} \ - "$@" - ''; - }; - - # A helper for using doas instead of sudo when doas is enabled - runAsUserCmd = mkOption { - readOnly = true; - default = if config.security.doas.enable - # TODO: Use absolute path until https://github.com/NixOS/nixpkgs/pull/133622 is available. - then "/run/wrappers/bin/doas -u" - else "sudo -u"; - }; - }; - }; - - config = { - assertions = [ - { assertion = (config.services.lnd.enable -> ( !config.services.clightning.enable || config.services.clightning.port != config.services.lnd.port)); - message = '' - LND and clightning can't both bind to lightning port 9735. Either - disable LND/clightning or change services.clightning.bindPort or - services.lnd.port to a port other than 9735. - ''; - } - ]; - }; } diff --git a/modules/nix-bitcoin.nix b/modules/nix-bitcoin.nix index a3f67eb..48cb7c4 100644 --- a/modules/nix-bitcoin.nix +++ b/modules/nix-bitcoin.nix @@ -1,9 +1,44 @@ -# This file exists only for backwards compatibility +{ config, pkgs, lib, ... }: -{ lib, ... }: +with lib; { - imports = [ - ./presets/secure-node.nix - (lib.mkRemovedOptionModule [ "services" "nix-bitcoin" "enable" ] "Please directly import ./presets/secure-node.nix") - ]; + options = { + nix-bitcoin = { + pkgs = mkOption { + type = types.attrs; + default = (import ../pkgs { inherit pkgs; }).modulesPkgs; + }; + + lib = mkOption { + readOnly = true; + default = import ../pkgs/lib.nix lib pkgs; + }; + + torClientAddressWithPort = mkOption { + readOnly = true; + default = with config.services.tor.client.socksListenAddress; + "${addr}:${toString port}"; + }; + + # Torify binary that works with custom Tor SOCKS addresses + # Related issue: https://github.com/NixOS/nixpkgs/issues/94236 + torify = mkOption { + readOnly = true; + default = pkgs.writeScriptBin "torify" '' + ${pkgs.tor}/bin/torify \ + --address ${config.services.tor.client.socksListenAddress.addr} \ + "$@" + ''; + }; + + # A helper for using doas instead of sudo when doas is enabled + runAsUserCmd = mkOption { + readOnly = true; + default = if config.security.doas.enable + # TODO: Use absolute path until https://github.com/NixOS/nixpkgs/pull/133622 is available. + then "/run/wrappers/bin/doas -u" + else "sudo -u"; + }; + }; + }; } diff --git a/modules/nodeinfo.nix b/modules/nodeinfo.nix index 782446b..c395bfb 100644 --- a/modules/nodeinfo.nix +++ b/modules/nodeinfo.nix @@ -94,13 +94,13 @@ let """) ''; - mkIfOnionPort = name: fn: - if onionServices ? ${name} then - fn (toString (builtins.elemAt onionServices.${name}.map 0).port) - else - ""; + mkIfOnionPort = name: fn: + if onionServices ? ${name} then + fn (toString (builtins.elemAt onionServices.${name}.map 0).port) + else + ""; - inherit (config.services.tor.relay) onionServices; + inherit (config.services.tor.relay) onionServices; in { options = { nix-bitcoin.nodeinfo = { diff --git a/modules/recurring-donations.nix b/modules/recurring-donations.nix index 63a5333..7d0680b 100644 --- a/modules/recurring-donations.nix +++ b/modules/recurring-donations.nix @@ -10,7 +10,7 @@ let pay_tallycoin() { NAME=$1 AMOUNT=$2 - echo Attempting to pay $AMOUNT sat to $NAME + echo "Attempting to pay $AMOUNT sat to $NAME" INVOICE=$(curl --socks5-hostname ${config.nix-bitcoin.torClientAddressWithPort} -d "satoshi_amount=$AMOUNT&payment_method=ln&id=$NAME&type=profile" -X POST https://api.tallyco.in/v1/payment/request/ | jq -r '.lightning_pay_request') 2> /dev/null if [ -z "$INVOICE" ] || [ "$INVOICE" = "null" ]; then echo "ERROR: did not get invoice from tallycoin" @@ -23,10 +23,10 @@ let return fi if [ $DECODED_AMOUNT -eq $AMOUNT ]; then - echo Paying with invoice "$INVOICE" + echo "Paying with invoice $INVOICE" $LNCLI pay "$INVOICE" else - echo ERROR: requested amount and invoice amount do not match. $AMOUNT vs $DECODED_AMOUNT + echo "ERROR: requested amount and invoice amount do not match. $AMOUNT vs $DECODED_AMOUNT" return fi } diff --git a/modules/secrets/secrets.nix b/modules/secrets/secrets.nix index 30618e8..6c7e0a9 100644 --- a/modules/secrets/secrets.nix +++ b/modules/secrets/secrets.nix @@ -60,6 +60,7 @@ in }; secretsSetupMethod = mkOption { + internal = true; type = types.str; default = throw '' Error: No secrets setup method has been defined. @@ -110,17 +111,17 @@ in ''} setupSecret() { - file="$1" - user="$2" - group="$3" - permissions="$4" - if [[ ! -e $file ]]; then - echo "Error: Secret file '$file' is missing" - exit 1 - fi - chown "$user:$group" "$file" - chmod "$permissions" "$file" - processedFiles+=("$file") + file="$1" + user="$2" + group="$3" + permissions="$4" + if [[ ! -e $file ]]; then + echo "Error: Secret file '$file' is missing" + exit 1 + fi + chown "$user:$group" "$file" + chmod "$permissions" "$file" + processedFiles+=("$file") } dir="${cfg.secretsDir}" diff --git a/modules/versioning.nix b/modules/versioning.nix index 445edf8..6b0121e 100644 --- a/modules/versioning.nix +++ b/modules/versioning.nix @@ -1,5 +1,10 @@ { config, pkgs, lib, ... }: +# Workflow for releasing a new nix-bitcoin version with incompatible changes: +# Let V be the version of the upcoming, incompatible release. +# 1. Add change descriptions with `version = V` at the end of the `changes` list below. +# 2. Set `nix-bitcoin.configVersion = V` in ../examples/configuration.nix. + with lib; let version = config.nix-bitcoin.configVersion; diff --git a/pkgs/lib.nix b/pkgs/lib.nix index 660820b..c7a7480 100644 --- a/pkgs/lib.nix +++ b/pkgs/lib.nix @@ -3,7 +3,7 @@ lib: pkgs: with lib; # See `man systemd.exec` and `man systemd.resource-control` for an explanation -# of the systemd-related options available through this module. +# of the systemd-related options available through this file. let self = { # These settings roughly follow systemd's "strict" security profile defaultHardening = { @@ -70,7 +70,7 @@ let self = { ''; # Used for ExecStart* - privileged = name: src: "+${self.script name src}"; + rootScript = name: src: "+${self.script name src}"; cliExec = mkOption { # Used by netns-isolation to execute the cli in the service's private netns diff --git a/test/run-tests.sh b/test/run-tests.sh index 15e7283..7990308 100755 --- a/test/run-tests.sh +++ b/test/run-tests.sh @@ -274,10 +274,11 @@ examples() { script=" set -e ./deploy-container.sh + ./deploy-container-minimal.sh ./deploy-qemu-vm.sh ./deploy-krops.sh " - (cd $scriptDir/../examples && nix-shell --run "$script") + (cd "$scriptDir/../examples" && nix-shell --run "$script") } all() {