diff --git a/README.md b/README.md index 21c04c8..9cd76cd 100644 --- a/README.md +++ b/README.md @@ -510,13 +510,15 @@ setup, supported hardware, and Royal Membership. ## For developers -Sovran_SystemsOS combines [NixOS](https://nixos.org), an in-repository -Bitcoin and Lightning stack, desktop packages from +Sovran_SystemsOS combines [NixOS](https://nixos.org), the standalone +[Sovran_Bitcoin](https://github.com/naturallaw777/Sovran_Bitcoin) flake for +Bitcoin and Lightning, desktop packages from [btc-clients-nix](https://github.com/emmanuelrosa/btc-clients-nix), and the -Sovran Hub. The Bitcoin modules under `modules/bitcoin/` were adapted from -[nix-bitcoin](https://github.com/fort-nix/nix-bitcoin) and are now maintained -here. Builds no longer import or fetch nix-bitcoin. Legacy `nix-bitcoin.*` -option names and `/etc/nix-bitcoin-secrets` remain for compatibility. +Sovran Hub. The Bitcoin and Lightning modules live in the Sovran_Bitcoin +repository and are consumed as a flake input. OS-specific Bitcoin customizations +(Second_Drive paths, operator user, Hub integration) are bridged by +`modules/sovran-bitcoin-integration.nix`. Legacy `nix-bitcoin.*` option names +and `/etc/nix-bitcoin-secrets` remain for compatibility. ### Development workflow @@ -536,8 +538,9 @@ option names and `/etc/nix-bitcoin-secrets` remain for compatibility. - [NixOS](https://nixos.org) and [Nix flakes](https://nixos.wiki/wiki/Flakes) for declarative, pinned system configuration -- `modules/bitcoin/` for the in-repository Bitcoin and Lightning stack -- `packages/` for Sovran-maintained package definitions and patches +- [Sovran_Bitcoin](https://github.com/naturallaw777/Sovran_Bitcoin) for the + Bitcoin and Lightning stack (consumed as a flake input) +- `modules/sovran-bitcoin-integration.nix` for OS-specific Bitcoin overrides - [btc-clients-nix](https://github.com/emmanuelrosa/btc-clients-nix) for the Sparrow, Bisq, and Bisq 2 packages - [Python](https://www.python.org) and [FastAPI](https://fastapi.tiangolo.com) @@ -628,12 +631,11 @@ sudo nixos-rebuild switch --rollback | `flake.lock` | Pins dependencies for reproducible builds | | `configuration.nix` | Base host, boot, desktop, user, security, backup, and system configuration | | `modules/` | Core modules, self-hosted services, and optional features | -| `modules/bitcoin/` | In-repository Bitcoin and Lightning service modules | +| `modules/sovran-bitcoin-integration.nix` | OS-specific Bitcoin overrides (Second_Drive, operator, Hub wiring) | | `modules/core/` | Roles, Hub integration, Caddy, desktop, support, and other core behavior | | `app/` | Sovran Hub backend, templates, static assets, scripts, and web interface | | `scripts/` | Automated release, build, and CDN upload utility scripts | | `iso/` | Installer configuration, installer code, and installer assets | -| `packages/` | Sovran-maintained package definitions and patches | | `tests/` | Security and Nix integration checks | | `assets/` | Documentation images | | `custom.template.nix` | Template for local features and service overrides | @@ -689,14 +691,12 @@ rebuilds the machine into the selected declarative state. |---|---| | Core platform: roles, Hub, desktop integration, Caddy, domains, support, remote deployment | `modules/core/` | | Shared credentials | `modules/credentials.nix` | -| Bitcoin and Lightning stack | `modules/bitcoinecosystem.nix` | +| Bitcoin and Lightning stack | [Sovran_Bitcoin](https://github.com/naturallaw777/Sovran_Bitcoin) flake input + `modules/sovran-bitcoin-integration.nix` | | Automatic wallet-to-node connections | `modules/wallet-autoconnect.nix` | -| Alby Hub and Nostr Wallet Connect (NWC) on LND | `modules/nwc-wallets.nix`, `packages/albyhub/` | | Matrix Synapse | `modules/synapse.nix` | | Optional Element audio and video calling via LiveKit | `modules/element-calling.nix` | | Optional Haven Nostr relay | `modules/haven.nix` | | Nextcloud, Vaultwarden, WordPress | `modules/nextcloud.nix`, `modules/vaultwarden.nix`, `modules/wordpress.nix`, `modules/php.nix` | -| Optional Mempool explorer | `modules/mempool.nix` | | Optional remote desktop and public SSH | `modules/rdp.nix`, `modules/sshd.nix` | Feature availability and defaults may change as Sovran_SystemsOS develops. diff --git a/SECURITY.md b/SECURITY.md index 9216ec3..f95f02b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -35,12 +35,14 @@ external networks and are outside a “fully offline” model. The local Hub currently uses HTTP. Authentication does not encrypt local network traffic, so use a trusted LAN and avoid public or guest Wi-Fi. -### In-repository Bitcoin stack +### Bitcoin stack -Bitcoin and Lightning modules are maintained under `modules/bitcoin/`. They were -adapted from nix-bitcoin, but Sovran builds do not import or fetch nix-bitcoin. -The `nix-bitcoin.*` option namespace and `/etc/nix-bitcoin-secrets` path remain -only for upgrade compatibility. +Bitcoin and Lightning modules are maintained in the standalone +[Sovran_Bitcoin](https://github.com/naturallaw777/Sovran_Bitcoin) repository +and consumed as a flake input. OS-specific customizations (Second_Drive paths, +operator user, Hub integration) are bridged by +`modules/sovran-bitcoin-integration.nix`. The `nix-bitcoin.*` option namespace +and `/etc/nix-bitcoin-secrets` path remain only for upgrade compatibility. ### Supply chain and integrity diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index edff09e..bf2ddb9 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -2,10 +2,13 @@ ## nix-bitcoin -Portions of `modules/bitcoin/` and selected package definitions were adapted -from [fort-nix/nix-bitcoin](https://github.com/fort-nix/nix-bitcoin), commit +The Bitcoin and Lightning modules were originally adapted from +[fort-nix/nix-bitcoin](https://github.com/fort-nix/nix-bitcoin), commit [`360e30fee5ba32f9fecc89bc35628195d9d2dbbe`](https://github.com/fort-nix/nix-bitcoin/commit/360e30fee5ba32f9fecc89bc35628195d9d2dbbe). -The current Sovran build does not import or fetch nix-bitcoin. +They now live in the standalone +[Sovran_Bitcoin](https://github.com/naturallaw777/Sovran_Bitcoin) repository +and are consumed by Sovran_SystemsOS as a flake input. The current Sovran build +does not import or fetch nix-bitcoin. MIT License diff --git a/flake.nix b/flake.nix index d79d8aa..a7472bf 100644 --- a/flake.nix +++ b/flake.nix @@ -6,9 +6,12 @@ nixvim.url = "github:nix-community/nixvim"; btc-clients.url = "github:emmanuelrosa/btc-clients-nix"; nixpkgs-stable.url = "github:nixos/nixpkgs/nixos-26.05"; + + # Bitcoin / Lightning stack — standalone flake, consumed as a module. + sovran-bitcoin.url = "github:naturallaw777/Sovran_Bitcoin"; }; - outputs = { self, nixpkgs, nixvim, btc-clients, nixpkgs-stable, ... }: + outputs = { self, nixpkgs, nixvim, btc-clients, nixpkgs-stable, sovran-bitcoin, ... }: let overlay-stable = final: prev: { @@ -33,7 +36,8 @@ modules = [ { nixpkgs.hostPlatform = "x86_64-linux"; nixpkgs.overlays = [ overlay-stable ]; } ./iso/common.nix - ./modules/bitcoin + sovran-bitcoin.nixosModules.default + ./modules/sovran-bitcoin-integration.nix nixvim.nixosModules.nixvim ]; }; @@ -44,7 +48,8 @@ nixpkgs.overlays = [ overlay-stable ]; }) ./configuration.nix - ./modules/bitcoin + sovran-bitcoin.nixosModules.default + ./modules/sovran-bitcoin-integration.nix nixvim.nixosModules.nixvim ]; config = { @@ -60,17 +65,9 @@ pkgs = import nixpkgs { system = "x86_64-linux"; }; - fetchNodeModules = - pkgs.callPackage ./packages/build-support/fetch-node-modules.nix {}; - mempoolPkgs = - pkgs.callPackage ./packages/mempool { inherit fetchNodeModules; }; in { - bitcoin-btcpay-hardening = import ./tests/bitcoin-btcpay-hardening.nix { - inherit nixpkgs overlay-stable; - }; - mempool-backend = mempoolPkgs.mempool-backend; - mempool-frontend = mempoolPkgs.mempool-frontend; - rtl = pkgs.callPackage ./packages/rtl { inherit fetchNodeModules; }; + # Bitcoin hardening and package checks now live in the Sovran_Bitcoin flake. + # Run them with: nix build github:naturallaw777/Sovran_Bitcoin#checks.x86_64-linux }; }; } diff --git a/modules/bitcoin/bitcoind-rpc-public-whitelist.nix b/modules/bitcoin/bitcoind-rpc-public-whitelist.nix deleted file mode 100644 index a5749fd..0000000 --- a/modules/bitcoin/bitcoind-rpc-public-whitelist.nix +++ /dev/null @@ -1,70 +0,0 @@ -# RPC calls that are safe for public use -# Vendored from nix-bitcoin - do not fetch from upstream at runtime -[ - "echo" - "getinfo" - "getindexinfo" - "help" - "ping" - "uptime" - # Blockchain - "getbestblockhash" - "getblock" - "getblockchaininfo" - "getblockcount" - "getblockfilter" - "getblockfrompeer" - "getblockhash" - "getblockheader" - "getblockstats" - "getchaintips" - "getchaintxstats" - "getdeploymentinfo" - "getdifficulty" - "getmempoolancestors" - "getmempooldescendants" - "getmempoolentry" - "getmempoolinfo" - "getrawmempool" - "gettxout" - "gettxoutproof" - "gettxoutsetinfo" - "scantxoutset" - "verifytxoutproof" - # Mining - "getblocktemplate" - "getmininginfo" - "getnetworkhashps" - # Network - "getnetworkinfo" - "getnodeaddresses" - "getpeerinfo" - # Rawtransactions - "analyzepsbt" - "combinepsbt" - "combinerawtransaction" - "converttopsbt" - "createpsbt" - "createrawtransaction" - "decodepsbt" - "decoderawtransaction" - "decodescript" - "finalizepsbt" - "fundrawtransaction" - "getrawtransaction" - "joinpsbts" - "sendrawtransaction" - "signrawtransactionwithkey" - "testmempoolaccept" - "utxoupdatepsbt" - # Util - "createmultisig" - "deriveaddresses" - "estimatesmartfee" - "getdescriptorinfo" - "signmessagewithprivkey" - "validateaddress" - "verifymessage" - # Zmq - "getzmqnotifications" -] diff --git a/modules/bitcoin/bitcoind.nix b/modules/bitcoin/bitcoind.nix deleted file mode 100644 index 29b7e89..0000000 --- a/modules/bitcoin/bitcoind.nix +++ /dev/null @@ -1,507 +0,0 @@ -{ config, pkgs, lib, ... }: - -with lib; -let - options = { - services.bitcoind = { - enable = mkEnableOption "Bitcoin daemon"; - address = mkOption { - type = types.str; - default = "127.0.0.1"; - description = "Address to listen for peer connections."; - }; - port = mkOption { - type = types.port; - default = if !cfg.regtest then 8333 else 18444; - defaultText = "if !cfg.regtest then 8333 else 18444"; - description = "Port to listen for peer connections."; - }; - onionPort = mkOption { - type = types.nullOr types.port; - # When the bitcoind onion service is enabled, add an onion-tagged socket - # to distinguish local connections from Tor connections - default = if (config.nix-bitcoin.onionServices.bitcoind.enable or false) then 8334 else null; - description = '' - Port to listen for Tor peer connections. - If set, inbound connections to this port are tagged as onion peers. - ''; - }; - listen = mkOption { - type = types.bool; - default = false; - description = '' - Listen for peer connections at `address:port` - and `address:onionPort` (if {option}`onionPort` is set). - ''; - }; - listenWhitelisted = mkOption { - type = types.bool; - default = false; - description = '' - Listen for peer connections at `address:whitelistedPort`. - Peers connected through this socket are automatically whitelisted. - ''; - }; - whitelistedPort = mkOption { - type = types.port; - default = 8335; - description = "See `listenWhitelisted`."; - }; - 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. - ''; - }; - package = mkOption { - type = types.package; - default = pkgs.bitcoind; - defaultText = "pkgs.bitcoind"; - description = '' - The package providing bitcoind binaries. - - ''; - }; - extraConfig = mkOption { - type = types.lines; - default = ""; - example = '' - par=16 - logips=1 - ''; - description = "Extra lines appended to {file}`bitcoin.conf`."; - }; - dataDir = mkOption { - type = types.path; - default = "/var/lib/bitcoind"; - description = "The data directory for bitcoind."; - }; - rpc = { - address = mkOption { - type = types.str; - default = "127.0.0.1"; - description = '' - Address to listen for JSON-RPC connections. - ''; - }; - port = mkOption { - type = types.port; - default = if !cfg.regtest then 8332 else 18443; - defaultText = "if !cfg.regtest then 8332 else 18443"; - description = "Port to listen for JSON-RPC connections."; - }; - threads = mkOption { - type = types.nullOr types.ints.u16; - default = null; - description = "The number of threads to service RPC calls."; - }; - allowip = mkOption { - type = types.listOf types.str; - default = [ "127.0.0.1" ]; - description = '' - Allow JSON-RPC connections from specified sources. - ''; - }; - users = mkOption { - default = {}; - description = '' - Allowed users for JSON-RPC connections. - ''; - example = { - alice = { - passwordHMAC = "f7efda5c189b999524f151318c0c86$d5b51b3beffbc02b724e5d095828e0bc8b2456e9ac8757ae3211a5d9b16a22ae"; - rpcwhitelist = [ "sendtoaddress" "getnewaddress" ]; - }; - }; - type = with types; attrsOf (submodule ({ name, ... }: { - options = { - name = mkOption { - type = types.str; - default = name; - example = "alice"; - description = '' - Username for JSON-RPC connections. - ''; - }; - passwordHMAC = mkOption { - type = types.str; - example = "f7efda5c189b999524f151318c0c86$d5b51b3beffbc02b724e5d095828e0bc8b2456e9ac8757ae3211a5d9b16a22ae"; - description = '' - Password HMAC-SHA-256 for JSON-RPC connections. Must be a string of the - format `$`. - ''; - }; - passwordHMACFromFile = mkOption { - type = lib.types.bool; - internal = true; - default = false; - }; - rpcwhitelist = mkOption { - type = types.listOf types.str; - default = []; - description = '' - List of allowed rpc calls for each user. - If empty list, rpcwhitelist is disabled for that user. - ''; - }; - }; - })); - }; - }; - regtest = mkOption { - type = types.bool; - default = false; - description = "Enable regtest mode."; - }; - network = mkOption { - readOnly = true; - default = if cfg.regtest then "regtest" else "mainnet"; - }; - makeNetworkName = mkOption { - readOnly = true; - default = mainnet: regtest: if cfg.regtest then regtest else mainnet; - }; - proxy = mkOption { - type = types.nullOr types.str; - default = if cfg.tor.proxy then config.nix-bitcoin.torClientAddressWithPort else null; - description = "Connect through SOCKS5 proxy"; - }; - i2p = mkOption { - type = types.enum [ false true "only-outgoing" ]; - default = false; - description = '' - Enable peer connections via i2p. - With `only-outgoing`, incoming i2p connections are disabled. - ''; - }; - dataDirReadableByGroup = mkOption { - type = types.bool; - default = false; - description = '' - If enabled, data dir content is readable by the bitcoind service group. - Warning: This disables bitcoind's wallet support. - ''; - }; - sysperms = mkOption { - type = types.nullOr types.bool; - default = null; - description = '' - Create new files with system default permissions, instead of umask 077 - (only effective with disabled wallet functionality) - ''; - }; - disablewallet = mkOption { - type = types.nullOr types.bool; - default = null; - description = '' - Do not load the wallet and disable wallet RPC calls - ''; - }; - dbCache = mkOption { - type = types.nullOr (intAtLeast 4); - default = null; - example = 4000; - description = "Override the default database cache size in MiB."; - }; - prune = mkOption { - type = types.ints.unsigned; - default = 0; - example = 10000; - description = '' - Automatically prune block files to stay under the specified target size in MiB. - Value 0 disables pruning. - ''; - }; - txindex = mkOption { - type = types.bool; - default = false; - description = "Enable the transaction index."; - }; - zmqpubrawblock = mkOption { - type = types.nullOr types.str; - default = null; - example = "tcp://127.0.0.1:28332"; - description = "ZMQ address for zmqpubrawblock notifications"; - }; - zmqpubrawtx = mkOption { - type = types.nullOr types.str; - default = null; - example = "tcp://127.0.0.1:28333"; - description = "ZMQ address for zmqpubrawtx notifications"; - }; - assumevalid = mkOption { - type = types.nullOr types.str; - default = null; - example = "00000000000000000000e5abc3a74fe27dc0ead9c70ea1deb456f11c15fd7bc6"; - description = '' - If this block is in the chain assume that it and its ancestors are - valid and potentially skip their script verification. - ''; - }; - addnodes = mkOption { - type = types.listOf types.str; - default = []; - example = [ "ecoc5q34tmbq54wl.onion" ]; - description = "Add nodes to connect to and attempt to keep the connections open"; - }; - discover = mkOption { - type = types.nullOr types.bool; - default = null; - description = "Discover own IP addresses"; - }; - addresstype = mkOption { - type = types.nullOr types.str; - default = null; - example = "bech32"; - description = "The type of addresses to use"; - }; - user = mkOption { - type = types.str; - default = "bitcoin"; - description = "The user as which to run bitcoind."; - }; - group = mkOption { - type = types.str; - default = cfg.user; - description = "The group as which to run bitcoind."; - }; - cli = mkOption { - readOnly = true; - type = types.package; - default = pkgs.writers.writeBashBin "bitcoin-cli" '' - exec ${cfg.package}/bin/bitcoin-cli -datadir='${cfg.dataDir}' "$@" - ''; - defaultText = "(See source)"; - description = "Binary to connect with the bitcoind instance."; - }; - tor = nbLib.tor; - }; - }; - - cfg = config.services.bitcoind; - nbLib = config.nix-bitcoin.lib; - secretsDir = config.nix-bitcoin.secretsDir; - - # nixpkgs 26.11 moved i2pd's protocol configuration from - # `services.i2pd.proto` to the RFC42-style `services.i2pd.settings`. - i2pSAM = config.services.i2pd.settings.sam; - - configFile = builtins.toFile "bitcoin.conf" '' - # We're already logging via journald - nodebuglogfile=1 - logtimestamps=0 - - startupnotify=/run/current-system/systemd/bin/systemd-notify --ready - - ${optionalString cfg.regtest '' - regtest=1 - [regtest] - ''} - ${optionalString (cfg.dbCache != null) "dbcache=${toString cfg.dbCache}"} - prune=${toString cfg.prune} - ${optionalString cfg.txindex "txindex=1"} - ${optionalString (cfg.sysperms != null) "sysperms=${if cfg.sysperms then "1" else "0"}"} - ${optionalString (cfg.disablewallet != null) "disablewallet=${if cfg.disablewallet then "1" else "0"}"} - ${optionalString (cfg.assumevalid != null) "assumevalid=${cfg.assumevalid}"} - - # Connection options - listen=${if (cfg.listen || cfg.listenWhitelisted) then "1" else "0"} - ${optionalString cfg.listen - "bind=${cfg.address}:${toString cfg.port}"} - ${optionalString (cfg.listen && cfg.onionPort != null) - "bind=${cfg.address}:${toString cfg.onionPort}=onion"} - ${optionalString cfg.listenWhitelisted - "whitebind=${cfg.address}:${toString cfg.whitelistedPort}"} - ${optionalString (cfg.proxy != null) "proxy=${cfg.proxy}"} - ${optionalString (cfg.i2p != false) "i2psam=${nbLib.addressWithPort i2pSAM.address i2pSAM.port}"} - ${optionalString (cfg.i2p == "only-outgoing") "i2pacceptincoming=0"} - - ${optionalString (cfg.discover != null) "discover=${if cfg.discover then "1" else "0"}"} - ${lib.concatMapStrings (node: "addnode=${node}\n") cfg.addnodes} - - # RPC server options - rpcbind=${cfg.rpc.address} - rpcport=${toString cfg.rpc.port} - rpcconnect=${cfg.rpc.address} - ${optionalString (cfg.rpc.threads != null) "rpcthreads=${toString cfg.rpc.threads}"} - rpcwhitelistdefault=0 - ${concatMapStrings (user: '' - ${optionalString (!user.passwordHMACFromFile) "rpcauth=${user.name}:${user.passwordHMAC}"} - ${optionalString (user.rpcwhitelist != []) - "rpcwhitelist=${user.name}:${lib.strings.concatStringsSep "," user.rpcwhitelist}"} - '') (builtins.attrValues cfg.rpc.users) - } - ${lib.concatMapStrings (rpcallowip: "rpcallowip=${rpcallowip}\n") cfg.rpc.allowip} - - # Wallet options - ${optionalString (cfg.addresstype != null) "addresstype=${cfg.addresstype}"} - - # ZMQ options - ${optionalString (cfg.zmqpubrawblock != null) "zmqpubrawblock=${cfg.zmqpubrawblock}"} - ${optionalString (cfg.zmqpubrawtx != null) "zmqpubrawtx=${cfg.zmqpubrawtx}"} - - # Extra options - ${cfg.extraConfig} - ''; - - zmqServerEnabled = (cfg.zmqpubrawblock != null) || (cfg.zmqpubrawtx != null); - - intAtLeast = n: types.addCheck types.int (x: x >= n) // { - name = "intAtLeast"; - description = "integer >= ${toString n}"; - }; -in { - inherit options; - - config = mkIf cfg.enable { - environment.systemPackages = [ cfg.package (hiPrio cfg.cli) ]; - - services.bitcoind = mkMerge [ - (mkIf cfg.dataDirReadableByGroup { - disablewallet = true; - sysperms = true; - }) - { - rpc.users.privileged = { - passwordHMACFromFile = true; - }; - rpc.users.public = { - passwordHMACFromFile = true; - rpcwhitelist = import ./bitcoind-rpc-public-whitelist.nix; - }; - } - ]; - - services.i2pd = mkIf (cfg.i2p != false) { - enable = true; - settings.sam = { - enabled = true; - address = "127.0.0.1"; - port = 7656; - }; - }; - - systemd.tmpfiles.rules = [ - "d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -" - ]; - - systemd.services.bitcoind = rec { - wants = [ - "network-online.target" - # Use `wants` instead of `requires` for `nix-bitcoin-secrets.target` - # so that bitcoind and all dependent services are not restarted when - # the secrets target restarts. - # The secrets target always restarts when deploying with one of the methods - # in ./deployment. - # - # TODO-EXTERNAL: Instead of `wants`, use a future systemd dependency type - # that propagates initial start failures but no restarts - "nix-bitcoin-secrets.target" - ]; - after = wants; - wantedBy = [ "multi-user.target" ]; - - preStart = let - extraRpcauth = concatMapStrings (name: let - user = cfg.rpc.users.${name}; - in optionalString user.passwordHMACFromFile '' - hmacPayload="$(readValidatedRpcHmac '${secretsDir}/bitcoin-HMAC-${name}')" || exit 1 - printf '%s\n' "rpcauth=${user.name}:$hmacPayload" - '' - ) (builtins.attrNames cfg.rpc.users); - in '' - ${optionalString cfg.dataDirReadableByGroup '' - if [[ -e '${cfg.dataDir}/blocks' ]]; then - chmod -R g+rX '${cfg.dataDir}/blocks' - fi - ''} - - readValidatedRpcHmac() { - local hmacFile="$1" - local hmacPayload - - if [[ ! -e "$hmacFile" ]]; then - echo "Error: Bitcoin RPC HMAC file is missing: $hmacFile" >&2 - return 1 - fi - if [[ ! -r "$hmacFile" ]]; then - echo "Error: Bitcoin RPC HMAC file is unreadable: $hmacFile" >&2 - return 1 - fi - - hmacPayload="$(<"$hmacFile")" - if [[ -z "$hmacPayload" ]]; then - echo "Error: Bitcoin RPC HMAC file is empty: $hmacFile" >&2 - return 1 - fi - if [[ ! "$hmacPayload" =~ ^[[:xdigit:]]+\$[[:xdigit:]]+$ ]]; then - echo "Error: Bitcoin RPC HMAC file has invalid format: $hmacFile" >&2 - return 1 - fi - - printf '%s\n' "$hmacPayload" - } - - cfg=$( - cat ${configFile} - ${extraRpcauth} - echo - ${optionalString (cfg.getPublicAddressCmd != "") '' - echo "externalip=$(${cfg.getPublicAddressCmd})" - ''} - ) - confFile='${cfg.dataDir}/bitcoin.conf' - if [[ ! -e $confFile || $cfg != $(cat $confFile) ]]; then - install -o '${cfg.user}' -g '${cfg.group}' -m 640 <(echo "$cfg") $confFile - fi - ''; - - # Enable RPC access for group - postStart = '' - chmod g=r '${cfg.dataDir}/${optionalString cfg.regtest "regtest/"}.cookie' - '' + (optionalString cfg.regtest) '' - chmod g=x '${cfg.dataDir}/regtest' - ''; - - serviceConfig = nbLib.defaultHardening // { - Type = "notify"; - NotifyAccess = "all"; - User = cfg.user; - Group = cfg.group; - TimeoutStartSec = "30min"; - TimeoutStopSec = "30min"; - ExecStart = "${cfg.package}/bin/bitcoind -datadir='${cfg.dataDir}'"; - Restart = "on-failure"; - UMask = mkIf cfg.dataDirReadableByGroup "0027"; - ReadWritePaths = [ cfg.dataDir ]; - } // nbLib.allowedIPAddresses cfg.tor.enforce - // optionalAttrs zmqServerEnabled nbLib.allowNetlink; - }; - - users.users.${cfg.user} = { - isSystemUser = true; - group = cfg.group; - }; - users.groups.${cfg.group} = {}; - users.groups.bitcoinrpc-public = {}; - - nix-bitcoin.operator.groups = [ cfg.group ]; - - nix-bitcoin.secrets = { - bitcoin-rpcpassword-privileged.user = cfg.user; - bitcoin-rpcpassword-public = { - user = cfg.user; - group = "bitcoinrpc-public"; - }; - - bitcoin-HMAC-privileged.user = cfg.user; - bitcoin-HMAC-public.user = cfg.user; - }; - nix-bitcoin.generateSecretsCmds.bitcoind = '' - makeBitcoinRPCPassword privileged - makeBitcoinRPCPassword public - ''; - }; -} diff --git a/modules/bitcoin/btcpayserver.nix b/modules/bitcoin/btcpayserver.nix deleted file mode 100644 index 8978f31..0000000 --- a/modules/bitcoin/btcpayserver.nix +++ /dev/null @@ -1,334 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - options.services = { - nbxplorer = { - enable = mkOption { - type = types.bool; - default = false; - description = '' - Enable nbxplorer, a lightweight API for Bitcoin HD wallets. - - Access API documentation here: - {option}`services.nbxplorer.address`:{option}`services.nbxplorer.port` - ''; - }; - address = mkOption { - type = types.str; - default = "127.0.0.1"; - description = "Address to listen on."; - }; - port = mkOption { - type = types.port; - default = 24444; - description = "Port to listen on."; - }; - package = mkOption { - type = types.package; - default = pkgs.stable.nbxplorer; - defaultText = "pkgs.stable.nbxplorer"; - description = "The package providing nbxplorer binaries."; - }; - dataDir = mkOption { - type = types.path; - default = "/var/lib/nbxplorer"; - description = "The data directory for nbxplorer."; - }; - # TODO-EXTERNAL: - # The shortcut link `main` in the datadir has changed to a directory - # in version 2.3.3. - # Add a dummy symlink, if it does not already exist, to be compatible with older modules. - # When the old system uses a link and the new system a directory, switching fails with: - # mv: cannot move '/var/lib/nbxplorer/Main' to '/var/lib/nbxplorer/.Main.tmp': - # No such file or directory - # - # Remove this option when it is irrelevant (i.e. when the old system will never - # be nix-bitcoin <=0.0.91) - addNetworkSymlink = mkOption { - readOnly = true; - default = pkgs.stable.nbxplorer != cfg.nbxplorer.package; - description = '' - Whether to add a compatibility symlink (like `${cfg.nbxplorer.dataDir}/Main`) - to the dataDir. - This is enabled by default if the nbxplorer package is set to the version-locked package. - ''; - }; - user = mkOption { - type = types.str; - default = "nbxplorer"; - description = "The user as which to run NBXplorer."; - }; - group = mkOption { - type = types.str; - default = cfg.nbxplorer.user; - description = "The group as which to run NBXplorer."; - }; - tor = nbLib.tor; - }; - - btcpayserver = { - enable = mkOption { - type = types.bool; - default = false; - description = '' - Enable BTCPay Server, a self-hosted, open-source payment processor. - - Extra recommendations: - - Enable `services.btcpayserver.lightningBackend` to provide Lightning payment support. - - Secure this service if the instance is publically accessible. For example, set - {option}`services.btcpayserver.address` to `127.0.0.1` and use a reverse proxy - that enforces TLS (Transport Layer Security). - ''; - }; - package = mkOption { - type = types.package; - default = pkgs.stable.btcpayserver; - defaultText = "pkgs.stable.btcpayserver"; - description = "The package providing BTCPay Server binaries."; - }; - address = mkOption { - type = types.str; - default = "127.0.0.1"; - description = "Address to listen on."; - }; - port = mkOption { - type = types.port; - default = 23000; - description = "Port to listen on."; - }; - lightningBackend = mkOption { - type = types.nullOr (types.enum [ "lnd" ]); - default = null; - description = '' - The lightning node to use as a backend. - Enables the node service if not already enabled. - ''; - }; - lbtc = mkOption { - readOnly = true; - default = false; - description = '' - Enable Liquid support. - ''; - }; - dataDir = mkOption { - type = types.path; - default = "/var/lib/btcpayserver"; - description = "The data directory for BTCPay Server."; - }; - user = mkOption { - type = types.str; - default = "btcpayserver"; - description = "The user as which to run BTCPay Server."; - }; - group = mkOption { - type = types.str; - default = cfg.btcpayserver.user; - description = "The group as which to run BTCPay Server."; - }; - tor = nbLib.tor; - }; - }; - - cfg = { - inherit (config.services) - nbxplorer - btcpayserver - bitcoind; - }; - - nbLib = config.nix-bitcoin.lib; - secretsDir = config.nix-bitcoin.secretsDir; - -in { - inherit options; - - config = mkMerge [ - (mkIf cfg.nbxplorer.enable { - systemd.tmpfiles.rules = [ - "d '${cfg.nbxplorer.dataDir}' 0770 ${cfg.nbxplorer.user} ${cfg.nbxplorer.group} - -" - ] ++ optional cfg.nbxplorer.addNetworkSymlink - "L+ '${cfg.nbxplorer.dataDir}/Main' - - - - '${cfg.nbxplorer.dataDir}/main'"; - - systemd.services.nbxplorer = let - configFile = builtins.toFile "nbxplorer-config" '' - network=${cfg.bitcoind.network} - btcrpcuser=${cfg.bitcoind.rpc.users.btcpayserver.name} - btcrpcurl=http://${nbLib.addressWithPort cfg.bitcoind.rpc.address cfg.bitcoind.rpc.port} - btcnodeendpoint=${nbLib.addressWithPort cfg.bitcoind.address cfg.bitcoind.whitelistedPort} - bind=${cfg.nbxplorer.address} - port=${toString cfg.nbxplorer.port} - postgres=User ID=${cfg.nbxplorer.user};Host=/run/postgresql;Database=nbxplorer - ''; - in rec { - wantedBy = [ "multi-user.target" ]; - requires = [ "postgresql.target" ]; - wants = [ "bitcoind.service" ]; - after = requires ++ wants ++ [ "nix-bitcoin-secrets.target" ]; - preStart = '' - install -m 600 ${configFile} '${cfg.nbxplorer.dataDir}/settings.config' - printf '%s\n' "btcrpcpassword=$(<${secretsDir}/bitcoin-rpcpassword-btcpayserver)" \ - >> '${cfg.nbxplorer.dataDir}/settings.config' - ''; - serviceConfig = nbLib.defaultHardening // { - ExecStart = '' - ${cfg.nbxplorer.package}/bin/nbxplorer --conf=${cfg.nbxplorer.dataDir}/settings.config \ - --datadir='${cfg.nbxplorer.dataDir}' - ''; - RuntimeDirectory = "nbxplorer"; - StateDirectory = "nbxplorer"; - User = cfg.nbxplorer.user; - Group = cfg.nbxplorer.group; - Restart = "on-failure"; - RestartSec = "10s"; - ReadWritePaths = [ cfg.nbxplorer.dataDir ]; - MemoryDenyWriteExecute = false; - } // nbLib.allowedIPAddresses cfg.nbxplorer.tor.enforce; - }; - - services.bitcoind = { - enable = true; - listenWhitelisted = true; - txindex = true; - }; - - users.users.${cfg.nbxplorer.user} = { - isSystemUser = true; - group = cfg.nbxplorer.group; - home = cfg.nbxplorer.dataDir; - }; - users.groups.${cfg.nbxplorer.group} = {}; - }) - - (mkIf cfg.btcpayserver.enable { - services.nbxplorer.enable = true; - - services.bitcoind = { - listenWhitelisted = true; - rpc.users.btcpayserver = { - name = "btcpayserver"; - passwordHMACFromFile = true; - rpcwhitelist = [ - "getblockchaininfo" - "getblock" - "getblockhash" - "getblockheader" - "getblockstats" - "gettransaction" - "getrawtransaction" - "sendrawtransaction" - "getblockcount" - "getbestblockhash" - "getnetworkinfo" - "getpeerinfo" - "estimatesmartfee" - "getmempoolinfo" - "getmempoolentry" - "getrawmempool" - "gettxout" - "scantxoutset" - "importmulti" - "listunspent" - "getwalletinfo" - "listtransactions" - "listreceivedbyaddress" - "getnewaddress" - "uptime" - "getrpcinfo" - ]; - }; - }; - - systemd.tmpfiles.rules = [ - "d '${cfg.btcpayserver.dataDir}' 0770 ${cfg.btcpayserver.user} ${cfg.btcpayserver.group} - -" - ]; - - systemd.services.btcpayserver = let - nbExplorerUrl = "http://${nbLib.addressWithPort cfg.nbxplorer.address cfg.nbxplorer.port}/"; - nbExplorerCookie = - "${cfg.nbxplorer.dataDir}/${cfg.bitcoind.makeNetworkName "Main" "RegTest"}/.cookie"; - configFile = builtins.toFile "btcpayserver-config" ( - '' - network=${cfg.bitcoind.network} - bind=${cfg.btcpayserver.address} - port=${toString cfg.btcpayserver.port} - socksendpoint=${config.nix-bitcoin.torClientAddressWithPort} - btcexplorerurl=${nbExplorerUrl} - btcexplorercookiefile=${nbExplorerCookie} - explorer.postgres=User ID=${cfg.nbxplorer.user};Host=/run/postgresql;Database=nbxplorer - postgres=User ID=${cfg.btcpayserver.user};Host=/run/postgresql;Database=btcpayserver - '' + optionalString (cfg.btcpayserver.lightningBackend == "lnd") - ( - "btclightning=type=lnd-rest;" - + "server=https://${nbLib.address config.services.lnd.restAddress}:${toString config.services.lnd.restPort}/;" - + "macaroonfilepath=/run/lnd/btcpayserver.macaroon;" - + "certfilepath=${config.services.lnd.certPath}\n" - ) - ); - in rec { - wantedBy = [ "multi-user.target" ]; - requires = [ "postgresql.target" "nbxplorer.service" ]; - wants = optional (cfg.btcpayserver.lightningBackend == "lnd") "lnd.service"; - after = requires ++ wants; - serviceConfig = nbLib.defaultHardening // { - ExecStart = '' - ${cfg.btcpayserver.package}/bin/btcpayserver --conf=${configFile} \ - --datadir='${cfg.btcpayserver.dataDir}' - ''; - WorkingDirectory = "${cfg.btcpayserver.package}/lib/btcpayserver"; - RuntimeDirectory = "btcpayserver"; - StateDirectory = "btcpayserver"; - User = cfg.btcpayserver.user; - Group = cfg.btcpayserver.group; - Restart = "on-failure"; - RestartSec = "10s"; - ReadWritePaths = [ cfg.btcpayserver.dataDir ]; - MemoryDenyWriteExecute = false; - } // nbLib.allowedIPAddresses cfg.btcpayserver.tor.enforce; - }; - - services.postgresql = { - enable = true; - ensureDatabases = [ "btcpayserver" "nbxplorer" ]; - ensureUsers = [ - { name = cfg.btcpayserver.user; ensureDBOwnership = true; } - { name = cfg.nbxplorer.user; ensureDBOwnership = true; } - ]; - }; - - users.users.${cfg.btcpayserver.user} = { - isSystemUser = true; - group = cfg.btcpayserver.group; - home = cfg.btcpayserver.dataDir; - extraGroups = optional (cfg.btcpayserver.lightningBackend == "lnd") config.services.lnd.group; - }; - users.groups.${cfg.btcpayserver.group} = {}; - - nix-bitcoin.secrets = { - bitcoin-rpcpassword-btcpayserver = { - user = cfg.bitcoind.user; - group = cfg.nbxplorer.group; - }; - bitcoin-HMAC-btcpayserver.user = cfg.bitcoind.user; - }; - nix-bitcoin.generateSecretsCmds.btcpayserver = '' - makeBitcoinRPCPassword btcpayserver - ''; - }) - - (mkIf (cfg.btcpayserver.enable && cfg.btcpayserver.lightningBackend == "lnd") { - services.lnd = { - enable = true; - macaroons.btcpayserver = { - user = cfg.btcpayserver.user; - permissions = '' - {"entity":"address","action":"write"},{"entity":"info","action":"read"},{"entity":"invoices","action":"read"},{"entity":"invoices","action":"write"},{"entity":"offchain","action":"read"},{"entity":"offchain","action":"write"},{"entity":"onchain","action":"read"},{"entity":"onchain","action":"write"},{"entity":"peers","action":"read"},{"entity":"peers","action":"write"} - ''; - }; - }; - - users.users.${config.services.lnd.user}.extraGroups = [ cfg.btcpayserver.group ]; - }) - ]; -} diff --git a/modules/bitcoin/common.nix b/modules/bitcoin/common.nix deleted file mode 100644 index 1f0f5fa..0000000 --- a/modules/bitcoin/common.nix +++ /dev/null @@ -1,15 +0,0 @@ -# Common Bitcoin infrastructure: secrets, onion services, nodeinfo, security -# Extracted from nix-bitcoin, tailored for Sovran (lnd-only) -{ config, lib, pkgs, ... }: -{ - imports = [ - ./nix-bitcoin.nix - ./secrets/secrets.nix - ./operator.nix - ./security.nix - ./onion-addresses.nix - ./onion-services.nix - ./nodeinfo.nix - ./versioning.nix - ]; -} diff --git a/modules/bitcoin/default.nix b/modules/bitcoin/default.nix deleted file mode 100644 index be2035d..0000000 --- a/modules/bitcoin/default.nix +++ /dev/null @@ -1,16 +0,0 @@ -# Sovran Bitcoin stack - tailored from nix-bitcoin, lnd-only, nixpkgs packages -# Original: https://github.com/fort-nix/nix-bitcoin -{ - imports = [ - ./common.nix - ./bitcoind.nix - ./electrs.nix - ./lnd.nix - ./lndconnect.nix - ./rtl.nix - ./btcpayserver.nix - ./mempool.nix - ]; - - disabledModules = [ "services/networking/bitcoind.nix" ]; -} diff --git a/modules/bitcoin/electrs.nix b/modules/bitcoin/electrs.nix deleted file mode 100644 index 592d492..0000000 --- a/modules/bitcoin/electrs.nix +++ /dev/null @@ -1,106 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - options.services.electrs = { - enable = mkEnableOption "electrs, an Electrum server implemented in Rust"; - address = mkOption { - type = types.str; - default = "127.0.0.1"; - description = "Address to listen for RPC connections."; - }; - port = mkOption { - type = types.port; - default = 50001; - description = "Port to listen for RPC connections."; - }; - dataDir = mkOption { - type = types.path; - default = "/var/lib/electrs"; - description = "The data directory for electrs."; - }; - monitoringPort = mkOption { - type = types.port; - default = 4224; - description = "Prometheus monitoring port."; - }; - extraArgs = mkOption { - type = types.separatedString " "; - default = ""; - description = "Extra command line arguments passed to electrs."; - }; - user = mkOption { - type = types.str; - default = "electrs"; - description = "The user as which to run electrs."; - }; - group = mkOption { - type = types.str; - default = cfg.user; - description = "The group as which to run electrs."; - }; - tor.enforce = nbLib.tor.enforce; - }; - - cfg = config.services.electrs; - nbLib = config.nix-bitcoin.lib; - secretsDir = config.nix-bitcoin.secretsDir; - bitcoind = config.services.bitcoind; -in { - inherit options; - - config = mkIf cfg.enable { - assertions = [ - { assertion = bitcoind.prune == 0; - message = "electrs does not support bitcoind pruning."; - } - ]; - - services.bitcoind = { - enable = true; - listenWhitelisted = true; - }; - - systemd.tmpfiles.rules = [ - "d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -" - ]; - - systemd.services.electrs = { - wantedBy = [ "multi-user.target" ]; - requires = [ "bitcoind.service" ]; - after = [ "bitcoind.service" "nix-bitcoin-secrets.target" ]; - preStart = '' - echo "auth = \"${bitcoind.rpc.users.public.name}:$(cat ${secretsDir}/bitcoin-rpcpassword-public)\"" \ - > electrs.toml - ''; - serviceConfig = nbLib.defaultHardening // { - # electrs only uses the working directory for reading electrs.toml - WorkingDirectory = cfg.dataDir; - ExecStart = '' - ${pkgs.electrs}/bin/electrs \ - --log-filters=INFO \ - --network=${bitcoind.makeNetworkName "bitcoin" "regtest"} \ - --db-dir='${cfg.dataDir}' \ - --daemon-dir='${bitcoind.dataDir}' \ - --electrum-rpc-addr=${cfg.address}:${toString cfg.port} \ - --monitoring-addr=${cfg.address}:${toString cfg.monitoringPort} \ - --daemon-rpc-addr=${nbLib.addressWithPort bitcoind.rpc.address bitcoind.rpc.port} \ - --daemon-p2p-addr=${nbLib.addressWithPort bitcoind.address bitcoind.whitelistedPort} \ - ${cfg.extraArgs} - ''; - User = cfg.user; - Group = cfg.group; - Restart = "on-failure"; - RestartSec = "10s"; - ReadWritePaths = [ cfg.dataDir ]; - } // nbLib.allowedIPAddresses cfg.tor.enforce; - }; - - users.users.${cfg.user} = { - isSystemUser = true; - group = cfg.group; - extraGroups = [ "bitcoinrpc-public" ]; - }; - users.groups.${cfg.group} = {}; - }; -} diff --git a/modules/bitcoin/lib.nix b/modules/bitcoin/lib.nix deleted file mode 100644 index 5bfb639..0000000 --- a/modules/bitcoin/lib.nix +++ /dev/null @@ -1,132 +0,0 @@ -lib: pkgs: config: - -with lib; - -# See `man systemd.exec` and `man systemd.resource-control` for an explanation -# of the systemd-related options available through this file. -let self = { - # These settings roughly follow systemd's "strict" security profile - defaultHardening = { - PrivateTmp = true; - ProtectSystem = "strict"; - ProtectHome = true; - NoNewPrivileges = true; - PrivateDevices = true; - MemoryDenyWriteExecute = true; - ProtectKernelTunables = true; - ProtectKernelModules = true; - ProtectKernelLogs = true; - ProtectClock = true; - ProtectProc = "invisible"; - ProcSubset = "pid"; - ProtectControlGroups = true; - RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6"; - RestrictNamespaces = true; - LockPersonality = true; - IPAddressDeny = "any"; - PrivateUsers = true; - RestrictSUIDSGID = true; - RemoveIPC = true; - RestrictRealtime = true; - ProtectHostname = true; - CapabilityBoundingSet = ""; - # @system-service whitelist and docker seccomp blacklist (except for "clone" - # which is a core requirement for systemd services) - # @system-service is defined in src/shared/seccomp-util.c (systemd source) - SystemCallFilter = [ "@system-service" "~add_key kcmp keyctl mbind move_pages name_to_handle_at personality process_vm_readv process_vm_writev request_key setns unshare userfaultfd" ]; - SystemCallArchitectures = "native"; - }; - - allowNetlink = { - RestrictAddressFamilies = self.defaultHardening.RestrictAddressFamilies + " AF_NETLINK"; - }; - - nodejs = { - # Required for JIT compilation - MemoryDenyWriteExecute = false; - # Required by nodejs >= 18 - SystemCallFilter = self.defaultHardening.SystemCallFilter ++ [ "@pkey" ]; - }; - - # Allow takes precedence over Deny. - allowLocalIPAddresses = { - IPAddressAllow = [ - "127.0.0.1/32" - "::1/128" - "169.254.0.0/16" - ]; - }; - allowAllIPAddresses = { IPAddressAllow = "any"; }; - allowTor = self.allowLocalIPAddresses; - allowedIPAddresses = onlyLocal: - if onlyLocal - then self.allowLocalIPAddresses - else self.allowAllIPAddresses; - - tor = { - proxy = mkOption { - type = types.bool; - default = false; - description = "Whether to proxy outgoing connections with Tor."; - }; - enforce = mkOption { - type = types.bool; - default = false; - description = '' - Whether to enforce Tor on this service by only allowing connections - from and to localhost and link-local addresses. - ''; - }; - }; - - script = name: src: pkgs.writers.writeBash name '' - set -eo pipefail - ${src} - ''; - - # Used for ExecStart* - rootScript = name: src: "+${self.script name src}"; - - cliExec = mkOption { - # Used by netns-isolation to execute the cli in the service's private netns - internal = true; - type = types.str; - default = "exec"; - }; - - mkOnionService = map: { - map = [ map ]; - version = 3; - }; - - # Convert a bind address, which may be a special INADDR_ANY address, - # to an actual IP address - address = addr: - if addr == "0.0.0.0" then - "127.0.0.1" - else if addr == "::" then - "::1" - else - addr; - - addressWithPort = addr: port: "${self.address addr}:${toString port}"; - - optionalAttr = cond: name: if cond then name else null; - - mkCertExtraAltNames = cert: - builtins.concatStringsSep "," ( - (map (domain: "DNS:${domain}") cert.extraDomains) ++ - (map (ip: "IP:${ip}") cert.extraIPs) - ); - - test = { - mkIfTest = test: mkIf (config.tests.${test} or false); - }; - - mkAlias = default: mkOption { - internal = true; - readOnly = true; - inherit default; - }; - -}; in self diff --git a/modules/bitcoin/lnd.nix b/modules/bitcoin/lnd.nix deleted file mode 100644 index 3fae438..0000000 --- a/modules/bitcoin/lnd.nix +++ /dev/null @@ -1,316 +0,0 @@ -{ 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//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}' - ''; - }; -} diff --git a/modules/bitcoin/lndconnect.nix b/modules/bitcoin/lndconnect.nix deleted file mode 100644 index f231f36..0000000 --- a/modules/bitcoin/lndconnect.nix +++ /dev/null @@ -1,115 +0,0 @@ -{ config, lib, pkgs, ... }: - -# LND-only lndconnect wrapper. Restored to the fort-nix/nix-bitcoin contract -# after the LND-only rewrite shipped a Zeus QR that Zeus cannot use: -# - unknown flags (--cert / --macaroon instead of --tlscertpath / --adminmacaroonpath) -# - onion hostname read from /var/lib/tor/onion/free/lnd/hostname (does not exist) -# - REST hidden service named "lnd", colliding with the LND P2P onion -# - TLS cert embedded in the URI (localhost CN + QR too dense to scan) -# -# Zeus needs: lndconnect://:8080?macaroon= (no cert over Tor) - -with lib; -let - cfg = config.services.lnd; - operatorName = config.nix-bitcoin.operator.name; - nbLib = config.nix-bitcoin.lib; - runAsUser = config.nix-bitcoin.runAsUserCmd; - - mkLndconnect = { - name, - shebang ? "#!${pkgs.stdenv.shell} -e", - port, - authSecretPath, - enableOnion, - onionService ? null, - certPath ? null - }: - # lndconnect requires a --configfile argument, although it's unused - # https://github.com/LN-Zap/lndconnect/issues/25 - lib.hiPrio (pkgs.writeScriptBin name '' - ${shebang} - url=$( - ${getExe pkgs.lndconnect} --url \ - ${optionalString enableOnion "--host=$(cat ${config.nix-bitcoin.onionAddresses.dataDir}/${onionService})"} \ - --port=${toString port} \ - ${if enableOnion || certPath == null then "--nocert" else "--tlscertpath='${certPath}'"} \ - --adminmacaroonpath='${authSecretPath}' \ - --configfile=/dev/null "$@" - ) - - # If --url is in args - if [[ " $* " =~ " --url " ]]; then - echo "$url" - else - # UTF-8 QR is smaller than lndconnect's native output - echo -n "$url" | ${getExe pkgs.qrencode} -t UTF8 -o - - fi - ''); -in { - options.services.lnd.lndconnect = { - enable = mkOption { - type = types.bool; - default = false; - description = '' - Add a `lndconnect` binary to the system environment which prints - connection info for lnd clients (Zeus). - See: https://github.com/LN-Zap/lndconnect - - Usage: - ```bash - # Print QR code - lndconnect - - # Print URL - lndconnect --url - ``` - ''; - }; - onion = mkOption { - type = types.bool; - default = false; - description = '' - Create an onion service for the lnd REST server, - which is used by lndconnect / Zeus. - ''; - }; - }; - - config = mkIf (cfg.enable && cfg.lndconnect.enable) (mkMerge [ - { - environment.systemPackages = [( - mkLndconnect { - name = "lndconnect"; - # Run as lnd user because the macaroon and cert are not group-readable - shebang = "#!/usr/bin/env -S ${runAsUser} ${cfg.user} ${pkgs.bash}/bin/bash"; - enableOnion = cfg.lndconnect.onion; - onionService = "${cfg.user}/lnd-rest"; - port = cfg.restPort; - certPath = cfg.certPath; - authSecretPath = "${cfg.networkDir}/admin.macaroon"; - } - )]; - - # LAN / clearnet Zeus needs REST on all interfaces. Tor-only stays on - # the existing restAddress (loopback) and is reached via lnd-rest. - services.lnd.restAddress = mkIf (!cfg.lndconnect.onion) "0.0.0.0"; - } - - (mkIf cfg.lndconnect.onion { - services.tor = { - enable = true; - # Dedicated name — must not reuse onionServices.lnd (that's P2P :9735). - relay.onionServices.lnd-rest = nbLib.mkOnionService { - target.addr = nbLib.address cfg.restAddress; - target.port = cfg.restPort; - port = cfg.restPort; - }; - }; - nix-bitcoin.onionAddresses.access = { - ${cfg.user} = [ "lnd-rest" ]; - ${operatorName} = [ "lnd-rest" ]; - }; - }) - ]); -} diff --git a/modules/bitcoin/mempool.nix b/modules/bitcoin/mempool.nix deleted file mode 100644 index e70a799..0000000 --- a/modules/bitcoin/mempool.nix +++ /dev/null @@ -1,348 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - options.services = { - mempool = { - enable = mkOption { - type = types.bool; - default = false; - description = '' - Enable Mempool, a fully featured Bitcoin visualizer, explorer, and API service. - - Note: Mempool enables `txindex` in bitcoind (this is a requirement). - - This module has two components: - - A backend service (systemd service `mempool`) - - - An optional web interface run by nginx, defined by options `services.mempool.frontend.*`. - The frontend is enabled by default when mempool is enabled. - For details, see `services.mempool.frontend.enable`. - ''; - }; - - frontend = { - enable = mkOption { - type = types.bool; - default = cfg.enable; - description = '' - Enable the mempool frontend (web interface). - This starts a simple nginx instance, configured for local usage with - settings similar to the `mempool/frontend` Docker image. - - IMPORTANT: - If you want to expose the mempool frontend to the internet, you - should create a custom nginx config that includes TLS, backend caching, rate limiting - and performance tuning. - For this task, reuse the config snippets from option `services.mempool.frontend.nginxConfig`. - See also: https://github.com/fort-nix/nixbitcoin.org/blob/master/website/mempool.nix, - which contains a mempool nginx config for public hosting (running at - https://mempool.nixbitcoin.org). - ''; - }; - address = mkOption { - type = types.str; - default = "127.0.0.1"; - description = "HTTP server address."; - }; - port = mkOption { - type = types.port; - default = 60845; # A random private port - description = "HTTP server port."; - }; - settings = mkOption { - type = with types; attrsOf anything; - default = {}; - example = { - TESTNET_ENABLED = true; - MEMPOOL_WEBSITE_URL = "mempool.mynode.org"; - }; - description = '' - Mempool frontend settings. - See here for available options: - https://github.com/mempool/mempool/blob/master/frontend/src/app/services/state.service.ts - (`interface Env` and `defaultEnv`) - ''; - }; - staticContentRoot = mkOption { - type = types.path; - default = (pkgs.callPackage ../../packages/mempool { fetchNodeModules = pkgs.callPackage ../../packages/build-support/fetch-node-modules.nix {}; }).mempool-frontend.withConfig cfg.frontend.settings; - defaultText = "mempoolPkgs.mempool-frontend"; - description = " - Path of the static frontend content root. - "; - }; - nginxConfig = mkOption { - readOnly = true; - default = frontend.nginxConfig; - defaultText = "(See source)"; - description = " - An attrset of nginx config snippets for assembling a custom - mempool nginx config. - For details, see the source comments at the point of definition. - "; - }; - }; - - address = mkOption { - type = types.str; - default = "127.0.0.1"; - description = "Mempool backend address."; - }; - port = mkOption { - type = types.port; - default = 8999; - description = "Mempool backend port."; - }; - electrumServer = mkOption { - type = types.enum [ "electrs" ]; - default = "electrs"; - description = '' - The Electrum server to use for fetching address information. - - Possible options: - - electrs: - Small database size, slow when querying new addresses. - ''; - }; - settings = mkOption { - type = with types; attrsOf (attrsOf anything); - example = { - MEMPOOL = { - POLL_RATE_MS = 3000; - STDOUT_LOG_MIN_PRIORITY = "debug"; - }; - PRICE_DATA_SERVER = { - CLEARNET_URL = "https://myserver.org/prices"; - }; - }; - description = '' - Mempool backend settings. - See here for available options: - https://github.com/mempool/mempool/blob/master/backend/src/config.ts - ''; - }; - database = { - name = mkOption { - type = types.str; - default = "mempool"; - description = "Database name."; - }; - }; - package = mkOption { - type = types.package; - default = (pkgs.callPackage ../../packages/mempool { fetchNodeModules = pkgs.callPackage ../../packages/build-support/fetch-node-modules.nix {}; }).mempool-backend; - defaultText = "mempoolPkgs.mempool-backend"; - description = "The package providing mempool binaries."; - }; - user = mkOption { - type = types.str; - default = "mempool"; - description = "The user as which to run Mempool."; - }; - group = mkOption { - type = types.str; - default = cfg.user; - description = "The group as which to run Mempool."; - }; - tor = nbLib.tor; - }; - - # Internal read-only options used by `./nodeinfo.nix` and `./onion-services.nix` - mempool-frontend = let - inherit (nbLib) mkAlias; - in { - enable = mkAlias cfg.frontend.enable; - address = mkAlias cfg.frontend.address; - port = mkAlias cfg.frontend.port; - }; - }; - - cfg = config.services.mempool; - nbLib = config.nix-bitcoin.lib; - nbPkgs = pkgs; # vendored: now alias to pkgs - secretsDir = config.nix-bitcoin.secretsDir; - - configFile = builtins.toFile "mempool-config" (builtins.toJSON cfg.settings); - cacheDir = "/var/cache/mempool"; - - inherit (config.services) - bitcoind - electrs; - - torSocket = config.services.tor.client.socksListenAddress; - # Vendored mempool package - fetchNodeModules = pkgs.callPackage ../../packages/build-support/fetch-node-modules.nix {}; - mempoolPkgs = pkgs.callPackage ../../packages/mempool { inherit fetchNodeModules; }; - - # See the `services.nginx` definition further below - # on how to use these snippets. - frontend.nginxConfig = { - # This must be added to `services.nginx.commonHttpConfig` when - # `mempool/location-static.conf` is used - httpConfig = '' - include ${mempoolPkgs.mempool-nginx-conf}/http-language.conf; - ''; - - # Config for static website content. - # This should be added to `services.nginx.virtualHosts..extraConfig`. - # Adapted from mempool/nginx-mempool.conf and mempool/production/nginx/location-redirects.conf - staticContent = '' - index index.html; - - add_header Cache-Control "public, no-transform"; - add_header Vary Accept-Language; - add_header Vary Cookie; - - include ${mempoolPkgs.mempool-nginx-conf}/location-static.conf; - - # Redirect /api to /docs/api - location = /api { - return 308 https://$host/docs/api; - } - location = /api/ { - return 308 https://$host/docs/api; - } - ''; - - # Config for backend API. - # This should be added to `services.nginx.virtualHosts..extraConfig`. - # Adapted from mempool/nginx-mempool.conf and mempool/production/nginx/location-api.conf. - proxyApi = let - backend = "http://${nbLib.addressWithPort cfg.address cfg.port}"; - in '' - location /api/ { - proxy_pass ${backend}/api/v1/; - } - location /api/v1 { - proxy_pass ${backend}; - } - # Websocket API - location /api/v1/ws { - proxy_pass ${backend}; - - # Websocket header settings - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "Upgrade"; - - # Relevant settings from `recommendedProxyConfig` (nixos/nginx/default.nix) - # (In the above api locations, these are inherited from the parent scope) - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - ''; - }; - -in { - inherit options; - - config = mkIf cfg.enable { - services.bitcoind.txindex = true; - services.electrs.enable = true; - services.mysql = { - enable = true; - package = pkgs.mariadb; - ensureDatabases = [ cfg.database.name ]; - ensureUsers = [ - { - name = cfg.user; - ensurePermissions."${cfg.database.name}.*" = "ALL PRIVILEGES"; - } - ]; - }; - - # Available options: - # https://github.com/mempool/mempool/blob/master/backend/src/config.ts - services.mempool.settings = { - MEMPOOL = { - # mempool doesn't support regtest - NETWORK = "mainnet"; - BACKEND = "electrum"; - HTTP_PORT = cfg.port; - CACHE_DIR = "${cacheDir}/cache"; - STDOUT_LOG_MIN_PRIORITY = mkDefault "info"; - AUTOMATIC_POOLS_UPDATE = true; - }; - CORE_RPC = { - HOST = bitcoind.rpc.address; - PORT = bitcoind.rpc.port; - USERNAME = bitcoind.rpc.users.public.name; - PASSWORD = "@btcRpcPassword@"; - }; - ELECTRUM = let - server = config.services.${cfg.electrumServer}; - in { - HOST = server.address; - PORT = server.port; - TLS_ENABLED = false; - }; - DATABASE = { - ENABLED = true; - DATABASE = cfg.database.name; - SOCKET = "/run/mysqld/mysqld.sock"; - PID_DIR = cacheDir; - }; - } // optionalAttrs (cfg.tor.proxy) { - # Use Tor for rate fetching and pool updating - SOCKS5PROXY = { - ENABLED = true; - USE_ONION = true; - HOST = torSocket.addr; - PORT = torSocket.port; - }; - }; - - systemd.services.mempool = rec { - wantedBy = [ "multi-user.target" ]; - requires = [ "mysql.service" ]; - wants = [ "${cfg.electrumServer}.service" ]; - after = requires ++ wants; - preStart = '' - mkdir -p '${cacheDir}/cache' - <${configFile} sed \ - -e "s|@btcRpcPassword@|$(cat ${secretsDir}/bitcoin-rpcpassword-public)|" \ - > '${cacheDir}/config.json' - ''; - environment.MEMPOOL_CONFIG_FILE = "${cacheDir}/config.json"; - serviceConfig = nbLib.defaultHardening // { - ExecStart = "${cfg.package}/bin/mempool-backend"; - CacheDirectory = "mempool"; - CacheDirectoryMode = "770"; - # Show "mempool" instead of "node" in the journal - SyslogIdentifier = "mempool"; - User = cfg.user; - Restart = "on-failure"; - RestartSec = "10s"; - } // nbLib.allowedIPAddresses cfg.tor.enforce - // nbLib.nodejs; - }; - - services.nginx = mkIf cfg.frontend.enable { - enable = true; - enableReload = true; - recommendedBrotliSettings = true; - recommendedGzipSettings = true; - recommendedOptimisation = true; - recommendedProxySettings = true; - recommendedTlsSettings = true; - commonHttpConfig = frontend.nginxConfig.httpConfig; - virtualHosts."mempool" = { - serverName = "_"; - listen = [ { addr = cfg.frontend.address; port = cfg.frontend.port; } ]; - root = cfg.frontend.staticContentRoot; - extraConfig = - frontend.nginxConfig.staticContent + - frontend.nginxConfig.proxyApi; - }; - }; - - users.users.${cfg.user} = { - isSystemUser = true; - group = cfg.group; - extraGroups = [ "bitcoinrpc-public" ]; - }; - users.groups.${cfg.group} = {}; - }; -} diff --git a/modules/bitcoin/nix-bitcoin.nix b/modules/bitcoin/nix-bitcoin.nix deleted file mode 100644 index 609415d..0000000 --- a/modules/bitcoin/nix-bitcoin.nix +++ /dev/null @@ -1,60 +0,0 @@ -{ config, pkgs, lib, ... }: - -with lib; -{ - options = { - nix-bitcoin = { - # Kept for compatibility, now simply aliases system pkgs - pkgs = mkOption { - type = types.attrs; - default = pkgs; - defaultText = "pkgs"; - description = "Alias to system pkgs (vendored nix-bitcoin now uses nixpkgs directly)."; - }; - - useVersionLockedPkgs = mkOption { - type = types.bool; - default = false; - description = "Deprecated — vendored modules always use system pkgs."; - }; - - pkgOverlays = mkOption { - internal = true; - type = with types; functionTo attrs; - default = _: _: {}; - description = "Deprecated stub."; - }; - - lib = mkOption { - readOnly = true; - default = import ./lib.nix lib pkgs config; - defaultText = "vendor/nix-bitcoin/lib.nix"; - }; - - torClientAddressWithPort = mkOption { - readOnly = true; - default = with config.services.tor.client.socksListenAddress; - "${addr}:${toString port}"; - defaultText = "(See source)"; - }; - - torify = mkOption { - readOnly = true; - default = pkgs.writers.writeBashBin "torify" '' - ${pkgs.tor}/bin/torify \ - --address ${config.services.tor.client.socksListenAddress.addr} \ - "$@" - ''; - defaultText = "(See source)"; - }; - - runAsUserCmd = mkOption { - readOnly = true; - default = if config.security.doas.enable - then "doas -u" - else "sudo -u"; - defaultText = "(See source)"; - }; - }; - }; -} diff --git a/modules/bitcoin/nodeinfo.nix b/modules/bitcoin/nodeinfo.nix deleted file mode 100644 index acec892..0000000 --- a/modules/bitcoin/nodeinfo.nix +++ /dev/null @@ -1,154 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - options = { - nix-bitcoin.nodeinfo = { - enable = mkEnableOption "nodeinfo"; - - program = mkOption { - readOnly = true; - default = script; - defaultText = "(See source)"; - }; - - services = mkOption { - internal = true; - type = types.attrs; - default = {}; - defaultText = "(See source)"; - description = '' - Nodeinfo service definitions. - ''; - }; - - lib = mkOption { - internal = true; - readOnly = true; - default = nodeinfoLib; - defaultText = "(See source)"; - description = '' - Helper functions for defining nodeinfo services. - ''; - }; - }; - }; - - cfg = config.nix-bitcoin.nodeinfo; - nbLib = config.nix-bitcoin.lib; - - script = pkgs.writeScriptBin "nodeinfo" '' - #!${pkgs.python3}/bin/python - - import json - import subprocess - import sys - from collections import OrderedDict - - def success(*args): - return subprocess.call(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0 - - def is_active(unit): - return success("systemctl", "is-active", "--quiet", unit) - - def is_enabled(unit): - return success("systemctl", "is-enabled", "--quiet", unit) - - def cmd(*args): - return subprocess.run(args, stdout=subprocess.PIPE).stdout.decode('utf-8') - - def shell(*args): - return cmd("bash", "-c", *args).strip() - - infos = OrderedDict() - operator = "${config.nix-bitcoin.operator.name}" - - def get_onion_address(name, port): - path = f"/var/lib/onion-addresses/{operator}/{name}" - try: - with open(path, "r") as f: - onion_address = f.read().strip() - except OSError: - print(f"error reading file {path}", file=sys.stderr) - return - return f"{onion_address}:{port}" - - def add_service(service, make_info, systemd_service = None): - systemd_service = systemd_service or service - if not is_active(systemd_service): - infos[service] = f"'{systemd_service}.service' is not running" - else: - info = OrderedDict() - exec(make_info, globals(), locals()) - infos[service] = info - - if is_enabled("onion-addresses") and not is_active("onion-addresses"): - print("error: service 'onion-addresses' is not running") - exit(1) - - ${concatStrings infos} - - print(json.dumps(infos, indent=2)) - ''; - - infos = map (serviceName: - let serviceCfg = config.services.${serviceName}; - in optionalString serviceCfg.enable (cfg.services.${serviceName} serviceName serviceCfg) - ) (builtins.attrNames cfg.services); - - nodeinfoLib = rec { - mkInfo = extraCode: name: cfg: - mkInfoLong { - inherit extraCode name cfg; - }; - - mkInfoLong = { extraCode ? "", name, cfg, systemdServiceName ? name }: '' - add_service("${name}", """ - info["local_address"] = "${nbLib.addressWithPort cfg.address cfg.port}" - '' + mkIfOnionPort name (onionPort: '' - info["onion_address"] = get_onion_address("${name}", ${onionPort}) - '') + extraCode + '' - - """, "${systemdServiceName}") - ''; - - mkIfOnionPort = name: fn: - if onionServices ? ${name} then - fn (toString (builtins.elemAt onionServices.${name}.map 0).port) - else - ""; - }; - - inherit (config.services.tor.relay) onionServices; -in { - inherit options; - - config = mkIf cfg.enable { - environment.systemPackages = [ script ]; - - nix-bitcoin.operator.enable = true; - - nix-bitcoin.nodeinfo.services = with nodeinfoLib; { - bitcoind = mkInfo ""; - lnd = name: cfg: mkInfo ('' - info["rest_address"] = "${nbLib.addressWithPort cfg.restAddress cfg.restPort}" - '' + mkIfOnionPort "lnd-rest" (onionPort: '' - info["onion_rest_address"] = get_onion_address("lnd-rest", ${onionPort}) - '') + '' - info["nodeid"] = shell("lncli getinfo | jq -r '.identity_pubkey'") - '') name cfg; - electrs = mkInfo ""; - btcpayserver = mkInfo ""; - rtl = mkInfo ""; - mempool = mkInfo ""; - mempool-frontend = name: cfg: mkInfoLong { - inherit name cfg; - systemdServiceName = "nginx"; - }; - # Only add sshd when it has an onion service - sshd = name: cfg: mkIfOnionPort "sshd" (onionPort: '' - add_service("sshd", """info["onion_address"] = get_onion_address("sshd", ${onionPort})""") - ''); - }; - }; -} diff --git a/modules/bitcoin/onion-addresses.nix b/modules/bitcoin/onion-addresses.nix deleted file mode 100644 index 85193be..0000000 --- a/modules/bitcoin/onion-addresses.nix +++ /dev/null @@ -1,106 +0,0 @@ -# This module enables unprivileged users to read onion addresses. -# By default, onion addresses in /var/lib/tor/onion are only readable by the -# tor user. -# The included service copies onion addresses to /var/lib/onion-addresses// -# and sets permissions according to option 'access'. - -{ config, lib, ... }: - -with lib; -let - options.nix-bitcoin.onionAddresses = { - access = mkOption { - type = with types; attrsOf (listOf str); - default = {}; - description = '' - This option controls who is allowed to access onion addresses. - For example, the following allows user 'myuser' to access bitcoind - and clightning onion addresses: - ```nix - { - "myuser" = [ "bitcoind" "clightning" ]; - }; - ``` - The onion hostnames can then be read from - {file}`/var/lib/onion-addresses/myuser`. - ''; - }; - services = mkOption { - type = with types; listOf str; - default = []; - description = '' - Services that can access their onion address via file - {file}`/var/lib/onion-addresses/` - The file is readable only by the service user. - ''; - }; - dataDir = mkOption { - readOnly = true; - default = "/var/lib/onion-addresses"; - }; - }; - - cfg = config.nix-bitcoin.onionAddresses; - nbLib = config.nix-bitcoin.lib; -in { - inherit options; - - config = mkIf (cfg.access != {} || cfg.services != []) { - systemd.services.onion-addresses = { - wantedBy = [ "tor.service" ]; - bindsTo = [ "tor.service" ]; - after = [ "tor.service" ]; - serviceConfig = nbLib.defaultHardening // { - Type = "oneshot"; - RemainAfterExit = true; - StateDirectory = "onion-addresses"; - StateDirectoryMode = "771"; - PrivateNetwork = true; # This service needs no network access - PrivateUsers = false; - CapabilityBoundingSet = "CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_FOWNER CAP_IPC_OWNER"; - }; - script = '' - waitForFile() { - file=$1 - for ((i=0; i<300; i++)); do - if [[ -e $file ]]; then - return; - fi - sleep 0.1 - done - echo "Error: File $file did not appear after 30 sec." - exit 1 - } - - # Wait until tor is up - waitForFile /var/lib/tor/state - - cd ${cfg.dataDir} - rm -rf ./* - - ${concatMapStrings - (user: '' - mkdir -p -m 0700 ${user} - chown ${user} ${user} - ${concatMapStrings - (service: '' - onionFile='/var/lib/tor/onion/${service}/hostname' - waitForFile "$onionFile" - cp "$onionFile" '${user}/${service}' - chown '${user}' '${user}/${service}' - '') - cfg.access.${user} - } - '') - (builtins.attrNames cfg.access) - } - - ${concatMapStrings (service: '' - onionFile=/var/lib/tor/onion/${service}/hostname - waitForFile "$onionFile" - install -D -o ${config.systemd.services.${service}.serviceConfig.User} -m 400 "$onionFile" services/${service} - '') cfg.services} - ''; - }; - }; -} diff --git a/modules/bitcoin/onion-services.nix b/modules/bitcoin/onion-services.nix deleted file mode 100644 index cebbd1c..0000000 --- a/modules/bitcoin/onion-services.nix +++ /dev/null @@ -1,119 +0,0 @@ -# This module creates onion-services for NixOS services. -# An onion service can be enabled for every service that defines -# options 'address', 'port' and optionally 'getPublicAddressCmd'. -# -# See it in use at ./presets/enable-tor.nix - -{ config, lib, pkgs, ... }: - -with lib; -let - options.nix-bitcoin.onionServices = mkOption { - default = {}; - type = with types; attrsOf (submodule ( - { config, ... }: { - options = { - enable = mkOption { - type = types.bool; - default = config.public; - description = '' - Create an onion service for the given service. - The service must define options {option}`address` and {option}`onionPort` (or `port`). - ''; - }; - public = mkOption { - type = types.bool; - default = false; - description = '' - Make the onion address accessible to the service. - If enabled, the onion service is automatically enabled. - Only available for services that define option {option}`getPublicAddressCmd`. - ''; - }; - externalPort = mkOption { - type = types.nullOr types.port; - default = null; - description = "Override the external port of the onion service."; - }; - }; - } - )); - }; - - cfg = config.nix-bitcoin.onionServices; - nbLib = config.nix-bitcoin.lib; - - onionServices = builtins.attrNames cfg; - - activeServices = builtins.filter (service: - config.services.${service}.enable && cfg.${service}.enable - ) onionServices; - - publicServices = builtins.filter (service: cfg.${service}.public) activeServices; -in { - inherit options; - - config = mkMerge [ - (mkIf (activeServices != []) { - # Define hidden services - services.tor = { - enable = true; - relay.onionServices = genAttrs activeServices (name: - let - service = config.services.${name}; - inherit (cfg.${name}) externalPort; - in nbLib.mkOnionService { - port = if externalPort != null then externalPort else service.port; - target.port = service.onionPort or service.port; - target.addr = nbLib.address service.address; - } - ); - }; - - nix-bitcoin.onionAddresses = { - # Enable public services to access their own onion addresses - services = publicServices; - - # Allow the operator user to access onion addresses for all active services - access.${config.nix-bitcoin.operator.name} = mkIf config.nix-bitcoin.operator.enable activeServices; - }; - systemd.services = let - onionAddresses = [ "onion-addresses.service" ]; - in genAttrs publicServices (service: { - # TODO-EXTERNAL: Instead of `wants`, use a future systemd dependency type - # that propagates initial start failures but no restarts - wants = onionAddresses; - after = onionAddresses; - }); - }) - - # Set getPublicAddressCmd for public services - { - services = let - # publicServices' doesn't depend on config.services.*.enable, - # so we can use it to define config.services without causing infinite recursion - publicServices' = builtins.filter (service: - let srv = cfg.${service}; - in srv.public && srv.enable - ) onionServices; - in genAttrs publicServices' (service: { - getPublicAddressCmd = "cat ${config.nix-bitcoin.onionAddresses.dataDir}/services/${service}"; - }); - } - - # Set sensible defaults for some services - { - nix-bitcoin.onionServices = { - btcpayserver = { - externalPort = 80; - }; - rtl = { - externalPort = 80; - }; - mempool-frontend = { - externalPort = 80; - }; - }; - } - ]; -} diff --git a/modules/bitcoin/operator.nix b/modules/bitcoin/operator.nix deleted file mode 100644 index 173d2a4..0000000 --- a/modules/bitcoin/operator.nix +++ /dev/null @@ -1,58 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - options.nix-bitcoin.operator = { - enable = mkOption { - type = types.bool; - default = false; - description = '' - Whether to define a user named `operator` for convenient interactive access - to nix-bitcoin features (like `bitcoin-cli`). - - When using nix-bitcoin as part of a larger system config, it makes sense - to set your main system user as the operator, by setting option - `nix-bitcoin.operator.name = "MAIN_USER_NAME";`. - ''; - }; - name = mkOption { - type = types.str; - default = "operator"; - description = "Name of the operator user."; - }; - groups = mkOption { - type = with types; listOf str; - default = []; - description = "Extra groups of the operatur user."; - }; - allowRunAsUsers = mkOption { - type = with types; listOf str; - default = []; - description = "Users as which the operator is allowed to run commands."; - }; - }; - - cfg = config.nix-bitcoin.operator; -in { - inherit options; - - config = mkIf cfg.enable { - users.users.${cfg.name} = { - isNormalUser = true; - extraGroups = [ - "systemd-journal" - "proc" # Enable full /proc access and systemd-status - ] ++ cfg.groups; - }; - - security = mkIf (cfg.allowRunAsUsers != []) { - # Use doas instead of sudo if enabled - doas.extraConfig = mkIf config.security.doas.enable '' - ${lib.concatMapStrings (user: "permit nopass ${cfg.name} as ${user}\n") cfg.allowRunAsUsers} - ''; - sudo.extraConfig = mkIf (!config.security.doas.enable) '' - ${cfg.name} ALL=(${builtins.concatStringsSep "," cfg.allowRunAsUsers}) NOPASSWD: ALL - ''; - }; - }; -} diff --git a/modules/bitcoin/rtl.nix b/modules/bitcoin/rtl.nix deleted file mode 100644 index 40096f7..0000000 --- a/modules/bitcoin/rtl.nix +++ /dev/null @@ -1,216 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - options.services.rtl = { - enable = mkEnableOption "RTL, a web interface for LND"; - - address = mkOption { - type = types.str; - default = "127.0.0.1"; - description = "Address to listen for HTTP connections."; - }; - - port = mkOption { - type = types.port; - default = 3000; - description = "Port to listen for HTTP connections."; - }; - - dataDir = mkOption { - type = types.path; - default = "/var/lib/rtl"; - description = "The data directory for RTL."; - }; - - nightTheme = mkOption { - type = types.bool; - default = false; - description = "Enable night theme by default."; - }; - - extraCurrency = mkOption { - type = types.nullOr types.str; - default = null; - example = "USD"; - description = '' - Additional currency for displaying amounts. - When set, Tor is disabled for the RTL service to allow currency rate fetching. - ''; - }; - - nodes = { - lnd = { - enable = mkOption { - type = types.bool; - default = false; - description = "Enable LND node in RTL."; - }; - loop = mkOption { - type = types.bool; - default = false; - description = "Enable Lightning Loop integration (requires loopd)."; - }; - }; - clightning = { - enable = mkOption { - type = types.bool; - default = false; - description = "Enable Core Lightning node in RTL (not supported in Sovran)."; - }; - }; - }; - - user = mkOption { - type = types.str; - default = "rtl"; - description = "The user as which to run RTL."; - }; - - group = mkOption { - type = types.str; - default = cfg.user; - description = "The group as which to run RTL."; - }; - - tor = nbLib.tor; - }; - - cfg = config.services.rtl; - nbLib = config.nix-bitcoin.lib; - secretsDir = config.nix-bitcoin.secretsDir; - - # Vendored RTL package - fetchNodeModules = pkgs.callPackage ../../packages/build-support/fetch-node-modules.nix {}; - rtlPackage = pkgs.callPackage ../../packages/rtl { inherit fetchNodeModules; }; - - runePath = "${cfg.dataDir}/CLN-Rune.env"; - - rtlConfig = { - multiPass = "@multiPass@"; - port = cfg.port; - host = cfg.address; - defaultNodeIndex = 1; - dbDirectoryPath = cfg.dataDir; - SSO = { - rtlSSO = 0; - rtlCookiePath = ""; - logoutRedirectLink = ""; - }; - nodes = optional cfg.nodes.lnd.enable ({ - index = 1; - lnNode = "lnd"; - lnImplementation = "LND"; - authentication = { - macaroonPath = "${cfg.dataDir}/macaroons"; - swapMacaroonPath = if cfg.nodes.lnd.loop then "${cfg.dataDir}/loop-macaroons" else ""; - boltzMacaroonPath = ""; - }; - settings = { - userPersona = "OPERATOR"; - themeMode = if cfg.nightTheme then "NIGHT" else "DAY"; - themeColor = "PURPLE"; - channelBackupPath = "${cfg.dataDir}/backup"; - logLevel = "INFO"; - lnServerUrl = "https://${lnd.restAddress}:${toString lnd.restPort}"; - swapServerUrl = if cfg.nodes.lnd.loop then "https://127.0.0.1:8081" else ""; - boltzServerUrl = ""; - fiatConversion = cfg.extraCurrency != null; - unannouncedChannels = true; - } // optionalAttrs (cfg.extraCurrency != null) { - currencyUnit = cfg.extraCurrency; - }; - }) ++ optional cfg.nodes.clightning.enable { - index = 2; - lnNode = "clightning"; - lnImplementation = "CLN"; - authentication = { - runePath = runePath; - }; - settings = { - userPersona = "OPERATOR"; - themeMode = if cfg.nightTheme then "NIGHT" else "DAY"; - themeColor = "PURPLE"; - logLevel = "INFO"; - fiatConversion = cfg.extraCurrency != null; - } // optionalAttrs (cfg.extraCurrency != null) { - currencyUnit = cfg.extraCurrency; - }; - }; - }; - - configFile = builtins.toFile "config" (builtins.toJSON rtlConfig); - - inherit (config.services) - bitcoind - lnd; - - lndLoopEnabled = cfg.nodes.lnd.enable && cfg.nodes.lnd.loop; -in { - inherit options; - - config = mkIf cfg.enable { - assertions = [ - { assertion = cfg.nodes.lnd.enable; - message = '' - RTL: At least one node must be enabled. Sovran supports LND only. - ''; - } - { assertion = !cfg.nodes.clightning.enable; - message = '' - RTL: Core Lightning (clightning) is not supported in Sovran. Use LND instead. - ''; - } - ]; - - services.lnd.enable = mkIf cfg.nodes.lnd.enable true; - - systemd.tmpfiles.rules = [ - "d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -" - ]; - - services.rtl.tor.enforce = mkIf (cfg.extraCurrency != null) false; - - systemd.services.rtl = rec { - wantedBy = [ "multi-user.target" ]; - wants = optional cfg.nodes.lnd.enable "lnd.service"; - after = wants ++ [ "nix-bitcoin-secrets.target" ]; - environment.RTL_CONFIG_PATH = cfg.dataDir; - environment.DB_DIRECTORY_PATH = cfg.dataDir; - serviceConfig = nbLib.defaultHardening // { - ExecStartPre = [ - (nbLib.script "rtl-setup-config" '' - <${configFile} sed "s|@multiPass@|$(cat ${secretsDir}/rtl-password)|" \ - > '${cfg.dataDir}/RTL-Config.json' - '') - ] - ++ optional cfg.nodes.lnd.enable - # The lnd admin macaroon is not readable by group `lnd`, so copy it - (nbLib.rootScript "rtl-copy-macaroon" '' - install --compare -m 640 -o ${cfg.user} -g ${cfg.group} -D ${lnd.networkDir}/admin.macaroon \ - '${cfg.dataDir}/macaroons/admin.macaroon' - ''); - ExecStart = "${rtlPackage}/bin/rtl"; - # Show "rtl" instead of "node" in the journal - SyslogIdentifier = "rtl"; - User = cfg.user; - Restart = "on-failure"; - RestartSec = "10s"; - ReadWritePaths = [ cfg.dataDir ]; - } // nbLib.allowedIPAddresses cfg.tor.enforce - // nbLib.nodejs; - }; - - users.users.${cfg.user} = { - isSystemUser = true; - group = cfg.group; - extraGroups = optional lndLoopEnabled lnd.group; - }; - users.groups.${cfg.group} = {}; - - nix-bitcoin.secrets.rtl-password.user = cfg.user; - nix-bitcoin.generateSecretsCmds.rtl = '' - makePasswordSecret rtl-password - ''; - }; -} diff --git a/modules/bitcoin/secrets/secrets.nix b/modules/bitcoin/secrets/secrets.nix deleted file mode 100644 index 944c92b..0000000 --- a/modules/bitcoin/secrets/secrets.nix +++ /dev/null @@ -1,235 +0,0 @@ -{ config, pkgs, lib, ... }: - -with lib; -let - options.nix-bitcoin = { - secretsDir = mkOption { - type = types.path; - default = "/etc/nix-bitcoin-secrets"; - description = "Directory to store secrets"; - }; - - setupSecrets = mkOption { - type = types.bool; - default = false; - description = '' - Set permissions for existing secrets in {option}`nix-bitcoin.secretsDir` - before services are started. - ''; - }; - - generateSecrets = mkOption { - type = types.bool; - default = false; - description = '' - Automatically generate all required secrets before services are started. - Note: Make sure to create a backup of the generated secrets. - ''; - }; - - generateSecretsCmds = mkOption { - type = types.attrsOf types.lines; - default = {}; - description = '' - Bash expressions for generating secrets. - ''; - }; - - # Currently, this is used only by ../deployment/nixops.nix - deployment.secretsDir = mkOption { - type = types.path; - description = '' - Directory of local secrets that are transferred to the nix-bitcoin node on deployment - ''; - }; - - secrets = mkOption { - default = {}; - type = with types; attrsOf (submodule ( - { config, ... }: { - options = { - user = mkOption { - type = str; - default = "root"; - }; - group = mkOption { - type = str; - default = config.user; - }; - permissions = mkOption { - type = str; - default = "440"; - }; - }; - } - )); - }; - - secretsSetupMethod = mkOption { - type = with types; nullOr str; - default = null; - }; - - generateSecretsScript = mkOption { - internal = true; - default = let - rpcauthSrc = pkgs.fetchurl { - url = "https://raw.githubusercontent.com/bitcoin/bitcoin/d6cde007db9d3e6ee93bd98a9bbfdce9bfa9b15b/share/rpcauth/rpcauth.py"; - sha256 = "189mpplam6yzizssrgiyv70c9899ggh8cac76j4n7v0xqzfip07n"; - }; - rpcauth = pkgs.writers.writeBash "rpcauth" '' - exec ${pkgs.python3}/bin/python ${rpcauthSrc} "$@" - ''; - # Writes secrets to PWD - in pkgs.writers.writeBash "generate-secrets" '' - set -euo pipefail - - export PATH=${lib.makeBinPath (with pkgs; [ coreutils gnugrep ])} - - makePasswordSecret() { - # Passwords have alphabet {a-z, A-Z, 0-9} and ~119 bits of entropy - [[ -e $1 ]] || ${pkgs.pwgen}/bin/pwgen -s 20 1 > "$1" - } - makeBitcoinRPCPassword() { - user=$1 - file=bitcoin-rpcpassword-$user - HMACfile=bitcoin-HMAC-$user - makePasswordSecret "$file" - if [[ $file -nt $HMACfile ]]; then - ${rpcauth} $user $(cat "$file") | grep rpcauth | cut -d ':' -f 2 > "$HMACfile" - fi - } - makeCert() { - name=$1 - # Add leading comma if not empty - extraAltNames=''${2:+,}''${2:-} - if [[ ! -e $name-key ]]; then - # Create new key and cert - doMakeCert "-newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes -keyout $name-key" - elif [[ ! -e $name-cert \ - || $(cat "$name-cert-alt-names" 2>/dev/null) != $extraAltNames ]]; then - # Create cert from existing key - doMakeCert "-key $name-key" - fi; - } - doMakeCert() { - # This fn uses global variables `name` and `extraAltNames` - keyOpts=$1 - ${pkgs.openssl}/bin/openssl req -x509 \ - -sha256 -days 3650 $keyOpts -out "$name-cert" \ - -subj "/CN=localhost/O=$name" \ - -addext "subjectAltName=DNS:localhost,IP:127.0.0.1$extraAltNames" - echo "$extraAltNames" > "$name-cert-alt-names" - } - - umask u=rw,go= - ${builtins.concatStringsSep "\n" (builtins.attrValues cfg.generateSecretsCmds)} - ''; - }; - }; - - cfg = config.nix-bitcoin; -in { - inherit options; - - config = { - assertions = [ - { assertion = cfg.secretsSetupMethod != null; - message = '' - No secrets setup method has been defined. - To fix this, choose one of the following: - - - Use one of the deployment methods in ${toString ./../deployment} - - - Set `nix-bitcoin.generateSecrets = true` to automatically generate secrets - - - Set `nix-bitcoin.secretsSetupMethod = "manual"` if you want to manually setup secrets - ''; - } - ]; - - # This target is active when secrets have been setup successfully. - systemd.targets.nix-bitcoin-secrets = mkIf (cfg.secretsSetupMethod != "manual") { - # This ensures that the secrets target is always activated when switching - # configurations. - # In this way `switch-to-configuration` is guaranteed to show an error - # when activating the secrets target fails on deployment. - wantedBy = [ "multi-user.target" ]; - }; - - nix-bitcoin.setupSecrets = mkIf cfg.generateSecrets true; - - nix-bitcoin.secretsSetupMethod = mkIf cfg.setupSecrets "setup-secrets"; - - # Operation of this service: - # - Set owner and permissions for all used secrets - # - Make all other secrets accessible to root only - # For all steps make sure that no secrets are copied to the nix store. - # - systemd.services.setup-secrets = mkIf cfg.setupSecrets { - requiredBy = [ "nix-bitcoin-secrets.target" ]; - before = [ "nix-bitcoin-secrets.target" ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - }; - script = '' - # Use the same sort order for globbing and sorting as in Nix attrsets. - # Required for `comm` below. - export LC_COLLATE=C - - ${optionalString cfg.generateSecrets '' - mkdir -p "${cfg.secretsDir}" - cd "${cfg.secretsDir}" - chown root: . - chmod 0700 . - ${cfg.generateSecretsScript} - ''} - - 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") - } - - dir="${cfg.secretsDir}" - if [[ ! -e $dir ]]; then - echo "Error: Secrets dir '$dir' is missing" - exit 1 - fi - chown root: "$dir" - cd "$dir" - - processedFiles=() - ${ - concatStrings (mapAttrsToList (n: v: '' - setupSecret ${n} ${v.user} ${v.group} ${v.permissions} - '') cfg.secrets) - } - - # Make all other files accessible to root only - unprocessedFiles=$( - comm -23 <(shopt -s nullglob; printf '%s\n' *) <(printf '%s\n' "''${processedFiles[@]}") - ) - if [[ $unprocessedFiles ]]; then - IFS=$'\n' - # shellcheck disable=SC2086 - chown root: $unprocessedFiles - # shellcheck disable=SC2086 - chmod 0440 $unprocessedFiles - fi - - # Now make the secrets dir accessible to other users - chmod 0751 "$dir" - ''; - }; - }; -} diff --git a/modules/bitcoin/security.nix b/modules/bitcoin/security.nix deleted file mode 100644 index 338d82a..0000000 --- a/modules/bitcoin/security.nix +++ /dev/null @@ -1,48 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -{ - options = { - nix-bitcoin.security.dbusHideProcessInformation = mkOption { - type = types.bool; - default = false; - description = '' - Only allow users with group `proc` to retrieve systemd unit information like - cgroup paths (i.e. (sub)process command lines) via D-Bus. - - This mitigates a systemd security issue where (sub)process command lines can - be retrieved by services even when their access to /proc is restricted - (via ProtectProc). - - This option works by restricting the D-Bus method `GetUnitProcesses`, which - is also used internally by {command}`systemctl status`. - ''; - }; - }; - - config = mkIf config.nix-bitcoin.security.dbusHideProcessInformation { - users.groups.proc = {}; - nix-bitcoin.operator.groups = [ "proc" ]; # Enable operator access to systemd-status - - services.dbus.packages = lib.mkAfter [ # Apply at the end to override the default policy - (pkgs.writeTextDir "etc/dbus-1/system.d/dbus.conf" '' - - - - - - - - - '') - ]; - }; -} diff --git a/modules/bitcoin/versioning.nix b/modules/bitcoin/versioning.nix deleted file mode 100644 index c34d4ce..0000000 --- a/modules/bitcoin/versioning.nix +++ /dev/null @@ -1,12 +0,0 @@ -{ config, lib, ... }: -with lib; -let - options.nix-bitcoin.configVersion = mkOption { - type = with types; nullOr str; - default = null; - description = "Vendored stub — no version migration needed."; - }; -in { - inherit options; - config = {}; -} diff --git a/modules/bitcoinecosystem.nix b/modules/bitcoinecosystem.nix deleted file mode 100755 index 136fdf9..0000000 --- a/modules/bitcoinecosystem.nix +++ /dev/null @@ -1,128 +0,0 @@ -{ config, pkgs, lib, ... }: - -lib.mkIf config.sovran_systemsOS.services.bitcoin { - - services.bitcoind = { - enable = true; - # Keep the normal loopback P2P socket available for local clients such as - # Bisq. Because `address` defaults to 127.0.0.1 this does not expose a - # clearnet or LAN listener. When the existing bitcoind onion service is - # enabled, this also creates its Tor-tagged loopback target on port 8334. - listen = true; - package = pkgs.bitcoind; - dataDir = "/run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node"; - txindex = true; - tor.proxy = true; - tor.enforce = true; - disablewallet = true; - extraConfig = '' - peerbloomfilters=1 - server=1 - ''; - }; - - nix-bitcoin.onionServices.bitcoind = { - enable = true; - # This is a locally vendored option namespace, not an upstream dependency. - # The onion listener remains available to peers that already know it; - # advertising its address through Bitcoin peer gossip is opt-in in the Hub. - public = config.sovran_systemsOS.features.bitcoin-tor-gossip; - }; - nix-bitcoin.onionServices.electrs.enable = true; - nix-bitcoin.onionServices.rtl.enable = true; - - services.electrs = { - enable = true; - tor.enforce = true; - dataDir = "/run/media/Second_Drive/BTCEcoandBackup/Electrs_Data"; - }; - - services.lnd = { - enable = true; - tor.enforce = true; - tor.proxy = true; - extraConfig = '' - protocol.option-scid-alias=true - ''; - }; - - nix-bitcoin.onionServices.lnd.public = true; - - services.lnd.lndconnect = { - enable = true; - onion = true; - }; - - services.rtl = { - enable = true; - tor.enforce = true; - port = 3050; - nightTheme = true; - nodes = { - lnd = { - enable = true; - }; - }; - }; - - services.btcpayserver = { - enable = config.sovran_systemsOS.web.btcpayserver; - }; - - services.btcpayserver.lightningBackend = "lnd"; - - nix-bitcoin.generateSecrets = true; - nix-bitcoin.nodeinfo.enable = true; - - nix-bitcoin.operator = { - enable = true; - name = "free"; - }; - - # vendored: now no-op (always uses nixpkgs) - nix-bitcoin.useVersionLockedPkgs = false; - - systemd.services.bitcoind = { - requires = [ "run-media-Second_Drive.mount" ]; - after = [ "run-media-Second_Drive.mount" ]; - serviceConfig.PrivateUsers = lib.mkForce false; - }; - - systemd.services.electrs = { - requires = lib.mkForce [ "run-media-Second_Drive.mount" ]; - after = [ "run-media-Second_Drive.mount" "bitcoind.service" ]; - wants = [ "bitcoind.service" ]; - }; - - systemd.services.lnd = { - wants = [ "bitcoind.service" ]; - # requires for bitcoind set by nix-bitcoin; mkForce removes it - requires = lib.mkForce [ ]; - }; - - systemd.services.sovran-btc-permissions = { - description = "Fix Bitcoin/Electrs data directory ownership on second drive"; - wantedBy = [ "multi-user.target" ]; - after = [ "run-media-Second_Drive.mount" ]; - before = [ "bitcoind.service" "electrs.service" ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - }; - script = '' - if [ -d /run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node ]; then - chown -R bitcoin:bitcoin /run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node - fi - if [ -d /run/media/Second_Drive/BTCEcoandBackup/Electrs_Data ]; then - chown -R electrs:electrs /run/media/Second_Drive/BTCEcoandBackup/Electrs_Data - fi - ''; - }; - - networking.firewall.allowedTCPPorts = [ 3051 ]; - networking.firewall.allowedUDPPorts = [ 3051 ]; - - sovran_systemsOS.domainRequirements = [ - { name = "btcpayserver"; label = "BTCPay Server"; example = "pay.yourdomain.com"; } - ]; -} diff --git a/modules/core/role-logic.nix b/modules/core/role-logic.nix index 499b382..217c25e 100755 --- a/modules/core/role-logic.nix +++ b/modules/core/role-logic.nix @@ -3,7 +3,7 @@ { config = lib.mkMerge [ - # Vendored nix-bitcoin is always imported via modules/vendor/nix-bitcoin/modules.nix. + # The Sovran_Bitcoin flake module is always imported (see flake.nix). # This default satisfies the secrets assertion so Desktop-Only systems evaluate # without enabling any Bitcoin services. { diff --git a/modules/core/sovran-hub.nix b/modules/core/sovran-hub.nix index f1f9a48..0727eb4 100644 --- a/modules/core/sovran-hub.nix +++ b/modules/core/sovran-hub.nix @@ -135,12 +135,13 @@ let "bitcoind.service" = if pkgs ? bitcoind then pkgs.bitcoind.version else "27.1.0"; "electrs.service" = if pkgs ? electrs then pkgs.electrs.version else "0.10.6"; "lnd.service" = if pkgs ? lnd then pkgs.lnd.version else "0.18.0"; - # Evaluate locally vendored packages directly so we don't accidentally - # pick up older/different versions from upstream nixpkgs. - "rtl.service" = (pkgs.callPackage ../../packages/rtl { fetchNodeModules = pkgs.callPackage ../../packages/build-support/fetch-node-modules.nix {}; }).version; + # Vendored packages come from the Sovran_Bitcoin flake overlay + # (pkgs.sovran-bitcoin.*). Evaluate them directly so the Hub shows + # the exact versions this system ships. + "rtl.service" = pkgs.sovran-bitcoin.rtl.version; "btcpayserver.service" = lib.getVersion config.services.btcpayserver.package; - "albyhub.service" = (pkgs.callPackage ../../packages/albyhub {}).version; - "mempool.service" = (pkgs.callPackage ../../packages/mempool { fetchNodeModules = pkgs.callPackage ../../packages/build-support/fetch-node-modules.nix {}; }).version; + "albyhub.service" = pkgs.sovran-bitcoin.albyhub.version; + "mempool.service" = pkgs.sovran-bitcoin.mempool-backend.version; "matrix-synapse.service" = if pkgs ? matrix-synapse then pkgs.matrix-synapse.version else "1.115.0"; "livekit.service" = if pkgs ? livekit then pkgs.livekit.version else "1.5.2"; "vaultwarden.service" = if pkgs ? vaultwarden then pkgs.vaultwarden.version else "1.32.0"; diff --git a/modules/mempool.nix b/modules/mempool.nix deleted file mode 100755 index 51274e5..0000000 --- a/modules/mempool.nix +++ /dev/null @@ -1,14 +0,0 @@ -{ config, pkgs, lib, ... }: - -lib.mkIf config.sovran_systemsOS.features.mempool { - - services.mempool = { - enable = true; - frontend.enable = true; - }; - - services.mysql.package = lib.mkForce pkgs.mariadb; - - nix-bitcoin.onionServices.mempool-frontend.enable = true; - -} diff --git a/modules/modules.nix b/modules/modules.nix index 09cdff4..74e0eb6 100755 --- a/modules/modules.nix +++ b/modules/modules.nix @@ -28,14 +28,11 @@ ./wordpress.nix ./nextcloud.nix ./vaultwarden.nix - ./bitcoinecosystem.nix ./wallet-autoconnect.nix # ── Features (default OFF — enable in custom.nix) ───────── ./haven.nix - ./nwc-wallets.nix ./element-calling.nix - ./mempool.nix ./rdp.nix ./sshd.nix ]; diff --git a/modules/nwc-wallets.nix b/modules/nwc-wallets.nix deleted file mode 100644 index c07d4ed..0000000 --- a/modules/nwc-wallets.nix +++ /dev/null @@ -1,177 +0,0 @@ -{ config, pkgs, lib, ... }: - -let - albyHubPort = 18080; - albyHubApiBase = "http://127.0.0.1:${toString albyHubPort}"; - vendoredAlbyHub = pkgs.callPackage ../packages/albyhub {}; - patchedAlbyHub = vendoredAlbyHub; - - lndRpcAddress = lib.attrByPath [ "services" "lnd" "rpcAddress" ] "127.0.0.1" config; - lndRpcPort = toString (lib.attrByPath [ "services" "lnd" "rpcPort" ] 10009 config); - lndCertPath = config.services.lnd.certPath; - pythonManagerEnvironment = { - NWC_ALBY_HUB_API_BASE = albyHubApiBase; - NWC_LND_ADDRESS = "${lndRpcAddress}:${lndRpcPort}"; - NWC_LND_CERT_FILE = lndCertPath; - NWC_LND_MACAROON_FILE = "/run/lnd/albyhub.macaroon"; - NWC_RELAY = - if config.sovran_systemsOS.features.haven - then "wss://haven.${config.networking.domain}/nostr" - else "wss://relay.getalby.com,wss://relay2.getalby.com"; - }; - wrappedNwcWallet = lib.hiPrio (pkgs.writeShellScriptBin "nwc-wallet" '' - export NWC_ALBY_HUB_API_BASE='${pythonManagerEnvironment.NWC_ALBY_HUB_API_BASE}' - export NWC_LND_ADDRESS='${pythonManagerEnvironment.NWC_LND_ADDRESS}' - export NWC_LND_CERT_FILE='${pythonManagerEnvironment.NWC_LND_CERT_FILE}' - export NWC_LND_MACAROON_FILE='${pythonManagerEnvironment.NWC_LND_MACAROON_FILE}' - export NWC_RELAY='${pythonManagerEnvironment.NWC_RELAY}' - exec ${config.services.sovranHub.webPackage}/bin/nwc-wallet "$@" - ''); - - albyhubWrapper = pkgs.writeShellScript "albyhub-wrapper" '' - set -euo pipefail - password_file="/var/lib/albyhub/unlock-password" - if [ ! -s "$password_file" ]; then - umask 077 - ${pkgs.openssl}/bin/openssl rand -hex 32 > "$password_file" - fi - export AUTO_UNLOCK_PASSWORD="$(cat "$password_file")" - exec ${lib.getExe patchedAlbyHub} - ''; -in -lib.mkIf config.sovran_systemsOS.features."nwc-wallets" { - assertions = [ - { - assertion = config.services.lnd.enable; - message = "Lightning Wallet Connections requires services.lnd.enable = true."; - } - { - assertion = !(lib.attrByPath [ "nix-bitcoin" "netns-isolation" "enable" ] false config); - message = "Lightning Wallet Connections requires nix-bitcoin.netns-isolation.enable = false."; - } - { - assertion = albyHubPort != config.services.lnd.restPort; - message = "Alby Hub and LND REST must use different ports."; - } - { - assertion = albyHubPort != 8181; - message = "Alby Hub and the public LNURL service must use different ports."; - } - { - assertion = !(lib.elem albyHubPort config.networking.firewall.allowedTCPPorts); - message = "Alby Hub management port must not be opened on the public TCP firewall."; - } - ]; - - users.groups.albyhub = { }; - users.users.albyhub = { - isSystemUser = true; - group = "albyhub"; - home = "/var/lib/albyhub"; - createHome = false; - extraGroups = [ ]; - }; - - systemd.tmpfiles.rules = [ - "d /var/lib/albyhub 0700 albyhub albyhub -" - ]; - - services.lnd.macaroons.albyhub = { - user = "albyhub"; - permissions = lib.concatStringsSep "," [ - ''{"entity":"info","action":"read"}'' - ''{"entity":"offchain","action":"read"}'' - ''{"entity":"offchain","action":"write"}'' - ''{"entity":"invoices","action":"read"}'' - ''{"entity":"invoices","action":"write"}'' - ''{"entity":"onchain","action":"read"}'' - ''{"entity":"address","action":"read"}'' - ''{"entity":"message","action":"read"}'' - ''{"entity":"message","action":"write"}'' - ]; - }; - - systemd.services.albyhub = { - description = "Alby Hub — NWC wallet server"; - wantedBy = [ "multi-user.target" ]; - after = [ "network.target" "lnd.service" ]; - requires = [ "lnd.service" ]; - - environment = { - HOME = "/var/lib/albyhub"; - HOST = "127.0.0.1"; - LN_BACKEND_TYPE = "LND"; - ENABLE_ADVANCED_SETUP = "false"; - LND_ADDRESS = "${lndRpcAddress}:${lndRpcPort}"; - LND_CERT_FILE = lndCertPath; - LND_MACAROON_FILE = "/run/lnd/albyhub.macaroon"; - WORK_DIR = "/var/lib/albyhub"; - DATABASE_URI = "/var/lib/albyhub/nwc.db"; - PORT = toString albyHubPort; - # Use private Nostr relay if Haven is enabled, otherwise default to Alby's public relays - RELAY = - if config.sovran_systemsOS.features.haven - then "wss://haven.${config.networking.domain}/nostr" - else "wss://relay.getalby.com,wss://relay2.getalby.com"; - AUTO_LINK_ALBY_ACCOUNT = "false"; - SEND_EVENTS_TO_ALBY = "false"; - LOG_TO_FILE = "false"; - HIDE_UPDATE_BANNER = "true"; - }; - - serviceConfig = { - Type = "simple"; - User = "albyhub"; - Group = "albyhub"; - WorkingDirectory = "/var/lib/albyhub"; - ExecStart = albyhubWrapper; - Restart = "on-failure"; - RestartSec = "10s"; - UMask = "0077"; - NoNewPrivileges = true; - PrivateTmp = true; - ProtectHome = true; - ProtectSystem = "strict"; - ReadWritePaths = [ "/var/lib/albyhub" ]; - ReadOnlyPaths = [ lndCertPath "/run/lnd" ]; - }; - }; - - systemd.services.nwc-lnurl = { - description = "Lightning Wallet Connections public LNURL service"; - wantedBy = [ "multi-user.target" ]; - after = [ "albyhub.service" "sovran-hub-web.service" ]; - wants = [ "albyhub.service" ]; - environment = pythonManagerEnvironment; - - serviceConfig = { - Type = "simple"; - User = "albyhub"; - Group = "albyhub"; - ExecStart = "${config.services.sovranHub.webPackage}/bin/nwc-lnurl"; - Restart = "on-failure"; - RestartSec = "10s"; - UMask = "0027"; - NoNewPrivileges = true; - PrivateTmp = true; - ProtectHome = true; - ProtectSystem = "strict"; - ReadOnlyPaths = [ - "/var/lib/domains/lightning" - "/var/lib/albyhub/unlock-password" - ]; - }; - }; - - systemd.services.sovran-hub-web.environment = pythonManagerEnvironment; - environment.systemPackages = lib.mkBefore [ wrappedNwcWallet ]; - - sovran_systemsOS.domainRequirements = [ - { - name = "lightning"; - label = "Lightning Address Domain"; - example = "pay.yourdomain.com"; - needsDDNS = true; - } - ]; -} \ No newline at end of file diff --git a/modules/sovran-bitcoin-integration.nix b/modules/sovran-bitcoin-integration.nix new file mode 100644 index 0000000..e856c00 --- /dev/null +++ b/modules/sovran-bitcoin-integration.nix @@ -0,0 +1,128 @@ +# Sovran Bitcoin integration layer — bridges Sovran_SystemsOS options to the +# Sovran_Bitcoin flake module. +# +# This is the ONLY place where OS-specific Bitcoin customizations live. +# All bitcoin service modules (bitcoind, electrs, lnd, rtl, btcpayserver, +# mempool, albyhub, lnurl) and vendored packages come from the Sovran_Bitcoin +# flake input. +# +# What this file does: +# 1. Maps sovran_systemsOS.services.bitcoin → sovran-bitcoin.enable +# 2. Maps sovran_systemsOS.features.* → sovran-bitcoin.features.* +# 3. Applies OS-specific overrides (Second_Drive paths, operator "free", +# forced wallet enable, Hub firewall port, domain requirements) +# 4. Wires the Sovran Hub's NWC environment to the Alby Hub service +{ config, pkgs, lib, ... }: + +let + cfg = config.sovran_systemsOS; + + # ── NWC environment for the Sovran Hub web app ───────────────── + # The Hub's web package provides nwc-wallet and nwc-lnurl binaries that + # need to know where Alby Hub and LND are. Sovran_Bitcoin's modules + # handle the base Alby Hub service; this layers on the Hub-specific + # tooling environment. + lndRpcAddress = config.services.lnd.rpcAddress or "127.0.0.1"; + lndRpcPort = toString (config.services.lnd.rpcPort or 10009); + lndCertPath = config.services.lnd.certPath or "/var/lib/lnd/tls.cert"; + + hubNwcEnvironment = { + NWC_ALBY_HUB_API_BASE = "http://127.0.0.1:18080"; + NWC_LND_ADDRESS = "${lndRpcAddress}:${lndRpcPort}"; + NWC_LND_CERT_FILE = lndCertPath; + NWC_LND_MACAROON_FILE = "/run/lnd/albyhub.macaroon"; + NWC_RELAY = + if cfg.features.haven + then "wss://haven.${config.networking.domain}/nostr" + else "wss://relay.getalby.com,wss://relay2.getalby.com"; + }; +in { + # ── 1. Map OS options → Sovran_Bitcoin options ───────────────── + sovran-bitcoin = lib.mkIf cfg.services.bitcoin { + enable = true; + operatorName = "free"; + bitcoindTorGossip = cfg.features.bitcoin-tor-gossip; + + features = { + electrs = true; + lnd = true; + rtl = true; + btcpayserver = cfg.web.btcpayserver; + mempool = cfg.features.mempool; + nwc = cfg.features."nwc-wallets"; + lnurl = cfg.features."nwc-wallets"; + }; + }; + + # ── 2. Second_Drive data paths (OS-specific) ────────────────── + services.bitcoind = lib.mkIf cfg.services.bitcoin { + dataDir = "/run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node"; + # The OS always enables the bitcoind wallet — the Hub and BTCPay need it. + disablewallet = lib.mkForce false; + }; + + services.electrs = lib.mkIf cfg.services.bitcoin { + dataDir = "/run/media/Second_Drive/BTCEcoandBackup/Electrs_Data"; + }; + + # ── 3. Second_Drive mount dependencies ───────────────────────── + systemd.services.bitcoind = lib.mkIf cfg.services.bitcoin { + requires = [ "run-media-Second_Drive.mount" ]; + after = [ "run-media-Second_Drive.mount" ]; + serviceConfig.PrivateUsers = lib.mkForce false; + }; + + systemd.services.electrs = lib.mkIf cfg.services.bitcoin { + requires = lib.mkForce [ "run-media-Second_Drive.mount" ]; + after = [ "run-media-Second_Drive.mount" "bitcoind.service" ]; + wants = [ "bitcoind.service" ]; + }; + + systemd.services.lnd = lib.mkIf cfg.services.bitcoin { + wants = [ "bitcoind.service" ]; + # nix-bitcoin sets `requires = [ "bitcoind.service" ]`; the OS removes it + # so LND can start even if bitcoind is temporarily down. + requires = lib.mkForce [ ]; + }; + + # ── 4. Permission fixup for Second_Drive ─────────────────────── + systemd.services.sovran-btc-permissions = lib.mkIf cfg.services.bitcoin { + description = "Fix Bitcoin/Electrs data directory ownership on second drive"; + wantedBy = [ "multi-user.target" ]; + after = [ "run-media-Second_Drive.mount" ]; + before = [ "bitcoind.service" "electrs.service" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + if [ -d /run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node ]; then + chown -R bitcoin:bitcoin /run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node + fi + if [ -d /run/media/Second_Drive/BTCEcoandBackup/Electrs_Data ]; then + chown -R electrs:electrs /run/media/Second_Drive/BTCEcoandBackup/Electrs_Data + fi + ''; + }; + + # ── 5. Firewall — Hub management port ────────────────────────── + networking.firewall.allowedTCPPorts = lib.mkIf cfg.services.bitcoin [ 3051 ]; + networking.firewall.allowedUDPPorts = lib.mkIf cfg.services.bitcoin [ 3051 ]; + + # ── 6. NWC / LNURL — Sovran Hub integration ─────────────────── + # Sovran_Bitcoin's albyhub.nix and lnurl.nix handle the base services. + # This section wires the Hub's web app environment so the Hub can + # display NWC status and the nwc-wallet CLI works from the Hub shell. + systemd.services.sovran-hub-web.environment = lib.mkIf cfg.features."nwc-wallets" + hubNwcEnvironment; + + # ── 7. Domain requirements ───────────────────────────────────── + sovran_systemsOS.domainRequirements = lib.mkIf cfg.services.bitcoin ( + [ + { name = "btcpayserver"; label = "BTCPay Server"; example = "pay.yourdomain.com"; } + ] + ++ lib.optionals cfg.features."nwc-wallets" [ + { name = "lightning"; label = "Lightning Address Domain"; example = "pay.yourdomain.com"; needsDDNS = true; } + ] + ); +} diff --git a/packages/albyhub/default.nix b/packages/albyhub/default.nix deleted file mode 100644 index 456cd87..0000000 --- a/packages/albyhub/default.nix +++ /dev/null @@ -1,78 +0,0 @@ -{ lib -, buildGoModule -, fetchFromGitHub -, stdenv -}: - -buildGoModule rec { - pname = "albyhub"; - version = "1.24.0"; - - # sovran fork = upstream v1.24.0 + LND-only, no frontend, HOST bind, - # always-private route hints (the customization lives in the fork's commits) - src = fetchFromGitHub { - owner = "naturallaw777"; - repo = "hub"; - tag = "sovran-1.24.0"; - # round 1: copy the "got: sha256-..." from the build error - hash = "sha256-9l7xK7iA1p9qQRClgNV+eYg/vYJxbwBiRObeLRpyfDo="; - }; - - # `go mod vendor` strips the secp256k1-zkp cgo headers (include/), so use - # the full module cache. Round 2: copy the "got: sha256-..." from the error - proxyVendor = true; - vendorHash = "sha256-U0NLVV0gH2IERqaGsJwE2f+iZQ35D+czH8mUKXSM4Wc="; - - subPackages = [ "cmd/http" ]; - - # LND client needs cgo (secp256k1-zkp) - buildInputs = [ (lib.getLib stdenv.cc.cc) ]; - - # pin module downloads to a hermetic proxy and the local toolchain - # (otherwise GOPROXY/GOTOOLCHAIN can leak into the builder env and the - # cache comes back incomplete — the original failure) - overrideModAttrs = (finalAttrs: previousAttrs: { - modBuildPhase = '' - runHook preBuild - - export GIT_SSL_CAINFO=$NIX_SSL_CERT_FILE - export GOPROXY=https://proxy.golang.org,direct - export GOSUMDB=sum.golang.org - export GOTOOLCHAIN=local - - mkdir -p "$GOPATH/pkg/mod/cache/download" - go mod download all - - export GOPROXY="file://$GOPATH/pkg/mod/cache/download" - - go list ./cmd/http - - mkdir -p vendor - - runHook postBuild - ''; - }); - - ldflags = [ - "-X github.com/getAlby/hub/version.Tag=${version}" - "-s" - "-w" - ]; - - postInstall = '' - mv $out/bin/http $out/bin/albyhub - ''; - - preFixup = '' - # Go's external linker bakes the sandbox build dir into the RPATH; - # rewrite it to the real store paths before the fixup audit - patchelf --set-rpath ${ - lib.makeLibraryPath [ - (lib.getLib stdenv.cc.cc) - (lib.getLib stdenv.cc.libc) - ] - } $out/bin/albyhub - ''; - - meta.mainProgram = "albyhub"; -} diff --git a/packages/build-support/fetch-node-modules.nix b/packages/build-support/fetch-node-modules.nix deleted file mode 100644 index 80eee78..0000000 --- a/packages/build-support/fetch-node-modules.nix +++ /dev/null @@ -1,79 +0,0 @@ -# Vendored from fort-nix/nix-bitcoin commit 360e30fee. -# This local copy does not fetch or import nix-bitcoin. -# This is a modified version of -# https://github.com/NixOS/nixpkgs/pull/128749 - -{ lib, stdenvNoCC, makeWrapper, nodejs, cacert }: - -{ src -, hash ? "" -, runScripts ? false -, preferLocalBuild ? true -, npmFlags ? "" -, ... -} @ args: -stdenvNoCC.mkDerivation ({ - inherit src preferLocalBuild; - - name = "${src.name}-node_modules"; - nativeBuildInputs = [ - makeWrapper - (if args ? nodejs then args.nodejs else nodejs) - ]; - - outputHashMode = "recursive"; - - impureEnvVars = lib.fetchers.proxyImpureEnvVars; - - phases = "unpackPhase patchPhase buildPhase installPhase"; - - # npm doesn't support var `SSL_CERT_FILE`. - NODE_EXTRA_CA_CERTS = "${cacert}/etc/ssl/certs/ca-bundle.crt"; - - buildPhase = '' - runHook preBuild - - if [[ ! -f package.json ]]; then - echo "Error: file `package.json` doesn't exist" - exit 1 - fi - if [[ ! -f package-lock.json ]]; then - echo "Error: file `package-lock.json` doesn't exist" - exit 1 - fi - - export SOURCE_DATE_EPOCH=1 - export npm_config_cache=/tmp - NPM_FLAGS="--omit=dev --omit=optional --no-update-notifier $npmFlags" - # Scripts may result in non-deterministic behavior. - # Some packages (e.g., Puppeteer) use postinstall scripts to download extra data. - if [[ ! $runScripts ]]; then - NPM_FLAGS+=" --ignore-scripts" - fi - - echo "Running npm ci $NPM_FLAGS" - npm ci $NPM_FLAGS - - cp package.json \ - package-lock.json node_modules/ - rm -f node_modules/.package-lock.json - - runHook postBuild - ''; - - installPhase = '' - runHook preInstall - - mkdir -p $out/lib - cp -r node_modules $out/lib - - runHook postInstall - ''; -} // ( - if hash == "" then { - outputHashAlgo = "sha256"; - outputHash = ""; - } else { - outputHash = hash; - } -) // (builtins.removeAttrs args [ "hash" ])) diff --git a/packages/mempool/0001-allow-disabling-mining-pool-fetching.patch b/packages/mempool/0001-allow-disabling-mining-pool-fetching.patch deleted file mode 100644 index 0e00682..0000000 --- a/packages/mempool/0001-allow-disabling-mining-pool-fetching.patch +++ /dev/null @@ -1,41 +0,0 @@ -From e4b3ebaf0451c1bddbd7dcf8527c296938ebb607 Mon Sep 17 00:00:00 2001 -From: Erik Arvstedt -Date: Sun, 1 Jun 2025 11:17:22 +0200 -Subject: [PATCH] allow disabling mining pool fetching in offline environments - -Previously, Mempool strictly required fetching mining pool data from -Github and failed when this was not possible, e.g. in offline -environments. - -This patch allows disabling pool fetching. -When disabled, empty pool data is inserted into the DB, which -effectively turns off block pool classification. ---- - backend/src/tasks/pools-updater.ts | 5 ++++- - 1 file changed, 4 insertions(+), 1 deletion(-) - -diff --git a/backend/src/tasks/pools-updater.ts b/backend/src/tasks/pools-updater.ts -index 6b0520dfc..a74259b95 100644 ---- a/backend/src/tasks/pools-updater.ts -+++ b/backend/src/tasks/pools-updater.ts -@@ -75,7 +75,7 @@ class PoolsUpdater { - } else { - logger.warn(`pools-v2.json is outdated, fetching latest from ${this.poolsUrl} over ${network}`, this.tag); - } -- const poolsJson = await this.query(this.poolsUrl); -+ const poolsJson = (githubSha == "disable-pool-fetching") ? [] : await this.query(this.poolsUrl); - if (poolsJson === undefined) { - return; - } -@@ -136,6 +136,9 @@ class PoolsUpdater { - * Fetch our latest pools-v2.json sha from github - */ - private async fetchPoolsSha(): Promise { -+ if (this.poolsUrl == "disable-pool-fetching") { -+ return "disable-pool-fetching"; -+ } - const response = await this.query(this.treeUrl); - - if (response !== undefined) { --- -2.47.2 diff --git a/packages/mempool/default.nix b/packages/mempool/default.nix deleted file mode 100644 index b9df6a1..0000000 --- a/packages/mempool/default.nix +++ /dev/null @@ -1,203 +0,0 @@ -# Packaging adapted from fort-nix/nix-bitcoin commit 360e30fee. -# This local copy does not fetch or import nix-bitcoin. -{ lib -, stdenvNoCC -, nodejs_22 -, nodejs-slim_22 -, fetchFromGitHub -, fetchNodeModules -, runCommand -, makeWrapper -, curl -, cacert -, rsync -# for rust-gbt (backend module) -, cargo -, rustc -, rustPlatform -, napi-rs-cli -}: -rec { - nodejs = nodejs_22; - nodejsRuntime = nodejs-slim_22; - - version = "3.2.1"; - - src = fetchFromGitHub { - owner = "mempool"; - repo = "mempool"; - tag = "v${version}"; - hash = "sha256-O2XPD1/BXQnzuOP/vMVyRfmFZEgjA85r+PShWne0vqU="; - }; - - nodeModules = { - frontend = fetchNodeModules { - inherit src nodejs; - sourceRoot = "source/frontend"; - hash = "sha256-+jfgsAkDdYvgso8uSHaBj/sQL3fC/ABQWzVTXfdZcU0="; - }; - backend = fetchNodeModules { - inherit src nodejs; - sourceRoot = "source/backend"; - hash = "sha256-y5l2SYZYK9SKSy6g0+mtTWD6JFkkdQHHBboECpEvWZ4="; - }; - }; - - frontendAssets = fetchFiles { - name = "mempool-frontend-assets"; - hash = "sha256-r6GfOY8Pdh15o2OQMk8syfvWMV6WMCReToAEkQm7tqQ="; - fetcher = ./frontend-assets-fetch.sh; - }; - - mempool-backend = mkDerivationMempool { - pname = "mempool-backend"; - - patches = [ ./0001-allow-disabling-mining-pool-fetching.patch ]; - - buildPhase = '' - cd backend - ${sync} --chmod=+w ${nodeModules.backend}/lib/node_modules . - patchShebangs node_modules - - ${sync} ${mempool-rust-gbt}/ rust-gbt - npm run package - - runHook postBuild - ''; - - installPhase = '' - mkdir -p $out/lib/mempool-backend - ${sync} package/ $out/lib/mempool-backend - - makeWrapper ${nodejsRuntime}/bin/node $out/bin/mempool-backend \ - --add-flags $out/lib/mempool-backend/index.js - - runHook postInstall - ''; - - passthru = { - inherit nodejs nodejsRuntime; - nodeModules = nodeModules.backend; - }; - }; - - mempool-frontend = mkFrontend {}; - - # Argument `config` (type: attrset) defines the mempool frontend config. - # If `{}`, the default config is used. - # See here for available options: - # https://github.com/mempool/mempool/blob/master/frontend/src/app/services/state.service.ts - # (`interface Env` and `defaultEnv`) - mkFrontend = config: mkDerivationMempool { - pname = "mempool-frontend"; - - buildPhase = '' - cd frontend - - ${sync} --chmod=+w ${nodeModules.frontend}/lib/node_modules . - patchShebangs node_modules - - # sync-assets.js is called during `npm run build` and downloads assets from the - # internet. Disable this script and instead add the assets manually after building. - : > sync-assets.js - - ${lib.optionalString (config != {}) '' - ln -s ${builtins.toFile "mempool-frontend-config" (builtins.toJSON config)} mempool-frontend-config.json - ''} - - npm run build - - # Add assets that would otherwise be downloaded by sync-assets.js - ${sync} ${frontendAssets}/ dist/mempool/browser/resources - - runHook postBuild - ''; - - installPhase = '' - ${sync} dist/mempool/browser/ $out - - runHook postInstall - ''; - - passthru = { - withConfig = mkFrontend; - assets = frontendAssets; - nodeModules = nodeModules.frontend; - }; - }; - - mempool-rust-gbt = stdenvNoCC.mkDerivation rec { - pname = "mempool-rust-gbt"; - inherit version src meta; - - sourceRoot = "source/rust/gbt"; - - nativeBuildInputs = [ - rustPlatform.cargoSetupHook - cargo - rustc - napi-rs-cli - ]; - - cargoDeps = rustPlatform.fetchCargoVendor { - inherit src; - name = "${pname}-${version}"; - inherit sourceRoot; - hash = "sha256-eox/K3ipjAqNyFt87lZnxaU/okQLF/KIhqXrX86n+qw="; - }; - - buildPhase = '' - runHook preBuild - # napi doesn't accept an absolute path as dest dir, so we can't directly write to $out - napi build --platform --release --strip out - runHook postBuild - ''; - - installPhase = '' - mv out $out - cp package.json $out - ''; - - passthru = { inherit cargoDeps; }; - }; - - mempool-nginx-conf = runCommand "mempool-nginx-conf" {} '' - ${sync} --chmod=u+w ${./nginx-conf}/ $out - ${sync} ${src}/production/nginx/http-language.conf $out - ''; - - sync = "${rsync}/bin/rsync -a --inplace"; - - mkDerivationMempool = args: stdenvNoCC.mkDerivation ({ - inherit version src meta; - - nativeBuildInputs = [ - makeWrapper - nodejs - rsync - ]; - - phases = "unpackPhase patchPhase buildPhase installPhase"; - } // args); - - fetchFiles = { name, hash, fetcher }: stdenvNoCC.mkDerivation { - inherit name; - outputHashMode = "recursive"; - outputHashAlgo = "sha256"; - outputHash = hash; - nativeBuildInputs = [ curl cacert ]; - buildCommand = '' - mkdir $out - cd $out - ${builtins.readFile fetcher} - ''; - }; - - meta = with lib; { - description = "Bitcoin blockchain and mempool explorer"; - homepage = "https://github.com/mempool/mempool/"; - license = licenses.agpl3Plus; - maintainers = with maintainers; [ erikarvstedt ]; - platforms = platforms.unix; - }; -} diff --git a/packages/mempool/frontend-assets-fetch.sh b/packages/mempool/frontend-assets-fetch.sh deleted file mode 100755 index 07f69f8..0000000 --- a/packages/mempool/frontend-assets-fetch.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Fetch hash-locked versions of assets that are dynamically fetched via -# https://github.com/mempool/mempool/blob/master/frontend/sync-assets.js -# when running `npm run build` in the frontend. -# -# This file is updated by ./frontend-assets-update.sh - -declare -A revs=( - ["mempool/mining-pool-logos"]=53972ebbd08373cf4910cbb3e6421a1f3bba4563 -) - -fetchFile() { - repo=$1 - file=$2 - rev=${revs["$repo"]} - curl -fsS "https://raw.githubusercontent.com/$repo/$rev/$file" -} - -fetchRepo() { - repo=$1 - rev=${revs["$repo"]} - curl -fsSL "https://github.com/$repo/archive/$rev.tar.gz" -} - -mkdir mining-pools -fetchRepo "mempool/mining-pool-logos" | tar xz --strip-components=1 -C mining-pools diff --git a/packages/mempool/nginx-conf/location-static.conf b/packages/mempool/nginx-conf/location-static.conf deleted file mode 100644 index fd6d4fc..0000000 --- a/packages/mempool/nginx-conf/location-static.conf +++ /dev/null @@ -1,47 +0,0 @@ -# Settings adapted from -# https://github.com/mempool/mempool/blob/v3.2.1/production/nginx/server-common.conf - -# see order of nginx location rules -# https://stackoverflow.com/questions/5238377/nginx-location-priority - -# for exact / requests, redirect based on $lang -# cache redirect for 5 minutes -location = / { - if ($lang != '') { - return 302 $scheme://$host/$lang/; - } - try_files /en-US/index.html =404; - expires 5m; -} - -# cache //main.f40e91d908a068a2.js forever since they never change -location ~ ^/([a-z][a-z])/(.+\..+\.(js|css))$ { - try_files $uri =404; - expires 1y; -} -# cache everything else for 5 minutes -location ~ ^/([a-z][a-z])$ { - try_files $uri /$1/index.html /en-US/index.html =404; - expires 5m; -} -location ~ ^/([a-z][a-z])/ { - try_files $uri /$1/index.html /en-US/index.html =404; - expires 5m; -} - -# cache /resources/** for 1 week since they don't change often -location /resources { - try_files $uri /en-US/index.html; - expires 1w; -} -# cache /main.f40e91d908a068a2.js forever since they never change -location ~* ^/.+\..+\.(js|css)$ { - try_files /$lang/$uri /en-US/$uri =404; - expires 1y; -} -# catch-all for all URLs i.e. /address/foo /tx/foo /block/000 -# cache 5 minutes since they change frequently -location / { - try_files /$lang/$uri $uri /en-US/$uri /en-US/index.html =404; - expires 5m; -} diff --git a/packages/rtl/default.nix b/packages/rtl/default.nix deleted file mode 100644 index 9dbe631..0000000 --- a/packages/rtl/default.nix +++ /dev/null @@ -1,66 +0,0 @@ -# Packaging adapted from fort-nix/nix-bitcoin commit 360e30fee. -# This local copy does not fetch or import nix-bitcoin. -{ lib -, stdenvNoCC -, nodejs_22 -, nodejs-slim_22 -, fetchNodeModules -, fetchurl -, makeWrapper -}: -let self = stdenvNoCC.mkDerivation { - pname = "rtl"; - version = "0.15.10"; - - src = fetchurl { - url = "https://github.com/Ride-The-Lightning/RTL/archive/refs/tags/v${self.version}.tar.gz"; - hash = "sha256-r5riYV2FN0OKi0mwj9I1jBeeU1LOv2HVB6CEovPlUuY="; - }; - - passthru = { - nodejs = nodejs_22; - nodejsRuntime = nodejs-slim_22; - - nodeModules = fetchNodeModules { - inherit (self) src nodejs; - # TODO-EXTERNAL: Remove `npmFlags` when no longer required - # See: https://github.com/Ride-The-Lightning/RTL/issues/1182 - npmFlags = "--legacy-peer-deps"; - hash = "sha256-NKiWcjqYcHBVIB+vbF3aKXLe2fJRmh/quu8obztP3TA="; - }; - }; - - nativeBuildInputs = [ - makeWrapper - ]; - - phases = "unpackPhase patchPhase installPhase"; - - # `src` already contains the precompiled frontend and backend. - # Copy all files required for packaging, like in - # https://github.com/Ride-The-Lightning/RTL/blob/master/dockerfiles/Dockerfile - installPhase = '' - dest=$out/lib/node_modules/rtl - mkdir -p $dest - cp -r \ - rtl.js \ - package.json \ - frontend \ - backend \ - ${self.nodeModules}/lib/node_modules \ - $dest - - makeWrapper ${self.nodejsRuntime}/bin/node "$out/bin/rtl" \ - --add-flags "$dest/rtl.js" - - runHook postInstall - ''; - - meta = with lib; { - description = "A web interface for LND, c-lightning and Eclair"; - homepage = "https://github.com/Ride-The-Lightning/RTL"; - license = licenses.mit; - maintainers = with maintainers; [ nixbitcoin erikarvstedt ]; - platforms = platforms.unix; - }; -}; in self diff --git a/tests/bitcoin-btcpay-hardening.nix b/tests/bitcoin-btcpay-hardening.nix deleted file mode 100644 index 7694c08..0000000 --- a/tests/bitcoin-btcpay-hardening.nix +++ /dev/null @@ -1,146 +0,0 @@ -{ nixpkgs, overlay-stable, system ? "x86_64-linux" }: - -let - lib = nixpkgs.lib; - pkgs = import nixpkgs { - inherit system; - overlays = [ overlay-stable ]; - }; - - normalize = s: - lib.replaceStrings [ "\n" "\\" " " ] [ " " "" " " ] s; - - extractAfter = prefix: str: - let - match = builtins.match ".*${prefix} ([^ ]+).*" (normalize str); - in - if match == null then - throw "Unable to extract ${prefix} from: ${normalize str}" - else - builtins.head match; - - extractFlagValue = flag: str: - let - match = builtins.match ".*${flag}=([^ ]+).*" (normalize str); - in - if match == null then - throw "Unable to extract ${flag} from: ${normalize str}" - else - builtins.head match; - - config = (lib.nixosSystem { - inherit system; - modules = [ - { nixpkgs.hostPlatform = system; nixpkgs.overlays = [ overlay-stable ]; } - ../modules/bitcoin - { - nix-bitcoin.generateSecrets = true; - nix-bitcoin.secretsDir = "/build/secrets"; - - services.btcpayserver.enable = true; - services.btcpayserver.lightningBackend = "lnd"; - services.nbxplorer.dataDir = "/build/nbxplorer"; - services.btcpayserver.dataDir = "/build/btcpayserver"; - services.lnd.dataDir = "/build/lnd"; - services.bitcoind.dataDir = "/build/bitcoind"; - } - ]; - }).config; - - nbxplorerPreStart = config.systemd.services.nbxplorer.preStart; - bitcoindPreStart = config.systemd.services.bitcoind.preStart; - btcpayExecStart = config.systemd.services.btcpayserver.serviceConfig.ExecStart; - btcpayWorkingDir = config.systemd.services.btcpayserver.serviceConfig.WorkingDirectory; - - nbxplorerConfigPath = extractAfter "install -m 600" nbxplorerPreStart; - btcpayConfigPath = extractFlagValue "--conf" btcpayExecStart; - - nbxplorerConfig = builtins.readFile nbxplorerConfigPath; - btcpayConfig = builtins.readFile btcpayConfigPath; -in -assert lib.assertMsg - (config.users.users.${config.services.btcpayserver.user}.home == config.services.btcpayserver.dataDir) - "btcpayserver user home must match btcpayserver dataDir"; -assert lib.assertMsg - (config.users.users.${config.services.nbxplorer.user}.home == config.services.nbxplorer.dataDir) - "nbxplorer user home must match nbxplorer dataDir"; -assert lib.assertMsg - (config.nix-bitcoin.secrets.bitcoin-HMAC-btcpayserver.user == config.services.bitcoind.user) - "bitcoin-HMAC-btcpayserver must be owned by bitcoind"; -assert lib.assertMsg - (config.nix-bitcoin.secrets.bitcoin-rpcpassword-btcpayserver.user == config.services.bitcoind.user) - "bitcoin-rpcpassword-btcpayserver must be owned by bitcoind"; -assert lib.assertMsg - (config.nix-bitcoin.secrets.bitcoin-rpcpassword-btcpayserver.group == config.services.nbxplorer.group) - "bitcoin-rpcpassword-btcpayserver must be group-readable by nbxplorer"; -assert lib.assertMsg - (!(lib.elem config.services.nbxplorer.group config.users.users.${config.services.btcpayserver.user}.extraGroups)) - "btcpayserver must not receive the nbxplorer group"; -assert lib.assertMsg - (lib.elem "nix-bitcoin-secrets.target" config.systemd.services.nbxplorer.after) - "nbxplorer must wait for nix-bitcoin-secrets.target"; -assert lib.assertMsg - (config.systemd.services.nbxplorer.serviceConfig.MemoryDenyWriteExecute == false) - "nbxplorer needs MemoryDenyWriteExecute = false"; -assert lib.assertMsg - (config.systemd.services.btcpayserver.serviceConfig.MemoryDenyWriteExecute == false) - "btcpayserver needs MemoryDenyWriteExecute = false"; -assert lib.assertMsg - (lib.hasInfix "network=mainnet" nbxplorerConfig - && lib.hasInfix "btcrpcuser=btcpayserver" nbxplorerConfig - && lib.hasInfix "btcnodeendpoint=127.0.0.1:8335" nbxplorerConfig - && lib.hasInfix "bind=127.0.0.1" nbxplorerConfig - && lib.hasInfix "port=24444" nbxplorerConfig - && lib.hasInfix "postgres=User ID=nbxplorer;Host=/run/postgresql;Database=nbxplorer" nbxplorerConfig) - "nbxplorer base config must contain the expected non-secret settings"; -assert lib.assertMsg - (lib.hasInfix "btcexplorerurl=http://127.0.0.1:24444/" btcpayConfig - && lib.hasInfix "btcexplorercookiefile=/build/nbxplorer/Main/.cookie" btcpayConfig) - "btcpayserver config must contain btcexplorerurl and btcexplorercookiefile"; -assert lib.assertMsg - (lib.hasSuffix "/lib/btcpayserver" btcpayWorkingDir) - "btcpayserver WorkingDirectory must end with /lib/btcpayserver"; -assert lib.assertMsg - (!lib.hasInfix "/build/btcpayserver/settings.config" btcpayExecStart - && lib.hasInfix "--datadir='/build/btcpayserver'" btcpayExecStart) - "btcpayserver must use a deterministic config file plus --datadir"; -assert lib.assertMsg - (lib.hasInfix "network=mainnet" btcpayConfig - && lib.hasInfix "bind=127.0.0.1" btcpayConfig - && lib.hasInfix "port=23000" btcpayConfig - && lib.hasInfix "btcexplorerurl=http://127.0.0.1:24444/" btcpayConfig - && lib.hasInfix "explorer.postgres=User ID=nbxplorer;Host=/run/postgresql;Database=nbxplorer" btcpayConfig - && lib.hasInfix "postgres=User ID=btcpayserver;Host=/run/postgresql;Database=btcpayserver" btcpayConfig - && lib.hasInfix "btclightning=type=lnd-rest;server=https://127.0.0.1:8080/;macaroonfilepath=/run/lnd/btcpayserver.macaroon;certfilepath=/build/secrets/lnd-cert" btcpayConfig) - "btcpayserver config must preserve BTCPay, NBXplorer, database, and LND settings"; -assert lib.assertMsg - (lib.hasInfix "readValidatedRpcHmac()" bitcoindPreStart - && lib.hasInfix ''if [[ ! -e "$hmacFile" ]]; then'' bitcoindPreStart - && lib.hasInfix ''if [[ ! -r "$hmacFile" ]]; then'' bitcoindPreStart - && lib.hasInfix ''if [[ -z "$hmacPayload" ]]; then'' bitcoindPreStart - && lib.hasInfix ''^[[:xdigit:]]+\$[[:xdigit:]]+$'' bitcoindPreStart - && lib.hasInfix ''Bitcoin RPC HMAC file has invalid format'' bitcoindPreStart - && lib.hasInfix ''hmacPayload="$(readValidatedRpcHmac '/build/secrets/bitcoin-HMAC-btcpayserver')" || exit 1'' bitcoindPreStart) - "bitcoind preStart must validate missing, unreadable, empty, and malformed HMAC files"; -pkgs.runCommand "bitcoin-btcpay-hardening" {} '' - mkdir -p /build/secrets /build/nbxplorer - - printf '%s' 'first-password' > /build/secrets/bitcoin-rpcpassword-btcpayserver - bash -euo pipefail -c ${lib.escapeShellArg nbxplorerPreStart} - - test "$(stat -c '%a' /build/nbxplorer/settings.config)" = "600" - test "$(grep -c '^btcrpcuser=' /build/nbxplorer/settings.config)" = "1" - test "$(grep -c '^btcrpcpassword=' /build/nbxplorer/settings.config)" = "1" - test "$(grep -c '^postgres=' /build/nbxplorer/settings.config)" = "1" - - printf '%s' 'rotated-password' > /build/secrets/bitcoin-rpcpassword-btcpayserver - bash -euo pipefail -c ${lib.escapeShellArg nbxplorerPreStart} - - test "$(grep -c '^btcrpcuser=' /build/nbxplorer/settings.config)" = "1" - test "$(grep -c '^btcrpcpassword=' /build/nbxplorer/settings.config)" = "1" - test "$(grep -c '^postgres=' /build/nbxplorer/settings.config)" = "1" - ! grep -q 'first-password' /build/nbxplorer/settings.config - grep -q 'rotated-password' /build/nbxplorer/settings.config - - touch "$out" -'' diff --git a/tests/test_bitcoin_tor_gossip.py b/tests/test_bitcoin_tor_gossip.py index 8308ebb..805a58e 100644 --- a/tests/test_bitcoin_tor_gossip.py +++ b/tests/test_bitcoin_tor_gossip.py @@ -27,17 +27,12 @@ def _literal_assignment(source: str, name: str): class TestBitcoinTorGossipNixWiring(unittest.TestCase): - def test_bitcoind_loopback_listener_is_always_enabled(self): - ecosystem = _read("modules", "bitcoinecosystem.nix") - self.assertIn("listen = true;", ecosystem) - self.assertIn("peerbloomfilters=1", ecosystem) - - def test_gossip_is_opt_in(self): - ecosystem = _read("modules", "bitcoinecosystem.nix") - self.assertIn( - "public = config.sovran_systemsOS.features.bitcoin-tor-gossip;", - ecosystem, - ) + def test_integration_layer_maps_tor_gossip_option(self): + """The integration layer must bridge sovran_systemsOS.features.bitcoin-tor-gossip + to sovran-bitcoin.bitcoindTorGossip.""" + integration = _read("modules", "sovran-bitcoin-integration.nix") + self.assertIn("bitcoindTorGossip", integration) + self.assertIn("bitcoin-tor-gossip", integration) def test_hub_option_and_evaluated_state_are_declared(self): roles = _read("modules", "core", "roles.nix") diff --git a/tests/test_security.py b/tests/test_security.py index 7c07744..08e214c 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -333,33 +333,6 @@ class TestSshPubkeyValidation(unittest.TestCase): _validate_ssh_pubkey("ssh-ed25519") -# --------------------------------------------------------------------------- -# LND macaroon command-line safety -# --------------------------------------------------------------------------- - -class TestLndMacaroonCommandLineSafety(unittest.TestCase): - """The LND admin macaroon must never be exposed in curl's argv.""" - - @classmethod - def setUpClass(cls): - path = os.path.join(_REPO_ROOT, "modules", "bitcoin", "lnd.nix") - with open(path, encoding="utf-8") as f: - cls.lnd_module = f.read() - - def test_admin_macaroon_not_interpolated_into_header_argument(self): - self.assertNotIn( - '-H "Grpc-Metadata-macaroon: $(', - self.lnd_module, - ) - - def test_admin_macaroon_header_is_passed_via_file_descriptor(self): - self.assertIn("adminMacaroonHex=$(", self.lnd_module) - self.assertIn( - """-H @<(printf 'Grpc-Metadata-macaroon: %s\\n' "$adminMacaroonHex")""", - self.lnd_module, - ) - - # --------------------------------------------------------------------------- # Auth-exempt paths # --------------------------------------------------------------------------- diff --git a/tests/test_zeus_connect.py b/tests/test_zeus_connect.py index 22da13f..03d4fae 100644 --- a/tests/test_zeus_connect.py +++ b/tests/test_zeus_connect.py @@ -1,8 +1,8 @@ """Regression tests for the Hub Zeus Connect QR. -The LND-only rewrite of modules/bitcoin/lndconnect.nix shipped a wrapper -that Zeus cannot use. These tests lock the contract the Hub QR depends on -without needing lnd / tor / qrencode at test time. +The Zeus Connect setup service (modules/wallet-autoconnect.nix) and the Hub QR +encoding are tested here. The lndconnect wrapper itself is now part of the +Sovran_Bitcoin flake — its contract tests live in that repository. """ import os @@ -17,41 +17,6 @@ def _read(relpath: str) -> str: return fh.read() -class TestLndconnectWrapper(unittest.TestCase): - """The system `lndconnect` wrapper must emit a Zeus-scannable URI.""" - - @classmethod - def setUpClass(cls): - cls.src = _read("modules/bitcoin/lndconnect.nix") - - def test_uses_official_lndconnect_flags(self): - self.assertIn("--adminmacaroonpath=", self.src) - self.assertIn("--configfile=/dev/null", self.src) - self.assertIn("--nocert", self.src) - self.assertIn("--tlscertpath=", self.src) - - def test_does_not_pass_unknown_short_flags(self): - # The broken rewrite called `lndconnect --cert … --macaroon …`. - # Those flags do not exist; Zeus then never got a valid URI. - self.assertIsNone(re.search(r"--cert=", self.src)) - self.assertIsNone(re.search(r"--macaroon=", self.src)) - - def test_uses_dedicated_lnd_rest_onion(self): - self.assertIn("lnd-rest", self.src) - self.assertIn('onionServices.lnd-rest', self.src) - # Must not collide with the LND P2P onion named `lnd`. - self.assertNotIn("onionServices.lnd =", self.src) - self.assertNotIn('onionService = "${operatorName}/lnd"', self.src) - - def test_reads_onion_from_onion_addresses_dir(self): - self.assertIn("onionAddresses.dataDir", self.src) - self.assertNotIn("/var/lib/tor/onion/${onionService}/hostname", self.src) - - def test_omits_tls_cert_over_tor(self): - # Onion host + embedded localhost cert = Zeus rejects the QR. - self.assertIn('then "--nocert"', self.src) - - class TestZeusConnectSetup(unittest.TestCase): """zeus-connect-setup must wait for the REST onion and validate the URI."""