485 lines
15 KiB
Nix
485 lines
15 KiB
Nix
{ lib, config, pkgs, ... }:
|
|
|
|
let
|
|
cfg = config.services.every-channel.ethereum;
|
|
|
|
mkNetworkSubmodule =
|
|
name: defaults:
|
|
{ ... }:
|
|
{
|
|
options = {
|
|
enable = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = "Whether to run the ${name} Ethereum execution and consensus pair.";
|
|
};
|
|
|
|
rootDir = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "${cfg.rootDir}/${name}";
|
|
description = "Persistent root directory for the ${name} node state.";
|
|
};
|
|
|
|
reth = {
|
|
httpPort = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = defaults.rethHttpPort;
|
|
description = "Local HTTP JSON-RPC port for the ${name} Reth node.";
|
|
};
|
|
|
|
wsPort = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = defaults.rethWsPort;
|
|
description = "Local WebSocket JSON-RPC port for the ${name} Reth node.";
|
|
};
|
|
|
|
authPort = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = defaults.rethAuthPort;
|
|
description = "Local Engine API port for the ${name} Reth node.";
|
|
};
|
|
|
|
p2pPort = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = defaults.rethP2pPort;
|
|
description = "RLPx/P2P TCP port for the ${name} Reth node.";
|
|
};
|
|
|
|
discoveryPort = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = defaults.rethDiscoveryPort;
|
|
description = "Discovery UDP port for the ${name} Reth node.";
|
|
};
|
|
|
|
metricsPort = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = defaults.rethMetricsPort;
|
|
description = "Prometheus port for the ${name} Reth node.";
|
|
};
|
|
};
|
|
|
|
lighthouse = {
|
|
httpPort = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = defaults.lighthouseHttpPort;
|
|
description = "Local Beacon API port for the ${name} Lighthouse node.";
|
|
};
|
|
|
|
p2pPort = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = defaults.lighthouseP2pPort;
|
|
description = "TCP libp2p port for the ${name} Lighthouse node.";
|
|
};
|
|
|
|
discoveryPort = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = defaults.lighthouseDiscoveryPort;
|
|
description = "UDP discovery port for the ${name} Lighthouse node.";
|
|
};
|
|
|
|
quicPort = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = defaults.lighthouseQuicPort;
|
|
description = "UDP QUIC port for the ${name} Lighthouse node.";
|
|
};
|
|
|
|
metricsPort = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = defaults.lighthouseMetricsPort;
|
|
description = "Prometheus port for the ${name} Lighthouse node.";
|
|
};
|
|
};
|
|
};
|
|
};
|
|
|
|
networks = {
|
|
mainnet = cfg.mainnet;
|
|
sepolia = cfg.sepolia;
|
|
};
|
|
|
|
enabledNetworks = lib.filterAttrs (_: networkCfg: networkCfg.enable) networks;
|
|
|
|
rethContainerName = network: "every-channel-ethereum-${network}-reth";
|
|
lighthouseContainerName = network: "every-channel-ethereum-${network}-lighthouse";
|
|
|
|
networkDatasetLines = lib.concatStringsSep "\n" (
|
|
lib.mapAttrsToList
|
|
(network: networkCfg: ''
|
|
ensure_dataset ${lib.escapeShellArg "${cfg.poolName}/${network}"}
|
|
ensure_dataset ${lib.escapeShellArg "${cfg.poolName}/${network}/reth"}
|
|
ensure_dataset ${lib.escapeShellArg "${cfg.poolName}/${network}/lighthouse"}
|
|
ensure_jwt ${lib.escapeShellArg "${networkCfg.rootDir}/jwt.hex"}
|
|
'')
|
|
enabledNetworks
|
|
);
|
|
|
|
mkNatArgs = lib.optionals (cfg.publicIp != null) [ "--nat" "extip:${cfg.publicIp}" ];
|
|
mkEnrArgs = lib.optionals (cfg.publicIp != null) [ "--enr-address" cfg.publicIp ];
|
|
|
|
mkRethContainer =
|
|
network: networkCfg: {
|
|
image = cfg.images.reth;
|
|
autoStart = true;
|
|
extraOptions = [ "--network=host" ];
|
|
volumes = [ "${networkCfg.rootDir}:/state" ];
|
|
cmd =
|
|
[
|
|
"node"
|
|
"--chain"
|
|
network
|
|
"--datadir"
|
|
"/state/reth"
|
|
"--full"
|
|
"--http"
|
|
"--http.addr"
|
|
"127.0.0.1"
|
|
"--http.port"
|
|
(toString networkCfg.reth.httpPort)
|
|
"--http.api"
|
|
"eth,net,web3,rpc"
|
|
"--ws"
|
|
"--ws.addr"
|
|
"127.0.0.1"
|
|
"--ws.port"
|
|
(toString networkCfg.reth.wsPort)
|
|
"--ws.api"
|
|
"eth,net,web3,rpc"
|
|
"--authrpc.addr"
|
|
"127.0.0.1"
|
|
"--authrpc.port"
|
|
(toString networkCfg.reth.authPort)
|
|
"--authrpc.jwtsecret"
|
|
"/state/jwt.hex"
|
|
"--port"
|
|
(toString networkCfg.reth.p2pPort)
|
|
"--discovery.port"
|
|
(toString networkCfg.reth.discoveryPort)
|
|
"--metrics"
|
|
"127.0.0.1:${toString networkCfg.reth.metricsPort}"
|
|
"--log.stdout.format"
|
|
"json"
|
|
]
|
|
++ mkNatArgs;
|
|
};
|
|
|
|
mkLighthouseContainer =
|
|
network: networkCfg: {
|
|
image = cfg.images.lighthouse;
|
|
autoStart = true;
|
|
extraOptions = [ "--network=host" ];
|
|
volumes = [ "${networkCfg.rootDir}:/state" ];
|
|
entrypoint = "/usr/local/bin/lighthouse";
|
|
cmd =
|
|
[
|
|
"beacon_node"
|
|
"--network"
|
|
network
|
|
"--datadir"
|
|
"/state/lighthouse"
|
|
"--http"
|
|
"--http-address"
|
|
"127.0.0.1"
|
|
"--http-port"
|
|
(toString networkCfg.lighthouse.httpPort)
|
|
"--execution-endpoint"
|
|
"http://127.0.0.1:${toString networkCfg.reth.authPort}"
|
|
"--execution-jwt"
|
|
"/state/jwt.hex"
|
|
"--allow-insecure-genesis-sync"
|
|
"--port"
|
|
(toString networkCfg.lighthouse.p2pPort)
|
|
"--discovery-port"
|
|
(toString networkCfg.lighthouse.discoveryPort)
|
|
"--quic-port"
|
|
(toString networkCfg.lighthouse.quicPort)
|
|
"--metrics"
|
|
"--metrics-address"
|
|
"127.0.0.1"
|
|
"--metrics-port"
|
|
(toString networkCfg.lighthouse.metricsPort)
|
|
]
|
|
++ mkEnrArgs;
|
|
};
|
|
|
|
caddyRootBody = ''
|
|
every.channel ethereum nodes
|
|
mainnet sync: /mainnet/sync
|
|
mainnet finality: /mainnet/finality
|
|
sepolia sync: /sepolia/sync
|
|
sepolia finality: /sepolia/finality
|
|
raw execution and beacon RPC remain local-only on ecp-forge for now.
|
|
'';
|
|
in
|
|
{
|
|
options.services.every-channel.ethereum = {
|
|
enable = lib.mkEnableOption "every.channel dual-network Ethereum full nodes";
|
|
|
|
poolName = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "eth";
|
|
description = "Dedicated ZFS pool name used for Ethereum node state.";
|
|
};
|
|
|
|
poolDevice = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Block device used to create the dedicated Ethereum ZFS pool if it does not already exist.";
|
|
};
|
|
|
|
rootDir = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "/eth";
|
|
description = "Mountpoint for the dedicated Ethereum ZFS pool.";
|
|
};
|
|
|
|
publicIp = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Public IP to advertise in Ethereum P2P metadata.";
|
|
};
|
|
|
|
publicHost = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Optional HTTPS host that publishes node sync and finality surfaces.";
|
|
};
|
|
|
|
images = {
|
|
reth = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "ghcr.io/paradigmxyz/reth:v1.9.3";
|
|
description = "Pinned Reth OCI image.";
|
|
};
|
|
|
|
lighthouse = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "docker.io/sigp/lighthouse:v8.1.1";
|
|
description = "Pinned Lighthouse OCI image.";
|
|
};
|
|
};
|
|
|
|
mainnet = lib.mkOption {
|
|
type = lib.types.submodule (mkNetworkSubmodule "mainnet" {
|
|
rethHttpPort = 8545;
|
|
rethWsPort = 8546;
|
|
rethAuthPort = 8551;
|
|
rethP2pPort = 30303;
|
|
rethDiscoveryPort = 30303;
|
|
rethMetricsPort = 19001;
|
|
lighthouseHttpPort = 5052;
|
|
lighthouseP2pPort = 9000;
|
|
lighthouseDiscoveryPort = 9000;
|
|
lighthouseQuicPort = 9001;
|
|
lighthouseMetricsPort = 5054;
|
|
});
|
|
default = { };
|
|
description = "Mainnet Ethereum node configuration.";
|
|
};
|
|
|
|
sepolia = lib.mkOption {
|
|
type = lib.types.submodule (mkNetworkSubmodule "sepolia" {
|
|
rethHttpPort = 18545;
|
|
rethWsPort = 18546;
|
|
rethAuthPort = 18551;
|
|
rethP2pPort = 31303;
|
|
rethDiscoveryPort = 31303;
|
|
rethMetricsPort = 29001;
|
|
lighthouseHttpPort = 15052;
|
|
lighthouseP2pPort = 19000;
|
|
lighthouseDiscoveryPort = 19000;
|
|
lighthouseQuicPort = 19001;
|
|
lighthouseMetricsPort = 15054;
|
|
});
|
|
default = { };
|
|
description = "Sepolia Ethereum node configuration.";
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
assertions = [
|
|
{
|
|
assertion = cfg.poolDevice != null;
|
|
message = "services.every-channel.ethereum.poolDevice must be set when the Ethereum node is enabled";
|
|
}
|
|
{
|
|
assertion = enabledNetworks != { };
|
|
message = "At least one Ethereum network must be enabled";
|
|
}
|
|
];
|
|
|
|
boot.zfs.extraPools = [ cfg.poolName ];
|
|
|
|
networking.firewall = {
|
|
allowedTCPPorts =
|
|
lib.flatten (
|
|
lib.mapAttrsToList
|
|
(_: networkCfg: [
|
|
networkCfg.reth.p2pPort
|
|
networkCfg.lighthouse.p2pPort
|
|
])
|
|
enabledNetworks
|
|
);
|
|
allowedUDPPorts =
|
|
lib.flatten (
|
|
lib.mapAttrsToList
|
|
(_: networkCfg: [
|
|
networkCfg.reth.discoveryPort
|
|
networkCfg.lighthouse.discoveryPort
|
|
networkCfg.lighthouse.quicPort
|
|
])
|
|
enabledNetworks
|
|
);
|
|
};
|
|
|
|
virtualisation.oci-containers.containers =
|
|
(lib.mapAttrs'
|
|
(network: networkCfg:
|
|
lib.nameValuePair (rethContainerName network) (mkRethContainer network networkCfg))
|
|
enabledNetworks)
|
|
// (lib.mapAttrs'
|
|
(network: networkCfg:
|
|
lib.nameValuePair (lighthouseContainerName network) (mkLighthouseContainer network networkCfg))
|
|
enabledNetworks);
|
|
|
|
systemd.services =
|
|
{
|
|
every-channel-ethereum-storage = {
|
|
description = "every.channel Ethereum NVMe ZFS pool and dataset bootstrap";
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [ "local-fs.target" "zfs.target" ];
|
|
wants = [ "zfs.target" ];
|
|
before =
|
|
lib.flatten (
|
|
lib.mapAttrsToList
|
|
(network: _: [
|
|
"podman-${rethContainerName network}.service"
|
|
"podman-${lighthouseContainerName network}.service"
|
|
])
|
|
enabledNetworks
|
|
);
|
|
path = with pkgs; [
|
|
coreutils
|
|
openssl
|
|
util-linux
|
|
zfs
|
|
];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
};
|
|
script = ''
|
|
set -euo pipefail
|
|
|
|
pool=${lib.escapeShellArg cfg.poolName}
|
|
root_dir=${lib.escapeShellArg cfg.rootDir}
|
|
device=${lib.escapeShellArg cfg.poolDevice}
|
|
|
|
ensure_dataset() {
|
|
local dataset="$1"
|
|
if ! zfs list -H "$dataset" >/dev/null 2>&1; then
|
|
zfs create -p "$dataset"
|
|
fi
|
|
zfs set atime=off compression=lz4 xattr=sa "$dataset" >/dev/null
|
|
}
|
|
|
|
ensure_jwt() {
|
|
local path="$1"
|
|
if [[ ! -s "$path" ]]; then
|
|
umask 077
|
|
openssl rand -hex 32 | tr -d '\n' > "$path"
|
|
printf '\n' >> "$path"
|
|
fi
|
|
chmod 0400 "$path"
|
|
}
|
|
|
|
if ! zpool list -H "$pool" >/dev/null 2>&1; then
|
|
if [[ -z "$device" ]]; then
|
|
echo "every-channel-ethereum-storage: missing poolDevice for pool $pool" >&2
|
|
exit 1
|
|
fi
|
|
if [[ ! -b "$device" ]]; then
|
|
echo "every-channel-ethereum-storage: device $device not present" >&2
|
|
exit 1
|
|
fi
|
|
if blkid "$device" >/dev/null 2>&1; then
|
|
echo "every-channel-ethereum-storage: device $device already has signatures; refusing to overwrite automatically" >&2
|
|
exit 1
|
|
fi
|
|
|
|
zpool create -f \
|
|
-o ashift=12 \
|
|
-O mountpoint="$root_dir" \
|
|
-O atime=off \
|
|
-O compression=lz4 \
|
|
-O xattr=sa \
|
|
"$pool" "$device"
|
|
else
|
|
zfs set mountpoint="$root_dir" "$pool" >/dev/null
|
|
fi
|
|
|
|
${networkDatasetLines}
|
|
'';
|
|
};
|
|
}
|
|
// (lib.mapAttrs'
|
|
(network: networkCfg:
|
|
lib.nameValuePair "podman-${rethContainerName network}" {
|
|
after = [ "network-online.target" "every-channel-ethereum-storage.service" ];
|
|
wants = [ "network-online.target" "every-channel-ethereum-storage.service" ];
|
|
requires = [ "every-channel-ethereum-storage.service" ];
|
|
unitConfig.RequiresMountsFor = [ networkCfg.rootDir ];
|
|
})
|
|
enabledNetworks)
|
|
// (lib.mapAttrs'
|
|
(network: networkCfg:
|
|
lib.nameValuePair "podman-${lighthouseContainerName network}" {
|
|
after = [
|
|
"network-online.target"
|
|
"every-channel-ethereum-storage.service"
|
|
"podman-${rethContainerName network}.service"
|
|
];
|
|
wants = [
|
|
"network-online.target"
|
|
"every-channel-ethereum-storage.service"
|
|
"podman-${rethContainerName network}.service"
|
|
];
|
|
requires = [
|
|
"every-channel-ethereum-storage.service"
|
|
"podman-${rethContainerName network}.service"
|
|
];
|
|
unitConfig.RequiresMountsFor = [ networkCfg.rootDir ];
|
|
})
|
|
enabledNetworks);
|
|
|
|
services.caddy.virtualHosts = lib.mkIf (cfg.publicHost != null) {
|
|
"${cfg.publicHost}".extraConfig = ''
|
|
encode zstd gzip
|
|
|
|
handle /mainnet/sync {
|
|
uri replace /mainnet/sync /eth/v1/node/syncing
|
|
reverse_proxy http://127.0.0.1:${toString cfg.mainnet.lighthouse.httpPort}
|
|
}
|
|
|
|
handle /mainnet/finality {
|
|
uri replace /mainnet/finality /eth/v1/beacon/states/head/finality_checkpoints
|
|
reverse_proxy http://127.0.0.1:${toString cfg.mainnet.lighthouse.httpPort}
|
|
}
|
|
|
|
handle /sepolia/sync {
|
|
uri replace /sepolia/sync /eth/v1/node/syncing
|
|
reverse_proxy http://127.0.0.1:${toString cfg.sepolia.lighthouse.httpPort}
|
|
}
|
|
|
|
handle /sepolia/finality {
|
|
uri replace /sepolia/finality /eth/v1/beacon/states/head/finality_checkpoints
|
|
reverse_proxy http://127.0.0.1:${toString cfg.sepolia.lighthouse.httpPort}
|
|
}
|
|
|
|
handle {
|
|
header Content-Type text/plain
|
|
respond "${caddyRootBody}" 200
|
|
}
|
|
'';
|
|
};
|
|
};
|
|
}
|