diff --git a/examples/configuration.nix b/examples/configuration.nix index 08ea8bf..25abdb2 100644 --- a/examples/configuration.nix +++ b/examples/configuration.nix @@ -172,6 +172,16 @@ # and electrs data directory, enable # services.backups.with-bulk-data = true; + ### JOINMARKET + # Enable this module to allow using JoinMarket's user interactive scripts (including + # tumbler.py). + # Note: JoinMarket has full access to bitcoind, including its wallet functionality. + # services.joinmarket.enable = true; + # Enable this option to enable the JoinMarket Yield Generator Bot. You will be able to + # earn sats by providing CoinJoin liquidity. This makes it impossible to use other + # scripts that access your wallet. + # services.joinmarket.yieldgenerator.enable = true; + # FIXME: Define your hostname. networking.hostName = "nix-bitcoin"; time.timeZone = "UTC"; diff --git a/modules/default.nix b/modules/default.nix index 2ca1ebe..d41d032 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -12,4 +12,5 @@ spark-wallet = ./spark-wallet.nix; recurring-donations = ./recurring-donations.nix; lnd = ./lnd.nix; + joinmarket = ./joinmarket.nix; } diff --git a/modules/joinmarket.nix b/modules/joinmarket.nix new file mode 100644 index 0000000..80c2593 --- /dev/null +++ b/modules/joinmarket.nix @@ -0,0 +1,202 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.joinmarket; + inherit (config) nix-bitcoin-services; + secretsDir = config.nix-bitcoin.secretsDir; + + torAddress = builtins.head (builtins.split ":" config.services.tor.client.socksListenAddress); + configFile = builtins.toFile "config" '' + # Based on https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/master/jmclient/jmclient/configure.py + [DAEMON] + no_daemon = 0 + daemon_port = 27183 + daemon_host = localhost + use_ssl = false + + [BLOCKCHAIN] + blockchain_source = bitcoin-rpc + network = mainnet + rpc_host = ${builtins.elemAt config.services.bitcoind.rpcbind 0} + rpc_port = 8332 + rpc_user = ${config.services.bitcoind.rpc.users.privileged.name} + @@RPC_PASSWORD@@ + + [MESSAGING:server1] + host = darksci3bfoka7tw.onion + channel = joinmarket-pit + port = 6697 + usessl = true + socks5 = true + socks5_host = ${torAddress} + socks5_port = 9050 + + [MESSAGING:server2] + host = ncwkrwxpq2ikcngxq3dy2xctuheniggtqeibvgofixpzvrwpa77tozqd.onion + channel = joinmarket-pit + port = 6667 + usessl = false + socks5 = true + socks5_host = ${torAddress} + socks5_port = 9050 + + [LOGGING] + console_log_level = INFO + color = false + + [POLICY] + segwit = true + native = false + merge_algorithm = default + tx_fees = 3 + absurd_fee_per_kb = 350000 + tx_broadcast = self + minimum_makers = 4 + max_sats_freeze_reuse = -1 + taker_utxo_retries = 3 + taker_utxo_age = 5 + taker_utxo_amtpercent = 20 + accept_commitment_broadcasts = 1 + commit_file_location = cmtdata/commitments.json + ''; + + # The jm scripts create a 'logs' dir in the working dir, + # so run them inside dataDir. + cli = pkgs.runCommand "joinmarket-cli" {} '' + mkdir -p $out/bin + jm=${pkgs.nix-bitcoin.joinmarket}/bin + cd $jm + for bin in jm-*; do + { + echo "#!${pkgs.bash}/bin/bash"; + echo "cd '${cfg.dataDir}' && ${cfg.cliExec} sudo -u ${cfg.user} $jm/$bin --datadir='${cfg.dataDir}' \"\$@\""; + } > $out/bin/$bin + done + chmod -R +x $out/bin + ''; +in { + options.services.joinmarket = { + enable = mkEnableOption "JoinMarket"; + yieldgenerator = { + enable = mkEnableOption "yield generator bot"; + customParameters = mkOption { + type = types.str; + default = ""; + example = '' + txfee = 200 + cjfee_a = 300 + ''; + description = '' + Python code to define custom yield generator parameters, as described in + https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/master/docs/YIELDGENERATOR.md + ''; + }; + }; + dataDir = mkOption { + type = types.path; + default = "/var/lib/joinmarket"; + description = "The data directory for JoinMarket."; + }; + user = mkOption { + type = types.str; + default = "joinmarket"; + description = "The user as which to run JoinMarket."; + }; + group = mkOption { + type = types.str; + default = cfg.user; + description = "The group as which to run JoinMarket."; + }; + cli = mkOption { + default = cli; + }; + inherit (nix-bitcoin-services) cliExec; + }; + + config = mkIf cfg.enable (mkMerge [{ + environment.systemPackages = [ + (hiPrio cfg.cli) + ]; + users.users.${cfg.user} = { + description = "joinmarket User"; + group = "${cfg.group}"; + home = cfg.dataDir; + }; + users.groups.${cfg.group} = {}; + + systemd.tmpfiles.rules = [ + "d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -" + ]; + + services.bitcoind.disablewallet = false; + + # Joinmarket is TOR-only + services.tor = { + enable = true; + client.enable = true; + }; + + systemd.services.joinmarket = { + description = "JoinMarket Daemon"; + wantedBy = [ "multi-user.target" ]; + requires = [ "bitcoind.service" ]; + after = [ "bitcoind.service" ]; + serviceConfig = nix-bitcoin-services.defaultHardening // { + ExecStartPre = nix-bitcoin-services.privileged '' + install -o '${cfg.user}' -g '${cfg.group}' -m 640 ${configFile} ${cfg.dataDir}/joinmarket.cfg + sed -i \ + "s|@@RPC_PASSWORD@@|rpc_password = $(cat ${config.nix-bitcoin.secretsDir}/bitcoin-rpcpassword-privileged)|" \ + '${cfg.dataDir}/joinmarket.cfg' + ''; + ExecStart = "${pkgs.nix-bitcoin.joinmarket}/bin/joinmarketd"; + WorkingDirectory = "${cfg.dataDir}"; # The service creates 'commitmentlist' in the working dir + User = "${cfg.user}"; + Restart = "on-failure"; + RestartSec = "10s"; + ReadWritePaths = "${cfg.dataDir}"; + } // nix-bitcoin-services.allowTor; + }; + } + + (mkIf cfg.yieldgenerator.enable { + nix-bitcoin.secrets.jm-wallet-password.user = cfg.user; + + systemd.services.joinmarket-yieldgenerator = let + ygDefault = "${pkgs.nix-bitcoin.joinmarket}/bin/jm-yg-privacyenhanced"; + ygBinary = if cfg.yieldgenerator.customParameters == "" then + ygDefault + else + pkgs.runCommand "jm-yieldgenerator-custom" { + inherit (cfg.yieldgenerator) customParameters; + } '' + substitute ${ygDefault} $out \ + --replace "# end of settings customization" "$customParameters" + chmod +x $out + ''; + in { + description = "CoinJoin maker bot to gain privacy and passively generate income"; + wantedBy = [ "joinmarket.service" ]; + requires = [ "joinmarket.service" ]; + after = [ "joinmarket.service" ]; + preStart = let + start = '' + exec ${ygBinary} --datadir='${cfg.dataDir}' --wallet-password-stdin wallet.jmdat + ''; + in '' + pw=$(cat "${secretsDir}"/jm-wallet-password) + echo "echo -n $pw | ${start}" > $RUNTIME_DIRECTORY/start + ''; + serviceConfig = nix-bitcoin-services.defaultHardening // rec { + RuntimeDirectory = "joinmarket-yieldgenerator"; # Only used to create start script + RuntimeDirectoryMode = "700"; + WorkingDirectory = "${cfg.dataDir}"; # The service creates dir 'logs' in the working dir + ExecStart = "${pkgs.bash}/bin/bash /run/${RuntimeDirectory}/start"; + User = "${cfg.user}"; + ReadWritePaths = "${cfg.dataDir}"; + } // nix-bitcoin-services.allowTor; + }; + }) + ]); +} diff --git a/modules/modules.nix b/modules/modules.nix index e86772d..967053b 100644 --- a/modules/modules.nix +++ b/modules/modules.nix @@ -19,6 +19,7 @@ ./security.nix ./backups.nix ./btcpayserver.nix + ./joinmarket.nix ]; disabledModules = [ "services/networking/bitcoind.nix" ]; diff --git a/modules/netns-isolation.nix b/modules/netns-isolation.nix index 7b05139..d456fa4 100644 --- a/modules/netns-isolation.nix +++ b/modules/netns-isolation.nix @@ -131,6 +131,7 @@ in { ${ip} link del nb-br ''; }; + } // (let makeNetnsServices = n: v: let @@ -242,6 +243,10 @@ in { ++ optional (config.services.btcpayserver.lightningBackend == "lnd") "lnd"; # communicates with clightning over rpc socket }; + joinmarket = { + id = 25; + connections = [ "bitcoind" ]; + }; }; services.bitcoind = { @@ -314,6 +319,9 @@ in { services.nbxplorer.bind = netns.nbxplorer.address; services.btcpayserver.bind = netns.btcpayserver.address; + + services.joinmarket.cliExec = mkCliExec "joinmarket"; + systemd.services.joinmarket-yieldgenerator.serviceConfig.NetworkNamespacePath = "/var/run/netns/nb-joinmarket"; } ]); } diff --git a/modules/presets/secure-node.nix b/modules/presets/secure-node.nix index f1e80e1..dd1839b 100644 --- a/modules/presets/secure-node.nix +++ b/modules/presets/secure-node.nix @@ -171,7 +171,8 @@ in { ++ (optionals cfg.lnd.enable [ "lnd" ]) ++ (optionals cfg.liquidd.enable [ cfg.liquidd.group ]) ++ (optionals (cfg.hardware-wallets.ledger || cfg.hardware-wallets.trezor) - [ cfg.hardware-wallets.group ]); + [ cfg.hardware-wallets.group ]) + ++ (optionals cfg.joinmarket.enable [ cfg.joinmarket.group ]); openssh.authorizedKeys.keys = config.users.users.root.openssh.authorizedKeys.keys; }; nix-bitcoin.netns-isolation.allowedUser = operatorName; @@ -182,6 +183,9 @@ in { security.sudo.configFile = (optionalString cfg.lnd.enable '' ${operatorName} ALL=(lnd) NOPASSWD: ALL + '') + + (optionalString cfg.joinmarket.enable '' + ${operatorName} ALL=(${cfg.joinmarket.user}) NOPASSWD: ALL ''); # Enable nixops ssh for operator (`nixops ssh operator@mynode`) on nixops-vbox deployments diff --git a/pkgs/generate-secrets/generate-secrets.sh b/pkgs/generate-secrets/generate-secrets.sh index 87bdaa4..06b3f69 100755 --- a/pkgs/generate-secrets/generate-secrets.sh +++ b/pkgs/generate-secrets/generate-secrets.sh @@ -18,6 +18,7 @@ makePasswordSecret liquid-rpcpassword makePasswordSecret lightning-charge-token makePasswordSecret spark-wallet-password makePasswordSecret backup-encryption-password +touch jm-wallet-password [[ -e bitcoin-HMAC-privileged ]] || makeHMAC privileged [[ -e bitcoin-HMAC-public ]] || makeHMAC public diff --git a/pkgs/netns-exec/src/main.c b/pkgs/netns-exec/src/main.c index 60cc85a..67c75b2 100644 --- a/pkgs/netns-exec/src/main.c +++ b/pkgs/netns-exec/src/main.c @@ -13,7 +13,8 @@ static char *allowed_netns[] = { "nb-lnd", "nb-lightning-loop", "nb-bitcoind", - "nb-liquidd" + "nb-liquidd", + "nb-joinmarket" }; int is_netns_allowed(char *netns) {