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