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 };
212
213 ports = mkOption {
214 description = "Ports exposed by rippled";
215 type = types.attrsOf types.optionSet;
216 options = [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 = types.nullOr types.optionSet;
241 options = [dbOptions];
242 default = {
243 type = "rocksdb";
244 extraOpts = ''
245 open_files=2000
246 filter_bits=12
247 cache_mb=256
248 file_size_pb=8
249 file_size_mult=2;
250 '';
251 };
252 };
253
254 tempDb = mkOption {
255 description = "Rippled temporary database options.";
256 type = types.nullOr types.optionSet;
257 options = [dbOptions];
258 default = null;
259 };
260
261 importDb = mkOption {
262 description = "Settings for performing a one-time import.";
263 type = types.nullOr types.optionSet;
264 options = [dbOptions];
265 default = null;
266 };
267
268 nodeSize = mkOption {
269 description = ''
270 Rippled size of the node you are running.
271 "tiny", "small", "medium", "large", and "huge"
272 '';
273 type = types.enum ["tiny" "small" "medium" "large" "huge"];
274 default = "small";
275 };
276
277 ips = mkOption {
278 description = ''
279 List of hostnames or ips where the Ripple protocol is served.
280 For a starter list, you can either copy entries from:
281 https://ripple.com/ripple.txt or if you prefer you can let it
282 default to r.ripple.com 51235
283
284 A port may optionally be specified after adding a space to the
285 address. By convention, if known, IPs are listed in from most
286 to least trusted.
287 '';
288 type = types.listOf types.str;
289 default = ["r.ripple.com 51235"];
290 };
291
292 ipsFixed = mkOption {
293 description = ''
294 List of IP addresses or hostnames to which rippled should always
295 attempt to maintain peer connections with. This is useful for
296 manually forming private networks, for example to configure a
297 validation server that connects to the Ripple network through a
298 public-facing server, or for building a set of cluster peers.
299
300 A port may optionally be specified after adding a space to the address
301 '';
302 type = types.listOf types.str;
303 default = [];
304 };
305
306 validators = mkOption {
307 description = ''
308 List of nodes to always accept as validators. Nodes are specified by domain
309 or public key.
310 '';
311 type = types.listOf types.str;
312 default = [
313 "n949f75evCHwgyP4fPVgaHqNHxUVN15PsJEZ3B3HnXPcPjcZAoy7 RL1"
314 "n9MD5h24qrQqiyBC8aeqqCWvpiBiYQ3jxSr91uiDvmrkyHRdYLUj RL2"
315 "n9L81uNCaPgtUJfaHh89gmdvXKAmSt5Gdsw2g1iPWaPkAHW5Nm4C RL3"
316 "n9KiYM9CgngLvtRCQHZwgC2gjpdaZcCcbt3VboxiNFcKuwFVujzS RL4"
317 "n9LdgEtkmGB9E2h3K4Vp7iGUaKuq23Zr32ehxiU8FWY7xoxbWTSA RL5"
318 ];
319 };
320
321 databasePath = mkOption {
322 description = ''
323 Path to the ripple database.
324 '';
325 type = types.path;
326 default = "/var/lib/rippled";
327 };
328
329 validationQuorum = mkOption {
330 description = ''
331 The minimum number of trusted validations a ledger must have before
332 the server considers it fully validated.
333 '';
334 type = types.int;
335 default = 3;
336 };
337
338 ledgerHistory = mkOption {
339 description = ''
340 The number of past ledgers to acquire on server startup and the minimum
341 to maintain while running.
342 '';
343 type = types.either types.int (types.enum ["full"]);
344 default = 1296000; # 1 month
345 };
346
347 fetchDepth = mkOption {
348 description = ''
349 The number of past ledgers to serve to other peers that request historical
350 ledger data (or "full" for no limit).
351 '';
352 type = types.either types.int (types.enum ["full"]);
353 default = "full";
354 };
355
356 sntpServers = mkOption {
357 description = ''
358 IP address or domain of NTP servers to use for time synchronization.;
359 '';
360 type = types.listOf types.str;
361 default = [
362 "time.windows.com"
363 "time.apple.com"
364 "time.nist.gov"
365 "pool.ntp.org"
366 ];
367 };
368
369 logLevel = mkOption {
370 description = "Logging verbosity.";
371 type = types.enum ["debug" "error" "info"];
372 default = "error";
373 };
374
375 statsd = {
376 enable = mkEnableOption "statsd monitoring for rippled";
377
378 address = mkOption {
379 description = "The UDP address and port of the listening StatsD server.";
380 default = "127.0.0.1:8125";
381 type = types.str;
382 };
383
384 prefix = mkOption {
385 description = "A string prepended to each collected metric.";
386 default = "";
387 type = types.str;
388 };
389 };
390
391 extraConfig = mkOption {
392 default = "";
393 description = ''
394 Extra lines to be added verbatim to the rippled.cfg configuration file.
395 '';
396 };
397
398 config = mkOption {
399 internal = true;
400 default = pkgs.writeText "rippled.conf" rippledCfg;
401 };
402 };
403 };
404
405
406 ###### implementation
407
408 config = mkIf cfg.enable {
409
410 users.extraUsers = singleton
411 { name = "rippled";
412 description = "Ripple server user";
413 uid = config.ids.uids.rippled;
414 home = cfg.databasePath;
415 createHome = true;
416 };
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}