{ config, pkgs, lib, ... }: with lib; let options = { services.liquidd = { enable = mkEnableOption "Liquid Bitcoin sidechain daemon"; address = mkOption { type = types.str; default = "127.0.0.1"; description = mdDoc "Address to listen for peer connections."; }; port = mkOption { type = types.port; default = 7042; description = mdDoc "Override the default port on which to listen for connections."; }; onionPort = mkOption { type = types.nullOr types.port; # When the liquidd onion service is enabled, add an onion-tagged socket # to distinguish local connections from Tor connections default = if (config.nix-bitcoin.onionServices.liquidd.enable or false) then 7043 else null; description = mdDoc '' Port to listen for Tor peer connections. If set, inbound connections to this port are tagged as onion peers. ''; }; listen = mkOption { type = types.bool; default = false; description = mdDoc '' Listen for peer connections at `address:port` and `address:onionPort` (if {option}`onionPort` is set). ''; }; listenWhitelisted = mkOption { type = types.bool; default = false; description = mdDoc '' Listen for peer connections at `address:whitelistedPort`. Peers connected through this socket are automatically whitelisted. ''; }; whitelistedPort = mkOption { type = types.port; default = 7044; description = mdDoc "See {option}`listenWhitelisted`."; }; extraConfig = mkOption { type = types.lines; default = ""; example = '' par=16 rpcthreads=16 logips=1 ''; description = mdDoc "Extra lines appended to {file}`elements.conf`."; }; dataDir = mkOption { type = types.path; default = "/var/lib/liquidd"; description = mdDoc "The data directory for liquidd."; }; rpc = { address = mkOption { type = types.str; default = "127.0.0.1"; description = mdDoc "Address to listen for JSON-RPC connections."; }; port = mkOption { type = types.port; default = 7041; description = mdDoc "Port to listen for JSON-RPC connections."; }; users = mkOption { default = {}; example = { alice.passwordHMAC = "f7efda5c189b999524f151318c0c86$d5b51b3beffbc02b724e5d095828e0bc8b2456e9ac8757ae3211a5d9b16a22ae"; bob.passwordHMAC = "b2dd077cb54591a2f3139e69a897ac$4e71f08d48b4347cf8eff3815c0e25ae2e9a4340474079f55705f40574f4ec99"; }; type = with types; attrsOf (submodule rpcUserOpts); description = mdDoc '' RPC user information for JSON-RPC connections. ''; }; }; rpcallowip = mkOption { type = types.listOf types.str; default = [ "127.0.0.1" ]; description = mdDoc '' Allow JSON-RPC connections from specified source. ''; }; rpcuser = mkOption { type = types.str; default = "liquidrpc"; description = mdDoc "Username for JSON-RPC connections"; }; proxy = mkOption { type = types.nullOr types.str; default = if cfg.tor.proxy then config.nix-bitcoin.torClientAddressWithPort else null; description = mdDoc "Connect through SOCKS5 proxy"; }; dbCache = mkOption { type = types.nullOr (types.ints.between 4 16384); default = null; example = 4000; description = mdDoc "Override the default database cache size in megabytes."; }; prune = mkOption { type = types.nullOr (types.coercedTo (types.enum [ "disable" "manual" ]) (x: if x == "disable" then 0 else 1) types.ints.unsigned ); default = null; example = 10000; description = mdDoc '' Reduce storage requirements by enabling pruning (deleting) of old blocks. This allows the pruneblockchain RPC to be called to delete specific blocks, and enables automatic pruning of old blocks if a target size in MiB is provided. This mode is incompatible with -txindex and -rescan. Warning: Reverting this setting requires re-downloading the entire blockchain. (`disable` = disable pruning blocks, `manual` = allow manual pruning via RPC, >=550 = automatically prune block files to stay under the specified target size in MiB) ''; }; validatepegin = mkOption { type = types.nullOr types.bool; default = null; description = mdDoc '' Validate pegin claims. All functionaries must run this. ''; }; user = mkOption { type = types.str; default = "liquid"; description = mdDoc "The user as which to run liquidd."; }; group = mkOption { type = types.str; default = cfg.user; description = mdDoc "The group as which to run liquidd."; }; cli = mkOption { readOnly = true; default = pkgs.writers.writeBashBin "elements-cli" '' ${nbPkgs.elementsd}/bin/elements-cli -datadir='${cfg.dataDir}' "$@" ''; defaultText = "(See source)"; description = mdDoc "Binary to connect with the liquidd instance."; }; swapCli = mkOption { default = pkgs.writers.writeBashBin "liquidswap-cli" '' ${nbPkgs.liquid-swap}/bin/liquidswap-cli -c '${cfg.dataDir}/elements.conf' "$@" ''; defaultText = "(See source)"; description = mdDoc "Binary for managing liquid swaps."; }; tor = nbLib.tor; }; }; cfg = config.services.liquidd; nbLib = config.nix-bitcoin.lib; nbPkgs = config.nix-bitcoin.pkgs; secretsDir = config.nix-bitcoin.secretsDir; bitcoind = config.services.bitcoind; configFile = pkgs.writeText "elements.conf" '' # We're already logging via journald nodebuglogfile=1 startupnotify=/run/current-system/systemd/bin/systemd-notify --ready chain=${bitcoind.makeNetworkName "liquidv1" '' regtest [regtest]'' # Add [regtest] config section } ${optionalString (cfg.dbCache != null) "dbcache=${toString cfg.dbCache}"} ${optionalString (cfg.prune != null) "prune=${toString cfg.prune}"} ${optionalString (cfg.validatepegin != null) "validatepegin=${if cfg.validatepegin then "1" else "0"}"} # Connection options listen=${if (cfg.listen || cfg.listenWhitelisted) then "1" else "0"} ${optionalString cfg.listen "bind=${cfg.address}:${toString cfg.port}"} ${optionalString (cfg.listen && cfg.onionPort != null) "bind=${cfg.address}:${toString cfg.onionPort}=onion"} ${optionalString cfg.listenWhitelisted "whitebind=${cfg.address}:${toString cfg.whitelistedPort}"} ${optionalString (cfg.proxy != null) "proxy=${cfg.proxy}"} # RPC server options rpcport=${toString cfg.rpc.port} ${concatMapStringsSep "\n" (rpcUser: "rpcauth=${rpcUser.name}:${rpcUser.passwordHMAC}") (attrValues cfg.rpc.users) } rpcbind=${cfg.rpc.address} rpcconnect=${cfg.rpc.address} ${lib.concatMapStrings (rpcallowip: "rpcallowip=${rpcallowip}\n") cfg.rpcallowip} rpcuser=${cfg.rpcuser} mainchainrpchost=${nbLib.address bitcoind.rpc.address} mainchainrpcport=${toString bitcoind.rpc.port} mainchainrpcuser=${bitcoind.rpc.users.public.name} # Extra config options (from liquidd nixos service) ${cfg.extraConfig} ''; rpcUserOpts = { name, ... }: { options = { name = mkOption { type = types.str; example = "alice"; description = mdDoc '' Username for JSON-RPC connections. ''; }; passwordHMAC = mkOption { type = with types; uniq (strMatching "[0-9a-f]+\\$[0-9a-f]{64}"); example = "f7efda5c189b999524f151318c0c86$d5b51b3beffbc02b724e5d095828e0bc8b2456e9ac8757ae3211a5d9b16a22ae"; description = mdDoc '' Password HMAC-SHA-256 for JSON-RPC connections. Must be a string of the format `$`. ''; }; }; config = { name = mkDefault name; }; }; in { inherit options; config = mkIf cfg.enable { assertions = [ { assertion = bitcoind.regtest -> cfg.validatepegin != true; message = "liquidd: `validatepegin` is incompatible with regtest."; } ]; services.bitcoind.enable = true; environment.systemPackages = [ nbPkgs.elementsd (hiPrio cfg.cli) (hiPrio cfg.swapCli) ]; systemd.tmpfiles.rules = [ "d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -" ]; systemd.services.liquidd = { requires = [ "bitcoind.service" ]; after = [ "bitcoind.service" "nix-bitcoin-secrets.target" ]; wantedBy = [ "multi-user.target" ]; preStart = '' install -m 640 ${configFile} '${cfg.dataDir}/elements.conf' { echo "rpcpassword=$(cat ${secretsDir}/liquid-rpcpassword)" echo "mainchainrpcpassword=$(cat ${secretsDir}/bitcoin-rpcpassword-public)" } >> '${cfg.dataDir}/elements.conf' ''; serviceConfig = nbLib.defaultHardening // { Type = "notify"; NotifyAccess = "all"; User = cfg.user; Group = cfg.group; TimeoutStartSec = "2h"; TimeoutStopSec = "2h"; ExecStart = "${nbPkgs.elementsd}/bin/elementsd -datadir='${cfg.dataDir}'"; Restart = "on-failure"; ReadWritePaths = [ cfg.dataDir ]; } // nbLib.allowedIPAddresses cfg.tor.enforce; }; users.users.${cfg.user} = { isSystemUser = true; group = cfg.group; extraGroups = [ "bitcoinrpc-public" ]; }; users.groups.${cfg.group} = {}; nix-bitcoin.operator.groups = [ cfg.group ]; nix-bitcoin.secrets.liquid-rpcpassword.user = cfg.user; nix-bitcoin.generateSecretsCmds.liquid = '' makePasswordSecret liquid-rpcpassword ''; }; }