From 9f416dbf568ad99675d1b82725d81292f7aff06c Mon Sep 17 00:00:00 2001 From: josie Date: Fri, 22 May 2026 14:44:22 +0200 Subject: [PATCH 1/2] add personal use module --- flake.nix | 41 ++++ modules/presets/personal-frigate.nix | 237 +++++++++++++++++++ templates/personal-frigate/configuration.nix | 68 ++++++ templates/personal-frigate/flake.nix | 27 +++ test/regtest-personal.nix | 158 +++++++++++++ 5 files changed, 531 insertions(+) create mode 100644 modules/presets/personal-frigate.nix create mode 100644 templates/personal-frigate/configuration.nix create mode 100644 templates/personal-frigate/flake.nix create mode 100644 test/regtest-personal.nix diff --git a/flake.nix b/flake.nix index ca74040..770f37b 100644 --- a/flake.nix +++ b/flake.nix @@ -87,6 +87,7 @@ public-frigate = ./modules/presets/public-frigate.nix; frigate-edge = ./modules/presets/frigate-edge.nix; bitcoind-backend = ./modules/presets/bitcoind-backend.nix; + personal-frigate = ./modules/presets/personal-frigate.nix; wireguard-mesh = ./modules/wireguard-mesh.nix; # Batteries-included entry point for an all-in-one public @@ -116,6 +117,23 @@ ./modules/presets/bitcoind-backend.nix ]; }; + + # Batteries-included entry point for a personal Frigate node. + # Bundles nix-bitcoin so the consumer needs only `roost` in + # their flake inputs, and turns on both manage flags so + # bitcoind and electrs are configured automatically. Use + # `nixosModules.personal-frigate` directly if you operate + # bitcoind/electrs out of band. + personal-frigate-host = { + imports = [ + nix-bitcoin.nixosModules.default + ./modules/presets/personal-frigate.nix + ]; + services.personal-frigate = { + bitcoind.manage = nixpkgs.lib.mkDefault true; + electrs.manage = nixpkgs.lib.mkDefault true; + }; + }; }; formatter = forAllSystems (system: (pkgsFor system).nixfmt-tree); @@ -174,6 +192,21 @@ roost = self; }; + # End-to-end test against `nixosModules.personal-frigate-host` — + # the single-box personal deployment with bitcoind + electrs + + # plaintext Electrum on loopback. Boots one VM, mines 101 + # regtest blocks, then probes electrs and frigate over the + # Electrum protocol. + mkRegtestPersonalE2E = + { + pkgs, + extraModules ? [ ], + }: + import ./test/regtest-personal.nix { + inherit pkgs extraModules; + roost = self; + }; + # Single-VM test for the bitcoind-backend preset. Verifies the # backend stack (bitcoind RPC + ZMQ + fulcrum) comes up with # the right bindings and that an external-looking RPC call @@ -194,6 +227,9 @@ pkgs = pkgsFor system; inherit nix-bitcoin; }; + regtest-personal = self.lib.mkRegtestPersonalE2E { + pkgs = pkgsFor system; + }; regtest-preset = self.lib.mkRegtestPresetE2E { pkgs = pkgsFor system; }; @@ -213,6 +249,11 @@ description = "A starting point for a Frigate deployment"; }; + templates.personal-frigate = { + path = ./templates/personal-frigate; + description = "A starting point for a personal Frigate deployment (single box, no TLS)"; + }; + devShells = forAllSystems ( system: let diff --git a/modules/presets/personal-frigate.nix b/modules/presets/personal-frigate.nix new file mode 100644 index 0000000..d17d34d --- /dev/null +++ b/modules/presets/personal-frigate.nix @@ -0,0 +1,237 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.personal-frigate; +in +{ + imports = [ + ../frigate.nix + ]; + + options.services.personal-frigate = with lib; { + enable = mkEnableOption "personal Frigate (single-box, plaintext, electrs backend)"; + + network = mkOption { + type = types.enum [ + "mainnet" + "testnet" + "testnet4" + "signet" + "regtest" + ]; + default = "mainnet"; + description = '' + Chain Frigate scans. Propagated to services.frigate.network and + services.bitcoind. when `bitcoind.manage = true`. + ''; + }; + + listenAddress = mkOption { + type = types.str; + default = "127.0.0.1"; + example = "10.0.0.5"; + description = '' + Single address Frigate's plaintext Electrum listener binds to. + Default 127.0.0.1 is correct for a Sparrow wallet running on the + same box. Set to a LAN/VPN IP to let another machine (e.g. a + laptop running Sparrow) connect; in that case you must also open + `port` in your own `networking.firewall` config — this preset + does not touch the firewall for non-loopback binds. Prefer a + private transport (WireGuard / Tailscale) for that case since + v1 has no TLS. + ''; + }; + + port = mkOption { + type = types.port; + default = 50001; + description = '' + Plaintext Electrum port. 50001 is the convention Sparrow probes + for. Frigate occupies it; the electrs backend is moved to + `backendPort` (60001 by default) to avoid the conflict. + ''; + }; + + backendPort = mkOption { + type = types.port; + default = 60001; + description = '' + Loopback port electrs listens on; Frigate proxies non-silent- + payments queries here. Kept off the canonical 50001 so Frigate + can occupy that port. Loopback-only — never reachable from + outside this host regardless of `listenAddress`. + ''; + }; + + host = mkOption { + type = types.str; + default = "localhost"; + description = '' + Hostname advertised in Electrum `server.features`. Cosmetic for + a personal deployment — Sparrow displays it but does not validate + against it (no TLS in v1). Override if you want the wallet UI to + show something other than `localhost`. + ''; + }; + + bitcoind.manage = mkOption { + type = types.bool; + default = false; + description = '' + When true, the preset configures `services.bitcoind`: txindex, + loopback-only P2P, group-readable cookie, ZMQ sequence publisher + on loopback. Requires a bitcoind NixOS module already imported — + typically nix-bitcoin via `nixosModules.personal-frigate-host`. + When false (default), the preset asserts bitcoind is enabled + + txindex is on, and otherwise leaves it alone. + ''; + }; + + electrs.manage = mkOption { + type = types.bool; + default = false; + description = '' + When true, the preset enables nix-bitcoin's `services.electrs` + on `backendPort` (loopback). Frigate consumes it as + `electrumBackend`. When false (default), the preset asserts + electrs is enabled and leaves it alone (the preset still wires + frigate to `127.0.0.1:` — point your own electrs + there or override services.frigate.electrumBackend in the host + config). + ''; + }; + + dbCache = mkOption { + type = types.int; + default = 1024; + description = '' + bitcoind UTXO cache in MB. Only used when `bitcoind.manage = true`. + 1 GB suits a personal user on a NAS / small VPS / desktop; raise + it transiently during initial sync if RAM is plentiful. + ''; + }; + + preset-enabled = mkOption { + type = types.attrs; + default = { }; + internal = true; + description = "Sentinel mirroring nix-bitcoin convention; lets downstream detect activation."; + }; + }; + + config = lib.mkIf cfg.enable (lib.mkMerge [ + { services.personal-frigate.preset-enabled = { }; } + + { + assertions = [ + { + assertion = cfg.bitcoind.manage || (config.services ? bitcoind && config.services.bitcoind.enable); + message = '' + services.personal-frigate requires services.bitcoind.enable = true. + Either import nix-bitcoin and enable it, or set + services.personal-frigate.bitcoind.manage = true. + For a batteries-included setup, import + roost.nixosModules.personal-frigate-host. + ''; + } + { + assertion = + cfg.bitcoind.manage || !(config.services ? bitcoind) || config.services.bitcoind.txindex; + message = "services.personal-frigate requires services.bitcoind.txindex = true."; + } + { + assertion = cfg.electrs.manage || (config.services ? electrs && config.services.electrs.enable); + message = '' + services.personal-frigate requires services.electrs.enable = true. + Either import nix-bitcoin and enable it, or set + services.personal-frigate.electrs.manage = true. + ''; + } + { + assertion = !cfg.electrs.manage || cfg.bitcoind.manage; + message = '' + services.personal-frigate.electrs.manage = true requires + services.personal-frigate.bitcoind.manage = true. Either turn both + on (typical: use nixosModules.personal-frigate-host) or turn both + off and manage bitcoind + electrs out of band. + ''; + } + { + assertion = cfg.port != cfg.backendPort; + message = '' + services.personal-frigate.port and .backendPort must differ + (frigate listens on `port`, electrs on `backendPort`). + ''; + } + ]; + } + + { + services.frigate = { + enable = true; + host = cfg.host; + network = cfg.network; + tcp = "tcp://${cfg.listenAddress}:${toString cfg.port}"; + ssl = null; + bitcoind = { + enable = true; + # Hardcoded mainnet RPC port — regtest tests use mkForce to override. + server = "http://127.0.0.1:8332"; + authType = "COOKIE"; + cookieDir = "/var/lib/bitcoind"; + zmqSequenceEndpoint = "tcp://127.0.0.1:28336"; + }; + electrumBackend = "tcp://127.0.0.1:${toString cfg.backendPort}"; + }; + + users.users.frigate.extraGroups = [ "bitcoin" ]; + + systemd.services.frigate.after = [ + "bitcoind.service" + "electrs.service" + ]; + systemd.services.frigate.wants = [ + "bitcoind.service" + "electrs.service" + ]; + } + + (lib.mkIf cfg.bitcoind.manage { + nix-bitcoin.generateSecrets = lib.mkDefault true; + + services.bitcoind = { + enable = true; + txindex = true; + listen = false; + address = "127.0.0.1"; + dataDirReadableByGroup = true; + dbCache = lib.mkDefault cfg.dbCache; + extraConfig = '' + zmqpubsequence=tcp://127.0.0.1:28336 + ''; + }; + + # libzmq's `getifaddrs()` during `zmq_bind` needs AF_NETLINK, but + # nix-bitcoin's bitcoind module only widens RestrictAddressFamilies + # for its *typed* ZMQ options (`zmqpubrawblock`, `zmqpubrawtx`). + # Configuring `zmqpubsequence` via extraConfig bypasses that gate, + # so the daemon would abort on bind without this override. Same + # rationale as in _internal/bitcoin-stack.nix. + systemd.services.bitcoind.serviceConfig.RestrictAddressFamilies = + lib.mkForce "AF_UNIX AF_INET AF_INET6 AF_NETLINK"; + }) + + (lib.mkIf cfg.electrs.manage { + services.electrs = { + enable = true; + address = "127.0.0.1"; + port = cfg.backendPort; + }; + }) + ]); +} diff --git a/templates/personal-frigate/configuration.nix b/templates/personal-frigate/configuration.nix new file mode 100644 index 0000000..e292f82 --- /dev/null +++ b/templates/personal-frigate/configuration.nix @@ -0,0 +1,68 @@ +{ + config, + lib, + pkgs, + ... +}: + +# Personal Frigate deployment. Frigate + bitcoind + electrs all run on +# this single host, with Frigate's plaintext Electrum listener bound to +# loopback by default for a Sparrow wallet running on the same machine. +# +# Disk: electrs's mainnet index is roughly 80+ GB on top of bitcoind's +# own chainstate and (with txindex) ~700+ GB of block + index data. +# Size the data partition accordingly before the first sync. + +{ + # FIXME: hostname for this machine. + networking.hostName = "frigate-host"; + + # FIXME: pin once at install. Controls migration semantics for stateful + # services across NixOS releases. Never bump after initial deploy. + system.stateVersion = "25.11"; + + # FIXME: paste your SSH public key. + users.users.root.openssh.authorizedKeys.keys = [ + "ssh-ed25519 AAAA... your-key-here" + ]; + + services.openssh = { + enable = true; + settings = { + PasswordAuthentication = false; + PermitRootLogin = "prohibit-password"; + }; + }; + + networking.firewall = { + enable = true; + allowedTCPPorts = [ 22 ]; + }; + + # === Scenario A: single-box (default) === + # Frigate + bitcoind + electrs all live on this host. Sparrow runs on + # the same machine and connects to 127.0.0.1:50001. Nothing Electrum- + # related is exposed on the network. + services.personal-frigate = { + enable = true; + # listenAddress = "127.0.0.1"; # default + # port = 50001; # default + }; + + # === Scenario B: home node + remote Sparrow === + # This box hosts the Bitcoin + Frigate stack; Sparrow runs on a + # different machine on your LAN/VPN. Bind Frigate's plaintext Electrum + # listener to the LAN/VPN IP and open the port. Prefer a private + # transport (WireGuard / Tailscale) for this — v1 has no TLS, so treat + # the listener as you would any other plaintext service. Setting + # `host = config.networking.hostName` makes the wallet UI show this + # machine's hostname instead of `localhost`. + # + # services.personal-frigate = { + # enable = true; + # listenAddress = "10.0.0.5"; # FIXME: this host's LAN/VPN IP + # # port = 50001; # default + # host = config.networking.hostName; + # }; + # networking.firewall.allowedTCPPorts = [ 22 50001 ]; +} diff --git a/templates/personal-frigate/flake.nix b/templates/personal-frigate/flake.nix new file mode 100644 index 0000000..f7a996f --- /dev/null +++ b/templates/personal-frigate/flake.nix @@ -0,0 +1,27 @@ +{ + description = "Personal Frigate deployment scaffold"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + roost.url = "github:2140-dev/roost"; + roost.inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = + { + self, + nixpkgs, + roost, + }: + { + nixosConfigurations.frigate-host = nixpkgs.lib.nixosSystem { + # FIXME: set to match your target hardware. + system = "x86_64-linux"; + modules = [ + roost.nixosModules.personal-frigate-host + ./configuration.nix + ]; + }; + }; +} diff --git a/test/regtest-personal.nix b/test/regtest-personal.nix new file mode 100644 index 0000000..01946bd --- /dev/null +++ b/test/regtest-personal.nix @@ -0,0 +1,158 @@ +{ + pkgs, + roost, + extraModules ? [ ], +}: + +# End-to-end test for the personal-frigate preset, exercised through +# `nixosModules.personal-frigate-host`. Boots the entire stack — +# bitcoind + electrs + frigate — on regtest, mines 101 blocks, then +# probes electrs directly and frigate's plaintext Electrum listener to +# verify the full personal-deployment chain. +pkgs.testers.runNixOSTest { + name = "regtest-personal"; + + nodes.machine = + { + config, + pkgs, + lib, + ... + }: + { + imports = [ + roost.nixosModules.personal-frigate-host + ] + ++ extraModules; + + services.personal-frigate = { + enable = true; + network = "regtest"; + }; + + # Regtest overrides on top of the preset's manage-mode bitcoind config. + # The preset already sets txindex/listen/address/dataDirReadableByGroup; + # we only need to flip the network and shrink the cache for a lean VM. + services.bitcoind = { + regtest = true; + dbCache = lib.mkForce 100; + # nix-bitcoin pins the wallet off. The mining flow below is wallet- + # driven, so flip it on for the test only. `mkForce` is required + # because their default isn't `mkDefault`. + disablewallet = lib.mkForce false; + # Disable the "is the tip recent?" check that gates IBD exit. In a + # test VM the clock can drift between boot and mining, leaving the + # IBD flag stuck at `true` even with 101 freshly-mined blocks. Same + # trick bitcoind's own functional tests use. Plain assignment (no + # mkForce) so the preset's `zmqpubsequence=...` line still gets + # merged in via the `types.lines` accumulation. + extraConfig = '' + maxtipage=2147483647 + ''; + }; + + # regtest auto-flips via services.bitcoind.regtest -> bitcoind.makeNetworkName + + # Frigate's cookie path differs in regtest, and there's no GPU in the + # test VM. ufsecp falls back to CPU regardless, but being explicit + # avoids a startup probe. + services.frigate.bitcoind.cookieDir = lib.mkForce "/var/lib/bitcoind/regtest"; + services.frigate.computeBackend = lib.mkForce "CPU"; + + # The preset hardcodes mainnet's RPC port (8332). In regtest, bitcoind + # binds the chain-default port (18443). Point frigate at whatever + # nix-bitcoin actually configured. + services.frigate.bitcoind.server = lib.mkForce "http://127.0.0.1:${toString config.services.bitcoind.rpc.port}"; + + environment.systemPackages = [ + pkgs.netcat-openbsd + ]; + + virtualisation.cores = 4; + virtualisation.memorySize = 4096; + }; + + testScript = + { nodes, ... }: + let + cli = "bitcoin-cli -regtest -datadir=/var/lib/bitcoind"; + in + '' + machine.wait_for_unit("bitcoind.service") + machine.wait_until_succeeds("${cli} getblockchaininfo", timeout=30) + + # 101 blocks: first coinbase matures, electrs/frigate get a real tip. + machine.succeed("${cli} createwallet test") + addr = machine.succeed("${cli} -rpcwallet=test getnewaddress").strip() + machine.succeed(f"${cli} generatetoaddress 101 {addr}") + + machine.wait_until_succeeds( + "${cli} getblockchaininfo | grep -q '\"initialblockdownload\": false'", + timeout=30, + ) + + machine.wait_for_unit("electrs.service") + machine.wait_for_open_port(60001) + + # Direct probe against electrs isolates backend health from the + # frigate→electrs proxy below — a failure here pins the problem on + # electrs (or its bitcoind dependency), not on frigate. + # + # Polling loop instead of `wait_until_succeeds` so each attempt's + # captured response is in the test log — the test driver doesn't + # surface stdout/stderr from wait-loop attempts otherwise, which + # makes diagnosing real failures effectively impossible. + import time + deadline = time.time() + 120 + electrs_probe = ( + "echo '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"blockchain.headers.subscribe\",\"params\":[]}'" + " | nc -q 1 127.0.0.1 60001" + ) + electrs_resp = "" + while time.time() < deadline: + _status, electrs_resp = machine.execute(electrs_probe) + print(f"electrs direct probe ({len(electrs_resp)}B): {electrs_resp!r}") + if '"height":101' in electrs_resp: + break + time.sleep(2) + else: + raise Exception( + f"electrs direct probe never returned height:101 within 120s. " + f"Last response: {electrs_resp!r}" + ) + + assert '"height":101' in electrs_resp, f"electrs direct probe missing height:101: {electrs_resp}" + + # Frigate's plaintext listener sits on the canonical Electrum port + # (50001), bound to the preset's default `listenAddress` of + # 127.0.0.1. Electrum protocol requires `server.version` as the + # first message on any new connection; frigate enforces this and + # rejects anything else with VersionNotNegotiatedException. Share + # one connection across all three requests via the brace group. + machine.wait_for_unit("frigate.service") + machine.wait_for_open_port(50001, addr="127.0.0.1") + + deadline = time.time() + 120 + probe = ( + "{ echo '{\"jsonrpc\":\"2.0\",\"id\":0,\"method\":\"server.version\",\"params\":[\"test\",\"1.4\"]}'" + "; echo '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"server.features\",\"params\":[]}'" + "; echo '{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"blockchain.headers.subscribe\",\"params\":[]}'; }" + " | nc -q 3 127.0.0.1 50001" + ) + frigate_resp = "" + while time.time() < deadline: + _status, frigate_resp = machine.execute(probe) + print(f"frigate plaintext probe ({len(frigate_resp)}B): {frigate_resp!r}") + if "localhost" in frigate_resp and '"height":101' in frigate_resp: + break + time.sleep(2) + else: + raise Exception( + f"frigate plaintext probe never returned expected content within 120s. " + f"Last response: {frigate_resp!r}" + ) + + assert "localhost" in frigate_resp, f"plaintext server.features missing default host: {frigate_resp}" + assert '"height":101' in frigate_resp, f"plaintext blockchain.headers.subscribe missing height:101 (frigate→electrs proxy): {frigate_resp}" + ''; +} From 9daa3f82bb6f790025a129d93064343520c0a027 Mon Sep 17 00:00:00 2001 From: josie Date: Fri, 22 May 2026 14:49:48 +0200 Subject: [PATCH 2/2] nix fmt --- modules/presets/personal-frigate.nix | 214 ++++++++++++++------------- 1 file changed, 108 insertions(+), 106 deletions(-) diff --git a/modules/presets/personal-frigate.nix b/modules/presets/personal-frigate.nix index d17d34d..f3d9013 100644 --- a/modules/presets/personal-frigate.nix +++ b/modules/presets/personal-frigate.nix @@ -124,114 +124,116 @@ in }; }; - config = lib.mkIf cfg.enable (lib.mkMerge [ - { services.personal-frigate.preset-enabled = { }; } - - { - assertions = [ - { - assertion = cfg.bitcoind.manage || (config.services ? bitcoind && config.services.bitcoind.enable); - message = '' - services.personal-frigate requires services.bitcoind.enable = true. - Either import nix-bitcoin and enable it, or set - services.personal-frigate.bitcoind.manage = true. - For a batteries-included setup, import - roost.nixosModules.personal-frigate-host. - ''; - } - { - assertion = - cfg.bitcoind.manage || !(config.services ? bitcoind) || config.services.bitcoind.txindex; - message = "services.personal-frigate requires services.bitcoind.txindex = true."; - } - { - assertion = cfg.electrs.manage || (config.services ? electrs && config.services.electrs.enable); - message = '' - services.personal-frigate requires services.electrs.enable = true. - Either import nix-bitcoin and enable it, or set - services.personal-frigate.electrs.manage = true. - ''; - } - { - assertion = !cfg.electrs.manage || cfg.bitcoind.manage; - message = '' - services.personal-frigate.electrs.manage = true requires - services.personal-frigate.bitcoind.manage = true. Either turn both - on (typical: use nixosModules.personal-frigate-host) or turn both - off and manage bitcoind + electrs out of band. - ''; - } - { - assertion = cfg.port != cfg.backendPort; - message = '' - services.personal-frigate.port and .backendPort must differ - (frigate listens on `port`, electrs on `backendPort`). - ''; - } - ]; - } - - { - services.frigate = { - enable = true; - host = cfg.host; - network = cfg.network; - tcp = "tcp://${cfg.listenAddress}:${toString cfg.port}"; - ssl = null; - bitcoind = { + config = lib.mkIf cfg.enable ( + lib.mkMerge [ + { services.personal-frigate.preset-enabled = { }; } + + { + assertions = [ + { + assertion = cfg.bitcoind.manage || (config.services ? bitcoind && config.services.bitcoind.enable); + message = '' + services.personal-frigate requires services.bitcoind.enable = true. + Either import nix-bitcoin and enable it, or set + services.personal-frigate.bitcoind.manage = true. + For a batteries-included setup, import + roost.nixosModules.personal-frigate-host. + ''; + } + { + assertion = + cfg.bitcoind.manage || !(config.services ? bitcoind) || config.services.bitcoind.txindex; + message = "services.personal-frigate requires services.bitcoind.txindex = true."; + } + { + assertion = cfg.electrs.manage || (config.services ? electrs && config.services.electrs.enable); + message = '' + services.personal-frigate requires services.electrs.enable = true. + Either import nix-bitcoin and enable it, or set + services.personal-frigate.electrs.manage = true. + ''; + } + { + assertion = !cfg.electrs.manage || cfg.bitcoind.manage; + message = '' + services.personal-frigate.electrs.manage = true requires + services.personal-frigate.bitcoind.manage = true. Either turn both + on (typical: use nixosModules.personal-frigate-host) or turn both + off and manage bitcoind + electrs out of band. + ''; + } + { + assertion = cfg.port != cfg.backendPort; + message = '' + services.personal-frigate.port and .backendPort must differ + (frigate listens on `port`, electrs on `backendPort`). + ''; + } + ]; + } + + { + services.frigate = { enable = true; - # Hardcoded mainnet RPC port — regtest tests use mkForce to override. - server = "http://127.0.0.1:8332"; - authType = "COOKIE"; - cookieDir = "/var/lib/bitcoind"; - zmqSequenceEndpoint = "tcp://127.0.0.1:28336"; + host = cfg.host; + network = cfg.network; + tcp = "tcp://${cfg.listenAddress}:${toString cfg.port}"; + ssl = null; + bitcoind = { + enable = true; + # Hardcoded mainnet RPC port — regtest tests use mkForce to override. + server = "http://127.0.0.1:8332"; + authType = "COOKIE"; + cookieDir = "/var/lib/bitcoind"; + zmqSequenceEndpoint = "tcp://127.0.0.1:28336"; + }; + electrumBackend = "tcp://127.0.0.1:${toString cfg.backendPort}"; }; - electrumBackend = "tcp://127.0.0.1:${toString cfg.backendPort}"; - }; - users.users.frigate.extraGroups = [ "bitcoin" ]; + users.users.frigate.extraGroups = [ "bitcoin" ]; - systemd.services.frigate.after = [ - "bitcoind.service" - "electrs.service" - ]; - systemd.services.frigate.wants = [ - "bitcoind.service" - "electrs.service" - ]; - } - - (lib.mkIf cfg.bitcoind.manage { - nix-bitcoin.generateSecrets = lib.mkDefault true; - - services.bitcoind = { - enable = true; - txindex = true; - listen = false; - address = "127.0.0.1"; - dataDirReadableByGroup = true; - dbCache = lib.mkDefault cfg.dbCache; - extraConfig = '' - zmqpubsequence=tcp://127.0.0.1:28336 - ''; - }; - - # libzmq's `getifaddrs()` during `zmq_bind` needs AF_NETLINK, but - # nix-bitcoin's bitcoind module only widens RestrictAddressFamilies - # for its *typed* ZMQ options (`zmqpubrawblock`, `zmqpubrawtx`). - # Configuring `zmqpubsequence` via extraConfig bypasses that gate, - # so the daemon would abort on bind without this override. Same - # rationale as in _internal/bitcoin-stack.nix. - systemd.services.bitcoind.serviceConfig.RestrictAddressFamilies = - lib.mkForce "AF_UNIX AF_INET AF_INET6 AF_NETLINK"; - }) - - (lib.mkIf cfg.electrs.manage { - services.electrs = { - enable = true; - address = "127.0.0.1"; - port = cfg.backendPort; - }; - }) - ]); + systemd.services.frigate.after = [ + "bitcoind.service" + "electrs.service" + ]; + systemd.services.frigate.wants = [ + "bitcoind.service" + "electrs.service" + ]; + } + + (lib.mkIf cfg.bitcoind.manage { + nix-bitcoin.generateSecrets = lib.mkDefault true; + + services.bitcoind = { + enable = true; + txindex = true; + listen = false; + address = "127.0.0.1"; + dataDirReadableByGroup = true; + dbCache = lib.mkDefault cfg.dbCache; + extraConfig = '' + zmqpubsequence=tcp://127.0.0.1:28336 + ''; + }; + + # libzmq's `getifaddrs()` during `zmq_bind` needs AF_NETLINK, but + # nix-bitcoin's bitcoind module only widens RestrictAddressFamilies + # for its *typed* ZMQ options (`zmqpubrawblock`, `zmqpubrawtx`). + # Configuring `zmqpubsequence` via extraConfig bypasses that gate, + # so the daemon would abort on bind without this override. Same + # rationale as in _internal/bitcoin-stack.nix. + systemd.services.bitcoind.serviceConfig.RestrictAddressFamilies = + lib.mkForce "AF_UNIX AF_INET AF_INET6 AF_NETLINK"; + }) + + (lib.mkIf cfg.electrs.manage { + services.electrs = { + enable = true; + address = "127.0.0.1"; + port = cfg.backendPort; + }; + }) + ] + ); }