bbebd0b383
NixOS will soon use CommonMark Markdown for option descriptions. We can then switch back the old, slightly clearer bracket syntax.
474 lines
16 KiB
Nix
474 lines
16 KiB
Nix
{ config, pkgs, lib, ... }:
|
|
|
|
with lib;
|
|
let
|
|
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.
|
|
'';
|
|
};
|
|
package = mkOption {
|
|
type = types.package;
|
|
default = config.nix-bitcoin.pkgs.bitcoind;
|
|
defaultText = "config.nix-bitcoin.pkgs.bitcoind";
|
|
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>.";
|
|
};
|
|
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.
|
|
'';
|
|
};
|
|
port = mkOption {
|
|
type = types.port;
|
|
default = 8332;
|
|
description = "Port to listen for JSON-RPC connections.";
|
|
};
|
|
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.
|
|
'';
|
|
};
|
|
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.
|
|
'';
|
|
};
|
|
};
|
|
}));
|
|
description = ''
|
|
RPC user information for JSON-RPC connections.
|
|
'';
|
|
};
|
|
};
|
|
regtest = mkOption {
|
|
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;
|
|
};
|
|
proxy = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = if cfg.tor.proxy then config.nix-bitcoin.torClientAddressWithPort else null;
|
|
description = "Connect through SOCKS5 proxy";
|
|
};
|
|
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 = ''
|
|
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 = ''
|
|
Do not load the wallet and disable wallet RPC calls
|
|
'';
|
|
};
|
|
dbCache = mkOption {
|
|
type = types.nullOr (types.ints.between 4 16384);
|
|
default = null;
|
|
example = 4000;
|
|
description = "Override the default database cache size in MiB.";
|
|
};
|
|
prune = mkOption {
|
|
type = types.ints.unsigned;
|
|
default = 0;
|
|
example = 10000;
|
|
description = ''
|
|
Automatically prune block files to stay under the specified target size in MiB.
|
|
Value 0 disables pruning.
|
|
'';
|
|
};
|
|
txindex = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = "Enable the transaction index.";
|
|
};
|
|
zmqpubrawblock = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = null;
|
|
example = "tcp://127.0.0.1:28332";
|
|
description = "ZMQ address for zmqpubrawblock notifications";
|
|
};
|
|
zmqpubrawtx = mkOption {
|
|
type = types.nullOr types.str;
|
|
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.";
|
|
};
|
|
cli = mkOption {
|
|
readOnly = true;
|
|
type = types.package;
|
|
default = pkgs.writeScriptBin "bitcoin-cli" ''
|
|
exec ${cfg.package}/bin/bitcoin-cli -datadir='${cfg.dataDir}' "$@"
|
|
'';
|
|
defaultText = "(See source)";
|
|
description = "Binary to connect with the bitcoind instance.";
|
|
};
|
|
tor = nbLib.tor;
|
|
};
|
|
};
|
|
|
|
cfg = config.services.bitcoind;
|
|
nbLib = config.nix-bitcoin.lib;
|
|
secretsDir = config.nix-bitcoin.secretsDir;
|
|
|
|
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.txindex "txindex=1"}
|
|
${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}"}
|
|
${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;
|
|
|
|
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;
|
|
};
|
|
}
|
|
];
|
|
|
|
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} - -"
|
|
];
|
|
|
|
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" ];
|
|
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 ''
|
|
${optionalString cfg.dataDirReadableByGroup "chmod -R g+rX '${cfg.dataDir}/blocks'"}
|
|
cfg=$(
|
|
cat ${configFile}
|
|
${extraRpcauth}
|
|
echo
|
|
${optionalString (cfg.getPublicAddressCmd != "") ''
|
|
echo "externalip=$(${cfg.getPublicAddressCmd})"
|
|
''}
|
|
)
|
|
confFile='${cfg.dataDir}/bitcoin.conf'
|
|
if [[ ! -e $confFile || $cfg != $(cat $confFile) ]]; then
|
|
install -o '${cfg.user}' -g '${cfg.group}' -m 640 <(echo "$cfg") $confFile
|
|
fi
|
|
'';
|
|
|
|
# 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";
|
|
ExecStart = "${cfg.package}/bin/bitcoind -datadir='${cfg.dataDir}'";
|
|
Restart = "on-failure";
|
|
UMask = mkIf cfg.dataDirReadableByGroup "0027";
|
|
ReadWritePaths = cfg.dataDir;
|
|
} // nbLib.allowedIPAddresses cfg.tor.enforce
|
|
// 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;
|
|
};
|
|
users.groups.${cfg.group} = {};
|
|
users.groups.bitcoinrpc-public = {};
|
|
|
|
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
|
|
'';
|
|
};
|
|
}
|