Imported from nix-community/ethereum.nix (
AGENTS.md). Install upstream withnpx skills add nix-community/ethereum.nix. Copyright stays with the author.
Repository Guidelines
Project Structure & Module Organization
- Root:
flake.nix,flake.lock,devshells/{default.nix,ci.nix},formatter.nix,README.md. - Packages live under
packages/<tool>/withpackage.nix,default.nix, optionalupdate.py, and lockfiles when needed. - Formatting config:
formatter.nix. - Utilities and docs:
scripts/,docs/,.github/.
Build, Test, and Development Commands
- Enter dev shell:
nix develop. - Build a package:
nix build --accept-flake-config .#<package>(e.g.,nix build .#geth). - Run without installing:
nix run .#<package> -- --help. - Repo checks (builds + lints):
nix flake check. - Format everything:
nix fmt. - Regenerate README package section:
./scripts/generate-package-docs.py.
Coding Style & Naming Conventions
- Indentation: 2 spaces; avoid tabs.
- Nix: small, composable derivations; prefer
buildNpmPackage/rustPlatform.buildRustPackage/stdenv.mkDerivationas in existing packages. - File layout per package:
package.nix(definition),default.nix(wrapper),update.py(optional custom updater),nix-update-args(optional nix-update flags). - Tools via treefmt: nixfmt, deadnix, shfmt, shellcheck, mdformat, yamlfmt, taplo. Always run
nix fmtbefore committing.
Updating Packages
Prefer nix-update over custom update scripts. Most packages can be updated with:
nix run nixpkgs#nix-update -- --flake <package>
For this to work, package.nix must have version/hash attributes inline (not loaded from JSON):
buildGoModule rec {
pname = "example";
version = "1.0.0"; # nix-update finds and updates this
src = fetchFromGitHub {
owner = "owner";
repo = "repo";
rev = "v${version}";
hash = "sha256-..."; # nix-update updates this
};
subPackages = [ "." ]; # for go find the relevant packages containing the binary
vendorHash = "sha256-..."; # nix-update updates this too
}
Testing updates: After writing or modifying a package, verify updates work by:
- Temporarily downgrading the version in
package.nix - Running
nix run nixpkgs#nix-update -- --flake <package> - Confirming version and hashes are updated correctly
Only use custom update.py scripts when nix-update cannot handle the package, such as:
- Packages with complex version schemes nix-update cannot parse
- Sources not supported by nix-update (non-GitHub, custom APIs)
- Packages requiring special hash calculation logic
Custom updaters should use the scripts/updater/ library. See existing update.py files for examples.
Package Metadata Requirements
Every package MUST have proper metadata in package.nix:
meta = with lib; {
description = "Clear, concise description";
homepage = "https://project-homepage.com";
license = licenses.mit; # or licenses.unfree, etc.
sourceProvenance = with lib.sourceTypes; [ fromSource ];
maintainers = with maintainers; [ username ];
mainProgram = "binary-name";
platforms = platforms.all; # or specific platforms
};
Package Categories
Every package should have a category in passthru for README organization:
passthru.category = "Execution Clients";
meta = { ... };
Available categories (in display order):
- Execution Clients - Ethereum execution layer clients (geth, erigon, besu, reth, nethermind)
- Consensus Clients - Ethereum consensus layer clients (prysm, lighthouse, teku)
- Validators - Validator clients and key management (vouch, charon, web3signer, dirk)
- Staking Tools - Staking infrastructure and utilities (rocketpool, rocketpoold, eigenlayer, ethdo, ethstaker-deposit-cli)
- MEV - MEV infrastructure (mev-boost, mev-boost-relay, blutgang)
- SSV - Secret Shared Validators tooling (ssvnode, ssv-dkg)
- Account Abstraction - ERC-4337 bundlers and account abstraction tooling (alto)
- Polygon - Polygon blockchain clients and tools (bor, heimdall-v2)
- Development Tools - Smart contract development and testing (foundry, slither, heimdall, sedge, tx-fuzz, snarkjs)
- Libraries - Cryptographic and protocol libraries (blst, ckzg, mcl, evmc, bls)
- Utilities - Other Ethereum tools (eth-validator-watcher, kurtosis, rotki-bin, zcli, ethereal)
Custom Maintainers
For maintainers not yet in nixpkgs, define them in lib/default.nix:
{ inputs, ... }:
inputs.nixpkgs.lib.extend (
_final: prev: {
maintainers = prev.maintainers // {
username = {
github = "github-username";
githubId = 123456; # Get from: curl -s https://api.github.com/users/username | jq -r '.id'
name = "Full Name";
};
};
}
)
Then in packages/<package>/default.nix, pass flake to the package:
{ pkgs, flake }: pkgs.callPackage ./package.nix { inherit flake; }
And in packages/<package>/package.nix, reference custom maintainers:
{
lib,
flake,
# ... other args
}:
stdenv.mkDerivation rec {
# ...
meta = with lib; {
maintainers = with flake.lib.maintainers; [ username ];
# ... other meta
};
}
Version Check Hooks
Use versionCheckHook to verify packages report correct versions during build:
doInstallCheck = true;
nativeInstallCheckInputs = [ versionCheckHook ];
For tools that need a writable HOME directory (many CLI tools try to create config/cache directories), use versionCheckHomeHook:
-
In
packages/<package>/default.nix, pass the hook:{ pkgs, perSystem, ... }: pkgs.callPackage ./package.nix { inherit (perSystem.self) versionCheckHomeHook; } -
In
packages/<package>/package.nix, add it to inputs and use it:{ versionCheckHook, versionCheckHomeHook, # ... }: stdenv.mkDerivation { # ... doInstallCheck = true; nativeInstallCheckInputs = [ versionCheckHook versionCheckHomeHook ]; }
Testing Guidelines
- Build locally:
nix build .#<package>. - Run flake checks:
nix flake check. - Per-package checks (when defined):
nix build .#checks.$(nix eval --raw --impure --expr builtins.currentSystem).pkgs-<package>. - For scripts, ensure
shellcheckpasses; enabledoCheck = truein packages when feasible.
Commit & Pull Request Guidelines
- Commit style mirrors history:
<package>: summary.- Version bumps:
<package>: X -> Y (#123); new packages:<package>: init at X.Y.Z.
- Version bumps:
- PRs: clear description, rationale, and testing notes; link issues; include sample run output for CLIs.
- Before pushing: run
nix fmtandnix flake check.
Security & Configuration Tips
- Some tools are unfree; enable unfree if needed in your Nix config.
- Sandbox experiments: consider using confined execution wrappers for sensitive operations.
- Pin sources with hashes; avoid network access at build time.
NixOS Modules (RFC 42 Settings Pattern)
Modules live under modules/nixos/<client>/ with three files:
options.nix- Option definitions with RFC 42settingsfreeformTypedefault.nix- systemd service implementationdefault.test.nix- NixOS VM test (optional)
Module Structure
modules/nixos/<client>/
├── default.nix # systemd service implementation
├── options.nix # NixOS module options with settings
└── default.test.nix # NixOS VM test (optional)
options.nix Pattern
{
lib,
pkgs,
...
}: let
inherit (lib) mkEnableOption mkOption types literalExpression;
clientOpts = {
options = {
enable = mkEnableOption "Client description";
package = mkOption {
type = types.package;
default = pkgs.client;
defaultText = literalExpression "pkgs.client";
description = "Package to use.";
};
openFirewall = mkOption {
type = types.bool;
default = false;
description = "Open ports in the firewall.";
};
# RFC 42: freeformType allows any options to pass through
settings = mkOption {
type = types.submodule {
freeformType = types.attrsOf types.anything;
};
default = {};
description = ''
Client configuration. Converted to CLI arguments.
Use flat dotted keys (e.g., "http.addr" not http.addr).
'';
example = literalExpression ''
{
sepolia = true;
http = true;
"http.addr" = "0.0.0.0";
"http.api" = ["eth" "net" "web3"];
}
'';
};
extraArgs = mkOption {
type = types.listOf types.str;
default = [];
description = "Additional CLI arguments.";
};
};
};
in {
options.services.ethereum.client = mkOption {
type = types.attrsOf (types.submodule clientOpts);
default = {};
description = "Specification of one or more client instances.";
};
}
default.nix Pattern
{
config,
lib,
pkgs,
...
}: let
inherit (lib) mkIf mkMerge mapAttrs' nameValuePair;
inherit (lib) concatStringsSep filterAttrs mapAttrsToList flatten optionals elem mapAttrs;
inherit (lib.attrsets) zipAttrsWith;
inherit (builtins) isList;
modulesLib = import ../../../lib/modules.nix lib;
inherit (modulesLib) baseServiceConfig;
eachClient = config.services.ethereum.client;
# Convert lists to comma-separated strings for CLI
processSettings = mapAttrs (_: v:
if isList v
then concatStringsSep "," v
else v);
in {
inherit (import ./options.nix {inherit lib pkgs;}) options;
config = mkIf (eachClient != {}) {
# Firewall configuration
networking.firewall = let
openFirewall = filterAttrs (_: cfg: cfg.openFirewall) eachClient;
perService = mapAttrsToList (_: cfg: let
s = cfg.settings;
in {
allowedUDPPorts = [(s.port or 30303)];
allowedTCPPorts = [(s.port or 30303)]
++ optionals (s.http or false) [(s."http.port" or 8545)];
}) openFirewall;
in zipAttrsWith (_name: flatten) perService;
# systemd services
systemd.services = mapAttrs' (
clientName: cfg: let
serviceName = "client-${clientName}";
s = cfg.settings;
datadir = s.datadir or "%S/${serviceName}";
jwtSecret = s."authrpc.jwtsecret" or null;
# Keys handled separately
skipKeys = ["datadir" "authrpc.jwtsecret"];
normalSettings = filterAttrs (k: _: !elem k skipKeys) s;
# Use lib.cli.toCommandLine for RFC 42 settings
cliArgs = lib.cli.toCommandLine (name: {
option = "--${name}";
sep = null;
explicitBool = false;
}) (processSettings normalSettings);
allArgs = ["--datadir" datadir]
++ cliArgs
++ optionals (jwtSecret != null) ["--authrpc.jwtsecret" "%d/jwtsecret"]
++ cfg.extraArgs;
scriptArgs = concatStringsSep " \\\n " allArgs;
in nameValuePair serviceName (mkIf cfg.enable {
after = ["network.target"];
wantedBy = ["multi-user.target"];
description = "Client (${clientName})";
serviceConfig = mkMerge [
baseServiceConfig
{
StateDirectory = serviceName;
ExecStart = "${cfg.package}/bin/client ${scriptArgs}";
}
(mkIf (jwtSecret != null) {
LoadCredential = ["jwtsecret:${jwtSecret}"];
})
];
})
) eachClient;
};
}
Key Principles
- RFC 42 freeformType - Use
types.attrsOf types.anythingfor settings to allow any CLI options - Flat dotted keys - Use
"http.addr"instead of nestedhttp.addrattributes - lib.cli.toCommandLine - Standard nixpkgs function for settings → CLI conversion. Use
sep = null(space-separated--key value) for clients that accept it (geth, erigon, reth, besu, teku, lighthouse, prysm, mev-boost). Nimbus (confutils) only accepts--key=value, so its modules must usesep = "="— a space-separated value is rejected as a positional argument (does not accept arguments). - baseServiceConfig - Import from
lib/modules.nixfor hardened systemd defaults - LoadCredential - Use systemd credentials for secrets (JWT, etc.)
- DynamicUser - Services run without pre-created users
Testing Modules
# Build and run test
nix build .#checks.x86_64-linux.testing-geth-default
# Interactive mode
nix build .#checks.x86_64-linux.testing-geth-default.driver
./result/bin/nixos-test-driver --interactive
Installing Nix (Required for Package Testing)
When working on package requests or fixes, you MUST install Nix from the official installer to properly test changes, unless already present
# Install Nix with daemon mode
sh <(curl -L https://nixos.org/nix/install) --daemon
# Enable flakes and nix-command (required for this repository)
echo "experimental-features = nix-command flakes" | sudo tee -a /etc/nix/nix.conf
# Restart the Nix daemon to apply changes
if [[ "$OSTYPE" == "darwin"* ]]; then
sudo launchctl kickstart -k system/org.nixos.nix-daemon
else
sudo systemctl restart nix-daemon
fi
Common Issues and Solutions
-
Rust packages with git dependencies: May fail during cargo vendoring if dependencies have workspace inheritance issues. Consider using pre-built binaries as a workaround.
-
Binary packages: When packaging pre-built binaries:
- Use
dontUnpack = trueif the download is a single executable file - Use
autoPatchelfHookon Linux to handle dynamic library dependencies - Common missing libraries:
gcc-unwrapped.libfor libgcc_s.so.1
- Use
-
Update scripts: Follow shellcheck recommendations - declare and assign variables separately to avoid masking return values.
-
Custom nix-update arguments: For packages that need special nix-update flags (e.g., filtering out nightly releases), create a
nix-update-argsfile with one argument per line:# packages/qwen-code/nix-update-args --use-github-releases --version-regex ^v([0-9]+\.[0-9]+\.[0-9]+)$The CI workflow reads this file and passes the arguments to nix-update automatically.