From c6017d9895bd0251b5c3ab780cd9932187b49cb2 Mon Sep 17 00:00:00 2001 From: 1000101 Date: Tue, 21 Jul 2020 13:43:17 +0200 Subject: [PATCH 1/4] nixos/bitcoind: change to multi-instance --- .../modules/services/networking/bitcoind.nix | 198 ++++++++++++------ 1 file changed, 133 insertions(+), 65 deletions(-) diff --git a/nixos/modules/services/networking/bitcoind.nix b/nixos/modules/services/networking/bitcoind.nix index 4e00a8865474..4da719ac2f2d 100644 --- a/nixos/modules/services/networking/bitcoind.nix +++ b/nixos/modules/services/networking/bitcoind.nix @@ -3,31 +3,8 @@ with lib; let - cfg = config.services.bitcoind; - pidFile = "${cfg.dataDir}/bitcoind.pid"; - configFile = pkgs.writeText "bitcoin.conf" '' - ${optionalString cfg.testnet "testnet=1"} - ${optionalString (cfg.dbCache != null) "dbcache=${toString cfg.dbCache}"} - ${optionalString (cfg.prune != null) "prune=${toString cfg.prune}"} - # Connection options - ${optionalString (cfg.port != null) "port=${toString cfg.port}"} - - # RPC server options - ${optionalString (cfg.rpc.port != null) "rpcport=${toString cfg.rpc.port}"} - ${concatMapStringsSep "\n" - (rpcUser: "rpcauth=${rpcUser.name}:${rpcUser.passwordHMAC}") - (attrValues cfg.rpc.users) - } - - # Extra config options (from bitcoind nixos service) - ${cfg.extraConfig} - ''; - cmdlineOptions = escapeShellArgs [ - "-conf=${cfg.configFile}" - "-datadir=${cfg.dataDir}" - "-pid=${pidFile}" - ]; + eachBitcoind = config.services.bitcoind; rpcUserOpts = { name, ... }: { options = { @@ -44,6 +21,9 @@ let description = '' Password HMAC-SHA-256 for JSON-RPC connections. Must be a string of the format <SALT-HEX>$<HMAC-HEX>. + + Tool (Python script) for HMAC generation is available here: + ''; }; }; @@ -51,10 +31,10 @@ let name = mkDefault name; }; }; -in { - options = { - services.bitcoind = { + bitcoindOpts = { config, lib, name, ...}: { + options = { + enable = mkEnableOption "Bitcoin daemon"; package = mkOption { @@ -63,12 +43,14 @@ in { defaultText = "pkgs.bitcoind"; description = "The package providing bitcoin binaries."; }; + configFile = mkOption { - type = types.path; - default = configFile; - example = "/etc/bitcoind.conf"; + type = with types; nullOr path; + default = null; + example = "/var/lib/${name}/bitcoin.conf"; description = "The configuration file path to supply bitcoind."; }; + extraConfig = mkOption { type = types.lines; default = ""; @@ -79,20 +61,22 @@ in { ''; description = "Additional configurations to be appended to bitcoin.conf."; }; + dataDir = mkOption { type = types.path; - default = "/var/lib/bitcoind"; + default = "/var/lib/bitcoind-${name}"; description = "The data directory for bitcoind."; }; user = mkOption { type = types.str; - default = "bitcoin"; + default = "bitcoind-${name}"; description = "The user as which to run bitcoind."; }; + group = mkOption { type = types.str; - default = cfg.user; + default = config.user; description = "The group as which to run bitcoind."; }; @@ -110,29 +94,38 @@ in { bob.passwordHMAC = "b2dd077cb54591a2f3139e69a897ac$4e71f08d48b4347cf8eff3815c0e25ae2e9a4340474079f55705f40574f4ec99"; } ''; - type = with types; loaOf (submodule rpcUserOpts); + type = with types; attrsOf (submodule rpcUserOpts); description = '' RPC user information for JSON-RPC connnections. ''; }; }; + pidFile = mkOption { + type = types.path; + default = "${config.dataDir}/bitcoind.pid"; + description = "Location of bitcoind pid file."; + }; + testnet = mkOption { type = types.bool; default = false; - description = "Whether to use the test chain."; + description = "Whether to use the testnet instead of mainnet."; }; + port = mkOption { type = types.nullOr types.port; default = null; description = "Override the default port on which to listen for connections."; }; + dbCache = mkOption { type = types.nullOr (types.ints.between 4 16384); default = null; example = 4000; - description = "Override the default database cache size in megabytes."; + description = "Override the default database cache size in MiB."; }; + prune = mkOption { type = types.nullOr (types.coercedTo (types.enum [ "disable" "manual" ]) @@ -149,45 +142,120 @@ in { and -rescan. Warning: Reverting this setting requires re-downloading the entire blockchain. ("disable" = disable pruning blocks, "manual" = allow manual pruning via RPC, >=550 = automatically prune block files - to stay under the specified target size in MiB) + to stay under the specified target size in MiB). + ''; + }; + + extraCmdlineOptions = mkOption { + type = types.listOf types.str; + default = []; + description = '' + Extra command line options to pass to bitcoind. + Run bitcoind --help to list all available options. ''; }; }; }; +in +{ - config = mkIf cfg.enable { - environment.systemPackages = [ cfg.package ]; - systemd.tmpfiles.rules = [ - "d '${cfg.dataDir}' 0770 '${cfg.user}' '${cfg.group}' - -" - "L '${cfg.dataDir}/bitcoin.conf' - - - - '${cfg.configFile}'" - ]; - systemd.services.bitcoind = { - description = "Bitcoin daemon"; - after = [ "network.target" ]; - wantedBy = [ "multi-user.target" ]; - serviceConfig = { - User = cfg.user; - Group = cfg.group; - ExecStart = "${cfg.package}/bin/bitcoind ${cmdlineOptions}"; - Restart = "on-failure"; - - # Hardening measures - PrivateTmp = "true"; - ProtectSystem = "full"; - NoNewPrivileges = "true"; - PrivateDevices = "true"; - MemoryDenyWriteExecute = "true"; - }; + options = { + services.bitcoind = mkOption { + type = types.attrsOf (types.submodule bitcoindOpts); + default = {}; + description = "Specification of one or more bitcoind instances."; }; - users.users.${cfg.user} = { + }; + + config = mkIf (eachBitcoind != {}) { + + assertions = flatten (mapAttrsToList (bitcoindName: cfg: [ + { + assertion = (cfg.prune != null) -> (cfg.prune == "disable" || cfg.prune == "manual") || (cfg.prune == 0 || cfg.prune == 1) || (cfg.prune >= 550); + message = '' + If set, services.bitcoind.${bitcoindName}.prune has to be "disable", "manual", 0 , 1 or >= 550 + ''; + } + { + assertion = (cfg.rpc.users != {}) -> (cfg.configFile == null); + message = '' + You cannot set both services.bitcoind.${bitcoindName}.rpc.users and services.bitcoind.${bitcoindName}.configFile + as they are exclusive. RPC user setting would have no effect if custom configFile would be used. + ''; + } + ]) eachBitcoind); + + environment.systemPackages = flatten (mapAttrsToList (bitcoindName: cfg: [ + cfg.package + ]) eachBitcoind); + + systemd.services = mapAttrs' (bitcoindName: cfg: ( + nameValuePair "bitcoind-${bitcoindName}" ( + let + configFile = pkgs.writeText "bitcoin.conf" '' + # If Testnet is enabled, we need to add [test] section + # otherwise, some options (e.g.: custom RPC port) will not work + ${optionalString cfg.testnet "[test]"} + # RPC users + ${concatMapStringsSep "\n" + (rpcUser: "rpcauth=${rpcUser.name}:${rpcUser.passwordHMAC}") + (attrValues cfg.rpc.users) + } + # Extra config options (from bitcoind nixos service) + ${cfg.extraConfig} + ''; + in { + description = "Bitcoin daemon"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + User = cfg.user; + Group = cfg.group; + ExecStart = '' + ${cfg.package}/bin/bitcoind \ + ${if (cfg.configFile != null) then + "-conf=${cfg.configFile}" + else + "-conf=${configFile}" + } \ + -datadir=${cfg.dataDir} \ + -pid=${cfg.pidFile} \ + ${optionalString cfg.testnet "-testnet"}\ + ${optionalString (cfg.port != null) "-port=${toString cfg.port}"}\ + ${optionalString (cfg.prune != null) "-prune=${toString cfg.prune}"}\ + ${optionalString (cfg.dbCache != null) "-dbcache=${toString cfg.dbCache}"}\ + ${optionalString (cfg.rpc.port != null) "-rpcport=${toString cfg.rpc.port}"}\ + ${toString cfg.extraCmdlineOptions} + ''; + Restart = "on-failure"; + + # Hardening measures + PrivateTmp = "true"; + ProtectSystem = "full"; + NoNewPrivileges = "true"; + PrivateDevices = "true"; + MemoryDenyWriteExecute = "true"; + }; + } + ))) eachBitcoind; + + systemd.tmpfiles.rules = flatten (mapAttrsToList (bitcoindName: cfg: [ + "d '${cfg.dataDir}' 0770 '${cfg.user}' '${cfg.group}' - -" + ]) eachBitcoind); + + users.users = mapAttrs' (bitcoindName: cfg: ( + nameValuePair "bitcoind-${bitcoindName}" { name = cfg.user; group = cfg.group; description = "Bitcoin daemon user"; home = cfg.dataDir; isSystemUser = true; - }; - users.groups.${cfg.group} = { - name = cfg.group; - }; + })) eachBitcoind; + + users.groups = mapAttrs' (bitcoindName: cfg: ( + nameValuePair "${cfg.group}" { } + )) eachBitcoind; + }; + } From 7b76bc2c7d21c9fd75d31880f096219d8c97e02a Mon Sep 17 00:00:00 2001 From: 1000101 Date: Tue, 21 Jul 2020 13:43:32 +0200 Subject: [PATCH 2/4] nixos/bitcoind: add tests --- nixos/tests/all-tests.nix | 1 + nixos/tests/bitcoind.nix | 46 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 nixos/tests/bitcoind.nix diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 7f3bb9bcc819..d7ecfcf9cdd0 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -32,6 +32,7 @@ in beanstalkd = handleTest ./beanstalkd.nix {}; bees = handleTest ./bees.nix {}; bind = handleTest ./bind.nix {}; + bitcoind = handleTest ./bitcoind.nix {}; bittorrent = handleTest ./bittorrent.nix {}; blockbook-frontend = handleTest ./blockbook-frontend.nix {}; buildkite-agents = handleTest ./buildkite-agents.nix {}; diff --git a/nixos/tests/bitcoind.nix b/nixos/tests/bitcoind.nix new file mode 100644 index 000000000000..95c6a5b91bce --- /dev/null +++ b/nixos/tests/bitcoind.nix @@ -0,0 +1,46 @@ +import ./make-test-python.nix ({ pkgs, ... }: { + name = "bitcoind"; + meta = with pkgs.stdenv.lib; { + maintainers = with maintainers; [ maintainers."1000101" ]; + }; + + machine = { ... }: { + services.bitcoind."mainnet" = { + enable = true; + rpc = { + port = 8332; + users.rpc.passwordHMAC = "acc2374e5f9ba9e62a5204d3686616cf$53abdba5e67a9005be6a27ca03a93ce09e58854bc2b871523a0d239a72968033"; + users.rpc2.passwordHMAC = "1495e4a3ad108187576c68f7f9b5ddc5$accce0881c74aa01bb8960ff3bdbd39f607fd33178147679e055a4ac35f53225"; + }; + }; + services.bitcoind."testnet" = { + enable = true; + configFile = "/test.blank"; + testnet = true; + rpc = { + port = 18332; + }; + extraCmdlineOptions = [ "-rpcuser=rpc" "-rpcpassword=rpc" "-rpcauth=rpc2:1495e4a3ad108187576c68f7f9b5ddc5$accce0881c74aa01bb8960ff3bdbd39f607fd33178147679e055a4ac35f53225" ]; + }; + }; + + testScript = '' + start_all() + + machine.wait_for_unit("bitcoind-mainnet.service") + machine.wait_for_unit("bitcoind-testnet.service") + + machine.wait_until_succeeds( + 'curl --user rpc:rpc --data-binary \'{"jsonrpc": "1.0", "id":"curltest", "method": "getblockchaininfo", "params": [] }\' -H \'content-type: text/plain;\' localhost:8332 | grep \'"chain":"main"\' ' + ) + machine.wait_until_succeeds( + 'curl --user rpc2:rpc2 --data-binary \'{"jsonrpc": "1.0", "id":"curltest", "method": "getblockchaininfo", "params": [] }\' -H \'content-type: text/plain;\' localhost:8332 | grep \'"chain":"main"\' ' + ) + machine.wait_until_succeeds( + 'curl --user rpc:rpc --data-binary \'{"jsonrpc": "1.0", "id":"curltest", "method": "getblockchaininfo", "params": [] }\' -H \'content-type: text/plain;\' localhost:18332 | grep \'"chain":"test"\' ' + ) + machine.wait_until_succeeds( + 'curl --user rpc2:rpc2 --data-binary \'{"jsonrpc": "1.0", "id":"curltest", "method": "getblockchaininfo", "params": [] }\' -H \'content-type: text/plain;\' localhost:18332 | grep \'"chain":"test"\' ' + ) + ''; +}) From 95440f040eacee2f7887edcee283869735a0b168 Mon Sep 17 00:00:00 2001 From: 1000101 Date: Fri, 24 Jul 2020 15:48:11 +0200 Subject: [PATCH 3/4] nixos/bitcoind: minor refactoring --- nixos/modules/services/networking/bitcoind.nix | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/nixos/modules/services/networking/bitcoind.nix b/nixos/modules/services/networking/bitcoind.nix index 4da719ac2f2d..38537ad2de72 100644 --- a/nixos/modules/services/networking/bitcoind.nix +++ b/nixos/modules/services/networking/bitcoind.nix @@ -16,7 +16,7 @@ let ''; }; passwordHMAC = mkOption { - type = with types; uniq (strMatching "[0-9a-f]+\\$[0-9a-f]{64}"); + type = types.uniq (types.strMatching "[0-9a-f]+\\$[0-9a-f]{64}"); example = "f7efda5c189b999524f151318c0c86$d5b51b3beffbc02b724e5d095828e0bc8b2456e9ac8757ae3211a5d9b16a22ae"; description = '' Password HMAC-SHA-256 for JSON-RPC connections. Must be a string of the @@ -45,7 +45,7 @@ let }; configFile = mkOption { - type = with types; nullOr path; + type = types.nullOr types.path; default = null; example = "/var/lib/${name}/bitcoin.conf"; description = "The configuration file path to supply bitcoind."; @@ -94,10 +94,8 @@ let bob.passwordHMAC = "b2dd077cb54591a2f3139e69a897ac$4e71f08d48b4347cf8eff3815c0e25ae2e9a4340474079f55705f40574f4ec99"; } ''; - type = with types; attrsOf (submodule rpcUserOpts); - description = '' - RPC user information for JSON-RPC connnections. - ''; + type = types.attrsOf (types.submodule rpcUserOpts); + description = "RPC user information for JSON-RPC connnections."; }; }; @@ -171,9 +169,9 @@ in assertions = flatten (mapAttrsToList (bitcoindName: cfg: [ { - assertion = (cfg.prune != null) -> (cfg.prune == "disable" || cfg.prune == "manual") || (cfg.prune == 0 || cfg.prune == 1) || (cfg.prune >= 550); + assertion = (cfg.prune != null) -> (builtins.elem cfg.prune [ "disable" "manual" 0 1 ] || (builtins.isInt cfg.prune && cfg.prune >= 550)); message = '' - If set, services.bitcoind.${bitcoindName}.prune has to be "disable", "manual", 0 , 1 or >= 550 + If set, services.bitcoind.${bitcoindName}.prune has to be "disable", "manual", 0 , 1 or >= 550. ''; } { @@ -258,4 +256,6 @@ in }; + meta.maintainers = with maintainers; [ maintainers."1000101" ]; + } From a5ba1315c2155056f20040020c00807cfbd39d07 Mon Sep 17 00:00:00 2001 From: 1000101 Date: Mon, 27 Jul 2020 11:27:50 +0200 Subject: [PATCH 4/4] release-notes/rl-2009: document bitcoind incompatibility --- nixos/doc/manual/release-notes/rl-2009.xml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/nixos/doc/manual/release-notes/rl-2009.xml b/nixos/doc/manual/release-notes/rl-2009.xml index 97b94c5756a7..63361a3708d4 100644 --- a/nixos/doc/manual/release-notes/rl-2009.xml +++ b/nixos/doc/manual/release-notes/rl-2009.xml @@ -531,6 +531,23 @@ systemd.services.nginx.serviceConfig.ReadWritePaths = [ "/var/www" ]; to be used for every display-manager in NixOS. + + + The bitcoind module has changed to multi-instance, using submodules. + Therefore, it is now mandatory to name each instance, e.g.: + +services.bitcoind = { + enable = true; +}; + + requires a name now: + +services.bitcoind."example-mainnet" = { + enable = true; +}; + + +