Skip to content

Commit

Permalink
Implement Reth service module (#531)
Browse files Browse the repository at this point in the history
* Implement Reth service module

* Remove mdDoc

* Fix formatting
  • Loading branch information
scottbot95 committed Jul 19, 2024
1 parent 72d993c commit d3d8046
Show file tree
Hide file tree
Showing 4 changed files with 350 additions and 0 deletions.
1 change: 1 addition & 0 deletions modules/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
./prysm-beacon
./prysm-validator
./restore
./reth
];
};
}
166 changes: 166 additions & 0 deletions modules/reth/args.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
lib:
with lib; {
datadir = mkOption {
type = types.nullOr types.str;
default = null;
description = "Data directory for Reth. Defaults to '%S/reth-\<name\>', which generally resolves to /var/lib/reth-\<name\>.";
};

port = mkOption {
type = types.port;
default = 30303;
description = "Network listening port.";
};

chain = mkOption {
type = types.enum [
"mainnet"
"sepolia"
"holesky"
"dev"
];
default = "mainnet";
description = "Name of the network to join. If null the network is mainnet.";
};

full = mkOption {
type = types.bool;
default = false;
description = "Run full node. Only the most recent [`MINIMUM_PRUNING_DISTANCE`] block states are stored. This flag takes priority over pruning configuration in reth.toml";
};

http = {
enable = mkOption {
type = types.bool;
default = true;
description = "Enable HTTP-RPC server";
};

addr = mkOption {
type = types.str;
default = "127.0.0.1";
description = "HTTP-RPC server listening interface.";
};

port = mkOption {
type = types.port;
default = 8545;
description = "HTTP-RPC server listening port.";
};

corsdomain = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
description = "List of domains from which to accept cross origin requests.";
example = ["*"];
};

api = mkOption {
type = types.nullOr (types.listOf types.str);
description = "API's offered over the HTTP-RPC interface.";
example = ["net" "eth"];
};
};

ws = {
enable = mkEnableOption "Reth WebSocket API";
addr = mkOption {
type = types.nullOr types.str;
default = null;
description = "WS server listening interface.";
example = "127.0.0.1";
};

port = mkOption {
type = types.nullOr types.port;
default = null;
description = "WS server listening port.";
example = 8545;
};

origins = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
description = "List of origins from which to accept `WebSocket` requests";
};

api = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
description = "API's offered over the WS interface.";
example = ["net" "eth"];
};
};

authrpc = {
addr = mkOption {
type = types.str;
default = "127.0.0.1";
description = "HTTP-RPC server listening interface for the Engine API.";
};

port = mkOption {
type = types.port;
default = 8551;
description = "HTTP-RPC server listening port for the Engine API";
};

jwtsecret = mkOption {
type = types.nullOr types.str;
default = null;
description = "Path to the token that ensures safe connection between CL and EL.";
example = "/var/run/reth/jwtsecret";
};
};

metrics = {
enable = mkEnableOption "Enable Prometheus metrics collection and reporting.";

addr = mkOption {
type = types.str;
default = "127.0.0.1";
description = "Enable stand-alone metrics HTTP server listening interface.";
};

port = mkOption {
type = types.port;
default = 6060;
description = "Metrics HTTP server listening port";
};
};

log = let
mkFormatOpt = channel:
mkOption {
type = types.nullOr (types.enum ["terminal" "log-fmt" "json"]);
default = null;
description = "The format to use for logs written to ${channel}.";
example = "log-fmt";
};
mkFilterOpt = channel:
mkOption {
type = types.nullOr types.str;
default = null;
description = "The filter to use for logs written to ${channel}.";
example = "info";
};
in {
stdout = {
format = mkFormatOpt "stdout";
filter = mkFilterOpt "stdout";
};
file = {
format = mkFormatOpt "the log file";
filter = mkFilterOpt "the log file";
directory = mkOption {
type = types.nullOr types.str;
default = null;
description = "The path to put log files in";
example = "/var/log/reth";
};
};
journald = {
filter = mkFilterOpt "journald";
};
};
}
140 changes: 140 additions & 0 deletions modules/reth/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
{
config,
lib,
pkgs,
...
}: let
inherit (lib.lists) optionals findFirst;
inherit (lib.strings) hasPrefix;
inherit
(lib)
concatStringsSep
filterAttrs
flatten
mapAttrs'
mapAttrsToList
mkIf
mkMerge
nameValuePair
zipAttrsWith
;

modulesLib = import ../lib.nix lib;
inherit (modulesLib) baseServiceConfig mkArgs dotPathReducer;

eachNode = config.services.ethereum.reth;
in {
# Disable the service definition currently in nixpkgs
disabledModules = ["services/blockchain/ethereum/reth.nix"];

###### interface
inherit (import ./options.nix {inherit lib pkgs;}) options;

###### implementation

config = mkIf (eachNode != {}) {
# configure the firewall for each service
networking.firewall = let
openFirewall = filterAttrs (_: cfg: cfg.openFirewall) eachNode;
perService =
mapAttrsToList
(
_: cfg:
with cfg.args; {
allowedTCPPorts =
[port authrpc.port]
++ (optionals http.enable [http.port])
++ (optionals ws.enable [ws.port])
++ (optionals metrics.enable [metrics.port]);
}
)
openFirewall;
in
zipAttrsWith (_name: flatten) perService;

# configure systemd to create the state directory with a subvolume
systemd.tmpfiles.rules =
map
(name: "v /var/lib/private/reth-${name}")
(builtins.attrNames (filterAttrs (_: v: v.subVolume) eachNode));

# create a service for each instance
systemd.services =
mapAttrs'
(
rethName: let
serviceName = "reth-${rethName}";
in
cfg: let
scriptArgs = let
args = mkArgs {
opts = import ./args.nix lib;
pathReducer = dotPathReducer;
args = builtins.removeAttrs cfg.args ["ws"];
};

wsArgs = mkArgs {
opts = import ./args.nix lib;
args = {
ws = builtins.removeAttrs cfg.args.ws ["enable"];
};
};

# filter out certain args which need to be treated differently
specialArgs = [
"--authrpc.jwtsecret"
"--http.enable"
"--metrics.enable"
"--metrics.addr"
"--metrics.port"
];

isNormalArg = name: (findFirst (arg: hasPrefix arg name) null specialArgs) == null;
filteredArgs =
(builtins.filter isNormalArg args)
++ (optionals cfg.args.http.enable ["--http"])
++ (optionals cfg.args.ws.enable wsArgs)
++ (optionals cfg.args.metrics.enable ["--metrics" "${cfg.args.metrics.addr}:${toString cfg.args.metrics.port}"]);

jwtSecret =
if cfg.args.authrpc.jwtsecret != null
then "--authrpc.jwtsecret %d/jwtsecret"
else "";

datadir =
if cfg.args.datadir != null
then "${cfg.args.datadir}"
else "%S/${serviceName}";
in ''
--log.file.directory ${datadir}/logs \
--datadir ${datadir} \
${jwtSecret} \
${concatStringsSep " \\\n" filteredArgs} \
${lib.escapeShellArgs cfg.extraArgs}
'';
in
nameValuePair serviceName (mkIf cfg.enable {
description = "Reth Ethereum node (${rethName})";
wantedBy = ["multi-user.target"];
after = ["network.target"];

# create service config by merging with the base config
serviceConfig = mkMerge [
baseServiceConfig
{
User = serviceName;
StateDirectory = serviceName;
ExecStart = "${cfg.package}/bin/reth node ${scriptArgs}";

# Reth needs this system call for some reason
SystemCallFilter = ["@system-service" "~@privileged" "mincore"];
}
(mkIf (cfg.args.authrpc.jwtsecret != null) {
LoadCredential = ["jwtsecret:${cfg.args.authrpc.jwtsecret}"];
})
];
})
)
eachNode;
};
}
43 changes: 43 additions & 0 deletions modules/reth/options.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
lib,
pkgs,
...
}: let
args = import ./args.nix lib;

rethOpts = with lib; {
options = {
enable = mkEnableOption "Reth Ethereum Node.";

subVolume = mkEnableOption "Use a subvolume for the state directory if the underlying filesystem supports it e.g. btrfs";

inherit args;

extraArgs = mkOption {
type = types.listOf types.str;
description = "Additional arguments to pass to Reth.";
default = [];
};

package = mkOption {
type = types.package;
default = pkgs.reth;
defaultText = literalExpression "pkgs.reth";
description = "Package to use as Reth node.";
};

openFirewall = mkOption {
type = types.bool;
default = false;
description = lib."Open ports in the firewall for any enabled networking services";
};
};
};
in {
options.services.ethereum.reth = with lib;
mkOption {
type = types.attrsOf (types.submodule rethOpts);
default = {};
description = "Specification of one or more Reth instances.";
};
}

0 comments on commit d3d8046

Please sign in to comment.