1{ config, lib, options, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.rippled;
7 opt = options.services.rippled;
8
9 b2i = val: if val then "1" else "0";
10
11 dbCfg = db: ''
12 type=${db.type}
13 path=${db.path}
14 ${optionalString (db.compression != null) ("compression=${b2i db.compression}") }
15 ${optionalString (db.onlineDelete != null) ("online_delete=${toString db.onlineDelete}")}
16 ${optionalString (db.advisoryDelete != null) ("advisory_delete=${b2i db.advisoryDelete}")}
17 ${db.extraOpts}
18 '';
19
20 rippledCfg = ''
21 [server]
22 ${concatMapStringsSep "\n" (n: "port_${n}") (attrNames cfg.ports)}
23
24 ${concatMapStrings (p: ''
25 [port_${p.name}]
26 ip=${p.ip}
27 port=${toString p.port}
28 protocol=${concatStringsSep "," p.protocol}
29 ${optionalString (p.user != "") "user=${p.user}"}
30 ${optionalString (p.password != "") "user=${p.password}"}
31 admin=${concatStringsSep "," p.admin}
32 ${optionalString (p.ssl.key != null) "ssl_key=${p.ssl.key}"}
33 ${optionalString (p.ssl.cert != null) "ssl_cert=${p.ssl.cert}"}
34 ${optionalString (p.ssl.chain != null) "ssl_chain=${p.ssl.chain}"}
35 '') (attrValues cfg.ports)}
36
37 [database_path]
38 ${cfg.databasePath}
39
40 [node_db]
41 ${dbCfg cfg.nodeDb}
42
43 ${optionalString (cfg.tempDb != null) ''
44 [temp_db]
45 ${dbCfg cfg.tempDb}''}
46
47 ${optionalString (cfg.importDb != null) ''
48 [import_db]
49 ${dbCfg cfg.importDb}''}
50
51 [ips]
52 ${concatStringsSep "\n" cfg.ips}
53
54 [ips_fixed]
55 ${concatStringsSep "\n" cfg.ipsFixed}
56
57 [validators]
58 ${concatStringsSep "\n" cfg.validators}
59
60 [node_size]
61 ${cfg.nodeSize}
62
63 [ledger_history]
64 ${toString cfg.ledgerHistory}
65
66 [fetch_depth]
67 ${toString cfg.fetchDepth}
68
69 [validation_quorum]
70 ${toString cfg.validationQuorum}
71
72 [sntp_servers]
73 ${concatStringsSep "\n" cfg.sntpServers}
74
75 ${optionalString cfg.statsd.enable ''
76 [insight]
77 server=statsd
78 address=${cfg.statsd.address}
79 prefix=${cfg.statsd.prefix}
80 ''}
81
82 [rpc_startup]
83 { "command": "log_level", "severity": "${cfg.logLevel}" }
84 '' + cfg.extraConfig;
85
86 portOptions = { name, ...}: {
87 options = {
88 name = mkOption {
89 internal = true;
90 default = name;
91 };
92
93 ip = mkOption {
94 default = "127.0.0.1";
95 description = "Ip where rippled listens.";
96 type = types.str;
97 };
98
99 port = mkOption {
100 description = "Port where rippled listens.";
101 type = types.port;
102 };
103
104 protocol = mkOption {
105 description = "Protocols expose by rippled.";
106 type = types.listOf (types.enum ["http" "https" "ws" "wss" "peer"]);
107 };
108
109 user = mkOption {
110 description = "When set, these credentials will be required on HTTP/S requests.";
111 type = types.str;
112 default = "";
113 };
114
115 password = mkOption {
116 description = "When set, these credentials will be required on HTTP/S requests.";
117 type = types.str;
118 default = "";
119 };
120
121 admin = mkOption {
122 description = "A comma-separated list of admin IP addresses.";
123 type = types.listOf types.str;
124 default = ["127.0.0.1"];
125 };
126
127 ssl = {
128 key = mkOption {
129 description = ''
130 Specifies the filename holding the SSL key in PEM format.
131 '';
132 default = null;
133 type = types.nullOr types.path;
134 };
135
136 cert = mkOption {
137 description = ''
138 Specifies the path to the SSL certificate file in PEM format.
139 This is not needed if the chain includes it.
140 '';
141 default = null;
142 type = types.nullOr types.path;
143 };
144
145 chain = mkOption {
146 description = ''
147 If you need a certificate chain, specify the path to the
148 certificate chain here. The chain may include the end certificate.
149 '';
150 default = null;
151 type = types.nullOr types.path;
152 };
153 };
154 };
155 };
156
157 dbOptions = {
158 options = {
159 type = mkOption {
160 description = "Rippled database type.";
161 type = types.enum ["rocksdb" "nudb"];
162 default = "rocksdb";
163 };
164
165 path = mkOption {
166 description = "Location to store the database.";
167 type = types.path;
168 default = cfg.databasePath;
169 defaultText = literalExpression "config.${opt.databasePath}";
170 };
171
172 compression = mkOption {
173 description = "Whether to enable snappy compression.";
174 type = types.nullOr types.bool;
175 default = null;
176 };
177
178 onlineDelete = mkOption {
179 description = "Enable automatic purging of older ledger information.";
180 type = types.nullOr (types.addCheck types.int (v: v > 256));
181 default = cfg.ledgerHistory;
182 defaultText = literalExpression "config.${opt.ledgerHistory}";
183 };
184
185 advisoryDelete = mkOption {
186 description = ''
187 If set, then require administrative RPC call "can_delete"
188 to enable online deletion of ledger records.
189 '';
190 type = types.nullOr types.bool;
191 default = null;
192 };
193
194 extraOpts = mkOption {
195 description = "Extra database options.";
196 type = types.lines;
197 default = "";
198 };
199 };
200 };
201
202in
203
204{
205
206 ###### interface
207
208 options = {
209 services.rippled = {
210 enable = mkEnableOption "rippled, a decentralized cryptocurrency blockchain daemon implementing the XRP Ledger protocol in C++";
211
212 package = mkPackageOption pkgs "rippled" { };
213
214 ports = mkOption {
215 description = "Ports exposed by rippled";
216 type = with types; attrsOf (submodule portOptions);
217 default = {
218 rpc = {
219 port = 5005;
220 admin = ["127.0.0.1"];
221 protocol = ["http"];
222 };
223
224 peer = {
225 port = 51235;
226 ip = "0.0.0.0";
227 protocol = ["peer"];
228 };
229
230 ws_public = {
231 port = 5006;
232 ip = "0.0.0.0";
233 protocol = ["ws" "wss"];
234 };
235 };
236 };
237
238 nodeDb = mkOption {
239 description = "Rippled main database options.";
240 type = with types; nullOr (submodule dbOptions);
241 default = {
242 type = "rocksdb";
243 extraOpts = ''
244 open_files=2000
245 filter_bits=12
246 cache_mb=256
247 file_size_pb=8
248 file_size_mult=2;
249 '';
250 };
251 };
252
253 tempDb = mkOption {
254 description = "Rippled temporary database options.";
255 type = with types; nullOr (submodule dbOptions);
256 default = null;
257 };
258
259 importDb = mkOption {
260 description = "Settings for performing a one-time import.";
261 type = with types; nullOr (submodule dbOptions);
262 default = null;
263 };
264
265 nodeSize = mkOption {
266 description = ''
267 Rippled size of the node you are running.
268 "tiny", "small", "medium", "large", and "huge"
269 '';
270 type = types.enum ["tiny" "small" "medium" "large" "huge"];
271 default = "small";
272 };
273
274 ips = mkOption {
275 description = ''
276 List of hostnames or ips where the Ripple protocol is served.
277 For a starter list, you can either copy entries from:
278 https://ripple.com/ripple.txt or if you prefer you can let it
279 default to r.ripple.com 51235
280
281 A port may optionally be specified after adding a space to the
282 address. By convention, if known, IPs are listed in from most
283 to least trusted.
284 '';
285 type = types.listOf types.str;
286 default = ["r.ripple.com 51235"];
287 };
288
289 ipsFixed = mkOption {
290 description = ''
291 List of IP addresses or hostnames to which rippled should always
292 attempt to maintain peer connections with. This is useful for
293 manually forming private networks, for example to configure a
294 validation server that connects to the Ripple network through a
295 public-facing server, or for building a set of cluster peers.
296
297 A port may optionally be specified after adding a space to the address
298 '';
299 type = types.listOf types.str;
300 default = [];
301 };
302
303 validators = mkOption {
304 description = ''
305 List of nodes to always accept as validators. Nodes are specified by domain
306 or public key.
307 '';
308 type = types.listOf types.str;
309 default = [
310 "n949f75evCHwgyP4fPVgaHqNHxUVN15PsJEZ3B3HnXPcPjcZAoy7 RL1"
311 "n9MD5h24qrQqiyBC8aeqqCWvpiBiYQ3jxSr91uiDvmrkyHRdYLUj RL2"
312 "n9L81uNCaPgtUJfaHh89gmdvXKAmSt5Gdsw2g1iPWaPkAHW5Nm4C RL3"
313 "n9KiYM9CgngLvtRCQHZwgC2gjpdaZcCcbt3VboxiNFcKuwFVujzS RL4"
314 "n9LdgEtkmGB9E2h3K4Vp7iGUaKuq23Zr32ehxiU8FWY7xoxbWTSA RL5"
315 ];
316 };
317
318 databasePath = mkOption {
319 description = ''
320 Path to the ripple database.
321 '';
322 type = types.path;
323 default = "/var/lib/rippled";
324 };
325
326 validationQuorum = mkOption {
327 description = ''
328 The minimum number of trusted validations a ledger must have before
329 the server considers it fully validated.
330 '';
331 type = types.int;
332 default = 3;
333 };
334
335 ledgerHistory = mkOption {
336 description = ''
337 The number of past ledgers to acquire on server startup and the minimum
338 to maintain while running.
339 '';
340 type = types.either types.int (types.enum ["full"]);
341 default = 1296000; # 1 month
342 };
343
344 fetchDepth = mkOption {
345 description = ''
346 The number of past ledgers to serve to other peers that request historical
347 ledger data (or "full" for no limit).
348 '';
349 type = types.either types.int (types.enum ["full"]);
350 default = "full";
351 };
352
353 sntpServers = mkOption {
354 description = ''
355 IP address or domain of NTP servers to use for time synchronization.;
356 '';
357 type = types.listOf types.str;
358 default = [
359 "time.windows.com"
360 "time.apple.com"
361 "time.nist.gov"
362 "pool.ntp.org"
363 ];
364 };
365
366 logLevel = mkOption {
367 description = "Logging verbosity.";
368 type = types.enum ["debug" "error" "info"];
369 default = "error";
370 };
371
372 statsd = {
373 enable = mkEnableOption "statsd monitoring for rippled";
374
375 address = mkOption {
376 description = "The UDP address and port of the listening StatsD server.";
377 default = "127.0.0.1:8125";
378 type = types.str;
379 };
380
381 prefix = mkOption {
382 description = "A string prepended to each collected metric.";
383 default = "";
384 type = types.str;
385 };
386 };
387
388 extraConfig = mkOption {
389 default = "";
390 type = types.lines;
391 description = ''
392 Extra lines to be added verbatim to the rippled.cfg configuration file.
393 '';
394 };
395
396 config = mkOption {
397 internal = true;
398 default = pkgs.writeText "rippled.conf" rippledCfg;
399 defaultText = literalMD "generated config file";
400 };
401 };
402 };
403
404
405 ###### implementation
406
407 config = mkIf cfg.enable {
408
409 users.users.rippled = {
410 description = "Ripple server user";
411 isSystemUser = true;
412 group = "rippled";
413 home = cfg.databasePath;
414 createHome = true;
415 };
416 users.groups.rippled = {};
417
418 systemd.services.rippled = {
419 after = [ "network.target" ];
420 wantedBy = [ "multi-user.target" ];
421
422 serviceConfig = {
423 ExecStart = "${cfg.package}/bin/rippled --fg --conf ${cfg.config}";
424 User = "rippled";
425 Restart = "on-failure";
426 LimitNOFILE=10000;
427 };
428 };
429
430 environment.systemPackages = [ cfg.package ];
431
432 };
433}