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