nix-bitcoin/modules/bitcoind.nix

464 lines
16 KiB
Nix
Raw Normal View History

2018-11-23 00:46:56 +00:00
{ config, pkgs, lib, ... }:
2018-11-13 23:44:54 +00:00
with lib;
let
2018-11-23 00:46:56 +00:00
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.";
};
onionPort = mkOption {
type = types.nullOr types.port;
# When the bitcoind onion service is enabled, add an onion-tagged socket
# to distinguish local connections from Tor connections
default = if (config.nix-bitcoin.onionServices.bitcoind.enable or false) then 8334 else null;
description = ''
Port to listen for Tor peer connections.
If set, inbound connections to this port are tagged as onion peers.
'';
};
listen = mkOption {
type = types.bool;
default = false;
description = ''
Listen for peer connections at `address:port`
and `address:onionPort` (if `onionPort` is set).
'';
};
listenWhitelisted = mkOption {
type = types.bool;
default = false;
description = ''
Listen for peer connections at `address:whitelistedPort`.
Peers connected through this socket are automatically whitelisted.
'';
};
whitelistedPort = mkOption {
type = types.port;
default = 8335;
description = "See `listenWhitelisted`.";
};
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 00:46:56 +00:00
package = mkOption {
type = types.package;
default = config.nix-bitcoin.pkgs.bitcoind;
2018-11-23 00:46:56 +00: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 00:46:56 +00:00
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/bitcoind";
description = "The data directory for bitcoind.";
};
rpc = {
address = mkOption {
type = types.str;
default = "127.0.0.1";
description = ''
Address to listen for JSON-RPC connections.
'';
};
2018-11-23 00:46:56 +00:00
port = mkOption {
2020-06-02 15:09:52 +00:00
type = types.port;
default = 8332;
description = "Port to listen for JSON-RPC connections.";
2018-11-23 00:46:56 +00: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 00:46:56 +00: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 00:46:56 +00:00
description = ''
2020-08-04 13:32:06 +00:00
RPC user information for JSON-RPC connections.
2018-11-23 00:46:56 +00:00
'';
};
};
regtest = mkOption {
2018-11-23 00:46:56 +00: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 00:46:56 +00:00
};
proxy = mkOption {
type = types.nullOr types.str;
2021-08-04 22:49:00 +00:00
default = if cfg.enforceTor then config.nix-bitcoin.torClientAddressWithPort else null;
2018-11-23 00:46:56 +00:00
description = "Connect through SOCKS5 proxy";
};
2021-10-10 13:14:38 +00:00
i2p = mkOption {
type = types.enum [ false true "only-outgoing" ];
default = false;
description = ''
Enable peer connections via i2p.
With `only-outgoing`, incoming i2p connections are disabled.
'';
};
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-07 21:05:02 +00: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-07 21:05:02 +00:00
Do not load the wallet and disable wallet RPC calls
'';
};
2018-11-23 00:46:56 +00: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 00:46:56 +00:00
};
prune = mkOption {
type = types.ints.unsigned;
default = 0;
2018-11-23 00:46:56 +00:00
example = 10000;
description = ''
Automatically prune block files to stay under the specified target size in MiB.
Value 0 disables pruning.
2018-11-23 00:46:56 +00:00
'';
};
2019-08-05 08:44:38 +00:00
zmqpubrawblock = mkOption {
type = types.nullOr types.str;
2019-08-05 08:44:38 +00: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 08:44:38 +00: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";
};
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.";
};
2019-11-12 18:40:30 +00:00
cli = mkOption {
readOnly = true;
type = types.package;
2019-11-12 18:40:30 +00: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 18:40:30 +00:00
};
enforceTor = nbLib.enforceTor;
};
2018-11-13 23:44:54 +00:00
};
2018-11-23 00:46:56 +00:00
cfg = config.services.bitcoind;
nbLib = config.nix-bitcoin.lib;
secretsDir = config.nix-bitcoin.secretsDir;
2021-10-10 13:14:38 +00:00
i2pSAM = config.services.i2pd.proto.sam;
configFile = builtins.toFile "bitcoin.conf" ''
# We're already logging via journald
nodebuglogfile=1
startupnotify=/run/current-system/systemd/bin/systemd-notify --ready
${optionalString cfg.regtest ''
regtest=1
[regtest]
''}
${optionalString (cfg.dbCache != null) "dbcache=${toString cfg.dbCache}"}
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}"}
# Connection options
listen=${if (cfg.listen || cfg.listenWhitelisted) then "1" else "0"}
${optionalString cfg.listen
"bind=${cfg.address}:${toString cfg.port}"}
${optionalString (cfg.listen && cfg.onionPort != null)
"bind=${cfg.address}:${toString cfg.onionPort}=onion"}
${optionalString cfg.listenWhitelisted
"whitebind=${cfg.address}:${toString cfg.whitelistedPort}"}
${optionalString (cfg.proxy != null) "proxy=${cfg.proxy}"}
2021-10-10 13:14:38 +00:00
${optionalString (cfg.i2p != false) "i2psam=${nbLib.addressWithPort i2pSAM.address i2pSAM.port}"}
${optionalString (cfg.i2p == "only-outgoing") "i2pacceptincoming=0"}
${optionalString (cfg.discover != null) "discover=${if cfg.discover then "1" else "0"}"}
${lib.concatMapStrings (node: "addnode=${node}\n") cfg.addnodes}
# 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)
}
${lib.concatMapStrings (rpcallowip: "rpcallowip=${rpcallowip}\n") cfg.rpc.allowip}
# 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}"}
# Extra options
${cfg.extraConfig}
'';
zmqServerEnabled = (cfg.zmqpubrawblock != null) || (cfg.zmqpubrawtx != null);
in {
inherit options;
2018-11-13 23:44:54 +00: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;
};
}
];
2021-10-10 13:14:38 +00:00
services.i2pd = mkIf (cfg.i2p != false) {
enable = true;
proto.sam.enable = true;
};
systemd.tmpfiles.rules = [
"d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -"
"d '${cfg.dataDir}/blocks' 0770 ${cfg.user} ${cfg.group} - -"
];
2018-11-22 18:49:53 +00:00
systemd.services.bitcoind = {
# Use `wants` instead of `requires` so that bitcoind and all dependent services
# are not restarted when the secrets target restarts.
# The secrets target always restarts when deploying with one of the methods
# in ./deployment.
wants = [ "nix-bitcoin-secrets.target" ];
after = [ "network.target" "nix-bitcoin-secrets.target" ];
2018-11-22 18:49:53 +00: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 19:15:31 +00:00
${optionalString cfg.dataDirReadableByGroup "chmod -R g+rX '${cfg.dataDir}/blocks'"}
cfg=$(
2021-02-01 21:53:10 +00:00
cat ${configFile}
${extraRpcauth}
echo
${optionalString (cfg.getPublicAddressCmd != "") ''
echo "externalip=$(${cfg.getPublicAddressCmd})"
''}
)
2020-04-07 21:05:09 +00:00
confFile='${cfg.dataDir}/bitcoin.conf'
if [[ ! -e $confFile || $cfg != $(cat $confFile) ]]; then
2020-08-26 19:15:31 +00:00
install -o '${cfg.user}' -g '${cfg.group}' -m 640 <(echo "$cfg") $confFile
2020-04-07 21:05:09 +00:00
fi
2018-11-23 00:46:56 +00: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 = "5min";
TimeoutStopSec = "10min";
2020-04-07 21:04:58 +00:00
ExecStart = "${cfg.package}/bin/bitcoind -datadir='${cfg.dataDir}'";
2018-11-22 18:49:53 +00:00
Restart = "on-failure";
UMask = mkIf cfg.dataDirReadableByGroup "0027";
ReadWritePaths = cfg.dataDir;
} // nbLib.allowedIPAddresses cfg.enforceTor
// optionalAttrs zmqServerEnabled nbLib.allowNetlink;
};
# 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.allowLocalIPAddresses;
};
users.users.${cfg.user} = {
isSystemUser = true;
group = cfg.group;
};
2019-11-27 13:04:15 +00:00
users.groups.${cfg.group} = {};
users.groups.bitcoinrpc-public = {};
2020-09-28 11:09:03 +00:00
nix-bitcoin.operator.groups = [ cfg.group ];
nix-bitcoin.secrets = {
bitcoin-rpcpassword-privileged.user = cfg.user;
bitcoin-rpcpassword-public = {
user = cfg.user;
group = "bitcoinrpc-public";
};
bitcoin-HMAC-privileged.user = cfg.user;
bitcoin-HMAC-public.user = cfg.user;
};
nix-bitcoin.generateSecretsCmds.bitcoind = ''
makeBitcoinRPCPassword privileged
makeBitcoinRPCPassword public
'';
2018-11-22 18:49:53 +00:00
};
2018-11-13 23:44:54 +00:00
}