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}