nix-bitcoin/modules/bitcoind.nix

400 lines
14 KiB
Nix
Raw Normal View History

2018-11-23 03:46:56 +03:00
{ config, pkgs, lib, ... }:
2018-11-14 02:44:54 +03:00
with lib;
let
2018-11-23 03:46:56 +03:00
cfg = config.services.bitcoind;
nbLib = config.nix-bitcoin.lib;
secretsDir = config.nix-bitcoin.secretsDir;
2020-04-08 00:05:02 +03:00
configFile = builtins.toFile "bitcoin.conf" ''
2020-04-08 00:05:05 +03:00
# We're already logging via journald
nodebuglogfile=1
startupnotify=/run/current-system/systemd/bin/systemd-notify --ready
${optionalString cfg.regtest ''
regtest=1
[regtest]
''}
2018-11-23 03:46:56 +03:00
${optionalString (cfg.dbCache != null) "dbcache=${toString cfg.dbCache}"}
2020-07-28 17:16:22 +03:00
prune=${toString cfg.prune}
${optionalString (cfg.sysperms != null) "sysperms=${if cfg.sysperms then "1" else "0"}"}
${optionalString (cfg.disablewallet != null) "disablewallet=${if cfg.disablewallet then "1" else "0"}"}
${optionalString (cfg.assumevalid != null) "assumevalid=${cfg.assumevalid}"}
2018-11-23 03:46:56 +03:00
# Connection options
${optionalString cfg.listen "bind=${cfg.address}${optionalString cfg.enforceTor "=onion"}"}
port=${toString cfg.port}
2018-11-23 03:46:56 +03:00
${optionalString (cfg.proxy != null) "proxy=${cfg.proxy}"}
listen=${if cfg.listen then "1" else "0"}
${optionalString (cfg.discover != null) "discover=${if cfg.discover then "1" else "0"}"}
${lib.concatMapStrings (node: "addnode=${node}\n") cfg.addnodes}
2018-11-23 03:46:56 +03:00
# RPC server options
rpcbind=${cfg.rpc.address}
rpcport=${toString cfg.rpc.port}
rpcconnect=${cfg.rpc.address}
${optionalString (cfg.rpc.threads != null) "rpcthreads=${toString cfg.rpc.threads}"}
rpcwhitelistdefault=0
${concatMapStrings (user: ''
${optionalString (!user.passwordHMACFromFile) "rpcauth=${user.name}:${passwordHMAC}"}
${optionalString (user.rpcwhitelist != [])
"rpcwhitelist=${user.name}:${lib.strings.concatStringsSep "," user.rpcwhitelist}"}
'') (builtins.attrValues cfg.rpc.users)
2018-11-23 03:46:56 +03:00
}
${lib.concatMapStrings (rpcallowip: "rpcallowip=${rpcallowip}\n") cfg.rpc.allowip}
2018-11-23 03:46:56 +03:00
# Wallet options
${optionalString (cfg.addresstype != null) "addresstype=${cfg.addresstype}"}
# ZMQ options
${optionalString (cfg.zmqpubrawblock != null) "zmqpubrawblock=${cfg.zmqpubrawblock}"}
${optionalString (cfg.zmqpubrawtx != null) "zmqpubrawtx=${cfg.zmqpubrawtx}"}
2020-04-08 00:05:03 +03:00
# Extra options
2018-11-23 03:46:56 +03:00
${cfg.extraConfig}
'';
in {
options = {
services.bitcoind = {
enable = mkEnableOption "Bitcoin daemon";
address = mkOption {
type = types.str;
default = "127.0.0.1";
description = "Address to listen for peer connections.";
};
port = mkOption {
type = types.port;
default = 8333;
description = "Port to listen for peer connections.";
};
getPublicAddressCmd = mkOption {
type = types.str;
default = "";
description = ''
Bash expression which outputs the public service address to announce to peers.
If left empty, no address is announced.
'';
};
2018-11-23 03:46:56 +03:00
package = mkOption {
type = types.package;
default = config.nix-bitcoin.pkgs.bitcoind;
2019-10-02 12:32:12 +03:00
defaultText = "pkgs.blockchains.bitcoind";
2018-11-23 03:46:56 +03:00
description = "The package providing bitcoin binaries.";
};
extraConfig = mkOption {
type = types.lines;
default = "";
example = ''
par=16
logips=1
'';
description = "Extra lines appended to <filename>bitcoin.conf</filename>.";
2018-11-23 03:46:56 +03:00
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/bitcoind";
description = "The data directory for bitcoind.";
};
user = mkOption {
type = types.str;
default = "bitcoin";
description = "The user as which to run bitcoind.";
};
group = mkOption {
type = types.str;
default = cfg.user;
description = "The group as which to run bitcoind.";
};
rpc = {
address = mkOption {
type = types.str;
default = "127.0.0.1";
description = ''
Address to listen for JSON-RPC connections.
'';
};
2018-11-23 03:46:56 +03:00
port = mkOption {
2020-06-02 18:09:52 +03:00
type = types.port;
default = 8332;
description = "Port to listen for JSON-RPC connections.";
2018-11-23 03:46:56 +03:00
};
threads = mkOption {
type = types.nullOr types.ints.u16;
default = null;
description = "The number of threads to service RPC calls.";
};
allowip = mkOption {
type = types.listOf types.str;
default = [ "127.0.0.1" ];
description = ''
Allow JSON-RPC connections from specified sources.
'';
};
2018-11-23 03:46:56 +03:00
users = mkOption {
default = {};
example = {
alice.passwordHMAC = "f7efda5c189b999524f151318c0c86$d5b51b3beffbc02b724e5d095828e0bc8b2456e9ac8757ae3211a5d9b16a22ae";
bob.passwordHMAC = "b2dd077cb54591a2f3139e69a897ac$4e71f08d48b4347cf8eff3815c0e25ae2e9a4340474079f55705f40574f4ec99";
};
type = with types; attrsOf (submodule ({ name, ... }: {
options = {
name = mkOption {
type = types.str;
default = name;
example = "alice";
description = ''
Username for JSON-RPC connections.
'';
};
passwordHMAC = mkOption {
type = types.str;
example = "f7efda5c189b999524f151318c0c86$d5b51b3beffbc02b724e5d095828e0bc8b2456e9ac8757ae3211a5d9b16a22ae";
description = ''
Password HMAC-SHA-256 for JSON-RPC connections. Must be a string of the
format <SALT-HEX>$<HMAC-HEX>.
'';
};
passwordHMACFromFile = mkOption {
type = lib.types.bool;
internal = true;
default = false;
};
rpcwhitelist = mkOption {
type = types.listOf types.str;
default = [];
description = ''
List of allowed rpc calls for each user.
If empty list, rpcwhitelist is disabled for that user.
'';
};
};
}));
2018-11-23 03:46:56 +03:00
description = ''
2020-08-04 16:32:06 +03:00
RPC user information for JSON-RPC connections.
2018-11-23 03:46:56 +03:00
'';
};
};
regtest = mkOption {
2018-11-23 03:46:56 +03:00
type = types.bool;
default = false;
description = "Enable regtest mode.";
};
network = mkOption {
readOnly = true;
default = if cfg.regtest then "regtest" else "mainnet";
};
makeNetworkName = mkOption {
readOnly = true;
default = mainnet: regtest: if cfg.regtest then regtest else mainnet;
2018-11-23 03:46:56 +03:00
};
proxy = mkOption {
type = types.nullOr types.str;
default = if cfg.enforceTor then config.services.tor.client.socksListenAddress else null;
2018-11-23 03:46:56 +03:00
description = "Connect through SOCKS5 proxy";
};
listen = mkOption {
type = types.bool;
default = false;
description = "Accept incoming connections.";
2018-11-23 03:46:56 +03:00
};
dataDirReadableByGroup = mkOption {
type = types.bool;
default = false;
description = ''
If enabled, data dir content is readable by the bitcoind service group.
Warning: This disables bitcoind's wallet support.
'';
};
sysperms = mkOption {
type = types.nullOr types.bool;
default = null;
description = ''
2020-04-08 00:05:02 +03:00
Create new files with system default permissions, instead of umask 077
(only effective with disabled wallet functionality)
'';
};
disablewallet = mkOption {
type = types.nullOr types.bool;
default = null;
description = ''
2020-04-08 00:05:02 +03:00
Do not load the wallet and disable wallet RPC calls
'';
};
2018-11-23 03:46:56 +03:00
dbCache = mkOption {
type = types.nullOr (types.ints.between 4 16384);
default = null;
example = 4000;
description = "Override the default database cache size in MiB.";
2018-11-23 03:46:56 +03:00
};
prune = mkOption {
type = types.ints.unsigned;
default = 0;
2018-11-23 03:46:56 +03:00
example = 10000;
description = ''
Automatically prune block files to stay under the specified target size in MiB.
Value 0 disables pruning.
2018-11-23 03:46:56 +03:00
'';
};
2019-08-05 11:44:38 +03:00
zmqpubrawblock = mkOption {
type = types.nullOr types.str;
2019-08-05 11:44:38 +03:00
default = null;
example = "tcp://127.0.0.1:28332";
description = "ZMQ address for zmqpubrawblock notifications";
};
zmqpubrawtx = mkOption {
type = types.nullOr types.str;
2019-08-05 11:44:38 +03:00
default = null;
example = "tcp://127.0.0.1:28333";
description = "ZMQ address for zmqpubrawtx notifications";
};
assumevalid = mkOption {
type = types.nullOr types.str;
default = null;
example = "00000000000000000000e5abc3a74fe27dc0ead9c70ea1deb456f11c15fd7bc6";
description = ''
If this block is in the chain assume that it and its ancestors are
valid and potentially skip their script verification.
'';
};
addnodes = mkOption {
type = types.listOf types.str;
default = [];
example = [ "ecoc5q34tmbq54wl.onion" ];
description = "Add nodes to connect to and attempt to keep the connections open";
};
discover = mkOption {
type = types.nullOr types.bool;
default = null;
description = "Discover own IP addresses";
};
addresstype = mkOption {
type = types.nullOr types.str;
default = null;
example = "bech32";
description = "The type of addresses to use";
};
2019-11-12 21:40:30 +03:00
cli = mkOption {
readOnly = true;
type = types.package;
2019-11-12 21:40:30 +03:00
default = pkgs.writeScriptBin "bitcoin-cli" ''
exec ${cfg.package}/bin/bitcoin-cli -datadir='${cfg.dataDir}' "$@"
'';
description = "Binary to connect with the bitcoind instance.";
2019-11-12 21:40:30 +03:00
};
enforceTor = nbLib.enforceTor;
};
2018-11-14 02:44:54 +03:00
};
2018-11-23 03:46:56 +03:00
2018-11-14 02:44:54 +03:00
config = mkIf cfg.enable {
environment.systemPackages = [ cfg.package (hiPrio cfg.cli) ];
services.bitcoind = mkMerge [
(mkIf cfg.dataDirReadableByGroup {
disablewallet = true;
sysperms = true;
})
{
rpc.users.privileged = {
passwordHMACFromFile = true;
};
rpc.users.public = {
passwordHMACFromFile = true;
rpcwhitelist = import ./bitcoind-rpc-public-whitelist.nix;
};
}
];
systemd.tmpfiles.rules = [
"d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -"
"d '${cfg.dataDir}/blocks' 0770 ${cfg.user} ${cfg.group} - -"
];
2018-11-22 21:49:53 +03:00
systemd.services.bitcoind = {
requires = [ "nix-bitcoin-secrets.target" ];
after = [ "network.target" "nix-bitcoin-secrets.target" ];
2018-11-22 21:49:53 +03:00
wantedBy = [ "multi-user.target" ];
preStart = let
extraRpcauth = concatMapStrings (name: let
user = cfg.rpc.users.${name};
in optionalString user.passwordHMACFromFile ''
echo "rpcauth=${user.name}:$(cat ${secretsDir}/bitcoin-HMAC-${name})"
''
) (builtins.attrNames cfg.rpc.users);
in ''
2020-08-26 22:15:31 +03:00
${optionalString cfg.dataDirReadableByGroup "chmod -R g+rX '${cfg.dataDir}/blocks'"}
cfg=$(
2021-02-02 00:53:10 +03:00
cat ${configFile}
${extraRpcauth}
echo
${optionalString (cfg.getPublicAddressCmd != "") ''
echo "externalip=$(${cfg.getPublicAddressCmd})"
''}
)
2020-04-08 00:05:09 +03:00
confFile='${cfg.dataDir}/bitcoin.conf'
if [[ ! -e $confFile || $cfg != $(cat $confFile) ]]; then
2020-08-26 22:15:31 +03:00
install -o '${cfg.user}' -g '${cfg.group}' -m 640 <(echo "$cfg") $confFile
2020-04-08 00:05:09 +03:00
fi
2018-11-23 03:46:56 +03:00
'';
# Enable RPC access for group
postStart = ''
chmod g=r '${cfg.dataDir}/${optionalString cfg.regtest "regtest/"}.cookie'
'';
serviceConfig = nbLib.defaultHardening // {
Type = "notify";
NotifyAccess = "all";
User = cfg.user;
Group = cfg.group;
TimeoutStartSec = 300;
2020-04-08 00:04:58 +03:00
ExecStart = "${cfg.package}/bin/bitcoind -datadir='${cfg.dataDir}'";
2018-11-22 21:49:53 +03:00
Restart = "on-failure";
UMask = mkIf cfg.dataDirReadableByGroup "0027";
ReadWritePaths = cfg.dataDir;
} // (if cfg.enforceTor
then nbLib.allowTor
else nbLib.allowAnyIP)
// optionalAttrs (cfg.zmqpubrawblock != null || cfg.zmqpubrawtx != null) nbLib.allowAnyProtocol;
};
# Use this to update the banlist:
# wget https://people.xiph.org/~greg/banlist.cli.txt
systemd.services.bitcoind-import-banlist = {
description = "Bitcoin daemon banlist importer";
wantedBy = [ "bitcoind.service" ];
bindsTo = [ "bitcoind.service" ];
after = [ "bitcoind.service" ];
script = ''
cd ${cfg.cli}/bin
echo "Importing node banlist..."
cat ${./banlist.cli.txt} | while read line; do
if ! err=$(eval "$line" 2>&1) && [[ $err != *already\ banned* ]]; then
# unexpected error
echo "$err"
exit 1
fi
done
'';
serviceConfig = nbLib.defaultHardening // {
User = cfg.user;
Group = cfg.group;
ReadWritePaths = cfg.dataDir;
} // nbLib.allowTor;
};
users.users.${cfg.user}.group = cfg.group;
2019-11-27 16:04:15 +03:00
users.groups.${cfg.group} = {};
users.groups.bitcoinrpc-public = {};
2020-09-28 14:09:03 +03:00
nix-bitcoin.operator.groups = [ cfg.group ];
nix-bitcoin.secrets.bitcoin-rpcpassword-privileged.user = cfg.user;
nix-bitcoin.secrets.bitcoin-rpcpassword-public = {
user = cfg.user;
group = "bitcoinrpc-public";
};
nix-bitcoin.secrets.bitcoin-HMAC-privileged.user = cfg.user;
nix-bitcoin.secrets.bitcoin-HMAC-public.user = cfg.user;
2018-11-22 21:49:53 +03:00
};
2018-11-14 02:44:54 +03:00
}