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