diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index f831e302b16..44b30d0e6c2 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -256,6 +256,7 @@ ./services/backup/tsm.nix ./services/backup/zfs-replication.nix ./services/backup/znapzend.nix + ./services/blockchain/ethereum/geth.nix ./services/cluster/hadoop/default.nix ./services/cluster/k3s/default.nix ./services/cluster/kubernetes/addons/dns.nix diff --git a/nixos/modules/services/blockchain/ethereum/geth.nix b/nixos/modules/services/blockchain/ethereum/geth.nix new file mode 100644 index 00000000000..be3f40f6bd8 --- /dev/null +++ b/nixos/modules/services/blockchain/ethereum/geth.nix @@ -0,0 +1,178 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + eachGeth = config.services.geth; + + gethOpts = { config, lib, name, ...}: { + + options = { + + enable = lib.mkEnableOption "Go Ethereum Node"; + + port = mkOption { + type = types.port; + default = 30303; + description = "Port number Go Ethereum will be listening on, both TCP and UDP."; + }; + + http = { + enable = lib.mkEnableOption "Go Ethereum HTTP API"; + address = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "Listen address of Go Ethereum HTTP API."; + }; + + port = mkOption { + type = types.port; + default = 8545; + description = "Port number of Go Ethereum HTTP API."; + }; + + apis = mkOption { + type = types.nullOr (types.listOf types.str); + default = null; + description = "APIs to enable over WebSocket"; + example = ["net" "eth"]; + }; + }; + + websocket = { + enable = lib.mkEnableOption "Go Ethereum WebSocket API"; + address = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "Listen address of Go Ethereum WebSocket API."; + }; + + port = mkOption { + type = types.port; + default = 8546; + description = "Port number of Go Ethereum WebSocket API."; + }; + + apis = mkOption { + type = types.nullOr (types.listOf types.str); + default = null; + description = "APIs to enable over WebSocket"; + example = ["net" "eth"]; + }; + }; + + metrics = { + enable = lib.mkEnableOption "Go Ethereum prometheus metrics"; + address = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "Listen address of Go Ethereum metrics service."; + }; + + port = mkOption { + type = types.port; + default = 6060; + description = "Port number of Go Ethereum metrics service."; + }; + }; + + network = mkOption { + type = types.nullOr (types.enum [ "goerli" "rinkeby" "yolov2" "ropsten" ]); + default = null; + description = "The network to connect to. Mainnet (null) is the default ethereum network."; + }; + + syncmode = mkOption { + type = types.enum [ "fast" "full" "light" ]; + default = "fast"; + description = "Blockchain sync mode."; + }; + + gcmode = mkOption { + type = types.enum [ "full" "archive" ]; + default = "full"; + description = "Blockchain garbage collection mode."; + }; + + maxpeers = mkOption { + type = types.int; + default = 50; + description = "Maximum peers to connect to."; + }; + + extraArgs = mkOption { + type = types.listOf types.str; + description = "Additional arguments passed to Go Ethereum."; + default = []; + }; + + package = mkOption { + default = pkgs.go-ethereum.geth; + type = types.package; + description = "Package to use as Go Ethereum node."; + }; + }; + }; +in + +{ + + ###### interface + + options = { + services.geth = mkOption { + type = types.attrsOf (types.submodule gethOpts); + default = {}; + description = "Specification of one or more geth instances."; + }; + }; + + ###### implementation + + config = mkIf (eachGeth != {}) { + + environment.systemPackages = flatten (mapAttrsToList (gethName: cfg: [ + cfg.package + ]) eachGeth); + + systemd.services = mapAttrs' (gethName: cfg: ( + nameValuePair "geth-${gethName}" (mkIf cfg.enable { + description = "Go Ethereum node (${gethName})"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + + serviceConfig = { + DynamicUser = true; + Restart = "always"; + StateDirectory = "goethereum/${gethName}/${if (cfg.network == null) then "mainnet" else cfg.network}"; + + # Hardening measures + PrivateTmp = "true"; + ProtectSystem = "full"; + NoNewPrivileges = "true"; + PrivateDevices = "true"; + MemoryDenyWriteExecute = "true"; + }; + + script = '' + ${cfg.package}/bin/geth \ + --nousb \ + --ipcdisable \ + ${optionalString (cfg.network != null) ''--${cfg.network}''} \ + --syncmode ${cfg.syncmode} \ + --gcmode ${cfg.gcmode} \ + --port ${toString cfg.port} \ + --maxpeers ${toString cfg.maxpeers} \ + ${if cfg.http.enable then ''--http --http.addr ${cfg.http.address} --http.port ${toString cfg.http.port}'' else ""} \ + ${optionalString (cfg.http.apis != null) ''--http.api ${lib.concatStringsSep "," cfg.http.apis}''} \ + ${if cfg.websocket.enable then ''--ws --ws.addr ${cfg.websocket.address} --ws.port ${toString cfg.websocket.port}'' else ""} \ + ${optionalString (cfg.websocket.apis != null) ''--ws.api ${lib.concatStringsSep "," cfg.websocket.apis}''} \ + ${optionalString cfg.metrics.enable ''--metrics --metrics.addr ${cfg.metrics.address} --metrics.port ${toString cfg.metrics.port}''} \ + ${lib.escapeShellArgs cfg.extraArgs} \ + --datadir /var/lib/goethereum/${gethName}/${if (cfg.network == null) then "mainnet" else cfg.network} + ''; + }))) eachGeth; + + }; + +} diff --git a/nixos/tests/geth.nix b/nixos/tests/geth.nix new file mode 100644 index 00000000000..10cbd6d9038 --- /dev/null +++ b/nixos/tests/geth.nix @@ -0,0 +1,41 @@ +import ./make-test-python.nix ({ pkgs, ... }: { + name = "geth"; + meta = with pkgs.lib; { + maintainers = with maintainers; [bachp ]; + }; + + machine = { ... }: { + services.geth."mainnet" = { + enable = true; + http = { + enable = true; + }; + }; + services.geth."testnet" = { + enable = true; + port = 30304; + network = "goerli"; + http = { + enable = true; + port = 18545; + }; + }; + }; + + testScript = '' + start_all() + + machine.wait_for_unit("geth-mainnet.service") + machine.wait_for_unit("geth-testnet.service") + machine.wait_for_open_port(8545) + machine.wait_for_open_port(18545) + + machine.succeed( + 'geth attach --exec "eth.chainId()" http://localhost:8545 | grep \'"0x0"\' ' + ) + + machine.succeed( + 'geth attach --exec "eth.chainId()" http://localhost:18545 | grep \'"0x5"\' ' + ) + ''; +})