Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Reth service module #531

Merged
merged 3 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.";
};
}