diff --git a/docs/getting-started/running-lighthouse-beacon.md b/docs/getting-started/running-lighthouse-beacon.md new file mode 100644 index 00000000..12362787 --- /dev/null +++ b/docs/getting-started/running-lighthouse-beacon.md @@ -0,0 +1,84 @@ +# Running Lighthouse Beacon + +One or more [Lighthouse Beacon](https://lighthouse-book.sigmaprime.io/intro.html) services can be configured with the `services.ethereum.lighthouse-beacon` prefix. + +```nix title="server.nix" +{ pkgs, ...}: { + services.ethereum.lighthouse-beacon.sepolia = { + enable = true; + openFirewall = true; + args = { + network = "sepolia"; # (Optional) defaults to beacon name + execution-jwt = secrets.lighthouse_jwt_secret.path; + checkpoint-sync-url = "https://sepolia.checkpoint-sync.ethdevops.io"; + genesis-state-url = "https://sepolia.checkpoint-sync.ethdevops.io"; + }; + http-address = "0.0.0.0"; + }; + + services.ethereum.lighthouse-beacon.goerli = { + enable = true; + ... + }; +} +``` + +**Note:** It is recommended to use an attribute name that matches the network that Lighthouse Beacon is configured for. + +## Configuration + +Many of Lighthouse Beacon's process arguments have been mapped to NixOS types and can be provided via the `args` section of the config. +For a detailed list please refer to the [NixOS Options](../reference/module-options/lighthouse-beacon.md) reference. + +Additional arguments can be provided in a list directly to the Lighthouse Beacon process via the `extraArgs` attribute as shown above. + +## Systemd service + +For each instance that is configured a corresponding [Systemd](https://systemd.io/) service is created. The service name +follows a convention of `lighthouse-beacon-${name}.service`. + +| Config | Name | Service name | +| :-------------------------------------------- | :------ | :---------------------------------- | +| `services.ethereum.lighthouse-beacon.sepolia` | sepolia | `lighthouse-beacon-sepolia.service` | +| `services.ethereum.lighthouse-beacon.goerli` | goerli | `lighthouse-beacon-goerli.service` | +| `services.ethereum.lighthouse-beacon.mainnet` | mainnet | `lighthouse-beacon-mainnet.service` | + +The service that is created can then be introspected and managed via the standard Systemd toolset. + +| Action | Command | +| :------ | :---------------------------------------------------- | +| Status | `systemctl status lighthouse-beacon-sepolia.service` | +| Stop | `systemctl stop lighthouse-beacon-sepolia.service` | +| Start | `systemctl start lighthouse-beacon-sepolia.service` | +| Restart | `systemctl restart lighthouse-beacon-sepolia.service` | +| Logs | `journalctl -xefu lighthouse-beacon-sepolia.service` | + +## Using a Lighthouse Beacon fork + +A different version of Lighthouse Beacon can be configured via the [package](../reference/module-options/lighthouse-beacon.md#servicesethereumlighthouse-beaconnamepackage) option. + +To configure a custom fork for example: + +```nix title="server.nix" +{ pkgs, ...}: { + services.ethereum.lighthouse-beacon.sepolia = { + enable = true; + package = pkgs.my-lighthouse-beacon; + ... + }; +} +``` + +## Opening ports + +By default, [openFirewall](../reference/module-options/lighthouse-beacon.md#servicesethereumlighthouse-beaconnameopenfirewall) is set to `false`. +If set to `true` firewall rules are added which will expose the following ports: + +| Protocol | Config | Default value | +| :------- | :------------------------------------------------------------------------------------------------------------------------- | :------------ | +| UDP | [discovery-port](../reference/module-options/lighthouse-beacon.md#servicesethereumlighthouse-beaconnameargsdiscovery-port) | 9000 | +| UDP/TCP | [quic-port](../reference/module-options/lighthouse-beacon.md#servicesethereumlighthouse-beaconnameargsquic-port) | 9001 | +| TCP | [http-port](../reference/module-options/lighthouse-beacon.md#servicesethereumlighthouse-beaconnameargshttp-port) | 5052 | + +**Note:** it is important when running multiple instances of Lighthouse Beacon on the same machine that you ensure they are configured +with different ports. diff --git a/mkdocs.yml b/mkdocs.yml index 04057ca0..ea543d3c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -66,6 +66,7 @@ nav: - getting-started/installation.md - getting-started/running-geth.md - getting-started/running-prysm-beacon.md + - getting-started/running-lighthouse-beacon.md - getting-started/backup-and-restore.md - Reference: - NixOS Module Options: @@ -74,6 +75,7 @@ nav: - reference/module-options/geth-bootnode.md - reference/module-options/nethermind.md - reference/module-options/prysm-beacon.md + - reference/module-options/lighthouse-beacon.md extra: homepage: https://nix-community.github.io/ethereum.nix diff --git a/modules/default.nix b/modules/default.nix index d1e99924..a00b7774 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -10,6 +10,8 @@ ./erigon ./geth ./geth-bootnode + ./lighthouse-beacon + ./lighthouse-validator ./mev-boost ./nethermind ./prysm-beacon diff --git a/modules/lighthouse-beacon/args.nix b/modules/lighthouse-beacon/args.nix new file mode 100644 index 00000000..de47c9cb --- /dev/null +++ b/modules/lighthouse-beacon/args.nix @@ -0,0 +1,145 @@ +{ + lib, + name, + config, + ... +}: +with lib; { + network = mkOption { + type = types.nullOr (types.enum ["mainnet" "prater" "goerli" "gnosis" "chiado" "sepolia" "holesky"]); + default = name; + defaultText = "name"; + description = mdDoc "The network to connect to. Mainnet is the default ethereum network."; + }; + + execution-endpoint = mkOption { + type = types.str; + default = "http://127.0.0.1:8551"; + description = lib.mdDoc '' + Listen address for the execution layer. + ''; + }; + + execution-jwt = mkOption { + type = types.str; + default = null; + description = mdDoc '' + Path to a file containing a hex-encoded string representing a 32 byte secret + used for authentication with an execution node via HTTP + ''; + example = "/var/run/prysm/jwtsecret"; + }; + + checkpoint-sync-url = mkOption { + type = types.nullOr types.str; + default = null; + description = mdDoc '' + URL of a synced beacon node to trust in obtaining checkpoint sync data. + As an additional safety measure, it is strongly recommended to only use this option in conjunction with --wss-checkpoint flag + ''; + example = "https://goerli.checkpoint-sync.ethpandaops.io"; + }; + + disable-deposit-contract-sync = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Explicitly disables syncing of deposit logs from the execution node. + This overrides any previous option that depends on it. + Useful if you intend to run a non-validating beacon node. + ''; + }; + + genesis-state-url = mkOption { + type = types.nullOr types.str; + default = null; + description = mdDoc '' + URL of a synced beacon node to trust for obtaining genesis state. + As an additional safety measure, it is strongly recommended to only use this option in conjunction with --wss-checkpoint flag + ''; + example = "https://goerli.checkpoint-sync.ethpandaops.io"; + }; + + disable-quic = mkOption { + type = types.bool; + default = false; + description = mdDoc '' + Disables the quic transport. + The node will rely solely on the TCP transport for libp2p connections. + ''; + }; + + discovery-port = mkOption { + type = types.port; + default = 9000; + description = mdDoc "The port used by discv5."; + }; + + quic-port = mkOption { + type = types.port; + default = config.args.discovery-port + 1; + defaultText = literalExpression "args.discovery-port + 1"; + description = mdDoc '' + The port used by libp2p. + Will use TCP if disable-quic is set + ''; + }; + + metrics = { + enable = mkOption { + type = types.bool; + default = true; + description = mdDoc "Enable Prometheus metrics exporter."; + }; + + address = mkOption { + type = types.str; + default = "127.0.0.1"; + description = mdDoc "Host used to listen and respond with metrics for prometheus."; + }; + + port = mkOption { + type = types.port; + default = 5054; + description = mdDoc "Port used to listen and respond with metrics for prometheus."; + }; + }; + + http = { + enable = mkOption { + type = types.bool; + default = true; + description = mdDoc "Enable the HTTP RPC server"; + }; + + address = mkOption { + type = types.str; + default = "127.0.0.1"; + description = mdDoc "Host on which the RPC server should listen."; + }; + + port = mkOption { + type = types.port; + default = 5052; + description = mdDoc "RPC port exposed by a beacon node."; + }; + }; + + disable-upnp = mkOption { + type = types.bool; + default = true; + description = mdDoc "Disable the UPnP configuration"; + }; + + datadir = mkOption { + type = types.nullOr types.str; + default = null; + description = mdDoc "Data directory for the databases."; + }; + + user = mkOption { + type = types.nullOr types.str; + default = null; + description = mdDoc "User to run the systemd service."; + }; +} diff --git a/modules/lighthouse-beacon/default.nix b/modules/lighthouse-beacon/default.nix new file mode 100644 index 00000000..9f7c0ea5 --- /dev/null +++ b/modules/lighthouse-beacon/default.nix @@ -0,0 +1,130 @@ +{ + config, + lib, + pkgs, + ... +}: let + modulesLib = import ../lib.nix lib; + + inherit (lib.lists) optionals findFirst; + inherit (lib.strings) hasPrefix; + inherit (lib.attrsets) zipAttrsWith; + inherit + (lib) + concatStringsSep + filterAttrs + flatten + mapAttrs' + mapAttrsToList + mkIf + mkMerge + nameValuePair + ; + inherit (modulesLib) mkArgs baseServiceConfig; + + eachBeacon = config.services.ethereum.lighthouse-beacon; +in { + ###### interface + inherit (import ./options.nix {inherit lib pkgs;}) options; + + ###### implementation + + config = mkIf (eachBeacon != {}) { + # configure the firewall for each service + networking.firewall = let + openFirewall = filterAttrs (_: cfg: cfg.openFirewall) eachBeacon; + perService = + mapAttrsToList + ( + _: cfg: + with cfg.args; { + allowedUDPPorts = + [discovery-port] + ++ (optionals (!disable-quic) [quic-port]); + allowedTCPPorts = + (optionals disable-quic [quic-port]) + ++ (optionals http.enable [http.port]) + ++ (optionals metrics.enable [metrics.port]); + } + ) + openFirewall; + in + zipAttrsWith (_name: flatten) perService; + + systemd.services = + mapAttrs' + ( + beaconName: let + user = "lighthouse-${beaconName}"; + serviceName = "lighthouse-beacon-${beaconName}"; + in + cfg: let + scriptArgs = let + args = mkArgs { + opts = import ./args.nix { + inherit lib; + name = beaconName; + config = cfg; + }; + inherit (cfg) args; + }; + + # filter out certain args which need to be treated differently + specialArgs = [ + "--execution-jwt" + "--datadir" + "--http-enable" + "--http-address" + "--http-port" + "--metrics-enable" + "--metrics-address" + "--metrics-port" + "--user" + ]; + isNormalArg = name: (findFirst (arg: hasPrefix arg name) null specialArgs) == null; + filteredArgs = + (builtins.filter isNormalArg args) + ++ (optionals cfg.args.http.enable ["--http" "--http-address=${cfg.args.http.address}" "--http-port=${toString cfg.args.http.port}"]) + ++ (optionals cfg.args.metrics.enable ["--metrics" "--metrics-address=${cfg.args.metrics.address}" "--metrics-port=${toString cfg.args.metrics.port}"]); + + jwtSecret = + if cfg.args.execution-jwt != null + then "--execution-jwt %d/execution-jwt" + else ""; + + datadir = + if cfg.args.datadir != null + then "--datadir ${cfg.args.datadir}" + else "--datadir %S/${user}"; + in '' + ${jwtSecret} \ + ${datadir} \ + ${concatStringsSep " \\\n" filteredArgs} \ + ${lib.escapeShellArgs cfg.extraArgs} + ''; + in + nameValuePair serviceName (mkIf cfg.enable { + after = ["network.target"]; + wantedBy = ["multi-user.target"]; + description = "Lighthouse Beacon Node (${beaconName})"; + + # create service config by merging with the base config + serviceConfig = mkMerge [ + baseServiceConfig + { + User = + if cfg.args.user != null + then cfg.args.user + else user; + StateDirectory = user; + ExecStart = "${cfg.package}/bin/lighthouse beacon ${scriptArgs}"; + } + (mkIf (cfg.args.execution-jwt != null) { + LoadCredential = ["execution-jwt:${cfg.args.execution-jwt}"]; + }) + ]; + }) + ) + eachBeacon; + }; +} diff --git a/modules/lighthouse-beacon/options.nix b/modules/lighthouse-beacon/options.nix new file mode 100644 index 00000000..d85efbd6 --- /dev/null +++ b/modules/lighthouse-beacon/options.nix @@ -0,0 +1,56 @@ +{ + lib, + pkgs, + ... +}: let + beaconOpts = with lib; + { + name, + config, + ... + }: { + options = { + enable = mkEnableOption (mdDoc "Lighthouse Ethereum Beacon Chain Node written in Rust from Sigma Prime"); + + args = import ./args.nix {inherit lib name config;}; + + extraArgs = mkOption { + type = types.listOf types.str; + description = mdDoc "Additional arguments to pass to Lighthouse Beacon Chain."; + default = []; + }; + + package = mkOption { + default = pkgs.lighthouse; + defaultText = literalExpression "pkgs.lighthouse"; + type = types.package; + description = mdDoc "Package to use for Lighthouse binary"; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc "Open ports in the firewall for any enabled networking services"; + }; + + # mixin backup options + backup = let + inherit (import ../backup/lib.nix lib) options; + in + options; + + # mixin restore options + restore = let + inherit (import ../restore/lib.nix lib) options; + in + options; + }; + }; +in { + options.services.ethereum.lighthouse-beacon = with lib; + mkOption { + type = types.attrsOf (types.submodule beaconOpts); + default = {}; + description = mdDoc "Specification of one or more lighthouse beacon chain instances."; + }; +} diff --git a/modules/lighthouse-validator/args.nix b/modules/lighthouse-validator/args.nix new file mode 100644 index 00000000..15458a28 --- /dev/null +++ b/modules/lighthouse-validator/args.nix @@ -0,0 +1,92 @@ +{ + lib, + name, + ... +}: +with lib; { + network = mkOption { + type = types.nullOr (types.enum ["mainnet" "prater" "goerli" "gnosis" "chiado" "sepolia" "holesky"]); + default = name; + defaultText = "name"; + description = mdDoc "The network to connect to. Mainnet is the default ethereum network."; + }; + + metrics = { + enable = mkOption { + type = types.bool; + default = true; + description = mdDoc "Enable Prometheus metrics exporter."; + }; + + address = mkOption { + type = types.str; + default = "127.0.0.1"; + description = mdDoc "Host used to listen and respond with metrics for prometheus."; + }; + + port = mkOption { + type = types.port; + default = 5064; + description = mdDoc "Port used to listen and respond with metrics for prometheus."; + }; + }; + + http = { + enable = mkEnableOption (mdDoc "the HTTP REST API server"); + + address = mkOption { + type = types.str; + default = "127.0.0.1"; + description = mdDoc "Host on which the REST API server should listen."; + }; + + port = mkOption { + type = types.port; + default = 5062; + description = mdDoc "REST API port exposed by a beacon node."; + }; + }; + + datadir = mkOption { + type = types.nullOr types.str; + default = null; + description = mdDoc "Data directory for the databases."; + }; + + suggested-fee-recipient = mkOption { + type = types.nullOr types.str; + default = null; + description = mdDoc '' + Sets ALL validators' mapping to a suggested eth + address to receive gas fees when proposing a block. note + that this is only a suggestion when integrating with a Builder API, + which may choose to specify a different fee recipient as payment + for the blocks it builds. + ''; + }; + + beacon-nodes = mkOption { + type = with types; nullOr (listOf str); + default = null; + description = mdDoc '' + List of Lighthouse Beacon node HTTP APIs to connect to. + + When null, looks up the http address+port from the lighthouse + beacon node service with the same name. + (eg `services.ethereum.lighthouse-validator.holesky` will look + at the config of `services.ethereum.lighthouse-beacon.holesky`) + ''; + }; + + graffiti = mkOption { + type = types.nullOr types.str; + default = null; + description = mdDoc "String to include in proposed blocks"; + }; + + user = mkOption { + type = types.nullOr types.str; + default = null; + description = mdDoc "User to run the systemd service."; + }; +} diff --git a/modules/lighthouse-validator/default.nix b/modules/lighthouse-validator/default.nix new file mode 100644 index 00000000..2bfb4d80 --- /dev/null +++ b/modules/lighthouse-validator/default.nix @@ -0,0 +1,140 @@ +{ + config, + lib, + pkgs, + ... +}: let + modulesLib = import ../lib.nix lib; + + inherit (lib.lists) optionals findFirst; + inherit (lib.strings) hasPrefix; + inherit (lib.attrsets) hasAttr zipAttrsWith; + inherit + (lib) + concatStringsSep + filterAttrs + flatten + length + mapAttrs' + mapAttrsToList + mkIf + mkMerge + nameValuePair + ; + inherit (modulesLib) mkArgs baseServiceConfig; + + eachValidator = config.services.ethereum.lighthouse-validator; +in { + ###### interface + inherit (import ./options.nix {inherit lib pkgs;}) options; + + ###### implementation + + config = mkIf (eachValidator != {}) { + # configure the firewall for each service + networking.firewall = let + openFirewall = filterAttrs (_: cfg: cfg.openFirewall) eachValidator; + perService = + mapAttrsToList + ( + _: cfg: + with cfg.args; { + allowedTCPPorts = + (optionals http.enable [http.port]) + ++ (optionals metrics.enable [metrics.port]); + } + ) + openFirewall; + in + zipAttrsWith (_name: flatten) perService; + + systemd.services = + mapAttrs' + ( + name: let + user = "lighthouse-${name}"; + serviceName = "lighthouse-validator-${name}"; + in + cfg: let + scriptArgs = let + # generate args + args = let + opts = import ./args.nix {inherit name lib;}; + in + mkArgs { + inherit opts; + inherit (cfg) args; + }; + + # filter out certain args which need to be treated differently + specialArgs = [ + "--datadir" + "--http-enable" + "--http-address" + "--http-port" + "--metrics-enable" + "--metrics-address" + "--metrics-port" + "--beacon-nodes" + "--user" + ]; + isNormalArg = name: (findFirst (arg: hasPrefix arg name) null specialArgs) == null; + filteredArgs = + (builtins.filter isNormalArg args) + ++ (optionals cfg.args.http.enable ["--http" "--http-address=${cfg.args.http.address}" "--http-port=${toString cfg.args.http.port}"]) + ++ (optionals cfg.args.metrics.enable ["--metrics" "--metrics-address=${cfg.args.metrics.address}" "--metrics-port=${toString cfg.args.metrics.port}"]); + + datadir = + if cfg.args.datadir != null + then "--datadir ${cfg.args.datadir}" + else "--datadir %S/${user}"; + + beaconNodes = + if (cfg.args.beacon-nodes != null) && (length cfg.args.beacon-nodes == 0) + then "--beacon-nodes ${concatStringsSep "," cfg.args.beacon-nodes}" + else let + beaconCfg = config.services.ethereum.lighthouse-beacon.${name}; + beaconUrl = "http://${beaconCfg.args.http.address}:${toString beaconCfg.args.http.port}"; + in "--beacon-nodes ${beaconUrl}"; + in '' + ${datadir} \ + ${beaconNodes} \ + ${concatStringsSep " \\\n" filteredArgs} \ + ${lib.escapeShellArgs cfg.extraArgs} + ''; + in + nameValuePair serviceName (mkIf cfg.enable { + after = ["network.target"]; + wantedBy = ["multi-user.target"]; + description = "Lighthouse Validator Client (${name})"; + + serviceConfig = mkMerge [ + baseServiceConfig + { + User = + if cfg.args.user != null + then cfg.args.user + else user; + StateDirectory = user; + ExecStart = "${cfg.package}/bin/lighthouse validator ${scriptArgs}"; + } + ]; + }) + ) + eachValidator; + + assertions = + mapAttrsToList + ( + name: cfg: { + assertion = + !cfg.enable || (cfg.args.beacon-nodes != null) || (hasAttr name config.services.ethereum.lighthouse-beacon); + message = '' + Lighthouse Validator ${name} could not find a matching beacon. + Either set `services.ethereum.lighthouse-beacon.${name}` or `services.ethereum.lighthouse-validator.${name}.args.beacon-nodes` + ''; + } + ) + eachValidator; + }; +} diff --git a/modules/lighthouse-validator/options.nix b/modules/lighthouse-validator/options.nix new file mode 100644 index 00000000..ea7a295d --- /dev/null +++ b/modules/lighthouse-validator/options.nix @@ -0,0 +1,53 @@ +{ + lib, + pkgs, + ... +}: +with lib; let + validatorOpts = {name, ...}: { + options = { + enable = mkEnableOption (mdDoc "Lighthouse Ethereum Validator Client written in Rust from Sigma Prime"); + + args = import ./args.nix {inherit lib name;}; + + extraArgs = mkOption { + type = types.listOf types.str; + description = mdDoc "Additional arguments to pass to Lighthouse Validator Client."; + default = []; + }; + + package = mkOption { + default = pkgs.lighthouse; + defaultText = literalExpression "pkgs.lighthouse"; + type = types.package; + description = mdDoc "Package to use for Lighthouse binary"; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc "Open ports in the firewall for any enabled networking services"; + }; + + # mixin backup options + backup = let + inherit (import ../backup/lib.nix lib) options; + in + options; + + # mixin restore options + restore = let + inherit (import ../restore/lib.nix lib) options; + in + options; + }; + }; +in { + options.services.ethereum.lighthouse-validator = mkOption { + type = types.attrsOf (types.submodule validatorOpts); + default = {}; + description = mdDoc '' + Specification of one or more Lighthouse validator instances. + ''; + }; +}