diff --git a/nixos/doc/manual/from_md/release-notes/rl-2305.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2305.section.xml
index 61daeac86a64d50..4ad006b0b5b13a3 100644
--- a/nixos/doc/manual/from_md/release-notes/rl-2305.section.xml
+++ b/nixos/doc/manual/from_md/release-notes/rl-2305.section.xml
@@ -37,6 +37,13 @@
programs.fzf.
+
+
+ nimbus-beacon-node,
+ an Ethereum consensus layer client available as
+ services.nimbus-beacon-node.
+
+
diff --git a/nixos/doc/manual/release-notes/rl-2305.section.md b/nixos/doc/manual/release-notes/rl-2305.section.md
index f9c76b02f891c63..3f29e66dae6d2ee 100644
--- a/nixos/doc/manual/release-notes/rl-2305.section.md
+++ b/nixos/doc/manual/release-notes/rl-2305.section.md
@@ -18,6 +18,8 @@ In addition to numerous new and upgraded packages, this release has the followin
- [fzf](https://github.com/junegunn/fzf), a command line fuzzyfinder. Available as [programs.fzf](#opt-programs.fzf.fuzzyCompletion).
+- [nimbus-beacon-node](https://nimbus.guide/), an Ethereum consensus layer client available as [services.nimbus-beacon-node](options.html#opt-services.nimbus-beacon-node.enable).
+
## Backward Incompatibilities {#sec-release-23.05-incompatibilities}
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 24dd30e1575018e..0724004d685e25b 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -329,6 +329,7 @@
./services/blockchain/ethereum/geth.nix
./services/blockchain/ethereum/erigon.nix
./services/blockchain/ethereum/lighthouse.nix
+ ./services/blockchain/nimbus/beacon-node.nix
./services/backup/zrepl.nix
./services/cluster/corosync/default.nix
./services/cluster/hadoop/default.nix
diff --git a/nixos/modules/services/blockchain/nimbus/beacon-node.md b/nixos/modules/services/blockchain/nimbus/beacon-node.md
new file mode 100644
index 000000000000000..5371b5909a81d56
--- /dev/null
+++ b/nixos/modules/services/blockchain/nimbus/beacon-node.md
@@ -0,0 +1,64 @@
+# Nimbus Beacon Node {#module-services-nimbus-beacon-node}
+
+# Quick Start {#module-services-nimbus-beacon-node-quick-start}
+Nimbus is an extremely efficient consensus layer client implementation.
+While it's optimised for embedded systems and resource-restricted devices --
+including Raspberry Pis, its low resource usage also makes it an excellent choice
+for any server or desktop (where it simply takes up fewer resources).
+
+In order for the [Consensus Layer(CL) client](https://ethereum.org/en/developers/docs/nodes-and-clients/#consensus-clients)
+to function it requires access to an [Execution Layer(EL) client](https://ethereum.org/en/developers/docs/nodes-and-clients/#execution-clients)
+listening for AuthRPC requests on `http://localhost:8551/`.
+An example configuration using Geth as the EL client would look like this:
+
+```nix
+# Execution layer client
+services.geth.mainnet = {
+ enable = true;
+ authrpc = {
+ enable = true;
+ port = 8551;
+ vhosts = ["localhost"];
+ jwtsecret = "/var/run/geth/jwtsecret";
+ };
+};
+
+# Consensus layer client
+services.nimbus-beacon-node = {
+ enable = true;
+ web3Urls = ["http://localhost:${toString config.services.geth.mainnet.authrpc.port}"];
+ jwtSecret = "/var/run/geth/jwtsecret";
+ rest.enable = true;
+};
+```
+::: {.warning}
+This assumes you have a `/var/run/geth/jwtsecret` containing a 64 byte secret.
+You can create such a secret using `openssl rand -hex 32 | tr -d "\n"`.
+:::
+
+This should allow the CL node to communicate with EL node via Auth RPC.
+
+Keep in mind that the JWT secret needs to be the same and readable for both nodes.
+If both services run on the same host and listen on `localhost` it's just a formality.
+
+You can check if the node is working using the REST API:
+
+{command}`curl -s localhost:5052/eth/v1/node/version`
+```json
+{"data":{"version":"Nimbus/v22.9.1-ad286b-stateofus"}}
+```
+{command}`curl -s localhost:5052/eth/v1/node/syncing`
+```json
+{"data":{"head_slot":"45102","sync_distance":"4744542","is_syncing":true,"is_optimistic":false}}
+```
+
+# Configuration {#module-services-nimbus-beacon-node-configuration}
+
+Other settings worth looking at include:
+
+* {option}`services.nimbus-beacon-node.suggestedFeeRecipient` - Your address for receiving [transaction fees](https://nimbus.guide/suggested-fee-recipient.html).
+* {option}`services.nimbus-beacon-node.doppelganger` - Miss two epochs to avoid [validator slashing](https://ethereum.org/en/developers/docs/consensus-mechanisms/pos/rewards-and-penalties/#slashing).
+* {option}`services.nimbus-beacon-node.metrics.enable` - Prometheus metrics endpoint listening on `9100` by default.
+* {option}`services.nimbus-beacon-node.extraArgs` - Ability to provide flags not defined in the service.
+
+For documentation see:
diff --git a/nixos/modules/services/blockchain/nimbus/beacon-node.nix b/nixos/modules/services/blockchain/nimbus/beacon-node.nix
new file mode 100644
index 000000000000000..b883acb0f1e2a80
--- /dev/null
+++ b/nixos/modules/services/blockchain/nimbus/beacon-node.nix
@@ -0,0 +1,236 @@
+{ config, lib, pkgs, ... }:
+
+let
+ inherit (lib)
+ escapeShellArg mdDoc literalMD concatStringsSep mkMerge
+ toUpper boolToString length forEach optionals optionalAttrs
+ types mkEnableOption mkOption mkIf literalExpression;
+
+ cfg = config.services.nimbus-beacon-node;
+in {
+ options = {
+ services = {
+ nimbus-beacon-node = {
+ enable = mkEnableOption (mdDoc "Nimbus Beacon Node service");
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.nimbus;
+ defaultText = literalExpression "pkgs.nimbus";
+ description = mdDoc "Package to use as Go Ethereum node.";
+ };
+
+ service = {
+ user = mkOption {
+ type = types.str;
+ default = "nimbus";
+ description = mdDoc "Username for Nimbus beacon node service.";
+ };
+
+ group = mkOption {
+ type = types.str;
+ default = "nimbus";
+ description = mdDoc "Group name for Nimbus beacon node service.";
+ };
+ };
+
+ dataDir = mkOption {
+ type = types.path;
+ default = "/var/lib/nimbus-beacon-node";
+ description = mdDoc "Directory for Nimbus beacon node blockchain data.";
+ };
+
+ network = mkOption {
+ type = types.str;
+ default = "mainnet";
+ description = mdDoc "Name of beacon node network to connect to.";
+ };
+
+ log = {
+ level = mkOption {
+ type = types.enum ["trace" "debug" "info" "notice" "warn" "error" "fatal" "none"];
+ default = "info";
+ example = "debug";
+ description = mdDoc "Logging level.";
+ };
+
+ format = mkOption {
+ type = types.enum ["auto" "colors" "nocolors" "json"];
+ default = "auto";
+ example = "json";
+ description = mdDoc "Logging formatting.";
+ };
+ };
+
+ web3Urls = mkOption {
+ type = types.listOf types.str;
+ default = [];
+ example = ["http://localhost:8551/"];
+ description = mdDoc "Mandatory URL(s) for the Web3 RPC endpoints.";
+ };
+
+ jwtSecret = mkOption {
+ type = types.path;
+ default = null;
+ example = "/var/run/nimbus/jwtsecret";
+ description = mdDoc ''
+ Path of file with 32 bytes long JWT secret for Auth RPC endpoint.
+ Can be generated using 'openssl rand -hex 32'.
+ '';
+ };
+
+ subAllSubnets = mkOption {
+ type = types.bool;
+ default = false;
+ description = mdDoc "Subscribe to all attestation subnet topics.";
+ };
+
+ doppelganger = mkOption {
+ type = types.bool;
+ default = true;
+ description = mdDoc ''
+ Protection against slashing due to double-voting.
+ Means you will miss two attestations when restarting.
+ '';
+ };
+
+ suggestedFeeRecipient = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = mdDoc ''
+ Wallet address where transaction fee tips - priority fees,
+ unburnt portion of gas fees - will be sent.
+ '';
+ };
+
+ listenPort = mkOption {
+ type = types.port;
+ default = 9000;
+ description = mdDoc "Listen port for libp2p protocol.";
+ };
+
+ discoverPort = mkOption {
+ type = types.port;
+ default = 9000;
+ description = mdDoc "Listen port for libp2p protocol.";
+ };
+
+ nat = mkOption {
+ type = types.str;
+ default = "any";
+ example = "extip:12.34.56.78";
+ description = literalMD ''
+ Method for determining public address. (any, none, upnp, pmp, extip:)
+ '';
+ };
+
+ metrics = {
+ enable = lib.mkEnableOption (mdDoc "Nimbus beacon node metrics endpoint");
+ address = mkOption {
+ type = types.str;
+ default = "127.0.0.1";
+ description = mdDoc "Metrics address for beacon node.";
+ };
+ port = mkOption {
+ type = types.port;
+ default = 9100;
+ description = mdDoc "Metrics port for beacon node.";
+ };
+ };
+
+ rest = {
+ enable = lib.mkEnableOption (mdDoc "Nimbus beacon node REST API");
+ address = mkOption {
+ type = types.str;
+ default = "127.0.0.1";
+ description = mdDoc "Listening address of the REST API server.";
+ };
+
+ port = mkOption {
+ type = types.port;
+ default = 5052;
+ description = mdDoc "Port for the REST API server.";
+ };
+ };
+
+ extraArgs = mkOption {
+ type = types.listOf types.str;
+ default = [];
+ example = ["--num-threads=1" "--graffiti=1337_h4x0r"];
+ description = mdDoc "Additional arguments passed to node.";
+ };
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ assertions = [
+ { assertion = length cfg.web3Urls > 0;
+ message = "Nimbus beacon node requires at least one Web3 URL in services.nimbus-beacon-node.web3Urls to work!"; }
+ ];
+
+ users.users = optionalAttrs (cfg.service.user == "nimbus") {
+ nimbus = {
+ group = cfg.service.group;
+ home = cfg.dataDir;
+ description = "Nimbus beacon node service user";
+ isSystemUser = true;
+ };
+ };
+
+ users.groups = optionalAttrs (cfg.service.user == "nimbus") {
+ nimbus = { };
+ };
+
+ systemd.services.nimbus-beacon-node = {
+ description = "Nimbus Beacon Node (Ethereum consensus client)";
+
+ serviceConfig = mkMerge [{
+ User = cfg.service.user;
+ Group = cfg.service.group;
+
+ ExecStart = concatStringsSep " \\\n " ([
+ "${cfg.package}/bin/nimbus_beacon_node"
+ "--network=${cfg.network}"
+ "--data-dir=${cfg.dataDir}"
+ "--log-level=${toUpper cfg.log.level}"
+ "--log-format=${cfg.log.format}"
+ "--nat=${cfg.nat}"
+ "--tcp-port=${toString cfg.listenPort}"
+ "--udp-port=${toString cfg.discoverPort}"
+ "--subscribe-all-subnets=${boolToString cfg.subAllSubnets}"
+ "--doppelganger-detection=${boolToString cfg.doppelganger}"
+ "--rest=${boolToString cfg.rest.enable}"
+ ] ++ optionals cfg.rest.enable [
+ "--rest-address=${cfg.rest.address}"
+ "--rest-port=${toString cfg.rest.port}"
+ ] ++ [
+ "--metrics=${boolToString cfg.metrics.enable}"
+ ] ++ optionals cfg.metrics.enable [
+ "--metrics-address=${cfg.metrics.address}"
+ "--metrics-port=${toString cfg.metrics.port}"
+ ] ++ (forEach cfg.web3Urls (u: "--web3-url=${u}"))
+ ++ optionals (cfg.jwtSecret != null) ["--jwt-secret=${cfg.jwtSecret}"]
+ ++ optionals (cfg.suggestedFeeRecipient != null) ["--suggested-fee-recipient=${cfg.suggestedFeeRecipient}"]
+ ++ (forEach cfg.extraArgs escapeShellArg));
+
+ WorkingDirectory = cfg.dataDir;
+ TimeoutSec = 1200;
+ Restart = "on-failure";
+ # Don't restart when Doppelganger detection has been activated
+ RestartPreventExitStatus = 129;
+ }
+ (mkIf (cfg.dataDir == "/var/lib/nimbus-beacon-node") {
+ StateDirectory = "nimbus-beacon-node";
+ StateDirectoryMode = "0750";
+ })];
+ wantedBy = [ "multi-user.target" ];
+ requires = [ "network.target" ];
+ };
+ };
+
+ meta = {
+ doc = ./beacon-node.xml;
+ maintainers = with lib.maintainers; [ jakubgs ];
+ };
+}
diff --git a/nixos/modules/services/blockchain/nimbus/beacon-node.xml b/nixos/modules/services/blockchain/nimbus/beacon-node.xml
new file mode 100644
index 000000000000000..cafa032932c299c
--- /dev/null
+++ b/nixos/modules/services/blockchain/nimbus/beacon-node.xml
@@ -0,0 +1,98 @@
+
+ Nimbus Beacon Node
+
+ Source:
+ modules/services/blockchain/nimbus/beacon-node.nix
+
+
+ Upstream documentation:
+
+
+
+ Quickstart
+
+ Nimbus is an extremely efficient consensus layer client implementation.
+ While it's optimised for embedded systems and resource-restricted devices --
+ including Raspberry Pis, its low resource usage also makes it an excellent choice
+ for any server or desktop (where it simply takes up fewer resources).
+
+
+ In order for the Consensus Layer(CL) node to function it requires access to an
+ Execution Layer(EL) client listening for AuthRPC requests on http://localhost:8551/.
+ An example configuration using Geth as the EL client would look like this:
+
+ # Execution layer client
+ = {
+ mainnet = {
+ enable = true;
+ authrpc = {
+ enable = true;
+ port = 8551;
+ vhosts = ["localhost"];
+ jwtsecret = "/var/run/geth/jwtsecret";
+ };
+ };
+ };
+
+ # Consensus layer client
+ services.nimbus-beacon-node = {
+ enable = true;
+ web3Urls = ["http://localhost:${toString config.services.geth.mainnet.authrpc.port}"];
+ jwtSecret = "/var/run/geth/jwtsecret";
+ rest.enable = true;
+ };
+
+
+
+ This assumes you have a /var/run/geth/jwtsecret containing a 32 byte secret.
+ You can create such a secret using openssl rand -hex 32 | tr -d "\n".
+
+
+ This should allow the CL node to communicate with EL noded via Auth RPC.
+
+ Keep in mind that the JWT secret needs to be the same and readable for both nodes.
+ If both services run on the same host and listen on localhost it's just a formality.
+
+
+ You can check if the node is working using the REST API:
+
+ $ curl -s localhost:5052/eth/v1/node/version
+ {"data":{"version":"Nimbus/v22.9.1-ad286b-stateofus"}}
+ $ curl -s localhost:5052/eth/v1/node/syncing
+ {"data":{"head_slot":"45102","sync_distance":"4744542","is_syncing":true,"is_optimistic":false}}
+
+
+
+
+ Configuration
+
+ Other settings worth looking at include:
+
+
+
+
+ suggestedFeeRecipient - Your address for receiving transaction fees.
+
+
+
+
+ doppelganger - Miss two epochs to avoid validator slashing.
+
+
+
+
+ metrics.enable - Prometheus metrics endpoint listening on 9100 by default.
+
+
+
+
+ extraArgs - Ability to provide flags not defined in the service.
+
+
+
+
+