diff --git a/.cirrus.yml b/.cirrus.yml index 3ac3f61..5f82269 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -39,4 +39,4 @@ task: build_script: - echo "sandbox = true" >> /etc/nix/nix.conf - export NIX_PATH="nixpkgs=$(nix eval --raw -f pkgs/nixpkgs-pinned.nix $nixpkgs)" - - nix run -f '' bash cachix -c ./ci/build.sh + - nix run -f '' bash coreutils cachix -c ./ci/build.sh diff --git a/ci/build-to-cachix.sh b/ci/build-to-cachix.sh index ab99535..73a8260 100755 --- a/ci/build-to-cachix.sh +++ b/ci/build-to-cachix.sh @@ -13,11 +13,17 @@ trap 'echo Error at line $LINENO' ERR atExit() { rm -rf $tmpDir - if [[ -v cachixPid ]]; then kill $cachixPid; fi + if [[ -v cachixPid ]]; then stopCachix; fi } tmpDir=$(mktemp -d -p /tmp) trap atExit EXIT +stopCachix() { + kill $cachixPid 2>/dev/null || true + # Wait for cachix to finish + tail --pid=$cachixPid -f /dev/null +} + ## Instantiate time nix-instantiate "$@" --add-root $tmpDir/drv --indirect > /dev/null @@ -44,6 +50,7 @@ fi nix-build --out-link $tmpDir/result $tmpDir/drv >/dev/null if [[ $CACHIX_SIGNING_KEY ]]; then + stopCachix cachix push $cachixCache $outPath fi diff --git a/examples/README.md b/examples/README.md index 328c6aa..dba4e53 100644 --- a/examples/README.md +++ b/examples/README.md @@ -55,5 +55,11 @@ c systemctl status bitcoind # Explore a single feature ./run-tests.sh --scenario electrs container + +# Run a command in a container +./run-tests.sh --scenario '{ + services.clightning.enable = true; + nix-bitcoin.nodeinfo.enable = true; +}' container --run c nodeinfo ``` See [`run-tests.sh`](../test/run-tests.sh) for a complete documentation. diff --git a/examples/configuration.nix b/examples/configuration.nix index 4d92217..5d79067 100644 --- a/examples/configuration.nix +++ b/examples/configuration.nix @@ -28,6 +28,10 @@ # LND and electrs are not compatible with pruning. # services.bitcoind.prune = 100000; # + # Set this to accounce the onion service address to peers. + # The onion service allows accepting incoming connections via Tor. + # nix-bitcoin.onionServices.bitcoind.public = true; + # # You can add options that are not defined in modules/bitcoind.nix as follows # services.bitcoind.extraConfig = '' # maxorphantx=110 diff --git a/modules/bitcoind.nix b/modules/bitcoind.nix index 92f3516..7518606 100644 --- a/modules/bitcoind.nix +++ b/modules/bitcoind.nix @@ -11,6 +11,8 @@ let # We're already logging via journald nodebuglogfile=1 + startupnotify=/run/current-system/systemd/bin/systemd-notify --ready + ${optionalString cfg.regtest '' regtest=1 [regtest] @@ -22,7 +24,7 @@ let ${optionalString (cfg.assumevalid != null) "assumevalid=${cfg.assumevalid}"} # Connection options - ${optionalString cfg.listen "bind=${cfg.address}"} + ${optionalString cfg.listen "bind=${cfg.address}${optionalString cfg.enforceTor "=onion"}"} port=${toString cfg.port} ${optionalString (cfg.proxy != null) "proxy=${cfg.proxy}"} listen=${if cfg.listen then "1" else "0"} @@ -67,6 +69,14 @@ in { 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. + ''; + }; package = mkOption { type = types.package; default = config.nix-bitcoin.pkgs.bitcoind; @@ -328,21 +338,22 @@ in { ${extraRpcauth} ${/* Enable bitcoin-cli for group 'bitcoin' */ ""} printf "rpcuser=${cfg.rpc.users.privileged.name}\nrpcpassword="; cat "${secretsDir}/bitcoin-rpcpassword-privileged"; + 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 ''; - postStart = '' - # Poll until bitcoind accepts commands. This can take a long time. - while ! ${cfg.cli}/bin/bitcoin-cli getnetworkinfo &> /dev/null; do - sleep 1 - done - ''; serviceConfig = nix-bitcoin-services.defaultHardening // { + Type = "notify"; + NotifyAccess = "all"; User = "${cfg.user}"; Group = "${cfg.group}"; + TimeoutStartSec = 300; ExecStart = "${cfg.package}/bin/bitcoind -datadir='${cfg.dataDir}'"; Restart = "on-failure"; UMask = mkIf cfg.dataDirReadableByGroup "0027"; diff --git a/modules/onion-addresses.nix b/modules/onion-addresses.nix index f715337..df9c9dc 100644 --- a/modules/onion-addresses.nix +++ b/modules/onion-addresses.nix @@ -27,13 +27,22 @@ in { /var/lib/onion-addresses/myuser. ''; }; + services = mkOption { + type = with types; listOf str; + default = []; + description = '' + Services that can access their onion address via file + /var/lib/onion-addresses/ + The file is readable only by the service user. + ''; + }; dataDir = mkOption { readOnly = true; default = "/var/lib/onion-addresses"; }; }; - config = mkIf (cfg.access != {}) { + config = mkIf (cfg.access != {} || cfg.services != []) { systemd.services.onion-addresses = { wantedBy = [ "tor.service" ]; bindsTo = [ "tor.service" ]; @@ -42,6 +51,7 @@ in { Type = "oneshot"; RemainAfterExit = true; StateDirectory = "onion-addresses"; + StateDirectoryMode = "771"; PrivateNetwork = "true"; # This service needs no network access PrivateUsers = "false"; CapabilityBoundingSet = "CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_FOWNER CAP_IPC_OWNER"; @@ -70,6 +80,13 @@ in { '') (builtins.attrNames cfg.access) } + + ${concatMapStrings (service: '' + onionFile=/var/lib/tor/onion/${service}/hostname + if [[ -e $onionFile ]]; then + install -o ${config.systemd.services.${service}.serviceConfig.User} -m 400 $onionFile ${service} + fi + '') cfg.services} ''; }; }; diff --git a/modules/onion-services.nix b/modules/onion-services.nix index f73318f..52c6c2f 100644 --- a/modules/onion-services.nix +++ b/modules/onion-services.nix @@ -71,12 +71,12 @@ in { ); }; - # Enable public services to access their own onion addresses - nix-bitcoin.onionAddresses.access = ( - genAttrs publicServices singleton - ) // { + nix-bitcoin.onionAddresses = { + # Enable public services to access their own onion addresses + services = publicServices; + # Allow the operator user to access onion addresses for all active services - ${config.nix-bitcoin.operator.name} = mkIf config.nix-bitcoin.operator.enable activeServices; + access.${config.nix-bitcoin.operator.name} = mkIf config.nix-bitcoin.operator.enable activeServices; }; systemd.services = let onionAddresses = [ "onion-addresses.service" ]; @@ -96,7 +96,7 @@ in { in srv.public && srv.enable ) services; in genAttrs publicServices' (service: { - getPublicAddressCmd = "cat ${config.nix-bitcoin.onionAddresses.dataDir}/${service}/${service}"; + getPublicAddressCmd = "cat ${config.nix-bitcoin.onionAddresses.dataDir}/${service}"; }); } diff --git a/pkgs/nixpkgs-pinned.nix b/pkgs/nixpkgs-pinned.nix index dddceda..e3797a9 100644 --- a/pkgs/nixpkgs-pinned.nix +++ b/pkgs/nixpkgs-pinned.nix @@ -12,7 +12,7 @@ in sha256 = "1vjh0np1rlirbhhj9b2d0zhrqdmiji5svxh9baqq7r3680af1iif"; }; nixpkgs-unstable = fetch { - rev = "296793637b22bdb4d23b479879eba0a71c132a66"; - sha256 = "0j09yih9693w5vjx64ikfxyja1ha7pisygrwrpg3wfz3sssglg69"; + rev = "891f607d5301d6730cb1f9dcf3618bcb1ab7f10e"; + sha256 = "1cr39f0sbr0h5d83dv1q34mcpwnkwwbdk5fqlyqp2mnxghzwssng"; }; } diff --git a/pkgs/pinned.nix b/pkgs/pinned.nix index e9239e2..00c99ef 100644 --- a/pkgs/pinned.nix +++ b/pkgs/pinned.nix @@ -13,9 +13,6 @@ in lnd nbxplorer btcpayserver; - inherit (nixBitcoinPkgsUnstable) - electrs - lightning-loop; stable = nixBitcoinPkgsStable; unstable = nixBitcoinPkgsUnstable; diff --git a/test/pkgs-unstable.nix b/test/pkgs-unstable.nix index 3ad8583..86fd269 100644 --- a/test/pkgs-unstable.nix +++ b/test/pkgs-unstable.nix @@ -4,10 +4,15 @@ let nbPkgs = import ../pkgs { inherit pkgs; }; pkgsUnstable = with nbPkgs; [ electrs - elementsd hwi joinmarket lightning-loop + + ## elementsd fails with error + # test/key_properties.cpp:16:10: fatal error: rapidcheck/boost_test.h: No such file or directory + # 16 | #include + # | ^~~~~~~~~~~~~~~~~~~~~~~~~ + # elementsd ]; in pkgs.writeText "pkgs-unstable" (pkgs.lib.concatMapStringsSep "\n" toString pkgsUnstable) diff --git a/test/run-tests.sh b/test/run-tests.sh index b4da8d2..2593a3f 100755 --- a/test/run-tests.sh +++ b/test/run-tests.sh @@ -10,7 +10,13 @@ # Test specific scenario # ./run-tests.sh --scenario|-s # -# When is undefined, the test is run with an adhoc scenario +# - When contains a space, is treated as nix code defining +# a scenario. It is evaluated in the same context as other scenarios in ./tests.nix +# +# Example: +# ./run-tests.sh -s "{ nix-bitcoin.nodeinfo.enable = true; }" container --run c nodeinfo +# +# - When does not name a scenario, the test is run with an adhoc scenario # where services. is enabled. # # Example: @@ -80,13 +86,24 @@ numCPUs=${numCPUs:-$(nproc)} # Min. 800 MiB needed to avoid 'out of memory' errors memoryMiB=${memoryMiB:-2048} -export NIX_PATH=nixpkgs=$(nix eval --raw -f "$scriptDir/../pkgs/nixpkgs-pinned.nix" nixpkgs) +export NIX_PATH=nixpkgs=$(nix eval --raw -f "$scriptDir/../pkgs/nixpkgs-pinned.nix" nixpkgs):nix-bitcoin=$(realpath "$scriptDir/..") + +runAtExit= +trap 'eval "$runAtExit"' EXIT + +# Support explicit scenario definitions +if [[ $scenario = *' '* ]]; then + export scenarioOverridesFile=$(mktemp ${XDG_RUNTIME_DIR:-/tmp}/nb-scenario.XXX) + runAtExit+='rm -f "$scenarioOverridesFile";' + echo "{ testEnv, config, pkgs, lib }: with testEnv; with lib; { tmp = $scenario; }" > "$scenarioOverridesFile" + scenario=tmp +fi # Run the test. No temporary files are left on the host system. run() { # TMPDIR is also used by the test driver for VM tmp files export TMPDIR=$(mktemp -d /tmp/nix-bitcoin-test.XXX) - trap "rm -rf $TMPDIR" EXIT + runAtExit+="rm -rf $TMPDIR;" nix-build --out-link $TMPDIR/driver -E "(import \"$scriptDir/tests.nix\" { scenario = \"$scenario\"; }).vm" -A driver @@ -134,7 +151,8 @@ instantiate() { } container() { - . "$scriptDir/lib/make-container.sh" "$@" + export scriptDir scenario + "$scriptDir/lib/make-container.sh" "$@" } doBuild() { diff --git a/test/tests.nix b/test/tests.nix index 95a6604..b7ce86e 100644 --- a/test/tests.nix +++ b/test/tests.nix @@ -187,6 +187,7 @@ let testEnv = rec { services.bitcoind.regtest = true; systemd.services.bitcoind.postStart = mkAfter '' cli=${config.services.bitcoind.cli}/bin/bitcoin-cli + $cli createwallet "test" address=$($cli getnewaddress) $cli generatetoaddress 10 $address ''; diff --git a/test/tests.py b/test/tests.py index 49f881b..598bbd9 100644 --- a/test/tests.py +++ b/test/tests.py @@ -286,6 +286,9 @@ def _(): # Impure: stops bitcoind (and dependent services) @test("backups") def _(): + # For testing that bitcoind wallets are backed up + succeed("bitcoin-cli -named createwallet wallet_name=test blank=true >/dev/null") + succeed("systemctl stop bitcoind") succeed("systemctl start duplicity") machine.wait_until_succeeds(log_has_string("duplicity", "duplicity.service: Succeeded.")) @@ -296,12 +299,18 @@ def _(): "0 differences found", ) # Backup should include important files - files = succeed(f"{run_duplicity} list-current-files file:///var/lib/localBackups") - assert "var/lib/clightning/bitcoin/hsm_secret" in files - assert "secrets/lnd-seed-mnemonic" in files - assert "secrets/jm-wallet-seed" in files - assert "var/lib/bitcoind/wallet.dat" in files - assert "var/backup/postgresql/btcpaydb.sql.gz" in files + files = { + "bitcoind": "var/lib/bitcoind/test/wallet.dat", + "clightning": "var/lib/clightning/bitcoin/hsm_secret", + "lnd": "secrets/lnd-seed-mnemonic", + "joinmarket": "secrets/jm-wallet-seed", + "btcpayserver": "var/backup/postgresql/btcpaydb.sql.gz", + } + actual_files = succeed(f"{run_duplicity} list-current-files file:///var/lib/localBackups") + + for test, file in files.items(): + if test in enabled_tests and file not in actual_files: + raise Exception(f"Backup file '{file}' is missing.") # Impure: restarts services