From bc5d93a7fb5e064fec2f9f9173ffbf89931a0332 Mon Sep 17 00:00:00 2001 From: markoburcul Date: Thu, 9 Jan 2025 18:29:14 +0100 Subject: [PATCH] nix: add codex service definition Referenced issue: https://github.com/codex-storage/nim-codex/issues/940 Signed-off-by: markoburcul --- flake.nix | 2 + nix/README.md | 4 +- nix/codex.nix | 380 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 384 insertions(+), 2 deletions(-) create mode 100644 nix/codex.nix diff --git a/flake.nix b/flake.nix index b4fa8151..22c2838e 100644 --- a/flake.nix +++ b/flake.nix @@ -30,6 +30,8 @@ default = codex; }); + nixosModules.codex = import ./nix/codex.nix; + devShells = forAllSystems (system: let pkgs = pkgsFor.${system}; in { diff --git a/nix/README.md b/nix/README.md index ba663622..87eececf 100644 --- a/nix/README.md +++ b/nix/README.md @@ -19,11 +19,11 @@ https://github.com/NixOS/nix/issues/4423 It can be also done without even cloning the repo: ```sh -nix build 'github:codex-storage/nim-codex?submodules=1' +nix build 'git+https://github.com/codex-storage/nim-codex?submodules=1#' ``` ## Running ```sh -nix run 'github:codex-storage/nim-codex?submodules=1' +nix run 'git+https://github.com/codex-storage/nim-codex?submodules=1#'' ``` diff --git a/nix/codex.nix b/nix/codex.nix new file mode 100644 index 00000000..4e6b841d --- /dev/null +++ b/nix/codex.nix @@ -0,0 +1,380 @@ +{ config, lib, pkgs, ... }: + +let + inherit (lib) + types mkEnableOption mkOption mkIf length + escapeShellArgs literalExpression optionalString + concatStringsSep boolToString optionalAttrs; + + cfg = config.services.codex; +in { + options = { + services.codex = { + enable = mkEnableOption "Codex Node service."; + + package = mkOption { + type = types.package; + default = pkgs.callPackage ../nix/default.nix { }; + defaultText = literalExpression "pkgs.codex"; + description = lib.mdDoc "Package to use as Codex node."; + }; + + service = { + user = mkOption { + type = types.str; + default = "codex"; + description = "User for Codex service."; + }; + + group = mkOption { + type = types.str; + default = "codex"; + description = "Group for Codex service user."; + }; + }; + + configFile = mkOption { + type = types.nullOr types.str; + default = null; + description = "Path to the Codex configuration file."; + }; + + dataDir = mkOption { + type = types.nullOr types.str; + default = null; + description = "Directory for Codex data."; + }; + + logLevel = mkOption { + type = types.str; + default = "info"; + description = "Sets the log level [=info]."; + }; + + logFormat = mkOption { + type = types.str; + default = "auto"; + description = "Specifies what kind of logs should be written to stdout (auto, colors, nocolors, json) [=auto]."; + }; + + metrics = { + enable = lib.mkEnableOption "Enable the metrics server."; + address = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "Listening address of the metrics server."; + }; + + port = mkOption { + type = types.int; + default = 8008; + description = "Listening HTTP port of the metrics server."; + }; + }; + + listenAddrs = mkOption { + type = types.nullOr (types.listOf types.str); + default = null; + description = "Multi Addresses to listen on."; + }; + + nat = mkOption { + type = types.nullOr (types.listOf types.str); + default = null; + description = "IP Addresses to announce behind a NAT."; + }; + + discIp = mkOption { + type = types.nullOr types.str; + default = null; + description = "Discovery listen address."; + }; + + discPort = mkOption { + type = types.nullOr types.int; + default = null; + description = "Discovery (UDP) port."; + }; + + netPrivKey = mkOption { + type = types.nullOr types.str; + default = null; + description = "Source of network (secp256k1) private key file path or name."; + }; + + bootstrapNodes = mkOption { + type = types.nullOr (types.listOf types.str); + default = null; + description = "Specifies one or more bootstrap nodes to use when connecting to the network."; + }; + + maxPeers = mkOption { + type = types.nullOr types.int; + default = null; + description = "The maximum number of peers to connect to."; + }; + + agentString = mkOption { + type = types.nullOr types.str; + default = null; + description = "Node agent string used as identifier in the network."; + }; + + apiBindAddr = mkOption { + type = types.nullOr types.str; + default = null; + description = "The REST API bind address."; + }; + + apiPort = mkOption { + type = types.nullOr types.int; + default = null; + description = "The REST API port."; + }; + + apiCorsOrigin = mkOption { + type = types.nullOr types.str; + default = null; + description = "The REST API CORS allowed origin for downloading data."; + }; + + repoKind = mkOption { + type = types.nullOr types.str; + default = null; + description = "Backend for main repo store (fs, sqlite, leveldb)."; + }; + + storageQuota = mkOption { + type = types.nullOr types.str; + default = null; + description = "The size of the total storage quota dedicated to the node."; + }; + + blockTtl = mkOption { + type = types.nullOr types.int; + default = null; + description = "Default block timeout in seconds - 0 disables the ttl."; + }; + + blockMaintenanceInterval = mkOption { + type = types.nullOr types.int; + default = null; + description = "Time interval in seconds for block maintenance cycles."; + }; + + blockMaintenanceNumber = mkOption { + type = types.nullOr types.int; + default = null; + description = "Number of blocks to check every maintenance cycle."; + }; + + cacheSize = mkOption { + type = types.nullOr types.int; + default = null; + description = "The size of the block cache. 0 disables the cache."; + }; + + persistence = { + enable = mkEnableOption "Enable the 'persistence' subcommand."; + + ethProvider = mkOption { + type = types.nullOr types.str; + default = null; + description = "The URL of the JSON-RPC API of the Ethereum node."; + }; + + ethAccount = mkOption { + type = types.nullOr types.str; + default = null; + description = "The Ethereum account that is used for storage contracts."; + }; + + ethPrivateKey = mkOption { + type = types.nullOr types.str; + default = null; + description = "File containing Ethereum private key for storage contracts."; + }; + + marketplaceAddress = mkOption { + type = types.nullOr types.str; + default = null; + description = "Address of deployed Marketplace contract."; + }; + + validator = mkEnableOption "Enables validator, requires an Ethereum node."; + + validatorMaxSlots = mkOption { + type = types.nullOr types.int; + default = null; + description = "Maximum number of slots that the validator monitors."; + }; + + validatorGroups = mkOption { + type = types.nullOr types.str; + default = null; + description = "Slot validation groups."; + }; + + validatorGroupIndex = mkOption { + type = types.nullOr types.str; + default = null; + description = "Slot validation group index."; + }; + + rewardRecipient = mkOption { + type = types.nullOr types.str; + default = null; + description = "Address to send payouts to (eg rewards and refunds)."; + }; + }; + + prover = { + enable = mkEnableOption "Enable the 'persistence prover' subcommand."; + + circuitDir = mkOption { + type = types.nullOr types.str; + default = null; + description = "Directory where Codex will store proof circuit data."; + }; + + circomR1cs = mkOption { + type = types.nullOr types.str; + default = null; + description = "The r1cs file for the storage circuit."; + }; + + circomWasm = mkOption { + type = types.nullOr types.str; + default = null; + description = "The wasm file for the storage circuit."; + }; + + circomZkey = mkOption { + type = types.nullOr types.str; + default = null; + description = "The zkey file for the storage circuit."; + }; + + circomNoZkey = mkEnableOption "Ignore the zkey file - use only for testing!"; + + proofSamples = mkOption { + type = types.nullOr types.int; + default = null; + description = "Number of samples to prove."; + }; + + maxSlotDepth = mkOption { + type = types.nullOr types.int; + default = null; + description = "The maximum depth of the slot tree."; + }; + + maxDatasetDepth = mkOption { + type = types.nullOr types.int; + default = null; + description = "The maximum depth of the dataset tree."; + }; + + maxBlockDepth = mkOption { + type = types.nullOr types.int; + default = null; + description = "The maximum depth of the network block merkle tree."; + }; + + maxCellElements = mkOption { + type = types.nullOr types.int; + default = null; + description = "The maximum number of elements in a cell."; + }; + }; + + extraArgs = mkOption { + type = types.nullOr (types.listOf types.str); + default = null; + description = "Additional arguments to pass to the Codex binary."; + }; + + }; + }; + + config = mkIf cfg.enable { + users.users = optionalAttrs (cfg.service.user == "codex") { + codex = { + group = cfg.service.group; + home = cfg.dataDir; + description = "Codex service user"; + isSystemUser = true; + }; + }; + + users.groups = optionalAttrs (cfg.service.user == "codex") { + codex = { }; + }; + + systemd.tmpfiles.rules = [ + "d ${cfg.dataDir or "/var/lib/codex"} 0755 codex codex" + ]; + + systemd.services.codex = { + description = "Codex Node"; + wantedBy = [ "multi-user.target" ]; + requires = [ "network.target" ]; + serviceConfig = { + User = cfg.service.user; + Group = cfg.service.group; + ExecStart = '' + ${cfg.package}/bin/codex \ + ${optionalString (cfg.configFile != null) "--config-file=${cfg.configFile}"} \ + ${optionalString (cfg.dataDir != null) "--data-dir=${cfg.dataDir}"} \ + --log-level=${cfg.logLevel} \ + --log-format=${cfg.logFormat} \ + --metrics=${boolToString cfg.metrics.enable} ${optionalString cfg.metrics.enable ''--metrics-address=${cfg.metrics.address} --metrics-port=${toString cfg.metrics.port} ''}\ + ${optionalString (cfg.listenAddrs != null) (concatStringsSep " " (map (addr: "--listen-addrs=${addr}") cfg.listenAddrs))} \ + ${optionalString (cfg.nat != null) (concatStringsSep " " (map (addr: "--nat=${addr}") cfg.nat))} \ + ${optionalString (cfg.discIp != null) "--disc-ip=${cfg.discIp}"} \ + ${optionalString (cfg.discPort != null) "--disc-port=${toString cfg.discPort}"} \ + ${optionalString (cfg.netPrivKey != null) "--net-privkey=${cfg.netPrivKey}"} \ + ${optionalString (cfg.bootstrapNodes != null) (concatStringsSep " " (map (node: "--bootstrap-node=${node}") cfg.bootstrapNodes))} \ + ${optionalString (cfg.maxPeers != null) "--max-peers=${toString cfg.maxPeers}"} \ + ${optionalString (cfg.agentString != null) "--agent-string=${cfg.agentString}"} \ + ${optionalString (cfg.apiBindAddr != null) "--api-bindaddr=${cfg.apiBindAddr}"} \ + ${optionalString (cfg.apiPort != null) "--api-port=${toString cfg.apiPort}"} \ + ${optionalString (cfg.apiCorsOrigin != null) "--api-cors-origin=${cfg.apiCorsOrigin}"} \ + ${optionalString (cfg.repoKind != null) "--repo-kind=${cfg.repoKind}"} \ + ${optionalString (cfg.storageQuota != null) "--storage-quota=${cfg.storageQuota}"} \ + ${optionalString (cfg.blockTtl != null) "--block-ttl=${toString cfg.blockTtl}"} \ + ${optionalString (cfg.blockMaintenanceInterval != null) "--block-mi=${toString cfg.blockMaintenanceInterval}"} \ + ${optionalString (cfg.blockMaintenanceNumber != null) "--block-mn=${toString cfg.blockMaintenanceNumber}"} \ + ${optionalString (cfg.cacheSize != null) "--cache-size=${toString cfg.cacheSize}"} \ + ${mkIf cfg.subcommands.persistence.enable '' + persistence \ + ${optionalString (cfg.subcommands.persistence.ethProvider != null) "--eth-provider=${cfg.subcommands.persistence.ethProvider}"} \ + ${optionalString (cfg.subcommands.persistence.ethAccount != null) "--eth-account=${cfg.subcommands.persistence.ethAccount}"} \ + ${optionalString (cfg.subcommands.persistence.ethPrivateKey != null) "--eth-private-key=${cfg.subcommands.persistence.ethPrivateKey}"} \ + ${optionalString (cfg.subcommands.persistence.marketplaceAddress != null) "--marketplace-address=${cfg.subcommands.persistence.marketplaceAddress}"} \ + --validator=${boolToString cfg.subcommands.persistence.validator} \ + ${optionalString (cfg.subcommands.persistence.validatorMaxSlots != null) "--validator-max-slots=${cfg.subcommands.persistence.validatorMaxSlots}"} \ + ${optionalString (cfg.subcommands.persistence.validatorGroups != null) "--validator-groups=${cfg.subcommands.persistence.validatorGroups}"} \ + ${optionalString (cfg.subcommands.persistence.validatorGroupIndex != null) "--validator-group-index=${cfg.subcommands.persistence.validatorGroupIndex}"} \ + ${optionalString (cfg.subcommands.persistence.rewardRecipient != null) "--reward-recipient=${cfg.subcommands.persistence.rewardRecipient}"} \ + ''} \ + ${mkIf cfg.subcommands.prover.enable '' + "persistence prover" \ + ${optionalString (cfg.subcommands.prover.circuitDir != null) "--circuit-dir=${cfg.subcommands.prover.circuitDir}"} \ + ${optionalString (cfg.subcommands.prover.circomR1cs != null) "--circom-r1cs=${cfg.subcommands.prover.circomR1cs}"} \ + ${optionalString (cfg.subcommands.prover.circomWasm != null) "--circom-wasm=${cfg.subcommands.prover.circomWasm}"} \ + ${optionalString (cfg.subcommands.prover.circomZkey != null) "--circom-zkey=${cfg.subcommands.prover.circomZkey}"} \ + --circom-no-zkey=${boolToString cfg.subcommands.prover.circomNoZkey} \ + ${optionalString (cfg.subcommands.prover.proofSamples != null) "--proof-samples=${cfg.subcommands.prover.proofSamples}"} \ + ${optionalString (cfg.subcommands.prover.maxSlotDepth != null) "--max-slot-depth=${cfg.subcommands.prover.maxSlotDepth}"} \ + ${optionalString (cfg.subcommands.prover.maxDatasetDepth != null) "--max-dataset-depth=${cfg.subcommands.prover.maxDatasetDepth}"} \ + ${optionalString (cfg.subcommands.prover.maxBlockDepth != null) "--max-block-depth=${cfg.subcommands.prover.maxBlockDepth}"} \ + ${optionalString (cfg.subcommands.prover.maxCellElements != null) "--max-cell-elements=${cfg.subcommands.prover.maxCellElements}"} \ + ''} + ${optionalString (cfg.extraArgs != null) (concatStringsSep " " cfg.extraArgs)} + ''; + Restart = "on-failure"; + }; + }; + }; +}