init: dcrd, dcrwallet, and dcrctl at 2.0.6. init modules for dcrd and dcrwallet

Signed-off-by: stakeynet <git@stakey.net>
This commit is contained in:
stakeynet 2025-11-29 12:23:09 -08:00
commit 90d1972cdc
Signed by: stakey
GPG Key ID: 0E5D9B54F07D920D
10 changed files with 561 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.stfolder/

24
README.md Normal file
View File

@ -0,0 +1,24 @@
# nix-decred
nix-decred is a collection of Nix packages and NixOS modules, inspired by [nix-bitcoin](https://github.com/fort-nix/nix-bitcoin).
Overview
---
While nix-decred is used in production today, it is considered experimental.
nix-decred nodes can be deployed on dedicated hardware, virtual machines or containers.
The Nix packages and NixOS modules can be used independently and combined freely.
nix-decred is built on top of Nix and [NixOS](https://nixos.org/) which provide powerful abstractions to keep it highly customizable and maintainable. Running nix-decred currently requires previous experience with the Nix ecosystem, as no support is provided.
Security Considerations
---
Using this flake could be dangerous, as it provides the hashes for the software you'll be running.
If you want to use this today, the sane way would be to fork the project, review the codebase, and then review and merge all future changes as necessary. If you're familiar with nix, forking should still be faster than writing the packages and modules from scratch yourself.
The git server for this project runs on a shared virtual server, so don't trust unsigned or unverified messages.
Support
---
No support is provided. If you have ideas for improvement, general feedback, or a PR, you can tag @stakeynet on the Decred matrix channels.

49
docs/dcrd.md Normal file
View File

@ -0,0 +1,49 @@
# dcrd options
`dcrd` uses rpc credentials, so it's recommended to secure your secrets using a tool like [sops-nix](https://github.com/Mic92/sops-nix).
## sops-nix
Render `dcrd.conf` with `sops-nix` and point the service at it. Example:
```nix
{ config, lib, pkgs, ... }:
{
# Define credentials as secrets
sops.secrets."dcrd/rpcuser" = {};
sops.secrets."dcrd/rpcpass" = {};
# Render dcrd.conf owned by the dcrd service user/group
sops.templates."dcrd.conf" = {
owner = config.services.dcrd.user;
group = config.services.dcrd.group;
mode = "0440";
restartUnits = [ "dcrd.service" ];
content = ''
[Application Options]
# example settings
rpcuser=${config.sops.placeholder."dcrd/rpcuser"}
rpcpass=${config.sops.placeholder."dcrd/rpcpass"}
'';
};
# Ensure dcrd only starts when the config exists
systemd.services.dcrd.unitConfig.ConditionPathExists =
config.sops.templates."dcrd.conf".path;
# Point the module to the rendered config
services.dcrd = {
enable = true;
configFile = config.sops.templates."dcrd.conf".path;
};
}
```
## Notes
Instead of using DynamicUser, the module necessarily runs `dcrd` as a fixed `User`/`Group` and uses a non-private state directory so `rpc.cert` can be read by intended consumers.
- `dcrd` writes `rpc.cert` into its data dir (`--appdata`, defaulting to `/var/lib/dcrd`).
- Consumers (e.g., wallets, tooling) often need to read `rpc.cert` without elevated privileges.
- With `DynamicUser=true`, systemd places state under `/var/lib/private/dcrd` and uses an ephemeral UID, which prevents other users/services from traversing the directory and reading `rpc.cert`.

95
docs/dcrwallet.md Normal file
View File

@ -0,0 +1,95 @@
# dcrwallet options
`dcrwallet` uses rpc credentials, so it's recommended to secure your secrets using a tool like [sops-nix](https://github.com/Mic92/sops-nix).
## sops-nix
Render `dcrwallet.conf` with `sops-nix` and point the service at it. For example, here's a sample configuration for a voting wallet:
```nix
{ config, lib, pkgs, ... }:
{
# Define credentials as secrets
sops.secrets."dcrwallet/rpcuser" = {};
sops.secrets."dcrwallet/rpcpass" = {};
sops.templates."dcrctl.conf" = {
path = "/home/operator/.dcrctl/dcrctl.conf";
owner = "operator";
group = "users";
mode = "0400";
content = ''
[Application Options]
rpcuser=${config.sops.placeholder."dcrwallet/rpcuser"}
rpcpass=${config.sops.placeholder."dcrwallet/rpcpass"}
rpccert=/var/lib/dcrwallet/rpc.cert
wallet=1
'';
};
sops.templates."dcrwallet.conf" = {
owner = config.services.dcrwallet.user;
group = config.services.dcrwallet.group;
mode = "0440";
restartUnits = [ "dcrwallet.service" ];
content = ''
[Application Options]
CAFile=/var/lib/dcrd/rpc.cert
rpclisten=0.0.0.0:9110
username=${config.sops.placeholder."dcrwallet/rpcpass"}
password=${config.sops.placeholder."dcrwallet/rpcpass"}
enablevoting=1
manualtickets=1
'';
};
# Ensure dcrwallet only starts when the config exists
systemd.services.dcrwallet.unitConfig.ConditionPathExists = config.sops.templates."dcrwallet.conf".path;
services.dcrwallet = {
enable = true;
configFile = config.sops.templates."dcrwallet.conf".path;
extraPackages = [
pkgs.dcrctl
pkgs.dcrd # promptsecret
];
operator = {
enable = true;
name = "stakey";
};
};
}
```
## Initialization
If you run dcrwallet as a service, here's how to intialize a wallet as `root`.
```bash
cd /var/lib/dcrwallet
export DCRWALLET_BIN=$(systemctl cat --runtime dcrwallet.service | grep ExecStart | awk '{print $1}' | cut -d= -f2)
doas -u dcrwallet $DCRWALLET_BIN \
--configfile=/run/secrets/rendered/dcrwallet.conf \
--appdata=/var/lib/dcrwallet \
--create
```
Then you need to start dcrwallet manually the first time to sync.
```sh
tmux new "doas -u dcrwallet $DCRWALLET_BIN --configfile=/run/secrets/rendered/dcrwallet.conf --appdata=/var/lib/dcrwallet"
```
## Using the operator
```sh
su - operator
dcrctl help
```
## Enable Voting
Use the operator account.
```sh
promptsecret | dcrwallet walletpassphrase - 0
```

43
flake.nix Normal file
View File

@ -0,0 +1,43 @@
{
description = ''
A collection of Nix packages and NixOS modules for easily
installing full-featured Decred nodes.
'';
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};
outputs = { self, nixpkgs }: let
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
in {
packages = forAllSystems (system: let
pkgs = nixpkgs.legacyPackages.${system};
in {
dcrd = pkgs.callPackage ./pkgs/dcrd.nix {};
dcrctl = pkgs.callPackage ./pkgs/dcrctl.nix {};
dcrwallet = pkgs.callPackage ./pkgs/dcrwallet.nix {};
});
overlays.default = final: prev: {
dcrd = final.callPackage ./pkgs/dcrd.nix {};
dcrctl = final.callPackage ./pkgs/dcrctl.nix {};
dcrwallet = final.callPackage ./pkgs/dcrwallet.nix {};
};
nixosModules = {
dcrd = ./modules/dcrd.nix;
dcrwallet = ./modules/dcrwallet.nix;
default = { config, lib, pkgs, ... }: {
imports = [
self.nixosModules.dcrd
self.nixosModules.dcrwallet
];
nixpkgs.overlays = [ self.overlays.default ];
};
};
};
}

123
modules/dcrd.nix Normal file
View File

@ -0,0 +1,123 @@
{ config, lib, pkgs, ... }:
let
cfg = config.services.dcrd;
settingsFormat = pkgs.formats.ini {
listsAsDuplicateKeys = true;
};
networkPorts = {
mainnet = 9108;
testnet = 19108;
simnet = 18555;
regnet = 18655;
};
effectivePort = if cfg.port == null then networkPorts.${cfg.network} else cfg.port;
# dcrd requires settings to be lowercase
lowercaseKeys = attrs: lib.mapAttrs' (k: v: { name = lib.strings.toLower k; value = v; }) attrs;
baseAppOptions =
{}
// (if cfg.network == "testnet" then { testnet = true; }
else if cfg.network == "simnet" then { simnet = true; }
else if cfg.network == "regnet" then { regnet = true; }
else {})
// lib.optionalAttrs (cfg.port != null || cfg.listenAddress != "0.0.0.0") {
listen = "${cfg.listenAddress}:${toString effectivePort}";
};
combinedAppOptions = lib.recursiveUpdate baseAppOptions (lowercaseKeys cfg.settings);
finalSettings = { "Application Options" = combinedAppOptions; };
generatedConfig = settingsFormat.generate "dcrd.conf" finalSettings;
in
{
options.services.dcrd = with lib; {
enable = mkEnableOption "Decred daemon";
package = mkOption {
type = types.package;
default = pkgs.dcrd;
description = "Package providing the dcrd binary";
};
network = mkOption {
type = types.enum [ "mainnet" "testnet" "simnet" "regnet" ];
default = "mainnet";
description = "Select which network to run: mainnet, testnet, simnet, or regnet";
};
port = mkOption {
type = types.nullOr types.port;
default = null;
description = "P2P port override. Null uses the default for the selected network";
};
listenAddress = mkOption {
type = types.str;
default = "0.0.0.0";
description = "IP address to bind to listen for connections";
};
openFirewall = mkOption {
type = types.bool;
default = true;
description = "Open P2P port in the firewall";
};
configFile = mkOption {
type = types.nullOr types.path;
default = null;
description = "Absolute path to a dcrd.conf to use instead of the generated config. When set, the generated settings are ignored.";
};
user = mkOption {
type = types.str;
default = "dcrd";
description = "User to run dcrd as";
};
group = mkOption {
type = types.str;
default = cfg.user;
description = "Group to run dcrd as";
};
settings = mkOption {
type = types.submodule {
freeformType = types.attrsOf settingsFormat.lib.types.atom;
};
default = {};
example = {
externalip = [ "203.0.113.42" ];
rpclisten = [ "127.0.0.1:9109" "192.168.1.100:9109" ];
rpcuser = "dcrd";
rpcpass = "hunter2";
txindex = true;
proxy = "127.0.0.1:9050";
};
description = ''
Options that accept multiple values (like rpclisten, externalip,
listen, addpeer, miningaddr, altdnsnames, whitelist) should be
specified as lists.
Single-value options (like rpcuser, rpcpass, proxy, txindex)
should be specified as strings or booleans.
'';
};
};
config = lib.mkIf cfg.enable {
users.groups.${cfg.group} = {};
users.users.${cfg.user} = {
isSystemUser = true;
group = cfg.group;
description = "Decred daemon user";
};
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ effectivePort ];
systemd.services.dcrd = {
description = "Decred full node";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = cfg.user;
Group = cfg.group;
StateDirectory = "dcrd";
StateDirectoryMode = "0750";
ExecStart = ''${lib.getExe cfg.package} --appdata=/var/lib/dcrd --configfile=${if cfg.configFile != null then cfg.configFile else generatedConfig}'';
Restart = "on-failure";
RestartSec = 5;
};
restartTriggers = [ generatedConfig ] ++ lib.optional (cfg.configFile != null) cfg.configFile;
};
};
}

122
modules/dcrwallet.nix Normal file
View File

@ -0,0 +1,122 @@
{ config, lib, pkgs, ... }:
let
dcrdEnabled = config.services.dcrd.enable or false;
cfg = config.services.dcrwallet;
in
{
options.services.dcrwallet = with lib; {
enable = mkEnableOption "Decred wallet daemon";
package = mkOption {
type = types.package;
default = pkgs.dcrwallet;
description = "Package providing the dcrwallet binary";
};
user = mkOption {
type = types.str;
default = "dcrwallet";
description = "User to run dcrwallet as";
};
group = mkOption {
type = types.str;
default = cfg.user;
description = "Group to run dcrwallet as";
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/dcrwallet";
description = "State directory for dcrwallet (appdata)";
};
configFile = mkOption {
type = types.path;
description = "Path to dcrwallet configuration file (runtime path)";
};
extraPackages = mkOption {
type = types.listOf types.package;
default = [];
example = [ pkgs.dcrctl ];
description = "Additional packages to add to the dcrwallet user's environment";
};
operator = {
enable = mkEnableOption "Install dcrwallet CLI packages for an operator user";
name = mkOption {
type = types.str;
default = "operator";
description = "Operator username to receive dcrwallet package(s) in their environment.";
};
};
};
config = lib.mkIf cfg.enable (lib.mkMerge [
{
users.users.${cfg.user} = {
group = cfg.group;
home = cfg.dataDir;
isNormalUser = true;
packages = [ cfg.package ] ++ cfg.extraPackages;
# dcrwallet needs read access to the dcrd RPC certificate
extraGroups = lib.optional (dcrdEnabled && config.services.dcrd.group != cfg.group) config.services.dcrd.group;
};
users.groups.${cfg.group} = {};
systemd.tmpfiles.rules = [
"d ${cfg.dataDir} 0750 ${cfg.user} ${cfg.group} -"
];
systemd.services.dcrwallet = {
description = "Decred wallet daemon";
after = [ "network-online.target" ] ++ lib.optional dcrdEnabled "dcrd.service";
wants = [ "network-online.target" ] ++ lib.optional dcrdEnabled "dcrd.service";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = cfg.user;
Group = cfg.group;
RuntimeDirectory = "dcrwallet";
StateDirectory = "dcrwallet";
StateDirectoryMode = "0750";
ExecStart = ''${lib.getExe cfg.package} --appdata=${cfg.dataDir} --configfile=${cfg.configFile}'';
};
restartTriggers = [ cfg.configFile ];
};
}
(lib.mkIf cfg.operator.enable {
users.users.${cfg.operator.name} = {
packages = [ cfg.package ] ++ cfg.extraPackages;
# Add the operator user to the dcrwallet group and dcrd group
extraGroups = [ cfg.group ] ++ lib.optional (dcrdEnabled && config.services.dcrd.group != cfg.group) config.services.dcrd.group;
};
# Ensure operator (a member of cfg.group) can read rpc.cert which dcrwallet
# writes with mode 0600 by default. We watch for the file and set g+r.
systemd.services.dcrwallet-cert-perms = {
description = "Ensure group-read on dcrwallet rpc.cert";
after = [ "dcrwallet.service" ];
serviceConfig = {
Type = "oneshot";
};
script = ''
if [ -e ${cfg.dataDir}/rpc.cert ]; then
${pkgs.coreutils}/bin/chmod g+r ${cfg.dataDir}/rpc.cert
fi
'';
};
systemd.paths.dcrwallet-cert-perms = {
description = "Watch dcrwallet rpc.cert to fix permissions";
pathConfig = {
# PathModified = "${cfg.dataDir}/rpc.cert";
PathChanged = "${cfg.dataDir}/rpc.cert";
};
wantedBy = [ "multi-user.target" ];
};
})
]);
}

32
pkgs/dcrctl.nix Normal file
View File

@ -0,0 +1,32 @@
{
lib,
buildGoModule,
fetchFromGitHub,
}:
buildGoModule (finalAttrs: {
pname = "dcrctl";
version = "2.0.6";
src = fetchFromGitHub {
owner = "decred";
repo = "dcrctl";
rev = "release-v${finalAttrs.version}";
hash = "sha256-TxXPPe4AUEck3dFS0+TJgEPenAP43UOwqKWK/3unzjA=";
};
vendorHash = "sha256-YctyhZV6qf8xPnjrkkOwerazKHVOUAkpKe7kV3t6Tis=";
ldflags = [
"-s"
"-w"
];
meta = {
homepage = "https://decred.org";
description = "Decred CLI tool";
license = with lib.licenses; [ isc ];
mainProgram = "dcrctl";
};
})

37
pkgs/dcrd.nix Normal file
View File

@ -0,0 +1,37 @@
{
lib,
buildGoModule,
fetchFromGitHub,
}:
buildGoModule rec {
pname = "dcrd";
version = "2.0.6";
src = fetchFromGitHub {
owner = "decred";
repo = "dcrd";
rev = "dabb3760c7ae45e42ee55f0d86dd8def61b41a69";
hash = "sha256-mSq4SRSnZOoCuRKVwmb8Y6+KbaTtg+DLf4YX5oApx0k=";
};
vendorHash = "sha256-kzb8qh1j2+TlX+et0RSq5qU1LHSEs3Kaf0nHOnGjdd0=";
subPackages = [
"."
"cmd/promptsecret"
];
# Keep tests from writing outside the sandbox
preCheck = ''
export DCRD_APPDATA="$TMPDIR"
'';
meta = {
homepage = "https://decred.org";
description = "Decred daemon in Go (golang)";
license = with lib.licenses; [ isc ];
mainProgram = "dcrd";
};
}

35
pkgs/dcrwallet.nix Normal file
View File

@ -0,0 +1,35 @@
{
lib,
buildGoModule,
fetchFromGitHub,
}:
buildGoModule (finalAttrs: {
pname = "dcrwallet";
version = "2.0.6";
src = fetchFromGitHub {
owner = "decred";
repo = "dcrwallet";
rev = "release-v${finalAttrs.version}";
hash = "sha256-MrQrDip8vE0l5XHkx/zIegSZd/AkWq1aFZLUVPdMy50=";
};
vendorHash = "sha256-Ulh6RxK+PvS70mJ7TYiGMzKFsR79+asWuQ5W1FAI23I=";
subPackages = [ "." ];
checkFlags = [
# Test fails with:
# 'x509_test.go:201: server did not report bad certificate error;
# instead errored with [...] tls: unknown certificate authority (*url.Error)'
"-skip=^TestUntrustedClientCert$"
];
meta = {
homepage = "https://decred.org";
description = "Decred wallet daemon written in Go (golang)";
license = with lib.licenses; [ isc ];
mainProgram = "dcrwallet";
};
})