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 options = {
158 type = mkOption {
159 description = "Rippled database type.";
160 type = types.enum ["rocksdb" "nudb"];
161 default = "rocksdb";
162 };
163
164 path = mkOption {
165 description = "Location to store the database.";
166 type = types.path;
167 default = cfg.databasePath;
168 };
169
170 compression = mkOption {
171 description = "Whether to enable snappy compression.";
172 type = types.nullOr types.bool;
173 default = null;
174 };
175
176 onlineDelete = mkOption {
177 description = "Enable automatic purging of older ledger information.";
178 type = types.addCheck (types.nullOr types.int) (v: v > 256);
179 default = cfg.ledgerHistory;
180 };
181
182 advisoryDelete = mkOption {
183 description = ''
184 If set, then require administrative RPC call "can_delete"
185 to enable online deletion of ledger records.
186 '';
187 type = types.nullOr types.bool;
188 default = null;
189 };
190
191 extraOpts = mkOption {
192 description = "Extra database options.";
193 type = types.lines;
194 default = "";
195 };
196 };
197 };
198
199in
200
201{
202
203 ###### interface
204
205 options = {
206 services.rippled = {
207 enable = mkEnableOption "rippled";
208
209 package = mkOption {
210 description = "Which rippled package to use.";
211 type = types.package;
212 default = pkgs.rippled;
213 defaultText = "pkgs.rippled";
214 };
215
216 ports = mkOption {
217 description = "Ports exposed by rippled";
218 type = with types; attrsOf (submodule portOptions);
219 default = {
220 rpc = {
221 port = 5005;
222 admin = ["127.0.0.1"];
223 protocol = ["http"];
224 };
225
226 peer = {
227 port = 51235;
228 ip = "0.0.0.0";
229 protocol = ["peer"];
230 };
231
232 ws_public = {
233 port = 5006;
234 ip = "0.0.0.0";
235 protocol = ["ws" "wss"];
236 };
237 };
238 };
239
240 nodeDb = mkOption {
241 description = "Rippled main database options.";
242 type = with types; nullOr (submodule 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 = with types; nullOr (submodule dbOptions);
258 default = null;
259 };
260
261 importDb = mkOption {
262 description = "Settings for performing a one-time import.";
263 type = with types; nullOr (submodule dbOptions);
264 default = null;
265 };
266
267 nodeSize = mkOption {
268 description = ''
269 Rippled size of the node you are running.
270 "tiny", "small", "medium", "large", and "huge"
271 '';
272 type = types.enum ["tiny" "small" "medium" "large" "huge"];
273 default = "small";
274 };
275
276 ips = mkOption {
277 description = ''
278 List of hostnames or ips where the Ripple protocol is served.
279 For a starter list, you can either copy entries from:
280 https://ripple.com/ripple.txt or if you prefer you can let it
281 default to r.ripple.com 51235
282
283 A port may optionally be specified after adding a space to the
284 address. By convention, if known, IPs are listed in from most
285 to least trusted.
286 '';
287 type = types.listOf types.str;
288 default = ["r.ripple.com 51235"];
289 };
290
291 ipsFixed = mkOption {
292 description = ''
293 List of IP addresses or hostnames to which rippled should always
294 attempt to maintain peer connections with. This is useful for
295 manually forming private networks, for example to configure a
296 validation server that connects to the Ripple network through a
297 public-facing server, or for building a set of cluster peers.
298
299 A port may optionally be specified after adding a space to the address
300 '';
301 type = types.listOf types.str;
302 default = [];
303 };
304
305 validators = mkOption {
306 description = ''
307 List of nodes to always accept as validators. Nodes are specified by domain
308 or public key.
309 '';
310 type = types.listOf types.str;
311 default = [
312 "n949f75evCHwgyP4fPVgaHqNHxUVN15PsJEZ3B3HnXPcPjcZAoy7 RL1"
313 "n9MD5h24qrQqiyBC8aeqqCWvpiBiYQ3jxSr91uiDvmrkyHRdYLUj RL2"
314 "n9L81uNCaPgtUJfaHh89gmdvXKAmSt5Gdsw2g1iPWaPkAHW5Nm4C RL3"
315 "n9KiYM9CgngLvtRCQHZwgC2gjpdaZcCcbt3VboxiNFcKuwFVujzS RL4"
316 "n9LdgEtkmGB9E2h3K4Vp7iGUaKuq23Zr32ehxiU8FWY7xoxbWTSA RL5"
317 ];
318 };
319
320 databasePath = mkOption {
321 description = ''
322 Path to the ripple database.
323 '';
324 type = types.path;
325 default = "/var/lib/rippled";
326 };
327
328 validationQuorum = mkOption {
329 description = ''
330 The minimum number of trusted validations a ledger must have before
331 the server considers it fully validated.
332 '';
333 type = types.int;
334 default = 3;
335 };
336
337 ledgerHistory = mkOption {
338 description = ''
339 The number of past ledgers to acquire on server startup and the minimum
340 to maintain while running.
341 '';
342 type = types.either types.int (types.enum ["full"]);
343 default = 1296000; # 1 month
344 };
345
346 fetchDepth = mkOption {
347 description = ''
348 The number of past ledgers to serve to other peers that request historical
349 ledger data (or "full" for no limit).
350 '';
351 type = types.either types.int (types.enum ["full"]);
352 default = "full";
353 };
354
355 sntpServers = mkOption {
356 description = ''
357 IP address or domain of NTP servers to use for time synchronization.;
358 '';
359 type = types.listOf types.str;
360 default = [
361 "time.windows.com"
362 "time.apple.com"
363 "time.nist.gov"
364 "pool.ntp.org"
365 ];
366 };
367
368 logLevel = mkOption {
369 description = "Logging verbosity.";
370 type = types.enum ["debug" "error" "info"];
371 default = "error";
372 };
373
374 statsd = {
375 enable = mkEnableOption "statsd monitoring for rippled";
376
377 address = mkOption {
378 description = "The UDP address and port of the listening StatsD server.";
379 default = "127.0.0.1:8125";
380 type = types.str;
381 };
382
383 prefix = mkOption {
384 description = "A string prepended to each collected metric.";
385 default = "";
386 type = types.str;
387 };
388 };
389
390 extraConfig = mkOption {
391 default = "";
392 description = ''
393 Extra lines to be added verbatim to the rippled.cfg configuration file.
394 '';
395 };
396
397 config = mkOption {
398 internal = true;
399 default = pkgs.writeText "rippled.conf" rippledCfg;
400 };
401 };
402 };
403
404
405 ###### implementation
406
407 config = mkIf cfg.enable {
408
409 users.extraUsers = singleton
410 { name = "rippled";
411 description = "Ripple server user";
412 uid = config.ids.uids.rippled;
413 home = cfg.databasePath;
414 createHome = true;
415 };
416
417 systemd.services.rippled = {
418 after = [ "network.target" ];
419 wantedBy = [ "multi-user.target" ];
420
421 serviceConfig = {
422 ExecStart = "${cfg.package}/bin/rippled --fg --conf ${cfg.config}";
423 User = "rippled";
424 Restart = "on-failure";
425 LimitNOFILE=10000;
426 };
427 };
428
429 environment.systemPackages = [ cfg.package ];
430
431 };
432}