1{ config, pkgs, lib, ... }:
2
3with lib;
4
5let
6 eachBitcoind = filterAttrs (bitcoindName: cfg: cfg.enable) config.services.bitcoind;
7
8 rpcUserOpts = { name, ... }: {
9 options = {
10 name = mkOption {
11 type = types.str;
12 example = "alice";
13 description = ''
14 Username for JSON-RPC connections.
15 '';
16 };
17 passwordHMAC = mkOption {
18 type = types.uniq (types.strMatching "[0-9a-f]+\\$[0-9a-f]{64}");
19 example = "f7efda5c189b999524f151318c0c86$d5b51b3beffbc02b724e5d095828e0bc8b2456e9ac8757ae3211a5d9b16a22ae";
20 description = ''
21 Password HMAC-SHA-256 for JSON-RPC connections. Must be a string of the
22 format \<SALT-HEX\>$\<HMAC-HEX\>.
23
24 Tool (Python script) for HMAC generation is available here:
25 <https://github.com/bitcoin/bitcoin/blob/master/share/rpcauth/rpcauth.py>
26 '';
27 };
28 };
29 config = {
30 name = mkDefault name;
31 };
32 };
33
34 bitcoindOpts = { config, lib, name, ...}: {
35 options = {
36
37 enable = mkEnableOption "Bitcoin daemon";
38
39 package = mkPackageOption pkgs "bitcoind" { };
40
41 configFile = mkOption {
42 type = types.nullOr types.path;
43 default = null;
44 example = "/var/lib/${name}/bitcoin.conf";
45 description = "The configuration file path to supply bitcoind.";
46 };
47
48 extraConfig = mkOption {
49 type = types.lines;
50 default = "";
51 example = ''
52 par=16
53 rpcthreads=16
54 logips=1
55 '';
56 description = "Additional configurations to be appended to {file}`bitcoin.conf`.";
57 };
58
59 dataDir = mkOption {
60 type = types.path;
61 default = "/var/lib/bitcoind-${name}";
62 description = "The data directory for bitcoind.";
63 };
64
65 user = mkOption {
66 type = types.str;
67 default = "bitcoind-${name}";
68 description = "The user as which to run bitcoind.";
69 };
70
71 group = mkOption {
72 type = types.str;
73 default = config.user;
74 description = "The group as which to run bitcoind.";
75 };
76
77 rpc = {
78 port = mkOption {
79 type = types.nullOr types.port;
80 default = null;
81 description = "Override the default port on which to listen for JSON-RPC connections.";
82 };
83 users = mkOption {
84 default = {};
85 example = literalExpression ''
86 {
87 alice.passwordHMAC = "f7efda5c189b999524f151318c0c86$d5b51b3beffbc02b724e5d095828e0bc8b2456e9ac8757ae3211a5d9b16a22ae";
88 bob.passwordHMAC = "b2dd077cb54591a2f3139e69a897ac$4e71f08d48b4347cf8eff3815c0e25ae2e9a4340474079f55705f40574f4ec99";
89 }
90 '';
91 type = types.attrsOf (types.submodule rpcUserOpts);
92 description = "RPC user information for JSON-RPC connections.";
93 };
94 };
95
96 pidFile = mkOption {
97 type = types.path;
98 default = "${config.dataDir}/bitcoind.pid";
99 description = "Location of bitcoind pid file.";
100 };
101
102 testnet = mkOption {
103 type = types.bool;
104 default = false;
105 description = "Whether to use the testnet instead of mainnet.";
106 };
107
108 port = mkOption {
109 type = types.nullOr types.port;
110 default = null;
111 description = "Override the default port on which to listen for connections.";
112 };
113
114 dbCache = mkOption {
115 type = types.nullOr (types.ints.between 4 16384);
116 default = null;
117 example = 4000;
118 description = "Override the default database cache size in MiB.";
119 };
120
121 prune = mkOption {
122 type = types.nullOr (types.coercedTo
123 (types.enum [ "disable" "manual" ])
124 (x: if x == "disable" then 0 else 1)
125 types.ints.unsigned
126 );
127 default = null;
128 example = 10000;
129 description = ''
130 Reduce storage requirements by enabling pruning (deleting) of old
131 blocks. This allows the pruneblockchain RPC to be called to delete
132 specific blocks, and enables automatic pruning of old blocks if a
133 target size in MiB is provided. This mode is incompatible with -txindex
134 and -rescan. Warning: Reverting this setting requires re-downloading
135 the entire blockchain. ("disable" = disable pruning blocks, "manual"
136 = allow manual pruning via RPC, >=550 = automatically prune block files
137 to stay under the specified target size in MiB).
138 '';
139 };
140
141 extraCmdlineOptions = mkOption {
142 type = types.listOf types.str;
143 default = [];
144 description = ''
145 Extra command line options to pass to bitcoind.
146 Run bitcoind --help to list all available options.
147 '';
148 };
149 };
150 };
151in
152{
153
154 options = {
155 services.bitcoind = mkOption {
156 type = types.attrsOf (types.submodule bitcoindOpts);
157 default = {};
158 description = "Specification of one or more bitcoind instances.";
159 };
160 };
161
162 config = mkIf (eachBitcoind != {}) {
163
164 assertions = flatten (mapAttrsToList (bitcoindName: cfg: [
165 {
166 assertion = (cfg.prune != null) -> (builtins.elem cfg.prune [ "disable" "manual" 0 1 ] || (builtins.isInt cfg.prune && cfg.prune >= 550));
167 message = ''
168 If set, services.bitcoind.${bitcoindName}.prune has to be "disable", "manual", 0 , 1 or >= 550.
169 '';
170 }
171 {
172 assertion = (cfg.rpc.users != {}) -> (cfg.configFile == null);
173 message = ''
174 You cannot set both services.bitcoind.${bitcoindName}.rpc.users and services.bitcoind.${bitcoindName}.configFile
175 as they are exclusive. RPC user setting would have no effect if custom configFile would be used.
176 '';
177 }
178 ]) eachBitcoind);
179
180 environment.systemPackages = flatten (mapAttrsToList (bitcoindName: cfg: [
181 cfg.package
182 ]) eachBitcoind);
183
184 systemd.services = mapAttrs' (bitcoindName: cfg: (
185 nameValuePair "bitcoind-${bitcoindName}" (
186 let
187 configFile = pkgs.writeText "bitcoin.conf" ''
188 # If Testnet is enabled, we need to add [test] section
189 # otherwise, some options (e.g.: custom RPC port) will not work
190 ${optionalString cfg.testnet "[test]"}
191 # RPC users
192 ${concatMapStringsSep "\n"
193 (rpcUser: "rpcauth=${rpcUser.name}:${rpcUser.passwordHMAC}")
194 (attrValues cfg.rpc.users)
195 }
196 # Extra config options (from bitcoind nixos service)
197 ${cfg.extraConfig}
198 '';
199 in {
200 description = "Bitcoin daemon";
201 wants = [ "network-online.target" ];
202 after = [ "network-online.target" ];
203 wantedBy = [ "multi-user.target" ];
204 serviceConfig = {
205 User = cfg.user;
206 Group = cfg.group;
207 ExecStart = ''
208 ${cfg.package}/bin/bitcoind \
209 ${if (cfg.configFile != null) then
210 "-conf=${cfg.configFile}"
211 else
212 "-conf=${configFile}"
213 } \
214 -datadir=${cfg.dataDir} \
215 -pid=${cfg.pidFile} \
216 ${optionalString cfg.testnet "-testnet"}\
217 ${optionalString (cfg.port != null) "-port=${toString cfg.port}"}\
218 ${optionalString (cfg.prune != null) "-prune=${toString cfg.prune}"}\
219 ${optionalString (cfg.dbCache != null) "-dbcache=${toString cfg.dbCache}"}\
220 ${optionalString (cfg.rpc.port != null) "-rpcport=${toString cfg.rpc.port}"}\
221 ${toString cfg.extraCmdlineOptions}
222 '';
223 Restart = "on-failure";
224
225 # Hardening measures
226 PrivateTmp = "true";
227 ProtectSystem = "full";
228 NoNewPrivileges = "true";
229 PrivateDevices = "true";
230 MemoryDenyWriteExecute = "true";
231 };
232 }
233 ))) eachBitcoind;
234
235 systemd.tmpfiles.rules = flatten (mapAttrsToList (bitcoindName: cfg: [
236 "d '${cfg.dataDir}' 0770 '${cfg.user}' '${cfg.group}' - -"
237 ]) eachBitcoind);
238
239 users.users = mapAttrs' (bitcoindName: cfg: (
240 nameValuePair "bitcoind-${bitcoindName}" {
241 name = cfg.user;
242 group = cfg.group;
243 description = "Bitcoin daemon user";
244 home = cfg.dataDir;
245 isSystemUser = true;
246 })) eachBitcoind;
247
248 users.groups = mapAttrs' (bitcoindName: cfg: (
249 nameValuePair "${cfg.group}" { }
250 )) eachBitcoind;
251
252 };
253
254 meta.maintainers = with maintainers; [ _1000101 ];
255
256}