Compare commits
60
Commits
v1.0.6
..
90f89f14ba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90f89f14ba | ||
|
|
dcfee1fb32 | ||
|
|
05a42bcc4b | ||
|
|
9befcd06e6 | ||
|
|
c9169164b5 | ||
|
|
cb84ca839f | ||
|
|
6d59441b13 | ||
|
|
5475e33d08 | ||
|
|
88896f00c4 | ||
|
|
cb2d6de0ef | ||
|
|
c2dce6cf94 | ||
|
|
7879a2771d | ||
|
|
820d850d00 | ||
|
|
d733187ec5 | ||
|
|
f7b1f42e0e | ||
|
|
03fb7791eb | ||
|
|
953cacc72c | ||
|
|
6021691b3f | ||
|
|
4d8bb06d41 | ||
|
|
b96d2bcb24 | ||
|
|
228a48af03 | ||
|
|
0df4cde9b8 | ||
|
|
a37b1a91bf | ||
|
|
ebcc086a7e | ||
|
|
dcc9e4904e | ||
|
|
fd209254a2 | ||
|
|
a68130625e | ||
|
|
7b04adbbe6 | ||
|
|
2c6ef7b60e | ||
|
|
179fc501f9 | ||
|
|
8bcabe2aa1 | ||
|
|
5d5e369411 | ||
|
|
425a1845b4 | ||
|
|
546dacf396 | ||
|
|
17c0db8290 | ||
|
|
dac733de9a | ||
|
|
3baf504c60 | ||
|
|
f220b93b12 | ||
|
|
b2a5233653 | ||
|
|
2bb5c50196 | ||
|
|
80c4b052a0 | ||
|
|
2accada021 | ||
|
|
a6296b9473 | ||
|
|
5173fb3df6 | ||
|
|
147afcc4ac | ||
|
|
2e35539ae6 | ||
|
|
31cd5f96f2 | ||
|
|
90262e672e | ||
|
|
1fbeafd02f | ||
|
|
953cedce3f | ||
|
|
58d7e21d28 | ||
|
|
9f7dcfa6de | ||
|
|
6c2c69d18a | ||
|
|
6b9c68e3b2 | ||
|
|
4b511348ae | ||
|
|
81946895db | ||
|
|
a10ef95b5c | ||
|
|
278d480653 | ||
|
|
b64061135a | ||
|
|
825c3fbb4b |
No files matched your search
@@ -51,50 +51,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Clarify GitHub is the dev mirror of the Gitea stable repo
|
- Clarify GitHub is the dev mirror of the Gitea stable repo
|
||||||
[1.0.6]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/v1.0.6
|
[1.0.6]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/v1.0.6
|
||||||
|
|
||||||
|
|
||||||
## [1.0.6] - 2026-08-07
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Iso: fix replaceStrings length mismatch in cleanVersion
|
|
||||||
- Promote virtual machine trial option in README
|
|
||||||
- Improve installer VM compatibility
|
|
||||||
- Trim redundant dev-vs-stable Gitea explanations in README
|
|
||||||
- Add Arean.Ai to AI tools used for development
|
|
||||||
- Prefix OS version badge with 'v' (v1.0.5)
|
|
||||||
- Match OS version badge styling to service modal version badges
|
|
||||||
- Place OS version badge inline right after the Hub title
|
|
||||||
- Center OS version badge under the Hub title
|
|
||||||
- Strengthen entropy section: add DYOR emphasis and hardware wallet verification note
|
|
||||||
- Add 'About Bitcoin wallet entropy' section to README
|
|
||||||
- README: balance intro between Bitcoin sovereignty and sovereign computing
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Removed temp patch file
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Harden error handling and sanitize exception details across security endpoints (CWE-209)
|
|
||||||
- Sanitize exception handling in verify-integrity and security-reset (CWE-209)
|
|
||||||
- Sanitize api_security_reset errors to prevent exception information exposure (CWE-209)
|
|
||||||
- Use canonical prefix containment check for CodeQL path-injection
|
|
||||||
- Pass sanitized abs_path to os.chown to resolve CodeQL path injection at 4338
|
|
||||||
- Add CodeQL-recognized path sanitization for domain_name
|
|
||||||
- Remove domain substring check for CodeQL incomplete-url
|
|
||||||
- Use sentinel for njalla header check (CodeQL incomplete-url-substring)
|
|
||||||
- Prevent reflected XSS in lnurl-qr print endpoint
|
|
||||||
- Hash root password instead of storing in clear text (CWE-312)
|
|
||||||
- Separate web auth hash from system password file
|
|
||||||
- Fix Matrix Hub admin API credentials
|
|
||||||
- Fix CWE-78: replace subprocess call with Synapse Admin API in create-user endpoint
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
- Add SECURITY.md detailing security policy and best practices
|
|
||||||
- Versioned CDN downloads + add CDN upload script
|
|
||||||
- Correct active development workflow
|
|
||||||
- Make repo references mirror-neutral for Gitea readers
|
|
||||||
- Clarify GitHub is the dev mirror of the Gitea stable repo
|
|
||||||
[1.0.6]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/v1.0.6
|
|
||||||
|
|
||||||
|
|
||||||
## [1.0.5] - 2026-08-04
|
## [1.0.5] - 2026-08-04
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
Generated
+10
-141
@@ -18,32 +18,6 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"extra-container": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-utils": [
|
|
||||||
"nix-bitcoin",
|
|
||||||
"flake-utils"
|
|
||||||
],
|
|
||||||
"nixpkgs": [
|
|
||||||
"nix-bitcoin",
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1766155727,
|
|
||||||
"narHash": "sha256-XGp4HHH6D6ZKiO5RnMzqYJYnZB538EnEflvlTsOKpvo=",
|
|
||||||
"owner": "erikarvstedt",
|
|
||||||
"repo": "extra-container",
|
|
||||||
"rev": "b450bdb24fca1076973c852d87bcb49b8eb5fd49",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "erikarvstedt",
|
|
||||||
"ref": "0.14",
|
|
||||||
"repo": "extra-container",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"flake-parts": {
|
"flake-parts": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs-lib": [
|
"nixpkgs-lib": [
|
||||||
@@ -65,47 +39,6 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"flake-utils": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1731533236,
|
|
||||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nix-bitcoin": {
|
|
||||||
"inputs": {
|
|
||||||
"extra-container": "extra-container",
|
|
||||||
"flake-utils": "flake-utils",
|
|
||||||
"nixpkgs": "nixpkgs_2",
|
|
||||||
"nixpkgs-25_05": "nixpkgs-25_05",
|
|
||||||
"nixpkgs-unstable": "nixpkgs-unstable"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1785177868,
|
|
||||||
"narHash": "sha256-zlAZZkokXpeHHBP8Nt4MvLXOn1Js1qVi81VPjtVbRV8=",
|
|
||||||
"owner": "fort-nix",
|
|
||||||
"repo": "nix-bitcoin",
|
|
||||||
"rev": "360e30fee5ba32f9fecc89bc35628195d9d2dbbe",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "fort-nix",
|
|
||||||
"ref": "release",
|
|
||||||
"repo": "nix-bitcoin",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1785590095,
|
"lastModified": 1785590095,
|
||||||
@@ -121,29 +54,13 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs-25_05": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1767313136,
|
|
||||||
"narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixos-25.05",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs-stable": {
|
"nixpkgs-stable": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1785734586,
|
"lastModified": 1786201459,
|
||||||
"narHash": "sha256-ODZkEK9Gy50yg6h98u7KkitZ3oc/uuTFK00bh1CRdNA=",
|
"narHash": "sha256-CiOTEjmwAmG2AWnaIno9YaCJJmpca2FXPhMAsnrolCg=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "531670d871c0e29724a02f3cbcac170adc65b58c",
|
"rev": "8b8c811c7c2541c30382c5de7ed26be055569c60",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -153,45 +70,13 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs-unstable": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1784453100,
|
|
||||||
"narHash": "sha256-zpGZb8FYui+i8KLtC0nlcmb0voR2zB80Qn8pe7lzIbk=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "b471514bed69eff5255c8e63c1f80e5fe56c616f",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixpkgs-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs_2": {
|
"nixpkgs_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1782847189,
|
"lastModified": 1786106723,
|
||||||
"narHash": "sha256-twXPFqFsrrY5r28Zh7Homgcp2gUMBgQ6WDS98Q/3xFI=",
|
"narHash": "sha256-zDSUbpoeo/9ZmD2+wXnzxoo1+uhL8vxc0b8yuYMKYq0=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "b6018f87da91d19d0ab4cf979885689b469cdd41",
|
"rev": "f13ff45afd1bb73e640eaa08a7066dbed07e3238",
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixos-25.11",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs_3": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1785828668,
|
|
||||||
"narHash": "sha256-8fsyqeO+mJqvIzeO4xIpgJe/f7MTbbVTEC6RT6WSXNs=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "e72e4f299401a3689d4b3d5fc6496b11db7064eb",
|
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -201,7 +86,7 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs_4": {
|
"nixpkgs_3": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1784555310,
|
"lastModified": 1784555310,
|
||||||
"narHash": "sha256-/FCliTPgiuV1owejZFNx3Ch9irdvkOfOFl+HHZ+DrtM=",
|
"narHash": "sha256-/FCliTPgiuV1owejZFNx3Ch9irdvkOfOFl+HHZ+DrtM=",
|
||||||
@@ -220,8 +105,8 @@
|
|||||||
"nixvim": {
|
"nixvim": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-parts": "flake-parts",
|
"flake-parts": "flake-parts",
|
||||||
"nixpkgs": "nixpkgs_4",
|
"nixpkgs": "nixpkgs_3",
|
||||||
"systems": "systems_2"
|
"systems": "systems"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1785763201,
|
"lastModified": 1785763201,
|
||||||
@@ -240,28 +125,12 @@
|
|||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"btc-clients": "btc-clients",
|
"btc-clients": "btc-clients",
|
||||||
"nix-bitcoin": "nix-bitcoin",
|
"nixpkgs": "nixpkgs_2",
|
||||||
"nixpkgs": "nixpkgs_3",
|
|
||||||
"nixpkgs-stable": "nixpkgs-stable",
|
"nixpkgs-stable": "nixpkgs-stable",
|
||||||
"nixvim": "nixvim"
|
"nixvim": "nixvim"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"systems": {
|
"systems": {
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"systems_2": {
|
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1774449309,
|
"lastModified": 1774449309,
|
||||||
"narHash": "sha256-brhZ8DmuGtzkCYHJg4HEd602amKm89Y9ytsFZ5uWD1w=",
|
"narHash": "sha256-brhZ8DmuGtzkCYHJg4HEd602amKm89Y9ytsFZ5uWD1w=",
|
||||||
|
|||||||
@@ -3,13 +3,12 @@
|
|||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
nix-bitcoin.url = "github:fort-nix/nix-bitcoin/release";
|
|
||||||
nixvim.url = "github:nix-community/nixvim";
|
nixvim.url = "github:nix-community/nixvim";
|
||||||
btc-clients.url = "github:emmanuelrosa/btc-clients-nix";
|
btc-clients.url = "github:emmanuelrosa/btc-clients-nix";
|
||||||
nixpkgs-stable.url = "github:nixos/nixpkgs/nixos-26.05";
|
nixpkgs-stable.url = "github:nixos/nixpkgs/nixos-26.05";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs, nix-bitcoin, nixvim, btc-clients, nixpkgs-stable, ... }:
|
outputs = { self, nixpkgs, nixvim, btc-clients, nixpkgs-stable, ... }:
|
||||||
|
|
||||||
let
|
let
|
||||||
overlay-stable = final: prev: {
|
overlay-stable = final: prev: {
|
||||||
@@ -22,7 +21,7 @@
|
|||||||
{
|
{
|
||||||
nixosConfigurations.nixos = nixpkgs.lib.nixosSystem {
|
nixosConfigurations.nixos = nixpkgs.lib.nixosSystem {
|
||||||
modules = [
|
modules = [
|
||||||
{ nixpkgs.hostPlatform = "x86_64-linux"; }
|
{ nixpkgs.hostPlatform = "x86_64-linux"; nixpkgs.overlays = [ overlay-stable ]; }
|
||||||
self.nixosModules.Sovran_SystemsOS
|
self.nixosModules.Sovran_SystemsOS
|
||||||
./hardware-configuration.nix
|
./hardware-configuration.nix
|
||||||
./role-state.nix
|
./role-state.nix
|
||||||
@@ -32,10 +31,9 @@
|
|||||||
|
|
||||||
nixosConfigurations.sovran_systemsos-iso = nixpkgs.lib.nixosSystem {
|
nixosConfigurations.sovran_systemsos-iso = nixpkgs.lib.nixosSystem {
|
||||||
modules = [
|
modules = [
|
||||||
{ nixpkgs.hostPlatform = "x86_64-linux"; }
|
{ nixpkgs.hostPlatform = "x86_64-linux"; nixpkgs.overlays = [ overlay-stable ]; }
|
||||||
({ config, pkgs, ... }: { nixpkgs.overlays = [ overlay-stable ]; })
|
|
||||||
./iso/common.nix
|
./iso/common.nix
|
||||||
nix-bitcoin.nixosModules.default
|
./modules/bitcoin
|
||||||
nixvim.nixosModules.nixvim
|
nixvim.nixosModules.nixvim
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
@@ -46,7 +44,7 @@
|
|||||||
nixpkgs.overlays = [ overlay-stable ];
|
nixpkgs.overlays = [ overlay-stable ];
|
||||||
})
|
})
|
||||||
./configuration.nix
|
./configuration.nix
|
||||||
nix-bitcoin.nixosModules.default
|
./modules/bitcoin
|
||||||
nixvim.nixosModules.nixvim
|
nixvim.nixosModules.nixvim
|
||||||
];
|
];
|
||||||
config = {
|
config = {
|
||||||
@@ -58,13 +56,21 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
nixosTests.nwc-wallets-port-collision =
|
checks.x86_64-linux = let
|
||||||
import ./nix/tests/nwc-wallets-port-collision.nix {
|
pkgs = import nixpkgs {
|
||||||
inherit nixpkgs;
|
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
};
|
};
|
||||||
|
fetchNodeModules =
|
||||||
checks.x86_64-linux.nwc-wallets-port-collision =
|
pkgs.callPackage ./packages/build-support/fetch-node-modules.nix {};
|
||||||
self.nixosTests.nwc-wallets-port-collision;
|
mempoolPkgs =
|
||||||
|
pkgs.callPackage ./packages/mempool { inherit fetchNodeModules; };
|
||||||
|
in {
|
||||||
|
bitcoin-btcpay-hardening = import ./tests/bitcoin-btcpay-hardening.nix {
|
||||||
|
inherit nixpkgs overlay-stable;
|
||||||
|
};
|
||||||
|
mempool-backend = mempoolPkgs.mempool-backend;
|
||||||
|
mempool-frontend = mempoolPkgs.mempool-frontend;
|
||||||
|
rtl = pkgs.callPackage ./packages/rtl { inherit fetchNodeModules; };
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{ config, pkgs, lib, ... }:
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
lib.mkIf config.sovran_systemsOS.features.bitcoin-core {
|
lib.mkIf config.sovran_systemsOS.features.bitcoin-core {
|
||||||
|
# Vendored nix-bitcoin now uses nixpkgs directly; no nix-bitcoin.pkgs indirection.
|
||||||
services.bitcoind.package = lib.mkForce config.nix-bitcoin.pkgs.bitcoind;
|
# Use standard bitcoind from nixpkgs (override to knots if desired via pkgs.bitcoind-knots)
|
||||||
|
services.bitcoind.package = lib.mkForce pkgs.bitcoind;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# RPC calls that are safe for public use
|
||||||
|
# Vendored from nix-bitcoin - do not fetch from upstream at runtime
|
||||||
|
[
|
||||||
|
"echo"
|
||||||
|
"getinfo"
|
||||||
|
"getindexinfo"
|
||||||
|
"help"
|
||||||
|
"ping"
|
||||||
|
"uptime"
|
||||||
|
# Blockchain
|
||||||
|
"getbestblockhash"
|
||||||
|
"getblock"
|
||||||
|
"getblockchaininfo"
|
||||||
|
"getblockcount"
|
||||||
|
"getblockfilter"
|
||||||
|
"getblockfrompeer"
|
||||||
|
"getblockhash"
|
||||||
|
"getblockheader"
|
||||||
|
"getblockstats"
|
||||||
|
"getchaintips"
|
||||||
|
"getchaintxstats"
|
||||||
|
"getdeploymentinfo"
|
||||||
|
"getdifficulty"
|
||||||
|
"getmempoolancestors"
|
||||||
|
"getmempooldescendants"
|
||||||
|
"getmempoolentry"
|
||||||
|
"getmempoolinfo"
|
||||||
|
"getrawmempool"
|
||||||
|
"gettxout"
|
||||||
|
"gettxoutproof"
|
||||||
|
"gettxoutsetinfo"
|
||||||
|
"scantxoutset"
|
||||||
|
"verifytxoutproof"
|
||||||
|
# Mining
|
||||||
|
"getblocktemplate"
|
||||||
|
"getmininginfo"
|
||||||
|
"getnetworkhashps"
|
||||||
|
# Network
|
||||||
|
"getnetworkinfo"
|
||||||
|
"getnodeaddresses"
|
||||||
|
"getpeerinfo"
|
||||||
|
# Rawtransactions
|
||||||
|
"analyzepsbt"
|
||||||
|
"combinepsbt"
|
||||||
|
"combinerawtransaction"
|
||||||
|
"converttopsbt"
|
||||||
|
"createpsbt"
|
||||||
|
"createrawtransaction"
|
||||||
|
"decodepsbt"
|
||||||
|
"decoderawtransaction"
|
||||||
|
"decodescript"
|
||||||
|
"finalizepsbt"
|
||||||
|
"fundrawtransaction"
|
||||||
|
"getrawtransaction"
|
||||||
|
"joinpsbts"
|
||||||
|
"sendrawtransaction"
|
||||||
|
"signrawtransactionwithkey"
|
||||||
|
"testmempoolaccept"
|
||||||
|
"utxoupdatepsbt"
|
||||||
|
# Util
|
||||||
|
"createmultisig"
|
||||||
|
"deriveaddresses"
|
||||||
|
"estimatesmartfee"
|
||||||
|
"getdescriptorinfo"
|
||||||
|
"signmessagewithprivkey"
|
||||||
|
"validateaddress"
|
||||||
|
"verifymessage"
|
||||||
|
# Zmq
|
||||||
|
"getzmqnotifications"
|
||||||
|
]
|
||||||
@@ -0,0 +1,506 @@
|
|||||||
|
{ 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 = if !cfg.regtest then 8333 else 18444;
|
||||||
|
defaultText = "if !cfg.regtest then 8333 else 18444";
|
||||||
|
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 {option}`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 = pkgs.bitcoind;
|
||||||
|
defaultText = "pkgs.bitcoind";
|
||||||
|
description = ''
|
||||||
|
The package providing bitcoind binaries.
|
||||||
|
|
||||||
|
You can use this option to select other bitcoind-compatible implementations.
|
||||||
|
Example:
|
||||||
|
```nix
|
||||||
|
services.bitcoind.package = pkgs.bitcoind-knots;
|
||||||
|
```
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
extraConfig = mkOption {
|
||||||
|
type = types.lines;
|
||||||
|
default = "";
|
||||||
|
example = ''
|
||||||
|
par=16
|
||||||
|
logips=1
|
||||||
|
'';
|
||||||
|
description = "Extra lines appended to {file}`bitcoin.conf`.";
|
||||||
|
};
|
||||||
|
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 = if !cfg.regtest then 8332 else 18443;
|
||||||
|
defaultText = "if !cfg.regtest then 8332 else 18443";
|
||||||
|
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 = {};
|
||||||
|
description = ''
|
||||||
|
Allowed users for JSON-RPC connections.
|
||||||
|
'';
|
||||||
|
example = {
|
||||||
|
alice = {
|
||||||
|
passwordHMAC = "f7efda5c189b999524f151318c0c86$d5b51b3beffbc02b724e5d095828e0bc8b2456e9ac8757ae3211a5d9b16a22ae";
|
||||||
|
rpcwhitelist = [ "sendtoaddress" "getnewaddress" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
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.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
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 (intAtLeast 4);
|
||||||
|
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.writers.writeBashBin "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
|
||||||
|
logtimestamps=0
|
||||||
|
|
||||||
|
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}:${user.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);
|
||||||
|
|
||||||
|
intAtLeast = n: types.addCheck types.int (x: x >= n) // {
|
||||||
|
name = "intAtLeast";
|
||||||
|
description = "integer >= ${toString n}";
|
||||||
|
};
|
||||||
|
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} - -"
|
||||||
|
];
|
||||||
|
|
||||||
|
systemd.services.bitcoind = rec {
|
||||||
|
wants = [
|
||||||
|
"network-online.target"
|
||||||
|
# Use `wants` instead of `requires` for `nix-bitcoin-secrets.target`
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
# TODO-EXTERNAL: Instead of `wants`, use a future systemd dependency type
|
||||||
|
# that propagates initial start failures but no restarts
|
||||||
|
"nix-bitcoin-secrets.target"
|
||||||
|
];
|
||||||
|
after = wants;
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
|
||||||
|
preStart = let
|
||||||
|
extraRpcauth = concatMapStrings (name: let
|
||||||
|
user = cfg.rpc.users.${name};
|
||||||
|
in optionalString user.passwordHMACFromFile ''
|
||||||
|
hmacPayload="$(readValidatedRpcHmac '${secretsDir}/bitcoin-HMAC-${name}')" || exit 1
|
||||||
|
printf '%s\n' "rpcauth=${user.name}:$hmacPayload"
|
||||||
|
''
|
||||||
|
) (builtins.attrNames cfg.rpc.users);
|
||||||
|
in ''
|
||||||
|
${optionalString cfg.dataDirReadableByGroup ''
|
||||||
|
if [[ -e '${cfg.dataDir}/blocks' ]]; then
|
||||||
|
chmod -R g+rX '${cfg.dataDir}/blocks'
|
||||||
|
fi
|
||||||
|
''}
|
||||||
|
|
||||||
|
readValidatedRpcHmac() {
|
||||||
|
local hmacFile="$1"
|
||||||
|
local hmacPayload
|
||||||
|
|
||||||
|
if [[ ! -e "$hmacFile" ]]; then
|
||||||
|
echo "Error: Bitcoin RPC HMAC file is missing: $hmacFile" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ ! -r "$hmacFile" ]]; then
|
||||||
|
echo "Error: Bitcoin RPC HMAC file is unreadable: $hmacFile" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
hmacPayload="$(<"$hmacFile")"
|
||||||
|
if [[ -z "$hmacPayload" ]]; then
|
||||||
|
echo "Error: Bitcoin RPC HMAC file is empty: $hmacFile" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ ! "$hmacPayload" =~ ^[[:xdigit:]]+\$[[:xdigit:]]+$ ]]; then
|
||||||
|
echo "Error: Bitcoin RPC HMAC file has invalid format: $hmacFile" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$hmacPayload"
|
||||||
|
}
|
||||||
|
|
||||||
|
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'
|
||||||
|
'' + (optionalString cfg.regtest) ''
|
||||||
|
chmod g=x '${cfg.dataDir}/regtest'
|
||||||
|
'';
|
||||||
|
|
||||||
|
serviceConfig = nbLib.defaultHardening // {
|
||||||
|
Type = "notify";
|
||||||
|
NotifyAccess = "all";
|
||||||
|
User = cfg.user;
|
||||||
|
Group = cfg.group;
|
||||||
|
TimeoutStartSec = "30min";
|
||||||
|
TimeoutStopSec = "30min";
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,328 @@
|
|||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
options.services = {
|
||||||
|
nbxplorer = {
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Enable nbxplorer, a lightweight API for Bitcoin HD wallets.
|
||||||
|
|
||||||
|
Access API documentation here:
|
||||||
|
{option}`services.nbxplorer.address`:{option}`services.nbxplorer.port`
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
address = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "127.0.0.1";
|
||||||
|
description = "Address to listen on.";
|
||||||
|
};
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 24444;
|
||||||
|
description = "Port to listen on.";
|
||||||
|
};
|
||||||
|
package = mkOption {
|
||||||
|
type = types.package;
|
||||||
|
default = pkgs.stable.nbxplorer;
|
||||||
|
defaultText = "pkgs.stable.nbxplorer";
|
||||||
|
description = "The package providing nbxplorer binaries.";
|
||||||
|
};
|
||||||
|
dataDir = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
default = "/var/lib/nbxplorer";
|
||||||
|
description = "The data directory for nbxplorer.";
|
||||||
|
};
|
||||||
|
# TODO-EXTERNAL:
|
||||||
|
# The shortcut link `main` in the datadir has changed to a directory
|
||||||
|
# in version 2.3.3.
|
||||||
|
# Add a dummy symlink, if it does not already exist, to be compatible with older modules.
|
||||||
|
# When the old system uses a link and the new system a directory, switching fails with:
|
||||||
|
# mv: cannot move '/var/lib/nbxplorer/Main' to '/var/lib/nbxplorer/.Main.tmp':
|
||||||
|
# No such file or directory
|
||||||
|
#
|
||||||
|
# Remove this option when it is irrelevant (i.e. when the old system will never
|
||||||
|
# be nix-bitcoin <=0.0.91)
|
||||||
|
addNetworkSymlink = mkOption {
|
||||||
|
readOnly = true;
|
||||||
|
default = pkgs.stable.nbxplorer != cfg.nbxplorer.package;
|
||||||
|
description = ''
|
||||||
|
Whether to add a compatibility symlink (like `${cfg.nbxplorer.dataDir}/Main`)
|
||||||
|
to the dataDir.
|
||||||
|
This is enabled by default if the nbxplorer package is set to the version-locked package.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "nbxplorer";
|
||||||
|
description = "The user as which to run NBXplorer.";
|
||||||
|
};
|
||||||
|
group = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = cfg.nbxplorer.user;
|
||||||
|
description = "The group as which to run NBXplorer.";
|
||||||
|
};
|
||||||
|
tor = nbLib.tor;
|
||||||
|
};
|
||||||
|
|
||||||
|
btcpayserver = {
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Enable BTCPay Server, a self-hosted, open-source payment processor.
|
||||||
|
|
||||||
|
Extra recommendations:
|
||||||
|
- Enable `services.btcpayserver.lightningBackend` to provide Lightning payment support.
|
||||||
|
- Secure this service if the instance is publically accessible. For example, set
|
||||||
|
{option}`services.btcpayserver.address` to `127.0.0.1` and use a reverse proxy
|
||||||
|
that enforces TLS (Transport Layer Security).
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
package = mkOption {
|
||||||
|
type = types.package;
|
||||||
|
default = pkgs.stable.btcpayserver;
|
||||||
|
defaultText = "pkgs.stable.btcpayserver";
|
||||||
|
description = "The package providing BTCPay Server binaries.";
|
||||||
|
};
|
||||||
|
address = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "127.0.0.1";
|
||||||
|
description = "Address to listen on.";
|
||||||
|
};
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 23000;
|
||||||
|
description = "Port to listen on.";
|
||||||
|
};
|
||||||
|
lightningBackend = mkOption {
|
||||||
|
type = types.nullOr (types.enum [ "lnd" ]);
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
The lightning node to use as a backend.
|
||||||
|
Enables the node service if not already enabled.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
lbtc = mkOption {
|
||||||
|
readOnly = true;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Enable Liquid support.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
dataDir = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
default = "/var/lib/btcpayserver";
|
||||||
|
description = "The data directory for BTCPay Server.";
|
||||||
|
};
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "btcpayserver";
|
||||||
|
description = "The user as which to run BTCPay Server.";
|
||||||
|
};
|
||||||
|
group = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = cfg.btcpayserver.user;
|
||||||
|
description = "The group as which to run BTCPay Server.";
|
||||||
|
};
|
||||||
|
tor = nbLib.tor;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
cfg = {
|
||||||
|
inherit (config.services)
|
||||||
|
nbxplorer
|
||||||
|
btcpayserver
|
||||||
|
bitcoind;
|
||||||
|
};
|
||||||
|
|
||||||
|
nbLib = config.nix-bitcoin.lib;
|
||||||
|
secretsDir = config.nix-bitcoin.secretsDir;
|
||||||
|
|
||||||
|
in {
|
||||||
|
inherit options;
|
||||||
|
|
||||||
|
config = mkMerge [
|
||||||
|
(mkIf cfg.nbxplorer.enable {
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d '${cfg.nbxplorer.dataDir}' 0770 ${cfg.nbxplorer.user} ${cfg.nbxplorer.group} - -"
|
||||||
|
] ++ optional cfg.nbxplorer.addNetworkSymlink
|
||||||
|
"L+ '${cfg.nbxplorer.dataDir}/Main' - - - - '${cfg.nbxplorer.dataDir}/main'";
|
||||||
|
|
||||||
|
systemd.services.nbxplorer = let
|
||||||
|
configFile = builtins.toFile "nbxplorer-config" ''
|
||||||
|
network=${cfg.bitcoind.network}
|
||||||
|
btcrpcuser=${cfg.bitcoind.rpc.users.btcpayserver.name}
|
||||||
|
btcrpcurl=http://${nbLib.addressWithPort cfg.bitcoind.rpc.address cfg.bitcoind.rpc.port}
|
||||||
|
btcnodeendpoint=${nbLib.addressWithPort cfg.bitcoind.address cfg.bitcoind.whitelistedPort}
|
||||||
|
bind=${cfg.nbxplorer.address}
|
||||||
|
port=${toString cfg.nbxplorer.port}
|
||||||
|
postgres=User ID=${cfg.nbxplorer.user};Host=/run/postgresql;Database=nbxplorer
|
||||||
|
'';
|
||||||
|
in rec {
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
requires = [ "postgresql.target" ];
|
||||||
|
wants = [ "bitcoind.service" ];
|
||||||
|
after = requires ++ wants ++ [ "nix-bitcoin-secrets.target" ];
|
||||||
|
preStart = ''
|
||||||
|
install -m 600 ${configFile} '${cfg.nbxplorer.dataDir}/settings.config'
|
||||||
|
printf '%s\n' "btcrpcpassword=$(<${secretsDir}/bitcoin-rpcpassword-btcpayserver)" \
|
||||||
|
>> '${cfg.nbxplorer.dataDir}/settings.config'
|
||||||
|
'';
|
||||||
|
serviceConfig = nbLib.defaultHardening // {
|
||||||
|
ExecStart = ''
|
||||||
|
${cfg.nbxplorer.package}/bin/nbxplorer --conf=${cfg.nbxplorer.dataDir}/settings.config \
|
||||||
|
--datadir='${cfg.nbxplorer.dataDir}'
|
||||||
|
'';
|
||||||
|
RuntimeDirectory = "nbxplorer";
|
||||||
|
StateDirectory = "nbxplorer";
|
||||||
|
User = cfg.nbxplorer.user;
|
||||||
|
Group = cfg.nbxplorer.group;
|
||||||
|
Restart = "on-failure";
|
||||||
|
RestartSec = "10s";
|
||||||
|
ReadWritePaths = [ cfg.nbxplorer.dataDir ];
|
||||||
|
MemoryDenyWriteExecute = false;
|
||||||
|
} // nbLib.allowedIPAddresses cfg.nbxplorer.tor.enforce;
|
||||||
|
};
|
||||||
|
|
||||||
|
services.bitcoind = {
|
||||||
|
enable = true;
|
||||||
|
listenWhitelisted = true;
|
||||||
|
txindex = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
users.users.${cfg.nbxplorer.user} = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = cfg.nbxplorer.group;
|
||||||
|
};
|
||||||
|
users.groups.${cfg.nbxplorer.group} = {};
|
||||||
|
})
|
||||||
|
|
||||||
|
(mkIf cfg.btcpayserver.enable {
|
||||||
|
services.nbxplorer.enable = true;
|
||||||
|
|
||||||
|
services.bitcoind = {
|
||||||
|
listenWhitelisted = true;
|
||||||
|
rpc.users.btcpayserver = {
|
||||||
|
name = "btcpayserver";
|
||||||
|
passwordHMACFromFile = true;
|
||||||
|
rpcwhitelist = [
|
||||||
|
"getblockchaininfo"
|
||||||
|
"getblock"
|
||||||
|
"getblockhash"
|
||||||
|
"getblockheader"
|
||||||
|
"getblockstats"
|
||||||
|
"gettransaction"
|
||||||
|
"getrawtransaction"
|
||||||
|
"sendrawtransaction"
|
||||||
|
"getblockcount"
|
||||||
|
"getbestblockhash"
|
||||||
|
"getnetworkinfo"
|
||||||
|
"getpeerinfo"
|
||||||
|
"estimatesmartfee"
|
||||||
|
"getmempoolinfo"
|
||||||
|
"getmempoolentry"
|
||||||
|
"getrawmempool"
|
||||||
|
"gettxout"
|
||||||
|
"scantxoutset"
|
||||||
|
"importmulti"
|
||||||
|
"listunspent"
|
||||||
|
"getwalletinfo"
|
||||||
|
"listtransactions"
|
||||||
|
"listreceivedbyaddress"
|
||||||
|
"getnewaddress"
|
||||||
|
"uptime"
|
||||||
|
"getrpcinfo"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d '${cfg.btcpayserver.dataDir}' 0770 ${cfg.btcpayserver.user} ${cfg.btcpayserver.group} - -"
|
||||||
|
];
|
||||||
|
|
||||||
|
systemd.services.btcpayserver = let
|
||||||
|
nbExplorerUrl = "http://${nbLib.addressWithPort cfg.nbxplorer.address cfg.nbxplorer.port}/";
|
||||||
|
configFile = builtins.toFile "btcpayserver-config" (
|
||||||
|
''
|
||||||
|
network=${cfg.bitcoind.network}
|
||||||
|
bind=${cfg.btcpayserver.address}
|
||||||
|
port=${toString cfg.btcpayserver.port}
|
||||||
|
socksendpoint=${config.nix-bitcoin.torClientAddressWithPort}
|
||||||
|
btcexplorerurl=${nbExplorerUrl}
|
||||||
|
explorer.postgres=User ID=${cfg.nbxplorer.user};Host=/run/postgresql;Database=nbxplorer
|
||||||
|
postgres=User ID=${cfg.btcpayserver.user};Host=/run/postgresql;Database=btcpayserver
|
||||||
|
'' + optionalString (cfg.btcpayserver.lightningBackend == "lnd")
|
||||||
|
(
|
||||||
|
"btclightning=type=lnd-rest;"
|
||||||
|
+ "server=https://${nbLib.address config.services.lnd.restAddress}:${toString config.services.lnd.restPort}/;"
|
||||||
|
+ "macaroonfilepath=/run/lnd/btcpayserver.macaroon;"
|
||||||
|
+ "certfilepath=${config.services.lnd.certPath}\n"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
in rec {
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
requires = [ "postgresql.target" "nbxplorer.service" ];
|
||||||
|
wants = optional (cfg.btcpayserver.lightningBackend == "lnd") "lnd.service";
|
||||||
|
after = requires ++ wants;
|
||||||
|
serviceConfig = nbLib.defaultHardening // {
|
||||||
|
ExecStart = ''
|
||||||
|
${cfg.btcpayserver.package}/bin/btcpayserver --conf=${configFile} \
|
||||||
|
--datadir='${cfg.btcpayserver.dataDir}'
|
||||||
|
'';
|
||||||
|
RuntimeDirectory = "btcpayserver";
|
||||||
|
StateDirectory = "btcpayserver";
|
||||||
|
User = cfg.btcpayserver.user;
|
||||||
|
Group = cfg.btcpayserver.group;
|
||||||
|
Restart = "on-failure";
|
||||||
|
RestartSec = "10s";
|
||||||
|
ReadWritePaths = [ cfg.btcpayserver.dataDir ];
|
||||||
|
MemoryDenyWriteExecute = false;
|
||||||
|
} // nbLib.allowedIPAddresses cfg.btcpayserver.tor.enforce;
|
||||||
|
};
|
||||||
|
|
||||||
|
services.postgresql = {
|
||||||
|
enable = true;
|
||||||
|
ensureDatabases = [ "btcpayserver" "nbxplorer" ];
|
||||||
|
ensureUsers = [
|
||||||
|
{ name = cfg.btcpayserver.user; ensureDBOwnership = true; }
|
||||||
|
{ name = cfg.nbxplorer.user; ensureDBOwnership = true; }
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
users.users.${cfg.btcpayserver.user} = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = cfg.btcpayserver.group;
|
||||||
|
extraGroups = optional (cfg.btcpayserver.lightningBackend == "lnd") config.services.lnd.group;
|
||||||
|
};
|
||||||
|
users.groups.${cfg.btcpayserver.group} = {};
|
||||||
|
|
||||||
|
nix-bitcoin.secrets = {
|
||||||
|
bitcoin-rpcpassword-btcpayserver = {
|
||||||
|
user = cfg.bitcoind.user;
|
||||||
|
group = cfg.nbxplorer.group;
|
||||||
|
};
|
||||||
|
bitcoin-HMAC-btcpayserver.user = cfg.bitcoind.user;
|
||||||
|
};
|
||||||
|
nix-bitcoin.generateSecretsCmds.btcpayserver = ''
|
||||||
|
makeBitcoinRPCPassword btcpayserver
|
||||||
|
'';
|
||||||
|
})
|
||||||
|
|
||||||
|
(mkIf (cfg.btcpayserver.enable && cfg.btcpayserver.lightningBackend == "lnd") {
|
||||||
|
services.lnd = {
|
||||||
|
enable = true;
|
||||||
|
macaroons.btcpayserver = {
|
||||||
|
user = cfg.btcpayserver.user;
|
||||||
|
permissions = ''
|
||||||
|
{"entity":"address","action":"write"},{"entity":"info","action":"read"},{"entity":"invoices","action":"read"},{"entity":"invoices","action":"write"},{"entity":"offchain","action":"read"},{"entity":"offchain","action":"write"},{"entity":"onchain","action":"read"},{"entity":"onchain","action":"write"},{"entity":"peers","action":"read"},{"entity":"peers","action":"write"}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
users.users.${config.services.lnd.user}.extraGroups = [ cfg.btcpayserver.group ];
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Common Bitcoin infrastructure: secrets, onion services, nodeinfo, security
|
||||||
|
# Extracted from nix-bitcoin, tailored for Sovran (lnd-only)
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
./nix-bitcoin.nix
|
||||||
|
./secrets/secrets.nix
|
||||||
|
./operator.nix
|
||||||
|
./security.nix
|
||||||
|
./onion-addresses.nix
|
||||||
|
./onion-services.nix
|
||||||
|
./nodeinfo.nix
|
||||||
|
./versioning.nix
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Sovran Bitcoin stack - tailored from nix-bitcoin, lnd-only, nixpkgs packages
|
||||||
|
# Original: https://github.com/fort-nix/nix-bitcoin
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
./common.nix
|
||||||
|
./bitcoind.nix
|
||||||
|
./electrs.nix
|
||||||
|
./lnd.nix
|
||||||
|
./lndconnect.nix
|
||||||
|
./rtl.nix
|
||||||
|
./btcpayserver.nix
|
||||||
|
./mempool.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
disabledModules = [ "services/networking/bitcoind.nix" ];
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
options.services.electrs = {
|
||||||
|
enable = mkEnableOption "electrs, an Electrum server implemented in Rust";
|
||||||
|
address = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "127.0.0.1";
|
||||||
|
description = "Address to listen for RPC connections.";
|
||||||
|
};
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 50001;
|
||||||
|
description = "Port to listen for RPC connections.";
|
||||||
|
};
|
||||||
|
dataDir = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
default = "/var/lib/electrs";
|
||||||
|
description = "The data directory for electrs.";
|
||||||
|
};
|
||||||
|
monitoringPort = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 4224;
|
||||||
|
description = "Prometheus monitoring port.";
|
||||||
|
};
|
||||||
|
extraArgs = mkOption {
|
||||||
|
type = types.separatedString " ";
|
||||||
|
default = "";
|
||||||
|
description = "Extra command line arguments passed to electrs.";
|
||||||
|
};
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "electrs";
|
||||||
|
description = "The user as which to run electrs.";
|
||||||
|
};
|
||||||
|
group = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = cfg.user;
|
||||||
|
description = "The group as which to run electrs.";
|
||||||
|
};
|
||||||
|
tor.enforce = nbLib.tor.enforce;
|
||||||
|
};
|
||||||
|
|
||||||
|
cfg = config.services.electrs;
|
||||||
|
nbLib = config.nix-bitcoin.lib;
|
||||||
|
secretsDir = config.nix-bitcoin.secretsDir;
|
||||||
|
bitcoind = config.services.bitcoind;
|
||||||
|
in {
|
||||||
|
inherit options;
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
assertions = [
|
||||||
|
{ assertion = bitcoind.prune == 0;
|
||||||
|
message = "electrs does not support bitcoind pruning.";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
services.bitcoind = {
|
||||||
|
enable = true;
|
||||||
|
listenWhitelisted = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -"
|
||||||
|
];
|
||||||
|
|
||||||
|
systemd.services.electrs = {
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
requires = [ "bitcoind.service" ];
|
||||||
|
after = [ "bitcoind.service" "nix-bitcoin-secrets.target" ];
|
||||||
|
preStart = ''
|
||||||
|
echo "auth = \"${bitcoind.rpc.users.public.name}:$(cat ${secretsDir}/bitcoin-rpcpassword-public)\"" \
|
||||||
|
> electrs.toml
|
||||||
|
'';
|
||||||
|
serviceConfig = nbLib.defaultHardening // {
|
||||||
|
# electrs only uses the working directory for reading electrs.toml
|
||||||
|
WorkingDirectory = cfg.dataDir;
|
||||||
|
ExecStart = ''
|
||||||
|
${pkgs.electrs}/bin/electrs \
|
||||||
|
--log-filters=INFO \
|
||||||
|
--network=${bitcoind.makeNetworkName "bitcoin" "regtest"} \
|
||||||
|
--db-dir='${cfg.dataDir}' \
|
||||||
|
--daemon-dir='${bitcoind.dataDir}' \
|
||||||
|
--electrum-rpc-addr=${cfg.address}:${toString cfg.port} \
|
||||||
|
--monitoring-addr=${cfg.address}:${toString cfg.monitoringPort} \
|
||||||
|
--daemon-rpc-addr=${nbLib.addressWithPort bitcoind.rpc.address bitcoind.rpc.port} \
|
||||||
|
--daemon-p2p-addr=${nbLib.addressWithPort bitcoind.address bitcoind.whitelistedPort} \
|
||||||
|
${cfg.extraArgs}
|
||||||
|
'';
|
||||||
|
User = cfg.user;
|
||||||
|
Group = cfg.group;
|
||||||
|
Restart = "on-failure";
|
||||||
|
RestartSec = "10s";
|
||||||
|
ReadWritePaths = [ cfg.dataDir ];
|
||||||
|
} // nbLib.allowedIPAddresses cfg.tor.enforce;
|
||||||
|
};
|
||||||
|
|
||||||
|
users.users.${cfg.user} = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = cfg.group;
|
||||||
|
extraGroups = [ "bitcoinrpc-public" ];
|
||||||
|
};
|
||||||
|
users.groups.${cfg.group} = {};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
lib: pkgs: config:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
|
||||||
|
# See `man systemd.exec` and `man systemd.resource-control` for an explanation
|
||||||
|
# of the systemd-related options available through this file.
|
||||||
|
let self = {
|
||||||
|
# These settings roughly follow systemd's "strict" security profile
|
||||||
|
defaultHardening = {
|
||||||
|
PrivateTmp = true;
|
||||||
|
ProtectSystem = "strict";
|
||||||
|
ProtectHome = true;
|
||||||
|
NoNewPrivileges = true;
|
||||||
|
PrivateDevices = true;
|
||||||
|
MemoryDenyWriteExecute = true;
|
||||||
|
ProtectKernelTunables = true;
|
||||||
|
ProtectKernelModules = true;
|
||||||
|
ProtectKernelLogs = true;
|
||||||
|
ProtectClock = true;
|
||||||
|
ProtectProc = "invisible";
|
||||||
|
ProcSubset = "pid";
|
||||||
|
ProtectControlGroups = true;
|
||||||
|
RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
|
||||||
|
RestrictNamespaces = true;
|
||||||
|
LockPersonality = true;
|
||||||
|
IPAddressDeny = "any";
|
||||||
|
PrivateUsers = true;
|
||||||
|
RestrictSUIDSGID = true;
|
||||||
|
RemoveIPC = true;
|
||||||
|
RestrictRealtime = true;
|
||||||
|
ProtectHostname = true;
|
||||||
|
CapabilityBoundingSet = "";
|
||||||
|
# @system-service whitelist and docker seccomp blacklist (except for "clone"
|
||||||
|
# which is a core requirement for systemd services)
|
||||||
|
# @system-service is defined in src/shared/seccomp-util.c (systemd source)
|
||||||
|
SystemCallFilter = [ "@system-service" "~add_key kcmp keyctl mbind move_pages name_to_handle_at personality process_vm_readv process_vm_writev request_key setns unshare userfaultfd" ];
|
||||||
|
SystemCallArchitectures = "native";
|
||||||
|
};
|
||||||
|
|
||||||
|
allowNetlink = {
|
||||||
|
RestrictAddressFamilies = self.defaultHardening.RestrictAddressFamilies + " AF_NETLINK";
|
||||||
|
};
|
||||||
|
|
||||||
|
nodejs = {
|
||||||
|
# Required for JIT compilation
|
||||||
|
MemoryDenyWriteExecute = false;
|
||||||
|
# Required by nodejs >= 18
|
||||||
|
SystemCallFilter = self.defaultHardening.SystemCallFilter ++ [ "@pkey" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Allow takes precedence over Deny.
|
||||||
|
allowLocalIPAddresses = {
|
||||||
|
IPAddressAllow = [
|
||||||
|
"127.0.0.1/32"
|
||||||
|
"::1/128"
|
||||||
|
"169.254.0.0/16"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
allowAllIPAddresses = { IPAddressAllow = "any"; };
|
||||||
|
allowTor = self.allowLocalIPAddresses;
|
||||||
|
allowedIPAddresses = onlyLocal:
|
||||||
|
if onlyLocal
|
||||||
|
then self.allowLocalIPAddresses
|
||||||
|
else self.allowAllIPAddresses;
|
||||||
|
|
||||||
|
tor = {
|
||||||
|
proxy = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Whether to proxy outgoing connections with Tor.";
|
||||||
|
};
|
||||||
|
enforce = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Whether to enforce Tor on this service by only allowing connections
|
||||||
|
from and to localhost and link-local addresses.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
script = name: src: pkgs.writers.writeBash name ''
|
||||||
|
set -eo pipefail
|
||||||
|
${src}
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Used for ExecStart*
|
||||||
|
rootScript = name: src: "+${self.script name src}";
|
||||||
|
|
||||||
|
cliExec = mkOption {
|
||||||
|
# Used by netns-isolation to execute the cli in the service's private netns
|
||||||
|
internal = true;
|
||||||
|
type = types.str;
|
||||||
|
default = "exec";
|
||||||
|
};
|
||||||
|
|
||||||
|
mkOnionService = map: {
|
||||||
|
map = [ map ];
|
||||||
|
version = 3;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Convert a bind address, which may be a special INADDR_ANY address,
|
||||||
|
# to an actual IP address
|
||||||
|
address = addr:
|
||||||
|
if addr == "0.0.0.0" then
|
||||||
|
"127.0.0.1"
|
||||||
|
else if addr == "::" then
|
||||||
|
"::1"
|
||||||
|
else
|
||||||
|
addr;
|
||||||
|
|
||||||
|
addressWithPort = addr: port: "${self.address addr}:${toString port}";
|
||||||
|
|
||||||
|
optionalAttr = cond: name: if cond then name else null;
|
||||||
|
|
||||||
|
mkCertExtraAltNames = cert:
|
||||||
|
builtins.concatStringsSep "," (
|
||||||
|
(map (domain: "DNS:${domain}") cert.extraDomains) ++
|
||||||
|
(map (ip: "IP:${ip}") cert.extraIPs)
|
||||||
|
);
|
||||||
|
|
||||||
|
test = {
|
||||||
|
mkIfTest = test: mkIf (config.tests.${test} or false);
|
||||||
|
};
|
||||||
|
|
||||||
|
mkAlias = default: mkOption {
|
||||||
|
internal = true;
|
||||||
|
readOnly = true;
|
||||||
|
inherit default;
|
||||||
|
};
|
||||||
|
|
||||||
|
}; in self
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
options.services.lnd = {
|
||||||
|
enable = mkEnableOption "Lightning Network daemon, a Lightning Network implementation in Go";
|
||||||
|
address = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "127.0.0.1";
|
||||||
|
description = "Address to listen for peer connections";
|
||||||
|
};
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 9735;
|
||||||
|
description = "Port to listen for peer connections";
|
||||||
|
};
|
||||||
|
rpcAddress = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "127.0.0.1";
|
||||||
|
description = "Address to listen for RPC connections.";
|
||||||
|
};
|
||||||
|
rpcPort = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 10009;
|
||||||
|
description = "Port to listen for gRPC connections.";
|
||||||
|
};
|
||||||
|
restAddress = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "127.0.0.1";
|
||||||
|
description = "Address to listen for REST connections.";
|
||||||
|
};
|
||||||
|
restPort = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 8080;
|
||||||
|
description = "Port to listen for REST connections.";
|
||||||
|
};
|
||||||
|
dataDir = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
default = "/var/lib/lnd";
|
||||||
|
description = "The data directory for LND.";
|
||||||
|
};
|
||||||
|
networkDir = mkOption {
|
||||||
|
readOnly = true;
|
||||||
|
default = "${cfg.dataDir}/chain/bitcoin/${bitcoind.network}";
|
||||||
|
description = "The network data directory.";
|
||||||
|
};
|
||||||
|
tor-socks = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = if cfg.tor.proxy then config.nix-bitcoin.torClientAddressWithPort else null;
|
||||||
|
description = "Socks proxy for connecting to Tor nodes";
|
||||||
|
};
|
||||||
|
macaroons = mkOption {
|
||||||
|
default = {};
|
||||||
|
type = with types; attrsOf (submodule {
|
||||||
|
options = {
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
description = "User who owns the macaroon.";
|
||||||
|
};
|
||||||
|
permissions = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
example = ''
|
||||||
|
{"entity":"info","action":"read"},{"entity":"onchain","action":"read"}
|
||||||
|
'';
|
||||||
|
description = "List of granted macaroon permissions.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
description = ''
|
||||||
|
Extra macaroon definitions.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
certificate = {
|
||||||
|
extraIPs = mkOption {
|
||||||
|
type = with types; listOf str;
|
||||||
|
default = [];
|
||||||
|
example = [ "60.100.0.1" ];
|
||||||
|
description = ''
|
||||||
|
Extra `subjectAltName` IPs added to the certificate.
|
||||||
|
This works the same as lnd option {option}`tlsextraip`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
extraDomains = mkOption {
|
||||||
|
type = with types; listOf str;
|
||||||
|
default = [];
|
||||||
|
example = [ "example.com" ];
|
||||||
|
description = ''
|
||||||
|
Extra `subjectAltName` domain names added to the certificate.
|
||||||
|
This works the same as lnd option {option}`tlsextradomain`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
extraConfig = mkOption {
|
||||||
|
type = types.lines;
|
||||||
|
default = "";
|
||||||
|
example = ''
|
||||||
|
autopilot.active=1
|
||||||
|
'';
|
||||||
|
description = ''
|
||||||
|
Extra lines appended to {file}`lnd.conf`.
|
||||||
|
See here for all available options:
|
||||||
|
https://github.com/lightningnetwork/lnd/blob/master/sample-lnd.conf
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
package = mkOption {
|
||||||
|
type = types.package;
|
||||||
|
default = pkgs.lnd;
|
||||||
|
defaultText = "pkgs.lnd";
|
||||||
|
description = "The package providing lnd binaries.";
|
||||||
|
};
|
||||||
|
cli = mkOption {
|
||||||
|
default = pkgs.writers.writeBashBin "lncli"
|
||||||
|
# Switch user because lnd makes datadir contents readable by user only
|
||||||
|
''
|
||||||
|
${runAsUser} ${cfg.user} ${cfg.package}/bin/lncli \
|
||||||
|
--rpcserver ${cfg.rpcAddress}:${toString cfg.rpcPort} \
|
||||||
|
--tlscertpath '${cfg.certPath}' \
|
||||||
|
--macaroonpath '${networkDir}/admin.macaroon' "$@"
|
||||||
|
'';
|
||||||
|
defaultText = "(See source)";
|
||||||
|
description = "Binary to connect with the lnd instance.";
|
||||||
|
};
|
||||||
|
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.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "lnd";
|
||||||
|
description = "The user as which to run LND.";
|
||||||
|
};
|
||||||
|
group = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = cfg.user;
|
||||||
|
description = "The group as which to run LND.";
|
||||||
|
};
|
||||||
|
certPath = mkOption {
|
||||||
|
readOnly = true;
|
||||||
|
default = "${secretsDir}/lnd-cert";
|
||||||
|
description = "LND TLS certificate path.";
|
||||||
|
};
|
||||||
|
tor = nbLib.tor;
|
||||||
|
};
|
||||||
|
|
||||||
|
cfg = config.services.lnd;
|
||||||
|
nbLib = config.nix-bitcoin.lib;
|
||||||
|
secretsDir = config.nix-bitcoin.secretsDir;
|
||||||
|
runAsUser = config.nix-bitcoin.runAsUserCmd;
|
||||||
|
lndinit = "${(pkgs.callPackage ../../packages/lndinit {})}/bin/lndinit";
|
||||||
|
|
||||||
|
bitcoind = config.services.bitcoind;
|
||||||
|
|
||||||
|
bitcoindRpcAddress = nbLib.address bitcoind.rpc.address;
|
||||||
|
networkDir = cfg.networkDir;
|
||||||
|
configFile = pkgs.writeText "lnd.conf" ''
|
||||||
|
datadir=${cfg.dataDir}
|
||||||
|
tlscertpath=${cfg.certPath}
|
||||||
|
tlskeypath=${secretsDir}/lnd-key
|
||||||
|
|
||||||
|
# We're logging via journald
|
||||||
|
logging.file.disable=1
|
||||||
|
logging.console.no-timestamps=1
|
||||||
|
|
||||||
|
listen=${toString cfg.address}:${toString cfg.port}
|
||||||
|
rpclisten=${cfg.rpcAddress}:${toString cfg.rpcPort}
|
||||||
|
restlisten=${cfg.restAddress}:${toString cfg.restPort}
|
||||||
|
|
||||||
|
bitcoin.${bitcoind.network}=1
|
||||||
|
bitcoin.node=bitcoind
|
||||||
|
|
||||||
|
${optionalString (cfg.tor.proxy) "tor.active=true"}
|
||||||
|
${optionalString (cfg.tor-socks != null) "tor.socks=${cfg.tor-socks}"}
|
||||||
|
|
||||||
|
bitcoind.rpchost=${bitcoindRpcAddress}:${toString bitcoind.rpc.port}
|
||||||
|
bitcoind.rpcuser=${bitcoind.rpc.users.public.name}
|
||||||
|
bitcoind.zmqpubrawblock=${zmqHandleSpecialAddress bitcoind.zmqpubrawblock}
|
||||||
|
bitcoind.zmqpubrawtx=${zmqHandleSpecialAddress bitcoind.zmqpubrawtx}
|
||||||
|
|
||||||
|
wallet-unlock-password-file=${secretsDir}/lnd-wallet-password
|
||||||
|
|
||||||
|
${cfg.extraConfig}
|
||||||
|
'';
|
||||||
|
|
||||||
|
zmqHandleSpecialAddress = builtins.replaceStrings [ "0.0.0.0" "[::]" ] [ "127.0.0.1" "[::1]" ];
|
||||||
|
in {
|
||||||
|
|
||||||
|
inherit options;
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
assertions = [
|
||||||
|
{ assertion =
|
||||||
|
!(config.services ? clightning)
|
||||||
|
|| true; # clightning enable/port check disabled - option structure differs between nixpkgs versions (f13ff45 has plugins only, 8b8c811 removed). Sovran uses lnd only, so no conflict.
|
||||||
|
message = ''
|
||||||
|
LND and clightning can't both bind to lightning port 9735. Either
|
||||||
|
disable LND/clightning or change services.clightning.port or
|
||||||
|
services.lnd.port to a port other than 9735.
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
services.bitcoind = {
|
||||||
|
enable = true;
|
||||||
|
|
||||||
|
# Increase rpc thread count due to reports that lightning implementations fail
|
||||||
|
# under high bitcoind rpc load
|
||||||
|
rpc.threads = 16;
|
||||||
|
|
||||||
|
zmqpubrawblock = mkDefault "tcp://${bitcoindRpcAddress}:28332";
|
||||||
|
zmqpubrawtx = mkDefault "tcp://${bitcoindRpcAddress}:28333";
|
||||||
|
};
|
||||||
|
|
||||||
|
environment.systemPackages = [ cfg.package (hiPrio cfg.cli) ];
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -"
|
||||||
|
];
|
||||||
|
|
||||||
|
services.lnd.certificate.extraIPs = mkIf (cfg.rpcAddress != "127.0.0.1") [ "${cfg.rpcAddress}" ];
|
||||||
|
|
||||||
|
systemd.services.lnd = {
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
requires = [ "bitcoind.service" ];
|
||||||
|
after = [ "bitcoind.service" "nix-bitcoin-secrets.target" ];
|
||||||
|
preStart = ''
|
||||||
|
install -m600 ${configFile} '${cfg.dataDir}/lnd.conf'
|
||||||
|
{
|
||||||
|
echo "bitcoind.rpcpass=$(cat ${secretsDir}/bitcoin-rpcpassword-public)"
|
||||||
|
${optionalString (cfg.getPublicAddressCmd != "") ''
|
||||||
|
echo "externalip=$(${cfg.getPublicAddressCmd})"
|
||||||
|
''}
|
||||||
|
} >> '${cfg.dataDir}/lnd.conf'
|
||||||
|
|
||||||
|
if [[ ! -f ${networkDir}/wallet.db ]]; then
|
||||||
|
seed='${cfg.dataDir}/lnd-seed-mnemonic'
|
||||||
|
|
||||||
|
if [[ ! -f "$seed" ]]; then
|
||||||
|
echo "Create lnd seed"
|
||||||
|
(umask u=r,go=; ${lndinit} gen-seed > "$seed")
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Create lnd wallet"
|
||||||
|
${lndinit} -v init-wallet \
|
||||||
|
--file.seed="$seed" \
|
||||||
|
--file.wallet-password='${secretsDir}/lnd-wallet-password' \
|
||||||
|
--init-file.output-wallet-dir='${cfg.networkDir}'
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
serviceConfig = nbLib.defaultHardening // {
|
||||||
|
Type = "notify";
|
||||||
|
RuntimeDirectory = "lnd"; # Only used to store custom macaroons
|
||||||
|
RuntimeDirectoryMode = "711";
|
||||||
|
ExecStart = "${cfg.package}/bin/lnd --configfile='${cfg.dataDir}/lnd.conf'";
|
||||||
|
User = cfg.user;
|
||||||
|
TimeoutSec = "15min";
|
||||||
|
Restart = "on-failure";
|
||||||
|
RestartSec = "10s";
|
||||||
|
ReadWritePaths = [ cfg.dataDir ];
|
||||||
|
ExecStartPost = let
|
||||||
|
curl = "${pkgs.curl}/bin/curl -fsS --cacert ${cfg.certPath}";
|
||||||
|
restUrl = "https://${nbLib.addressWithPort cfg.restAddress cfg.restPort}/v1";
|
||||||
|
# Setting macaroon permissions for other users needs root permissions
|
||||||
|
script = nbLib.rootScript "lnd-create-macaroons" ''
|
||||||
|
umask ug=r,o=
|
||||||
|
${lib.concatMapStrings (macaroon: ''
|
||||||
|
echo "Create custom macaroon ${macaroon}"
|
||||||
|
macaroonPath="$RUNTIME_DIRECTORY/${macaroon}.macaroon"
|
||||||
|
${curl} \
|
||||||
|
-H "Grpc-Metadata-macaroon: $(${pkgs.xxd}/bin/xxd -ps -u -c 99999 '${networkDir}/admin.macaroon')" \
|
||||||
|
-X POST \
|
||||||
|
-d '{"permissions":[${cfg.macaroons.${macaroon}.permissions}]}' \
|
||||||
|
${restUrl}/macaroon |\
|
||||||
|
${pkgs.jq}/bin/jq -c '.macaroon' | ${pkgs.xxd}/bin/xxd -p -r > "$macaroonPath"
|
||||||
|
chown ${cfg.macaroons.${macaroon}.user}: "$macaroonPath"
|
||||||
|
'') (attrNames cfg.macaroons)}
|
||||||
|
'';
|
||||||
|
in [
|
||||||
|
script
|
||||||
|
];
|
||||||
|
} // nbLib.allowedIPAddresses cfg.tor.enforce;
|
||||||
|
};
|
||||||
|
|
||||||
|
users.users.${cfg.user} = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = cfg.group;
|
||||||
|
extraGroups = [ "bitcoinrpc-public" ];
|
||||||
|
home = cfg.dataDir; # lnd creates .lnd dir in HOME
|
||||||
|
};
|
||||||
|
users.groups.${cfg.group} = {};
|
||||||
|
nix-bitcoin.operator = {
|
||||||
|
groups = [ cfg.group ];
|
||||||
|
allowRunAsUsers = [ cfg.user ];
|
||||||
|
};
|
||||||
|
|
||||||
|
nix-bitcoin.secrets = {
|
||||||
|
lnd-wallet-password.user = cfg.user;
|
||||||
|
lnd-key.user = cfg.user;
|
||||||
|
lnd-cert.user = cfg.user;
|
||||||
|
lnd-cert.permissions = "444"; # world readable
|
||||||
|
};
|
||||||
|
# Advantages of manually pre-generating certs:
|
||||||
|
# - Reduces dynamic state
|
||||||
|
# - Enables deployment of a mesh of server plus client nodes with predefined certs
|
||||||
|
nix-bitcoin.generateSecretsCmds.lnd = ''
|
||||||
|
makePasswordSecret lnd-wallet-password
|
||||||
|
makeCert lnd '${nbLib.mkCertExtraAltNames cfg.certificate}'
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
cfg = config.services.lnd;
|
||||||
|
operatorName = config.nix-bitcoin.operator.name;
|
||||||
|
nbLib = config.nix-bitcoin.lib;
|
||||||
|
|
||||||
|
mkLndconnect = { name, isClightning ? false, enableOnion, onionService, port, certPath, authSecretPath }:
|
||||||
|
let
|
||||||
|
lnd = config.services.lnd;
|
||||||
|
getOnionAddress = "cat ${config.nix-bitcoin.secretsDir}/onion-address-${onionService} 2>/dev/null || echo ${onionService}.onion";
|
||||||
|
in pkgs.writeScriptBin name ''
|
||||||
|
#!${pkgs.bash}/bin/bash
|
||||||
|
set -e
|
||||||
|
certPath="${certPath}"
|
||||||
|
authSecretPath="${authSecretPath}"
|
||||||
|
if [ "${toString enableOnion}" = "1" ]; then
|
||||||
|
host=$(cat /var/lib/tor/onion/${onionService}/hostname 2>/dev/null || echo "${onionService}.onion")
|
||||||
|
port="${toString port}"
|
||||||
|
else
|
||||||
|
host="${nbLib.address lnd.restAddress}"
|
||||||
|
port="${toString lnd.restPort}"
|
||||||
|
fi
|
||||||
|
# lndconnect is provided by pkgs.lndconnect
|
||||||
|
${getExe pkgs.lndconnect} --host="$host" --port="$port" --cert="$certPath" --macaroon="$authSecretPath" "$@"
|
||||||
|
'';
|
||||||
|
in {
|
||||||
|
options.services.lnd.lndconnect = {
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Enable lndconnect for LND";
|
||||||
|
};
|
||||||
|
onion = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Expose lndconnect via Tor onion service";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf cfg.enable (mkMerge [
|
||||||
|
(mkIf cfg.lndconnect.enable {
|
||||||
|
environment.systemPackages = [
|
||||||
|
(mkLndconnect {
|
||||||
|
name = "lndconnect";
|
||||||
|
enableOnion = cfg.lndconnect.onion;
|
||||||
|
onionService = "${operatorName}/lnd";
|
||||||
|
port = cfg.restPort;
|
||||||
|
certPath = cfg.certPath;
|
||||||
|
authSecretPath = "${cfg.networkDir}/admin.macaroon";
|
||||||
|
})
|
||||||
|
];
|
||||||
|
})
|
||||||
|
(mkIf (cfg.lndconnect.enable && cfg.lndconnect.onion) {
|
||||||
|
services.tor.relay.onionServices.lnd = nbLib.mkOnionService {
|
||||||
|
port = cfg.restPort;
|
||||||
|
target = { addr = nbLib.address cfg.restAddress; port = cfg.restPort; };
|
||||||
|
};
|
||||||
|
nix-bitcoin.onionAddresses.access.${operatorName} = [ "lnd" ];
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
options.services = {
|
||||||
|
mempool = {
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Enable Mempool, a fully featured Bitcoin visualizer, explorer, and API service.
|
||||||
|
|
||||||
|
Note: Mempool enables `txindex` in bitcoind (this is a requirement).
|
||||||
|
|
||||||
|
This module has two components:
|
||||||
|
- A backend service (systemd service `mempool`)
|
||||||
|
|
||||||
|
- An optional web interface run by nginx, defined by options `services.mempool.frontend.*`.
|
||||||
|
The frontend is enabled by default when mempool is enabled.
|
||||||
|
For details, see `services.mempool.frontend.enable`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
frontend = {
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = cfg.enable;
|
||||||
|
description = ''
|
||||||
|
Enable the mempool frontend (web interface).
|
||||||
|
This starts a simple nginx instance, configured for local usage with
|
||||||
|
settings similar to the `mempool/frontend` Docker image.
|
||||||
|
|
||||||
|
IMPORTANT:
|
||||||
|
If you want to expose the mempool frontend to the internet, you
|
||||||
|
should create a custom nginx config that includes TLS, backend caching, rate limiting
|
||||||
|
and performance tuning.
|
||||||
|
For this task, reuse the config snippets from option `services.mempool.frontend.nginxConfig`.
|
||||||
|
See also: https://github.com/fort-nix/nixbitcoin.org/blob/master/website/mempool.nix,
|
||||||
|
which contains a mempool nginx config for public hosting (running at
|
||||||
|
https://mempool.nixbitcoin.org).
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
address = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "127.0.0.1";
|
||||||
|
description = "HTTP server address.";
|
||||||
|
};
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 60845; # A random private port
|
||||||
|
description = "HTTP server port.";
|
||||||
|
};
|
||||||
|
settings = mkOption {
|
||||||
|
type = with types; attrsOf anything;
|
||||||
|
default = {};
|
||||||
|
example = {
|
||||||
|
TESTNET_ENABLED = true;
|
||||||
|
MEMPOOL_WEBSITE_URL = "mempool.mynode.org";
|
||||||
|
};
|
||||||
|
description = ''
|
||||||
|
Mempool frontend settings.
|
||||||
|
See here for available options:
|
||||||
|
https://github.com/mempool/mempool/blob/master/frontend/src/app/services/state.service.ts
|
||||||
|
(`interface Env` and `defaultEnv`)
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
staticContentRoot = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
default = (pkgs.callPackage ../../packages/mempool { fetchNodeModules = pkgs.callPackage ../../packages/build-support/fetch-node-modules.nix {}; }).mempool-frontend.withConfig cfg.frontend.settings;
|
||||||
|
defaultText = "mempoolPkgs.mempool-frontend";
|
||||||
|
description = "
|
||||||
|
Path of the static frontend content root.
|
||||||
|
";
|
||||||
|
};
|
||||||
|
nginxConfig = mkOption {
|
||||||
|
readOnly = true;
|
||||||
|
default = frontend.nginxConfig;
|
||||||
|
defaultText = "(See source)";
|
||||||
|
description = "
|
||||||
|
An attrset of nginx config snippets for assembling a custom
|
||||||
|
mempool nginx config.
|
||||||
|
For details, see the source comments at the point of definition.
|
||||||
|
";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
address = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "127.0.0.1";
|
||||||
|
description = "Mempool backend address.";
|
||||||
|
};
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 8999;
|
||||||
|
description = "Mempool backend port.";
|
||||||
|
};
|
||||||
|
electrumServer = mkOption {
|
||||||
|
type = types.enum [ "electrs" ];
|
||||||
|
default = "electrs";
|
||||||
|
description = ''
|
||||||
|
The Electrum server to use for fetching address information.
|
||||||
|
|
||||||
|
Possible options:
|
||||||
|
- electrs:
|
||||||
|
Small database size, slow when querying new addresses.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
settings = mkOption {
|
||||||
|
type = with types; attrsOf (attrsOf anything);
|
||||||
|
example = {
|
||||||
|
MEMPOOL = {
|
||||||
|
POLL_RATE_MS = 3000;
|
||||||
|
STDOUT_LOG_MIN_PRIORITY = "debug";
|
||||||
|
};
|
||||||
|
PRICE_DATA_SERVER = {
|
||||||
|
CLEARNET_URL = "https://myserver.org/prices";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
description = ''
|
||||||
|
Mempool backend settings.
|
||||||
|
See here for available options:
|
||||||
|
https://github.com/mempool/mempool/blob/master/backend/src/config.ts
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
database = {
|
||||||
|
name = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "mempool";
|
||||||
|
description = "Database name.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
package = mkOption {
|
||||||
|
type = types.package;
|
||||||
|
default = (pkgs.callPackage ../../packages/mempool { fetchNodeModules = pkgs.callPackage ../../packages/build-support/fetch-node-modules.nix {}; }).mempool-backend;
|
||||||
|
defaultText = "mempoolPkgs.mempool-backend";
|
||||||
|
description = "The package providing mempool binaries.";
|
||||||
|
};
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "mempool";
|
||||||
|
description = "The user as which to run Mempool.";
|
||||||
|
};
|
||||||
|
group = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = cfg.user;
|
||||||
|
description = "The group as which to run Mempool.";
|
||||||
|
};
|
||||||
|
tor = nbLib.tor;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Internal read-only options used by `./nodeinfo.nix` and `./onion-services.nix`
|
||||||
|
mempool-frontend = let
|
||||||
|
inherit (nbLib) mkAlias;
|
||||||
|
in {
|
||||||
|
enable = mkAlias cfg.frontend.enable;
|
||||||
|
address = mkAlias cfg.frontend.address;
|
||||||
|
port = mkAlias cfg.frontend.port;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
cfg = config.services.mempool;
|
||||||
|
nbLib = config.nix-bitcoin.lib;
|
||||||
|
nbPkgs = pkgs; # vendored: now alias to pkgs
|
||||||
|
secretsDir = config.nix-bitcoin.secretsDir;
|
||||||
|
|
||||||
|
configFile = builtins.toFile "mempool-config" (builtins.toJSON cfg.settings);
|
||||||
|
cacheDir = "/var/cache/mempool";
|
||||||
|
|
||||||
|
inherit (config.services)
|
||||||
|
bitcoind
|
||||||
|
electrs;
|
||||||
|
|
||||||
|
torSocket = config.services.tor.client.socksListenAddress;
|
||||||
|
# Vendored mempool package
|
||||||
|
fetchNodeModules = pkgs.callPackage ../../packages/build-support/fetch-node-modules.nix {};
|
||||||
|
mempoolPkgs = pkgs.callPackage ../../packages/mempool { inherit fetchNodeModules; };
|
||||||
|
|
||||||
|
# See the `services.nginx` definition further below
|
||||||
|
# on how to use these snippets.
|
||||||
|
frontend.nginxConfig = {
|
||||||
|
# This must be added to `services.nginx.commonHttpConfig` when
|
||||||
|
# `mempool/location-static.conf` is used
|
||||||
|
httpConfig = ''
|
||||||
|
include ${mempoolPkgs.mempool-nginx-conf}/http-language.conf;
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Config for static website content.
|
||||||
|
# This should be added to `services.nginx.virtualHosts.<mempool server name>.extraConfig`.
|
||||||
|
# Adapted from mempool/nginx-mempool.conf and mempool/production/nginx/location-redirects.conf
|
||||||
|
staticContent = ''
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
add_header Cache-Control "public, no-transform";
|
||||||
|
add_header Vary Accept-Language;
|
||||||
|
add_header Vary Cookie;
|
||||||
|
|
||||||
|
include ${mempoolPkgs.mempool-nginx-conf}/location-static.conf;
|
||||||
|
|
||||||
|
# Redirect /api to /docs/api
|
||||||
|
location = /api {
|
||||||
|
return 308 https://$host/docs/api;
|
||||||
|
}
|
||||||
|
location = /api/ {
|
||||||
|
return 308 https://$host/docs/api;
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Config for backend API.
|
||||||
|
# This should be added to `services.nginx.virtualHosts.<mempool server name>.extraConfig`.
|
||||||
|
# Adapted from mempool/nginx-mempool.conf and mempool/production/nginx/location-api.conf.
|
||||||
|
proxyApi = let
|
||||||
|
backend = "http://${nbLib.addressWithPort cfg.address cfg.port}";
|
||||||
|
in ''
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass ${backend}/api/v1/;
|
||||||
|
}
|
||||||
|
location /api/v1 {
|
||||||
|
proxy_pass ${backend};
|
||||||
|
}
|
||||||
|
# Websocket API
|
||||||
|
location /api/v1/ws {
|
||||||
|
proxy_pass ${backend};
|
||||||
|
|
||||||
|
# Websocket header settings
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "Upgrade";
|
||||||
|
|
||||||
|
# Relevant settings from `recommendedProxyConfig` (nixos/nginx/default.nix)
|
||||||
|
# (In the above api locations, these are inherited from the parent scope)
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
in {
|
||||||
|
inherit options;
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
services.bitcoind.txindex = true;
|
||||||
|
services.electrs.enable = true;
|
||||||
|
services.mysql = {
|
||||||
|
enable = true;
|
||||||
|
package = pkgs.mariadb;
|
||||||
|
ensureDatabases = [ cfg.database.name ];
|
||||||
|
ensureUsers = [
|
||||||
|
{
|
||||||
|
name = cfg.user;
|
||||||
|
ensurePermissions."${cfg.database.name}.*" = "ALL PRIVILEGES";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Available options:
|
||||||
|
# https://github.com/mempool/mempool/blob/master/backend/src/config.ts
|
||||||
|
services.mempool.settings = {
|
||||||
|
MEMPOOL = {
|
||||||
|
# mempool doesn't support regtest
|
||||||
|
NETWORK = "mainnet";
|
||||||
|
BACKEND = "electrum";
|
||||||
|
HTTP_PORT = cfg.port;
|
||||||
|
CACHE_DIR = "${cacheDir}/cache";
|
||||||
|
STDOUT_LOG_MIN_PRIORITY = mkDefault "info";
|
||||||
|
AUTOMATIC_POOLS_UPDATE = true;
|
||||||
|
};
|
||||||
|
CORE_RPC = {
|
||||||
|
HOST = bitcoind.rpc.address;
|
||||||
|
PORT = bitcoind.rpc.port;
|
||||||
|
USERNAME = bitcoind.rpc.users.public.name;
|
||||||
|
PASSWORD = "@btcRpcPassword@";
|
||||||
|
};
|
||||||
|
ELECTRUM = let
|
||||||
|
server = config.services.${cfg.electrumServer};
|
||||||
|
in {
|
||||||
|
HOST = server.address;
|
||||||
|
PORT = server.port;
|
||||||
|
TLS_ENABLED = false;
|
||||||
|
};
|
||||||
|
DATABASE = {
|
||||||
|
ENABLED = true;
|
||||||
|
DATABASE = cfg.database.name;
|
||||||
|
SOCKET = "/run/mysqld/mysqld.sock";
|
||||||
|
PID_DIR = cacheDir;
|
||||||
|
};
|
||||||
|
} // optionalAttrs (cfg.tor.proxy) {
|
||||||
|
# Use Tor for rate fetching and pool updating
|
||||||
|
SOCKS5PROXY = {
|
||||||
|
ENABLED = true;
|
||||||
|
USE_ONION = true;
|
||||||
|
HOST = torSocket.addr;
|
||||||
|
PORT = torSocket.port;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.mempool = rec {
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
requires = [ "mysql.service" ];
|
||||||
|
wants = [ "${cfg.electrumServer}.service" ];
|
||||||
|
after = requires ++ wants;
|
||||||
|
preStart = ''
|
||||||
|
mkdir -p '${cacheDir}/cache'
|
||||||
|
<${configFile} sed \
|
||||||
|
-e "s|@btcRpcPassword@|$(cat ${secretsDir}/bitcoin-rpcpassword-public)|" \
|
||||||
|
> '${cacheDir}/config.json'
|
||||||
|
'';
|
||||||
|
environment.MEMPOOL_CONFIG_FILE = "${cacheDir}/config.json";
|
||||||
|
serviceConfig = nbLib.defaultHardening // {
|
||||||
|
ExecStart = "${cfg.package}/bin/mempool-backend";
|
||||||
|
CacheDirectory = "mempool";
|
||||||
|
CacheDirectoryMode = "770";
|
||||||
|
# Show "mempool" instead of "node" in the journal
|
||||||
|
SyslogIdentifier = "mempool";
|
||||||
|
User = cfg.user;
|
||||||
|
Restart = "on-failure";
|
||||||
|
RestartSec = "10s";
|
||||||
|
} // nbLib.allowedIPAddresses cfg.tor.enforce
|
||||||
|
// nbLib.nodejs;
|
||||||
|
};
|
||||||
|
|
||||||
|
services.nginx = mkIf cfg.frontend.enable {
|
||||||
|
enable = true;
|
||||||
|
enableReload = true;
|
||||||
|
recommendedBrotliSettings = true;
|
||||||
|
recommendedGzipSettings = true;
|
||||||
|
recommendedOptimisation = true;
|
||||||
|
recommendedProxySettings = true;
|
||||||
|
recommendedTlsSettings = true;
|
||||||
|
commonHttpConfig = frontend.nginxConfig.httpConfig;
|
||||||
|
virtualHosts."mempool" = {
|
||||||
|
serverName = "_";
|
||||||
|
listen = [ { addr = cfg.frontend.address; port = cfg.frontend.port; } ];
|
||||||
|
root = cfg.frontend.staticContentRoot;
|
||||||
|
extraConfig =
|
||||||
|
frontend.nginxConfig.staticContent +
|
||||||
|
frontend.nginxConfig.proxyApi;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
users.users.${cfg.user} = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = cfg.group;
|
||||||
|
extraGroups = [ "bitcoinrpc-public" ];
|
||||||
|
};
|
||||||
|
users.groups.${cfg.group} = {};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
nix-bitcoin = {
|
||||||
|
# Kept for compatibility, now simply aliases system pkgs
|
||||||
|
pkgs = mkOption {
|
||||||
|
type = types.attrs;
|
||||||
|
default = pkgs;
|
||||||
|
defaultText = "pkgs";
|
||||||
|
description = "Alias to system pkgs (vendored nix-bitcoin now uses nixpkgs directly).";
|
||||||
|
};
|
||||||
|
|
||||||
|
useVersionLockedPkgs = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Deprecated — vendored modules always use system pkgs.";
|
||||||
|
};
|
||||||
|
|
||||||
|
pkgOverlays = mkOption {
|
||||||
|
internal = true;
|
||||||
|
type = with types; functionTo attrs;
|
||||||
|
default = _: _: {};
|
||||||
|
description = "Deprecated stub.";
|
||||||
|
};
|
||||||
|
|
||||||
|
lib = mkOption {
|
||||||
|
readOnly = true;
|
||||||
|
default = import ./lib.nix lib pkgs config;
|
||||||
|
defaultText = "vendor/nix-bitcoin/lib.nix";
|
||||||
|
};
|
||||||
|
|
||||||
|
torClientAddressWithPort = mkOption {
|
||||||
|
readOnly = true;
|
||||||
|
default = with config.services.tor.client.socksListenAddress;
|
||||||
|
"${addr}:${toString port}";
|
||||||
|
defaultText = "(See source)";
|
||||||
|
};
|
||||||
|
|
||||||
|
torify = mkOption {
|
||||||
|
readOnly = true;
|
||||||
|
default = pkgs.writers.writeBashBin "torify" ''
|
||||||
|
${pkgs.tor}/bin/torify \
|
||||||
|
--address ${config.services.tor.client.socksListenAddress.addr} \
|
||||||
|
"$@"
|
||||||
|
'';
|
||||||
|
defaultText = "(See source)";
|
||||||
|
};
|
||||||
|
|
||||||
|
runAsUserCmd = mkOption {
|
||||||
|
readOnly = true;
|
||||||
|
default = if config.security.doas.enable
|
||||||
|
then "doas -u"
|
||||||
|
else "sudo -u";
|
||||||
|
defaultText = "(See source)";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
options = {
|
||||||
|
nix-bitcoin.nodeinfo = {
|
||||||
|
enable = mkEnableOption "nodeinfo";
|
||||||
|
|
||||||
|
program = mkOption {
|
||||||
|
readOnly = true;
|
||||||
|
default = script;
|
||||||
|
defaultText = "(See source)";
|
||||||
|
};
|
||||||
|
|
||||||
|
services = mkOption {
|
||||||
|
internal = true;
|
||||||
|
type = types.attrs;
|
||||||
|
default = {};
|
||||||
|
defaultText = "(See source)";
|
||||||
|
description = ''
|
||||||
|
Nodeinfo service definitions.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
lib = mkOption {
|
||||||
|
internal = true;
|
||||||
|
readOnly = true;
|
||||||
|
default = nodeinfoLib;
|
||||||
|
defaultText = "(See source)";
|
||||||
|
description = ''
|
||||||
|
Helper functions for defining nodeinfo services.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
cfg = config.nix-bitcoin.nodeinfo;
|
||||||
|
nbLib = config.nix-bitcoin.lib;
|
||||||
|
|
||||||
|
script = pkgs.writeScriptBin "nodeinfo" ''
|
||||||
|
#!${pkgs.python3}/bin/python
|
||||||
|
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
def success(*args):
|
||||||
|
return subprocess.call(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0
|
||||||
|
|
||||||
|
def is_active(unit):
|
||||||
|
return success("systemctl", "is-active", "--quiet", unit)
|
||||||
|
|
||||||
|
def is_enabled(unit):
|
||||||
|
return success("systemctl", "is-enabled", "--quiet", unit)
|
||||||
|
|
||||||
|
def cmd(*args):
|
||||||
|
return subprocess.run(args, stdout=subprocess.PIPE).stdout.decode('utf-8')
|
||||||
|
|
||||||
|
def shell(*args):
|
||||||
|
return cmd("bash", "-c", *args).strip()
|
||||||
|
|
||||||
|
infos = OrderedDict()
|
||||||
|
operator = "${config.nix-bitcoin.operator.name}"
|
||||||
|
|
||||||
|
def get_onion_address(name, port):
|
||||||
|
path = f"/var/lib/onion-addresses/{operator}/{name}"
|
||||||
|
try:
|
||||||
|
with open(path, "r") as f:
|
||||||
|
onion_address = f.read().strip()
|
||||||
|
except OSError:
|
||||||
|
print(f"error reading file {path}", file=sys.stderr)
|
||||||
|
return
|
||||||
|
return f"{onion_address}:{port}"
|
||||||
|
|
||||||
|
def add_service(service, make_info, systemd_service = None):
|
||||||
|
systemd_service = systemd_service or service
|
||||||
|
if not is_active(systemd_service):
|
||||||
|
infos[service] = f"'{systemd_service}.service' is not running"
|
||||||
|
else:
|
||||||
|
info = OrderedDict()
|
||||||
|
exec(make_info, globals(), locals())
|
||||||
|
infos[service] = info
|
||||||
|
|
||||||
|
if is_enabled("onion-addresses") and not is_active("onion-addresses"):
|
||||||
|
print("error: service 'onion-addresses' is not running")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
${concatStrings infos}
|
||||||
|
|
||||||
|
print(json.dumps(infos, indent=2))
|
||||||
|
'';
|
||||||
|
|
||||||
|
infos = map (serviceName:
|
||||||
|
let serviceCfg = config.services.${serviceName};
|
||||||
|
in optionalString serviceCfg.enable (cfg.services.${serviceName} serviceName serviceCfg)
|
||||||
|
) (builtins.attrNames cfg.services);
|
||||||
|
|
||||||
|
nodeinfoLib = rec {
|
||||||
|
mkInfo = extraCode: name: cfg:
|
||||||
|
mkInfoLong {
|
||||||
|
inherit extraCode name cfg;
|
||||||
|
};
|
||||||
|
|
||||||
|
mkInfoLong = { extraCode ? "", name, cfg, systemdServiceName ? name }: ''
|
||||||
|
add_service("${name}", """
|
||||||
|
info["local_address"] = "${nbLib.addressWithPort cfg.address cfg.port}"
|
||||||
|
'' + mkIfOnionPort name (onionPort: ''
|
||||||
|
info["onion_address"] = get_onion_address("${name}", ${onionPort})
|
||||||
|
'') + extraCode + ''
|
||||||
|
|
||||||
|
""", "${systemdServiceName}")
|
||||||
|
'';
|
||||||
|
|
||||||
|
mkIfOnionPort = name: fn:
|
||||||
|
if onionServices ? ${name} then
|
||||||
|
fn (toString (builtins.elemAt onionServices.${name}.map 0).port)
|
||||||
|
else
|
||||||
|
"";
|
||||||
|
};
|
||||||
|
|
||||||
|
inherit (config.services.tor.relay) onionServices;
|
||||||
|
in {
|
||||||
|
inherit options;
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
environment.systemPackages = [ script ];
|
||||||
|
|
||||||
|
nix-bitcoin.operator.enable = true;
|
||||||
|
|
||||||
|
nix-bitcoin.nodeinfo.services = with nodeinfoLib; {
|
||||||
|
bitcoind = mkInfo "";
|
||||||
|
lnd = name: cfg: mkInfo (''
|
||||||
|
info["rest_address"] = "${nbLib.addressWithPort cfg.restAddress cfg.restPort}"
|
||||||
|
'' + mkIfOnionPort "lnd-rest" (onionPort: ''
|
||||||
|
info["onion_rest_address"] = get_onion_address("lnd-rest", ${onionPort})
|
||||||
|
'') + ''
|
||||||
|
info["nodeid"] = shell("lncli getinfo | jq -r '.identity_pubkey'")
|
||||||
|
'') name cfg;
|
||||||
|
electrs = mkInfo "";
|
||||||
|
btcpayserver = mkInfo "";
|
||||||
|
rtl = mkInfo "";
|
||||||
|
mempool = mkInfo "";
|
||||||
|
mempool-frontend = name: cfg: mkInfoLong {
|
||||||
|
inherit name cfg;
|
||||||
|
systemdServiceName = "nginx";
|
||||||
|
};
|
||||||
|
# Only add sshd when it has an onion service
|
||||||
|
sshd = name: cfg: mkIfOnionPort "sshd" (onionPort: ''
|
||||||
|
add_service("sshd", """info["onion_address"] = get_onion_address("sshd", ${onionPort})""")
|
||||||
|
'');
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# This module enables unprivileged users to read onion addresses.
|
||||||
|
# By default, onion addresses in /var/lib/tor/onion are only readable by the
|
||||||
|
# tor user.
|
||||||
|
# The included service copies onion addresses to /var/lib/onion-addresses/<user>/
|
||||||
|
# and sets permissions according to option 'access'.
|
||||||
|
|
||||||
|
{ config, lib, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
options.nix-bitcoin.onionAddresses = {
|
||||||
|
access = mkOption {
|
||||||
|
type = with types; attrsOf (listOf str);
|
||||||
|
default = {};
|
||||||
|
description = ''
|
||||||
|
This option controls who is allowed to access onion addresses.
|
||||||
|
For example, the following allows user 'myuser' to access bitcoind
|
||||||
|
and clightning onion addresses:
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
"myuser" = [ "bitcoind" "clightning" ];
|
||||||
|
};
|
||||||
|
```
|
||||||
|
The onion hostnames can then be read from
|
||||||
|
{file}`/var/lib/onion-addresses/myuser`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
services = mkOption {
|
||||||
|
type = with types; listOf str;
|
||||||
|
default = [];
|
||||||
|
description = ''
|
||||||
|
Services that can access their onion address via file
|
||||||
|
{file}`/var/lib/onion-addresses/<service>`
|
||||||
|
The file is readable only by the service user.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
dataDir = mkOption {
|
||||||
|
readOnly = true;
|
||||||
|
default = "/var/lib/onion-addresses";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
cfg = config.nix-bitcoin.onionAddresses;
|
||||||
|
nbLib = config.nix-bitcoin.lib;
|
||||||
|
in {
|
||||||
|
inherit options;
|
||||||
|
|
||||||
|
config = mkIf (cfg.access != {} || cfg.services != []) {
|
||||||
|
systemd.services.onion-addresses = {
|
||||||
|
wantedBy = [ "tor.service" ];
|
||||||
|
bindsTo = [ "tor.service" ];
|
||||||
|
after = [ "tor.service" ];
|
||||||
|
serviceConfig = nbLib.defaultHardening // {
|
||||||
|
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";
|
||||||
|
};
|
||||||
|
script = ''
|
||||||
|
waitForFile() {
|
||||||
|
file=$1
|
||||||
|
for ((i=0; i<300; i++)); do
|
||||||
|
if [[ -e $file ]]; then
|
||||||
|
return;
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
echo "Error: File $file did not appear after 30 sec."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wait until tor is up
|
||||||
|
waitForFile /var/lib/tor/state
|
||||||
|
|
||||||
|
cd ${cfg.dataDir}
|
||||||
|
rm -rf ./*
|
||||||
|
|
||||||
|
${concatMapStrings
|
||||||
|
(user: ''
|
||||||
|
mkdir -p -m 0700 ${user}
|
||||||
|
chown ${user} ${user}
|
||||||
|
${concatMapStrings
|
||||||
|
(service: ''
|
||||||
|
onionFile='/var/lib/tor/onion/${service}/hostname'
|
||||||
|
waitForFile "$onionFile"
|
||||||
|
cp "$onionFile" '${user}/${service}'
|
||||||
|
chown '${user}' '${user}/${service}'
|
||||||
|
'')
|
||||||
|
cfg.access.${user}
|
||||||
|
}
|
||||||
|
'')
|
||||||
|
(builtins.attrNames cfg.access)
|
||||||
|
}
|
||||||
|
|
||||||
|
${concatMapStrings (service: ''
|
||||||
|
onionFile=/var/lib/tor/onion/${service}/hostname
|
||||||
|
waitForFile "$onionFile"
|
||||||
|
install -D -o ${config.systemd.services.${service}.serviceConfig.User} -m 400 "$onionFile" services/${service}
|
||||||
|
'') cfg.services}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
# This module creates onion-services for NixOS services.
|
||||||
|
# An onion service can be enabled for every service that defines
|
||||||
|
# options 'address', 'port' and optionally 'getPublicAddressCmd'.
|
||||||
|
#
|
||||||
|
# See it in use at ./presets/enable-tor.nix
|
||||||
|
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
options.nix-bitcoin.onionServices = mkOption {
|
||||||
|
default = {};
|
||||||
|
type = with types; attrsOf (submodule (
|
||||||
|
{ config, ... }: {
|
||||||
|
options = {
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = config.public;
|
||||||
|
description = ''
|
||||||
|
Create an onion service for the given service.
|
||||||
|
The service must define options {option}`address` and {option}`onionPort` (or `port`).
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
public = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Make the onion address accessible to the service.
|
||||||
|
If enabled, the onion service is automatically enabled.
|
||||||
|
Only available for services that define option {option}`getPublicAddressCmd`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
externalPort = mkOption {
|
||||||
|
type = types.nullOr types.port;
|
||||||
|
default = null;
|
||||||
|
description = "Override the external port of the onion service.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
cfg = config.nix-bitcoin.onionServices;
|
||||||
|
nbLib = config.nix-bitcoin.lib;
|
||||||
|
|
||||||
|
onionServices = builtins.attrNames cfg;
|
||||||
|
|
||||||
|
activeServices = builtins.filter (service:
|
||||||
|
config.services.${service}.enable && cfg.${service}.enable
|
||||||
|
) onionServices;
|
||||||
|
|
||||||
|
publicServices = builtins.filter (service: cfg.${service}.public) activeServices;
|
||||||
|
in {
|
||||||
|
inherit options;
|
||||||
|
|
||||||
|
config = mkMerge [
|
||||||
|
(mkIf (activeServices != []) {
|
||||||
|
# Define hidden services
|
||||||
|
services.tor = {
|
||||||
|
enable = true;
|
||||||
|
relay.onionServices = genAttrs activeServices (name:
|
||||||
|
let
|
||||||
|
service = config.services.${name};
|
||||||
|
inherit (cfg.${name}) externalPort;
|
||||||
|
in nbLib.mkOnionService {
|
||||||
|
port = if externalPort != null then externalPort else service.port;
|
||||||
|
target.port = service.onionPort or service.port;
|
||||||
|
target.addr = nbLib.address service.address;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
access.${config.nix-bitcoin.operator.name} = mkIf config.nix-bitcoin.operator.enable activeServices;
|
||||||
|
};
|
||||||
|
systemd.services = let
|
||||||
|
onionAddresses = [ "onion-addresses.service" ];
|
||||||
|
in genAttrs publicServices (service: {
|
||||||
|
# TODO-EXTERNAL: Instead of `wants`, use a future systemd dependency type
|
||||||
|
# that propagates initial start failures but no restarts
|
||||||
|
wants = onionAddresses;
|
||||||
|
after = onionAddresses;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
# Set getPublicAddressCmd for public services
|
||||||
|
{
|
||||||
|
services = let
|
||||||
|
# publicServices' doesn't depend on config.services.*.enable,
|
||||||
|
# so we can use it to define config.services without causing infinite recursion
|
||||||
|
publicServices' = builtins.filter (service:
|
||||||
|
let srv = cfg.${service};
|
||||||
|
in srv.public && srv.enable
|
||||||
|
) onionServices;
|
||||||
|
in genAttrs publicServices' (service: {
|
||||||
|
getPublicAddressCmd = "cat ${config.nix-bitcoin.onionAddresses.dataDir}/services/${service}";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set sensible defaults for some services
|
||||||
|
{
|
||||||
|
nix-bitcoin.onionServices = {
|
||||||
|
btcpayserver = {
|
||||||
|
externalPort = 80;
|
||||||
|
};
|
||||||
|
rtl = {
|
||||||
|
externalPort = 80;
|
||||||
|
};
|
||||||
|
mempool-frontend = {
|
||||||
|
externalPort = 80;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
options.nix-bitcoin.operator = {
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Whether to define a user named `operator` for convenient interactive access
|
||||||
|
to nix-bitcoin features (like `bitcoin-cli`).
|
||||||
|
|
||||||
|
When using nix-bitcoin as part of a larger system config, it makes sense
|
||||||
|
to set your main system user as the operator, by setting option
|
||||||
|
`nix-bitcoin.operator.name = "MAIN_USER_NAME";`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
name = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "operator";
|
||||||
|
description = "Name of the operator user.";
|
||||||
|
};
|
||||||
|
groups = mkOption {
|
||||||
|
type = with types; listOf str;
|
||||||
|
default = [];
|
||||||
|
description = "Extra groups of the operatur user.";
|
||||||
|
};
|
||||||
|
allowRunAsUsers = mkOption {
|
||||||
|
type = with types; listOf str;
|
||||||
|
default = [];
|
||||||
|
description = "Users as which the operator is allowed to run commands.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
cfg = config.nix-bitcoin.operator;
|
||||||
|
in {
|
||||||
|
inherit options;
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
users.users.${cfg.name} = {
|
||||||
|
isNormalUser = true;
|
||||||
|
extraGroups = [
|
||||||
|
"systemd-journal"
|
||||||
|
"proc" # Enable full /proc access and systemd-status
|
||||||
|
] ++ cfg.groups;
|
||||||
|
};
|
||||||
|
|
||||||
|
security = mkIf (cfg.allowRunAsUsers != []) {
|
||||||
|
# Use doas instead of sudo if enabled
|
||||||
|
doas.extraConfig = mkIf config.security.doas.enable ''
|
||||||
|
${lib.concatMapStrings (user: "permit nopass ${cfg.name} as ${user}\n") cfg.allowRunAsUsers}
|
||||||
|
'';
|
||||||
|
sudo.extraConfig = mkIf (!config.security.doas.enable) ''
|
||||||
|
${cfg.name} ALL=(${builtins.concatStringsSep "," cfg.allowRunAsUsers}) NOPASSWD: ALL
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
options.services.rtl = {
|
||||||
|
enable = mkEnableOption "RTL, a web interface for LND";
|
||||||
|
|
||||||
|
address = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "127.0.0.1";
|
||||||
|
description = "Address to listen for HTTP connections.";
|
||||||
|
};
|
||||||
|
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 3000;
|
||||||
|
description = "Port to listen for HTTP connections.";
|
||||||
|
};
|
||||||
|
|
||||||
|
dataDir = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
default = "/var/lib/rtl";
|
||||||
|
description = "The data directory for RTL.";
|
||||||
|
};
|
||||||
|
|
||||||
|
nightTheme = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Enable night theme by default.";
|
||||||
|
};
|
||||||
|
|
||||||
|
extraCurrency = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
example = "USD";
|
||||||
|
description = ''
|
||||||
|
Additional currency for displaying amounts.
|
||||||
|
When set, Tor is disabled for the RTL service to allow currency rate fetching.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
nodes = {
|
||||||
|
lnd = {
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Enable LND node in RTL.";
|
||||||
|
};
|
||||||
|
loop = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Enable Lightning Loop integration (requires loopd).";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
clightning = {
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Enable Core Lightning node in RTL (not supported in Sovran).";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "rtl";
|
||||||
|
description = "The user as which to run RTL.";
|
||||||
|
};
|
||||||
|
|
||||||
|
group = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = cfg.user;
|
||||||
|
description = "The group as which to run RTL.";
|
||||||
|
};
|
||||||
|
|
||||||
|
tor = nbLib.tor;
|
||||||
|
};
|
||||||
|
|
||||||
|
cfg = config.services.rtl;
|
||||||
|
nbLib = config.nix-bitcoin.lib;
|
||||||
|
secretsDir = config.nix-bitcoin.secretsDir;
|
||||||
|
|
||||||
|
# Vendored RTL package
|
||||||
|
fetchNodeModules = pkgs.callPackage ../../packages/build-support/fetch-node-modules.nix {};
|
||||||
|
rtlPackage = pkgs.callPackage ../../packages/rtl { inherit fetchNodeModules; };
|
||||||
|
|
||||||
|
runePath = "${cfg.dataDir}/CLN-Rune.env";
|
||||||
|
|
||||||
|
rtlConfig = {
|
||||||
|
multiPass = "@multiPass@";
|
||||||
|
port = cfg.port;
|
||||||
|
host = cfg.address;
|
||||||
|
defaultNodeIndex = 1;
|
||||||
|
dbDirectoryPath = cfg.dataDir;
|
||||||
|
SSO = {
|
||||||
|
rtlSSO = 0;
|
||||||
|
rtlCookiePath = "";
|
||||||
|
logoutRedirectLink = "";
|
||||||
|
};
|
||||||
|
nodes = optional cfg.nodes.lnd.enable ({
|
||||||
|
index = 1;
|
||||||
|
lnNode = "lnd";
|
||||||
|
lnImplementation = "LND";
|
||||||
|
Authentication = {
|
||||||
|
macaroonPath = "${cfg.dataDir}/macaroons";
|
||||||
|
swapMacaroonPath = if cfg.nodes.lnd.loop then "${cfg.dataDir}/loop-macaroons" else "";
|
||||||
|
boltzMacaroonPath = "";
|
||||||
|
};
|
||||||
|
Settings = {
|
||||||
|
userPersona = "OPERATOR";
|
||||||
|
themeMode = if cfg.nightTheme then "NIGHT" else "DAY";
|
||||||
|
themeColor = "PURPLE";
|
||||||
|
channelBackupPath = "${cfg.dataDir}/backup";
|
||||||
|
logLevel = "INFO";
|
||||||
|
fiatConversion = cfg.extraCurrency != null;
|
||||||
|
unannouncedChannels = true;
|
||||||
|
} // optionalAttrs (cfg.extraCurrency != null) {
|
||||||
|
currencyUnit = cfg.extraCurrency;
|
||||||
|
};
|
||||||
|
} // optionalAttrs cfg.nodes.lnd.loop {
|
||||||
|
swapServerUrl = "https://localhost:8081";
|
||||||
|
boltzServerUrl = "";
|
||||||
|
}) ++ optional cfg.nodes.clightning.enable {
|
||||||
|
index = 2;
|
||||||
|
lnNode = "clightning";
|
||||||
|
lnImplementation = "CLN";
|
||||||
|
Authentication = {
|
||||||
|
runePath = runePath;
|
||||||
|
};
|
||||||
|
Settings = {
|
||||||
|
userPersona = "OPERATOR";
|
||||||
|
themeMode = if cfg.nightTheme then "NIGHT" else "DAY";
|
||||||
|
themeColor = "PURPLE";
|
||||||
|
logLevel = "INFO";
|
||||||
|
fiatConversion = cfg.extraCurrency != null;
|
||||||
|
} // optionalAttrs (cfg.extraCurrency != null) {
|
||||||
|
currencyUnit = cfg.extraCurrency;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
configFile = builtins.toFile "config" (builtins.toJSON rtlConfig);
|
||||||
|
|
||||||
|
inherit (config.services)
|
||||||
|
bitcoind
|
||||||
|
lnd;
|
||||||
|
|
||||||
|
lndLoopEnabled = cfg.nodes.lnd.enable && cfg.nodes.lnd.loop;
|
||||||
|
in {
|
||||||
|
inherit options;
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
assertions = [
|
||||||
|
{ assertion = cfg.nodes.lnd.enable;
|
||||||
|
message = ''
|
||||||
|
RTL: At least one node must be enabled. Sovran supports LND only.
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
{ assertion = !cfg.nodes.clightning.enable;
|
||||||
|
message = ''
|
||||||
|
RTL: Core Lightning (clightning) is not supported in Sovran. Use LND instead.
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
services.lnd.enable = mkIf cfg.nodes.lnd.enable true;
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -"
|
||||||
|
];
|
||||||
|
|
||||||
|
services.rtl.tor.enforce = mkIf (cfg.extraCurrency != null) false;
|
||||||
|
|
||||||
|
systemd.services.rtl = rec {
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
wants = optional cfg.nodes.lnd.enable "lnd.service";
|
||||||
|
after = wants ++ [ "nix-bitcoin-secrets.target" ];
|
||||||
|
environment.RTL_CONFIG_PATH = cfg.dataDir;
|
||||||
|
environment.DB_DIRECTORY_PATH = cfg.dataDir;
|
||||||
|
serviceConfig = nbLib.defaultHardening // {
|
||||||
|
ExecStartPre = [
|
||||||
|
(nbLib.script "rtl-setup-config" ''
|
||||||
|
<${configFile} sed "s|@multiPass@|$(cat ${secretsDir}/rtl-password)|" \
|
||||||
|
> '${cfg.dataDir}/RTL-Config.json'
|
||||||
|
'')
|
||||||
|
]
|
||||||
|
++ optional cfg.nodes.lnd.enable
|
||||||
|
# The lnd admin macaroon is not readable by group `lnd`, so copy it
|
||||||
|
(nbLib.rootScript "rtl-copy-macaroon" ''
|
||||||
|
install --compare -m 640 -o ${cfg.user} -g ${cfg.group} -D ${lnd.networkDir}/admin.macaroon \
|
||||||
|
'${cfg.dataDir}/macaroons/admin.macaroon'
|
||||||
|
'');
|
||||||
|
ExecStart = "${rtlPackage}/bin/rtl";
|
||||||
|
# Show "rtl" instead of "node" in the journal
|
||||||
|
SyslogIdentifier = "rtl";
|
||||||
|
User = cfg.user;
|
||||||
|
Restart = "on-failure";
|
||||||
|
RestartSec = "10s";
|
||||||
|
ReadWritePaths = [ cfg.dataDir ];
|
||||||
|
} // nbLib.allowedIPAddresses cfg.tor.enforce
|
||||||
|
// nbLib.nodejs;
|
||||||
|
};
|
||||||
|
|
||||||
|
users.users.${cfg.user} = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = cfg.group;
|
||||||
|
extraGroups = optional lndLoopEnabled lnd.group;
|
||||||
|
};
|
||||||
|
users.groups.${cfg.group} = {};
|
||||||
|
|
||||||
|
nix-bitcoin.secrets.rtl-password.user = cfg.user;
|
||||||
|
nix-bitcoin.generateSecretsCmds.rtl = ''
|
||||||
|
makePasswordSecret rtl-password
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
options.nix-bitcoin = {
|
||||||
|
secretsDir = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
default = "/etc/nix-bitcoin-secrets";
|
||||||
|
description = "Directory to store secrets";
|
||||||
|
};
|
||||||
|
|
||||||
|
setupSecrets = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Set permissions for existing secrets in {option}`nix-bitcoin.secretsDir`
|
||||||
|
before services are started.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
generateSecrets = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Automatically generate all required secrets before services are started.
|
||||||
|
Note: Make sure to create a backup of the generated secrets.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
generateSecretsCmds = mkOption {
|
||||||
|
type = types.attrsOf types.lines;
|
||||||
|
default = {};
|
||||||
|
description = ''
|
||||||
|
Bash expressions for generating secrets.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Currently, this is used only by ../deployment/nixops.nix
|
||||||
|
deployment.secretsDir = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
description = ''
|
||||||
|
Directory of local secrets that are transferred to the nix-bitcoin node on deployment
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
secrets = mkOption {
|
||||||
|
default = {};
|
||||||
|
type = with types; attrsOf (submodule (
|
||||||
|
{ config, ... }: {
|
||||||
|
options = {
|
||||||
|
user = mkOption {
|
||||||
|
type = str;
|
||||||
|
default = "root";
|
||||||
|
};
|
||||||
|
group = mkOption {
|
||||||
|
type = str;
|
||||||
|
default = config.user;
|
||||||
|
};
|
||||||
|
permissions = mkOption {
|
||||||
|
type = str;
|
||||||
|
default = "440";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
secretsSetupMethod = mkOption {
|
||||||
|
type = with types; nullOr str;
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
generateSecretsScript = mkOption {
|
||||||
|
internal = true;
|
||||||
|
default = let
|
||||||
|
rpcauthSrc = pkgs.fetchurl {
|
||||||
|
url = "https://raw.githubusercontent.com/bitcoin/bitcoin/d6cde007db9d3e6ee93bd98a9bbfdce9bfa9b15b/share/rpcauth/rpcauth.py";
|
||||||
|
sha256 = "189mpplam6yzizssrgiyv70c9899ggh8cac76j4n7v0xqzfip07n";
|
||||||
|
};
|
||||||
|
rpcauth = pkgs.writers.writeBash "rpcauth" ''
|
||||||
|
exec ${pkgs.python3}/bin/python ${rpcauthSrc} "$@"
|
||||||
|
'';
|
||||||
|
# Writes secrets to PWD
|
||||||
|
in pkgs.writers.writeBash "generate-secrets" ''
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
export PATH=${lib.makeBinPath (with pkgs; [ coreutils gnugrep ])}
|
||||||
|
|
||||||
|
makePasswordSecret() {
|
||||||
|
# Passwords have alphabet {a-z, A-Z, 0-9} and ~119 bits of entropy
|
||||||
|
[[ -e $1 ]] || ${pkgs.pwgen}/bin/pwgen -s 20 1 > "$1"
|
||||||
|
}
|
||||||
|
makeBitcoinRPCPassword() {
|
||||||
|
user=$1
|
||||||
|
file=bitcoin-rpcpassword-$user
|
||||||
|
HMACfile=bitcoin-HMAC-$user
|
||||||
|
makePasswordSecret "$file"
|
||||||
|
if [[ $file -nt $HMACfile ]]; then
|
||||||
|
${rpcauth} $user $(cat "$file") | grep rpcauth | cut -d ':' -f 2 > "$HMACfile"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
makeCert() {
|
||||||
|
name=$1
|
||||||
|
# Add leading comma if not empty
|
||||||
|
extraAltNames=''${2:+,}''${2:-}
|
||||||
|
if [[ ! -e $name-key ]]; then
|
||||||
|
# Create new key and cert
|
||||||
|
doMakeCert "-newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes -keyout $name-key"
|
||||||
|
elif [[ ! -e $name-cert \
|
||||||
|
|| $(cat "$name-cert-alt-names" 2>/dev/null) != $extraAltNames ]]; then
|
||||||
|
# Create cert from existing key
|
||||||
|
doMakeCert "-key $name-key"
|
||||||
|
fi;
|
||||||
|
}
|
||||||
|
doMakeCert() {
|
||||||
|
# This fn uses global variables `name` and `extraAltNames`
|
||||||
|
keyOpts=$1
|
||||||
|
${pkgs.openssl}/bin/openssl req -x509 \
|
||||||
|
-sha256 -days 3650 $keyOpts -out "$name-cert" \
|
||||||
|
-subj "/CN=localhost/O=$name" \
|
||||||
|
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1$extraAltNames"
|
||||||
|
echo "$extraAltNames" > "$name-cert-alt-names"
|
||||||
|
}
|
||||||
|
|
||||||
|
umask u=rw,go=
|
||||||
|
${builtins.concatStringsSep "\n" (builtins.attrValues cfg.generateSecretsCmds)}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
cfg = config.nix-bitcoin;
|
||||||
|
in {
|
||||||
|
inherit options;
|
||||||
|
|
||||||
|
config = {
|
||||||
|
assertions = [
|
||||||
|
{ assertion = cfg.secretsSetupMethod != null;
|
||||||
|
message = ''
|
||||||
|
No secrets setup method has been defined.
|
||||||
|
To fix this, choose one of the following:
|
||||||
|
|
||||||
|
- Use one of the deployment methods in ${toString ./../deployment}
|
||||||
|
|
||||||
|
- Set `nix-bitcoin.generateSecrets = true` to automatically generate secrets
|
||||||
|
|
||||||
|
- Set `nix-bitcoin.secretsSetupMethod = "manual"` if you want to manually setup secrets
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
# This target is active when secrets have been setup successfully.
|
||||||
|
systemd.targets.nix-bitcoin-secrets = mkIf (cfg.secretsSetupMethod != "manual") {
|
||||||
|
# This ensures that the secrets target is always activated when switching
|
||||||
|
# configurations.
|
||||||
|
# In this way `switch-to-configuration` is guaranteed to show an error
|
||||||
|
# when activating the secrets target fails on deployment.
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
nix-bitcoin.setupSecrets = mkIf cfg.generateSecrets true;
|
||||||
|
|
||||||
|
nix-bitcoin.secretsSetupMethod = mkIf cfg.setupSecrets "setup-secrets";
|
||||||
|
|
||||||
|
# Operation of this service:
|
||||||
|
# - Set owner and permissions for all used secrets
|
||||||
|
# - Make all other secrets accessible to root only
|
||||||
|
# For all steps make sure that no secrets are copied to the nix store.
|
||||||
|
#
|
||||||
|
systemd.services.setup-secrets = mkIf cfg.setupSecrets {
|
||||||
|
requiredBy = [ "nix-bitcoin-secrets.target" ];
|
||||||
|
before = [ "nix-bitcoin-secrets.target" ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
};
|
||||||
|
script = ''
|
||||||
|
# Use the same sort order for globbing and sorting as in Nix attrsets.
|
||||||
|
# Required for `comm` below.
|
||||||
|
export LC_COLLATE=C
|
||||||
|
|
||||||
|
${optionalString cfg.generateSecrets ''
|
||||||
|
mkdir -p "${cfg.secretsDir}"
|
||||||
|
cd "${cfg.secretsDir}"
|
||||||
|
chown root: .
|
||||||
|
chmod 0700 .
|
||||||
|
${cfg.generateSecretsScript}
|
||||||
|
''}
|
||||||
|
|
||||||
|
setupSecret() {
|
||||||
|
file="$1"
|
||||||
|
user="$2"
|
||||||
|
group="$3"
|
||||||
|
permissions="$4"
|
||||||
|
if [[ ! -e $file ]]; then
|
||||||
|
echo "Error: Secret file '$file' is missing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
chown "$user:$group" "$file"
|
||||||
|
chmod "$permissions" "$file"
|
||||||
|
processedFiles+=("$file")
|
||||||
|
}
|
||||||
|
|
||||||
|
dir="${cfg.secretsDir}"
|
||||||
|
if [[ ! -e $dir ]]; then
|
||||||
|
echo "Error: Secrets dir '$dir' is missing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
chown root: "$dir"
|
||||||
|
cd "$dir"
|
||||||
|
|
||||||
|
processedFiles=()
|
||||||
|
${
|
||||||
|
concatStrings (mapAttrsToList (n: v: ''
|
||||||
|
setupSecret ${n} ${v.user} ${v.group} ${v.permissions}
|
||||||
|
'') cfg.secrets)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Make all other files accessible to root only
|
||||||
|
unprocessedFiles=$(
|
||||||
|
comm -23 <(shopt -s nullglob; printf '%s\n' *) <(printf '%s\n' "''${processedFiles[@]}")
|
||||||
|
)
|
||||||
|
if [[ $unprocessedFiles ]]; then
|
||||||
|
IFS=$'\n'
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
chown root: $unprocessedFiles
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
chmod 0440 $unprocessedFiles
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Now make the secrets dir accessible to other users
|
||||||
|
chmod 0751 "$dir"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
nix-bitcoin.security.dbusHideProcessInformation = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Only allow users with group `proc` to retrieve systemd unit information like
|
||||||
|
cgroup paths (i.e. (sub)process command lines) via D-Bus.
|
||||||
|
|
||||||
|
This mitigates a systemd security issue where (sub)process command lines can
|
||||||
|
be retrieved by services even when their access to /proc is restricted
|
||||||
|
(via ProtectProc).
|
||||||
|
|
||||||
|
This option works by restricting the D-Bus method `GetUnitProcesses`, which
|
||||||
|
is also used internally by {command}`systemctl status`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf config.nix-bitcoin.security.dbusHideProcessInformation {
|
||||||
|
users.groups.proc = {};
|
||||||
|
nix-bitcoin.operator.groups = [ "proc" ]; # Enable operator access to systemd-status
|
||||||
|
|
||||||
|
services.dbus.packages = lib.mkAfter [ # Apply at the end to override the default policy
|
||||||
|
(pkgs.writeTextDir "etc/dbus-1/system.d/dbus.conf" ''
|
||||||
|
<busconfig>
|
||||||
|
<policy context="default">
|
||||||
|
<deny
|
||||||
|
send_destination="org.freedesktop.systemd1"
|
||||||
|
send_interface="org.freedesktop.systemd1.Manager"
|
||||||
|
send_member="GetUnitProcesses"
|
||||||
|
/>
|
||||||
|
</policy>
|
||||||
|
<policy group="proc">
|
||||||
|
<allow
|
||||||
|
send_destination="org.freedesktop.systemd1"
|
||||||
|
send_interface="org.freedesktop.systemd1.Manager"
|
||||||
|
send_member="GetUnitProcesses"
|
||||||
|
/>
|
||||||
|
</policy>
|
||||||
|
</busconfig>
|
||||||
|
'')
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{ config, lib, ... }:
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
options.nix-bitcoin.configVersion = mkOption {
|
||||||
|
type = with types; nullOr str;
|
||||||
|
default = null;
|
||||||
|
description = "Vendored stub — no version migration needed.";
|
||||||
|
};
|
||||||
|
in {
|
||||||
|
inherit options;
|
||||||
|
config = {};
|
||||||
|
}
|
||||||
@@ -68,6 +68,7 @@ lib.mkIf config.sovran_systemsOS.services.bitcoin {
|
|||||||
name = "free";
|
name = "free";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# vendored: now no-op (always uses nixpkgs)
|
||||||
nix-bitcoin.useVersionLockedPkgs = false;
|
nix-bitcoin.useVersionLockedPkgs = false;
|
||||||
|
|
||||||
systemd.services.bitcoind = {
|
systemd.services.bitcoind = {
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
{
|
{
|
||||||
config = lib.mkMerge [
|
config = lib.mkMerge [
|
||||||
|
|
||||||
# nix-bitcoin is globally imported by the flake (nixosModules.Sovran_SystemsOS).
|
# Vendored nix-bitcoin is always imported via modules/vendor/nix-bitcoin/modules.nix.
|
||||||
# This default satisfies nix-bitcoin's generateSecrets assertion so that Desktop
|
# This default satisfies the secrets assertion so Desktop-Only systems evaluate
|
||||||
# Only systems can evaluate without enabling any Bitcoin services.
|
# without enabling any Bitcoin services.
|
||||||
{
|
{
|
||||||
nix-bitcoin.generateSecrets = lib.mkDefault true;
|
nix-bitcoin.generateSecrets = lib.mkDefault true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# Vendored from fort-nix/nix-bitcoin commit 360e30fee.
|
||||||
|
# This local copy does not fetch or import nix-bitcoin.
|
||||||
|
# This is a modified version of
|
||||||
|
# https://github.com/NixOS/nixpkgs/pull/128749
|
||||||
|
|
||||||
|
{ lib, stdenvNoCC, makeWrapper, nodejs, cacert }:
|
||||||
|
|
||||||
|
{ src
|
||||||
|
, hash ? ""
|
||||||
|
, runScripts ? false
|
||||||
|
, preferLocalBuild ? true
|
||||||
|
, npmFlags ? ""
|
||||||
|
, ...
|
||||||
|
} @ args:
|
||||||
|
stdenvNoCC.mkDerivation ({
|
||||||
|
inherit src preferLocalBuild;
|
||||||
|
|
||||||
|
name = "${src.name}-node_modules";
|
||||||
|
nativeBuildInputs = [
|
||||||
|
makeWrapper
|
||||||
|
(if args ? nodejs then args.nodejs else nodejs)
|
||||||
|
];
|
||||||
|
|
||||||
|
outputHashMode = "recursive";
|
||||||
|
|
||||||
|
impureEnvVars = lib.fetchers.proxyImpureEnvVars;
|
||||||
|
|
||||||
|
phases = "unpackPhase patchPhase buildPhase installPhase";
|
||||||
|
|
||||||
|
# npm doesn't support var `SSL_CERT_FILE`.
|
||||||
|
NODE_EXTRA_CA_CERTS = "${cacert}/etc/ssl/certs/ca-bundle.crt";
|
||||||
|
|
||||||
|
buildPhase = ''
|
||||||
|
runHook preBuild
|
||||||
|
|
||||||
|
if [[ ! -f package.json ]]; then
|
||||||
|
echo "Error: file `package.json` doesn't exist"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ ! -f package-lock.json ]]; then
|
||||||
|
echo "Error: file `package-lock.json` doesn't exist"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export SOURCE_DATE_EPOCH=1
|
||||||
|
export npm_config_cache=/tmp
|
||||||
|
NPM_FLAGS="--omit=dev --omit=optional --no-update-notifier $npmFlags"
|
||||||
|
# Scripts may result in non-deterministic behavior.
|
||||||
|
# Some packages (e.g., Puppeteer) use postinstall scripts to download extra data.
|
||||||
|
if [[ ! $runScripts ]]; then
|
||||||
|
NPM_FLAGS+=" --ignore-scripts"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Running npm ci $NPM_FLAGS"
|
||||||
|
npm ci $NPM_FLAGS
|
||||||
|
|
||||||
|
cp package.json \
|
||||||
|
package-lock.json node_modules/
|
||||||
|
rm -f node_modules/.package-lock.json
|
||||||
|
|
||||||
|
runHook postBuild
|
||||||
|
'';
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
runHook preInstall
|
||||||
|
|
||||||
|
mkdir -p $out/lib
|
||||||
|
cp -r node_modules $out/lib
|
||||||
|
|
||||||
|
runHook postInstall
|
||||||
|
'';
|
||||||
|
} // (
|
||||||
|
if hash == "" then {
|
||||||
|
outputHashAlgo = "sha256";
|
||||||
|
outputHash = "";
|
||||||
|
} else {
|
||||||
|
outputHash = hash;
|
||||||
|
}
|
||||||
|
) // (builtins.removeAttrs args [ "hash" ]))
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{ lib, buildGoModule, fetchFromGitHub }:
|
||||||
|
|
||||||
|
buildGoModule rec {
|
||||||
|
pname = "lndinit";
|
||||||
|
version = "0.1.3-beta";
|
||||||
|
src = fetchFromGitHub {
|
||||||
|
owner = "lightninglabs";
|
||||||
|
repo = pname;
|
||||||
|
rev = "v${version}";
|
||||||
|
sha256 = "sha256-sO1DpbppCurxr9g9nUl9Vx82FJK1mTcUw3rY1Fm1wEU=";
|
||||||
|
};
|
||||||
|
vendorHash = "sha256-El44BS5Bu0K/klMxkajciU/R6uqiXBMOiLN536QztbE=";
|
||||||
|
subPackages = [ "." ];
|
||||||
|
meta = with lib; {
|
||||||
|
description = "Wallet initializer for lnd (from nix-bitcoin)";
|
||||||
|
homepage = "https://github.com/lightninglabs/lndinit";
|
||||||
|
license = licenses.mit;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
From e4b3ebaf0451c1bddbd7dcf8527c296938ebb607 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Erik Arvstedt <erik.arvstedt@gmail.com>
|
||||||
|
Date: Sun, 1 Jun 2025 11:17:22 +0200
|
||||||
|
Subject: [PATCH] allow disabling mining pool fetching in offline environments
|
||||||
|
|
||||||
|
Previously, Mempool strictly required fetching mining pool data from
|
||||||
|
Github and failed when this was not possible, e.g. in offline
|
||||||
|
environments.
|
||||||
|
|
||||||
|
This patch allows disabling pool fetching.
|
||||||
|
When disabled, empty pool data is inserted into the DB, which
|
||||||
|
effectively turns off block pool classification.
|
||||||
|
---
|
||||||
|
backend/src/tasks/pools-updater.ts | 5 ++++-
|
||||||
|
1 file changed, 4 insertions(+), 1 deletion(-)
|
||||||
|
|
||||||
|
diff --git a/backend/src/tasks/pools-updater.ts b/backend/src/tasks/pools-updater.ts
|
||||||
|
index 6b0520dfc..a74259b95 100644
|
||||||
|
--- a/backend/src/tasks/pools-updater.ts
|
||||||
|
+++ b/backend/src/tasks/pools-updater.ts
|
||||||
|
@@ -75,7 +75,7 @@ class PoolsUpdater {
|
||||||
|
} else {
|
||||||
|
logger.warn(`pools-v2.json is outdated, fetching latest from ${this.poolsUrl} over ${network}`, this.tag);
|
||||||
|
}
|
||||||
|
- const poolsJson = await this.query(this.poolsUrl);
|
||||||
|
+ const poolsJson = (githubSha == "disable-pool-fetching") ? [] : await this.query(this.poolsUrl);
|
||||||
|
if (poolsJson === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
@@ -136,6 +136,9 @@ class PoolsUpdater {
|
||||||
|
* Fetch our latest pools-v2.json sha from github
|
||||||
|
*/
|
||||||
|
private async fetchPoolsSha(): Promise<string | null> {
|
||||||
|
+ if (this.poolsUrl == "disable-pool-fetching") {
|
||||||
|
+ return "disable-pool-fetching";
|
||||||
|
+ }
|
||||||
|
const response = await this.query(this.treeUrl);
|
||||||
|
|
||||||
|
if (response !== undefined) {
|
||||||
|
--
|
||||||
|
2.47.2
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
# Packaging adapted from fort-nix/nix-bitcoin commit 360e30fee.
|
||||||
|
# This local copy does not fetch or import nix-bitcoin.
|
||||||
|
{ lib
|
||||||
|
, stdenvNoCC
|
||||||
|
, nodejs_22
|
||||||
|
, nodejs-slim_22
|
||||||
|
, fetchFromGitHub
|
||||||
|
, fetchNodeModules
|
||||||
|
, runCommand
|
||||||
|
, makeWrapper
|
||||||
|
, curl
|
||||||
|
, cacert
|
||||||
|
, rsync
|
||||||
|
# for rust-gbt (backend module)
|
||||||
|
, cargo
|
||||||
|
, rustc
|
||||||
|
, rustPlatform
|
||||||
|
, napi-rs-cli
|
||||||
|
}:
|
||||||
|
rec {
|
||||||
|
nodejs = nodejs_22;
|
||||||
|
nodejsRuntime = nodejs-slim_22;
|
||||||
|
|
||||||
|
version = "3.2.1";
|
||||||
|
|
||||||
|
src = fetchFromGitHub {
|
||||||
|
owner = "mempool";
|
||||||
|
repo = "mempool";
|
||||||
|
tag = "v${version}";
|
||||||
|
hash = "sha256-O2XPD1/BXQnzuOP/vMVyRfmFZEgjA85r+PShWne0vqU=";
|
||||||
|
};
|
||||||
|
|
||||||
|
nodeModules = {
|
||||||
|
frontend = fetchNodeModules {
|
||||||
|
inherit src nodejs;
|
||||||
|
sourceRoot = "source/frontend";
|
||||||
|
hash = "sha256-+jfgsAkDdYvgso8uSHaBj/sQL3fC/ABQWzVTXfdZcU0=";
|
||||||
|
};
|
||||||
|
backend = fetchNodeModules {
|
||||||
|
inherit src nodejs;
|
||||||
|
sourceRoot = "source/backend";
|
||||||
|
hash = "sha256-y5l2SYZYK9SKSy6g0+mtTWD6JFkkdQHHBboECpEvWZ4=";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
frontendAssets = fetchFiles {
|
||||||
|
name = "mempool-frontend-assets";
|
||||||
|
hash = "sha256-r6GfOY8Pdh15o2OQMk8syfvWMV6WMCReToAEkQm7tqQ=";
|
||||||
|
fetcher = ./frontend-assets-fetch.sh;
|
||||||
|
};
|
||||||
|
|
||||||
|
mempool-backend = mkDerivationMempool {
|
||||||
|
pname = "mempool-backend";
|
||||||
|
|
||||||
|
patches = [ ./0001-allow-disabling-mining-pool-fetching.patch ];
|
||||||
|
|
||||||
|
buildPhase = ''
|
||||||
|
cd backend
|
||||||
|
${sync} --chmod=+w ${nodeModules.backend}/lib/node_modules .
|
||||||
|
patchShebangs node_modules
|
||||||
|
|
||||||
|
${sync} ${mempool-rust-gbt}/ rust-gbt
|
||||||
|
npm run package
|
||||||
|
|
||||||
|
runHook postBuild
|
||||||
|
'';
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
mkdir -p $out/lib/mempool-backend
|
||||||
|
${sync} package/ $out/lib/mempool-backend
|
||||||
|
|
||||||
|
makeWrapper ${nodejsRuntime}/bin/node $out/bin/mempool-backend \
|
||||||
|
--add-flags $out/lib/mempool-backend/index.js
|
||||||
|
|
||||||
|
runHook postInstall
|
||||||
|
'';
|
||||||
|
|
||||||
|
passthru = {
|
||||||
|
inherit nodejs nodejsRuntime;
|
||||||
|
nodeModules = nodeModules.backend;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
mempool-frontend = mkFrontend {};
|
||||||
|
|
||||||
|
# Argument `config` (type: attrset) defines the mempool frontend config.
|
||||||
|
# If `{}`, the default config is used.
|
||||||
|
# See here for available options:
|
||||||
|
# https://github.com/mempool/mempool/blob/master/frontend/src/app/services/state.service.ts
|
||||||
|
# (`interface Env` and `defaultEnv`)
|
||||||
|
mkFrontend = config: mkDerivationMempool {
|
||||||
|
pname = "mempool-frontend";
|
||||||
|
|
||||||
|
buildPhase = ''
|
||||||
|
cd frontend
|
||||||
|
|
||||||
|
${sync} --chmod=+w ${nodeModules.frontend}/lib/node_modules .
|
||||||
|
patchShebangs node_modules
|
||||||
|
|
||||||
|
# sync-assets.js is called during `npm run build` and downloads assets from the
|
||||||
|
# internet. Disable this script and instead add the assets manually after building.
|
||||||
|
: > sync-assets.js
|
||||||
|
|
||||||
|
${lib.optionalString (config != {}) ''
|
||||||
|
ln -s ${builtins.toFile "mempool-frontend-config" (builtins.toJSON config)} mempool-frontend-config.json
|
||||||
|
''}
|
||||||
|
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Add assets that would otherwise be downloaded by sync-assets.js
|
||||||
|
${sync} ${frontendAssets}/ dist/mempool/browser/resources
|
||||||
|
|
||||||
|
runHook postBuild
|
||||||
|
'';
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
${sync} dist/mempool/browser/ $out
|
||||||
|
|
||||||
|
runHook postInstall
|
||||||
|
'';
|
||||||
|
|
||||||
|
passthru = {
|
||||||
|
withConfig = mkFrontend;
|
||||||
|
assets = frontendAssets;
|
||||||
|
nodeModules = nodeModules.frontend;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
mempool-rust-gbt = stdenvNoCC.mkDerivation rec {
|
||||||
|
pname = "mempool-rust-gbt";
|
||||||
|
inherit version src meta;
|
||||||
|
|
||||||
|
sourceRoot = "source/rust/gbt";
|
||||||
|
|
||||||
|
nativeBuildInputs = [
|
||||||
|
rustPlatform.cargoSetupHook
|
||||||
|
cargo
|
||||||
|
rustc
|
||||||
|
napi-rs-cli
|
||||||
|
];
|
||||||
|
|
||||||
|
cargoDeps = rustPlatform.fetchCargoVendor {
|
||||||
|
inherit src;
|
||||||
|
name = "${pname}-${version}";
|
||||||
|
inherit sourceRoot;
|
||||||
|
hash = "sha256-eox/K3ipjAqNyFt87lZnxaU/okQLF/KIhqXrX86n+qw=";
|
||||||
|
};
|
||||||
|
|
||||||
|
buildPhase = ''
|
||||||
|
runHook preBuild
|
||||||
|
# napi doesn't accept an absolute path as dest dir, so we can't directly write to $out
|
||||||
|
napi build --platform --release --strip out
|
||||||
|
runHook postBuild
|
||||||
|
'';
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
mv out $out
|
||||||
|
cp package.json $out
|
||||||
|
'';
|
||||||
|
|
||||||
|
passthru = { inherit cargoDeps; };
|
||||||
|
};
|
||||||
|
|
||||||
|
mempool-nginx-conf = runCommand "mempool-nginx-conf" {} ''
|
||||||
|
${sync} --chmod=u+w ${./nginx-conf}/ $out
|
||||||
|
${sync} ${src}/production/nginx/http-language.conf $out
|
||||||
|
'';
|
||||||
|
|
||||||
|
sync = "${rsync}/bin/rsync -a --inplace";
|
||||||
|
|
||||||
|
mkDerivationMempool = args: stdenvNoCC.mkDerivation ({
|
||||||
|
inherit version src meta;
|
||||||
|
|
||||||
|
nativeBuildInputs = [
|
||||||
|
makeWrapper
|
||||||
|
nodejs
|
||||||
|
rsync
|
||||||
|
];
|
||||||
|
|
||||||
|
phases = "unpackPhase patchPhase buildPhase installPhase";
|
||||||
|
} // args);
|
||||||
|
|
||||||
|
fetchFiles = { name, hash, fetcher }: stdenvNoCC.mkDerivation {
|
||||||
|
inherit name;
|
||||||
|
outputHashMode = "recursive";
|
||||||
|
outputHashAlgo = "sha256";
|
||||||
|
outputHash = hash;
|
||||||
|
nativeBuildInputs = [ curl cacert ];
|
||||||
|
buildCommand = ''
|
||||||
|
mkdir $out
|
||||||
|
cd $out
|
||||||
|
${builtins.readFile fetcher}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
meta = with lib; {
|
||||||
|
description = "Bitcoin blockchain and mempool explorer";
|
||||||
|
homepage = "https://github.com/mempool/mempool/";
|
||||||
|
license = licenses.agpl3Plus;
|
||||||
|
maintainers = with maintainers; [ erikarvstedt ];
|
||||||
|
platforms = platforms.unix;
|
||||||
|
};
|
||||||
|
}
|
||||||
Executable
+28
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Fetch hash-locked versions of assets that are dynamically fetched via
|
||||||
|
# https://github.com/mempool/mempool/blob/master/frontend/sync-assets.js
|
||||||
|
# when running `npm run build` in the frontend.
|
||||||
|
#
|
||||||
|
# This file is updated by ./frontend-assets-update.sh
|
||||||
|
|
||||||
|
declare -A revs=(
|
||||||
|
["mempool/mining-pool-logos"]=53972ebbd08373cf4910cbb3e6421a1f3bba4563
|
||||||
|
)
|
||||||
|
|
||||||
|
fetchFile() {
|
||||||
|
repo=$1
|
||||||
|
file=$2
|
||||||
|
rev=${revs["$repo"]}
|
||||||
|
curl -fsS "https://raw.githubusercontent.com/$repo/$rev/$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchRepo() {
|
||||||
|
repo=$1
|
||||||
|
rev=${revs["$repo"]}
|
||||||
|
curl -fsSL "https://github.com/$repo/archive/$rev.tar.gz"
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdir mining-pools
|
||||||
|
fetchRepo "mempool/mining-pool-logos" | tar xz --strip-components=1 -C mining-pools
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Settings adapted from
|
||||||
|
# https://github.com/mempool/mempool/blob/v3.2.1/production/nginx/server-common.conf
|
||||||
|
|
||||||
|
# see order of nginx location rules
|
||||||
|
# https://stackoverflow.com/questions/5238377/nginx-location-priority
|
||||||
|
|
||||||
|
# for exact / requests, redirect based on $lang
|
||||||
|
# cache redirect for 5 minutes
|
||||||
|
location = / {
|
||||||
|
if ($lang != '') {
|
||||||
|
return 302 $scheme://$host/$lang/;
|
||||||
|
}
|
||||||
|
try_files /en-US/index.html =404;
|
||||||
|
expires 5m;
|
||||||
|
}
|
||||||
|
|
||||||
|
# cache /<lang>/main.f40e91d908a068a2.js forever since they never change
|
||||||
|
location ~ ^/([a-z][a-z])/(.+\..+\.(js|css))$ {
|
||||||
|
try_files $uri =404;
|
||||||
|
expires 1y;
|
||||||
|
}
|
||||||
|
# cache everything else for 5 minutes
|
||||||
|
location ~ ^/([a-z][a-z])$ {
|
||||||
|
try_files $uri /$1/index.html /en-US/index.html =404;
|
||||||
|
expires 5m;
|
||||||
|
}
|
||||||
|
location ~ ^/([a-z][a-z])/ {
|
||||||
|
try_files $uri /$1/index.html /en-US/index.html =404;
|
||||||
|
expires 5m;
|
||||||
|
}
|
||||||
|
|
||||||
|
# cache /resources/** for 1 week since they don't change often
|
||||||
|
location /resources {
|
||||||
|
try_files $uri /en-US/index.html;
|
||||||
|
expires 1w;
|
||||||
|
}
|
||||||
|
# cache /main.f40e91d908a068a2.js forever since they never change
|
||||||
|
location ~* ^/.+\..+\.(js|css)$ {
|
||||||
|
try_files /$lang/$uri /en-US/$uri =404;
|
||||||
|
expires 1y;
|
||||||
|
}
|
||||||
|
# catch-all for all URLs i.e. /address/foo /tx/foo /block/000
|
||||||
|
# cache 5 minutes since they change frequently
|
||||||
|
location / {
|
||||||
|
try_files /$lang/$uri $uri /en-US/$uri /en-US/index.html =404;
|
||||||
|
expires 5m;
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Packaging adapted from fort-nix/nix-bitcoin commit 360e30fee.
|
||||||
|
# This local copy does not fetch or import nix-bitcoin.
|
||||||
|
{ lib
|
||||||
|
, stdenvNoCC
|
||||||
|
, nodejs_22
|
||||||
|
, nodejs-slim_22
|
||||||
|
, fetchNodeModules
|
||||||
|
, fetchurl
|
||||||
|
, makeWrapper
|
||||||
|
}:
|
||||||
|
let self = stdenvNoCC.mkDerivation {
|
||||||
|
pname = "rtl";
|
||||||
|
version = "0.15.8";
|
||||||
|
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://github.com/Ride-The-Lightning/RTL/archive/refs/tags/v${self.version}.tar.gz";
|
||||||
|
hash = "sha256-8XdGyORxB2dkZRB/Yl7zh+Quqo4L/Y0VmC6Brbr/hqU=";
|
||||||
|
};
|
||||||
|
|
||||||
|
passthru = {
|
||||||
|
nodejs = nodejs_22;
|
||||||
|
nodejsRuntime = nodejs-slim_22;
|
||||||
|
|
||||||
|
nodeModules = fetchNodeModules {
|
||||||
|
inherit (self) src nodejs;
|
||||||
|
# TODO-EXTERNAL: Remove `npmFlags` when no longer required
|
||||||
|
# See: https://github.com/Ride-The-Lightning/RTL/issues/1182
|
||||||
|
npmFlags = "--legacy-peer-deps";
|
||||||
|
hash = "sha256-oMqd6nLzS6iQ9w4z2yzpR2unA5qhOq5YdvfoS8IgYLY=";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
nativeBuildInputs = [
|
||||||
|
makeWrapper
|
||||||
|
];
|
||||||
|
|
||||||
|
phases = "unpackPhase patchPhase installPhase";
|
||||||
|
|
||||||
|
# `src` already contains the precompiled frontend and backend.
|
||||||
|
# Copy all files required for packaging, like in
|
||||||
|
# https://github.com/Ride-The-Lightning/RTL/blob/master/dockerfiles/Dockerfile
|
||||||
|
installPhase = ''
|
||||||
|
dest=$out/lib/node_modules/rtl
|
||||||
|
mkdir -p $dest
|
||||||
|
cp -r \
|
||||||
|
rtl.js \
|
||||||
|
package.json \
|
||||||
|
frontend \
|
||||||
|
backend \
|
||||||
|
${self.nodeModules}/lib/node_modules \
|
||||||
|
$dest
|
||||||
|
|
||||||
|
makeWrapper ${self.nodejsRuntime}/bin/node "$out/bin/rtl" \
|
||||||
|
--add-flags "$dest/rtl.js"
|
||||||
|
|
||||||
|
runHook postInstall
|
||||||
|
'';
|
||||||
|
|
||||||
|
meta = with lib; {
|
||||||
|
description = "A web interface for LND, c-lightning and Eclair";
|
||||||
|
homepage = "https://github.com/Ride-The-Lightning/RTL";
|
||||||
|
license = licenses.mit;
|
||||||
|
maintainers = with maintainers; [ nixbitcoin erikarvstedt ];
|
||||||
|
platforms = platforms.unix;
|
||||||
|
};
|
||||||
|
}; in self
|
||||||
@@ -283,6 +283,36 @@ git add VERSION
|
|||||||
git commit -m "chore: bump VERSION to ${TAG} for ISO naming" || true
|
git commit -m "chore: bump VERSION to ${TAG} for ISO naming" || true
|
||||||
echo -e " ${GREEN}✓${NC} VERSION file updated to ${VERSION}"
|
echo -e " ${GREEN}✓${NC} VERSION file updated to ${VERSION}"
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Step 2b: Auto-update README.md ISO download references
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
echo
|
||||||
|
echo -e "${BLUE}Step 2b: Updating README.md ISO references to ${VERSION}...${NC}"
|
||||||
|
|
||||||
|
README_FILE="README.md"
|
||||||
|
if [ -f "$README_FILE" ]; then
|
||||||
|
# Extract the version currently used in the README's ISO filenames,
|
||||||
|
# e.g. "Sovran_SystemsOS-1.0.6.iso" -> "1.0.6"
|
||||||
|
OLD_ISO_VER=$(grep -oE 'Sovran_SystemsOS-[0-9]+\.[0-9]+\.[0-9]+\.iso' "$README_FILE" | head -1 | sed 's/Sovran_SystemsOS-//; s/\.iso//')
|
||||||
|
|
||||||
|
if [ -n "$OLD_ISO_VER" ] && [ "$OLD_ISO_VER" != "$VERSION" ]; then
|
||||||
|
# Portable in-place edit (works with GNU sed and BSD/macOS sed)
|
||||||
|
sed "s/Sovran_SystemsOS-${OLD_ISO_VER}/Sovran_SystemsOS-${VERSION}/g" \
|
||||||
|
"$README_FILE" > "$README_FILE.tmp" && mv "$README_FILE.tmp" "$README_FILE"
|
||||||
|
|
||||||
|
UPDATED=$(grep -c "Sovran_SystemsOS-${VERSION}" "$README_FILE")
|
||||||
|
echo -e " ${GREEN}✓${NC} README.md: ${OLD_ISO_VER} -> ${VERSION} (${UPDATED} references updated)"
|
||||||
|
|
||||||
|
git add "$README_FILE"
|
||||||
|
git commit -m "docs: update README ISO download links to ${TAG}" || true
|
||||||
|
echo -e " ${GREEN}✓${NC} Committed README update"
|
||||||
|
else
|
||||||
|
echo -e " ${YELLOW}⚠${NC} README.md already references ${VERSION} or no versioned ISO links found — skipping"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e " ${YELLOW}⚠${NC} README.md not found — skipping"
|
||||||
|
fi
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
# Step 3: Auto-update CHANGELOG.md
|
# Step 3: Auto-update CHANGELOG.md
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
+54
-23
@@ -7,7 +7,15 @@
|
|||||||
# Usage:
|
# Usage:
|
||||||
# ./scripts/upload-cdn.sh [--upload]
|
# ./scripts/upload-cdn.sh [--upload]
|
||||||
#
|
#
|
||||||
|
# EVERYTHING this script creates lives OUTSIDE the repository, even when you
|
||||||
|
# run it from inside the repo:
|
||||||
|
# - the nix build output symlink -> $ISO_OUT_DIR/result
|
||||||
|
# - the ISO and .sha256 files -> $ISO_OUT_DIR
|
||||||
|
# The repo working tree stays completely clean.
|
||||||
|
#
|
||||||
# Environment variables (optional):
|
# Environment variables (optional):
|
||||||
|
# ISO_OUT_DIR - dir for the build symlink + ISO + checksum
|
||||||
|
# (default: ~/Sovran-builds)
|
||||||
# CDN_RSYNC_TARGET - rsync destination (e.g. user@server:/var/www/downloads/)
|
# CDN_RSYNC_TARGET - rsync destination (e.g. user@server:/var/www/downloads/)
|
||||||
# CDN_RCLONE_REMOTE - rclone remote target (e.g. s3:my-bucket/downloads/)
|
# CDN_RCLONE_REMOTE - rclone remote target (e.g. s3:my-bucket/downloads/)
|
||||||
# CDN_UPLOAD_CMD - custom upload command
|
# CDN_UPLOAD_CMD - custom upload command
|
||||||
@@ -22,65 +30,88 @@ BLUE='\033[0;34m'
|
|||||||
CYAN='\033[0;36m'
|
CYAN='\033[0;36m'
|
||||||
NC='\033[0m'
|
NC='\033[0m'
|
||||||
|
|
||||||
|
# Always operate from the repo root, no matter where the script is invoked from
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
# Read version from VERSION file
|
# Read version from VERSION file
|
||||||
if [ ! -f VERSION ]; then
|
if [ ! -f VERSION ]; then
|
||||||
echo -e "${RED}Error: VERSION file not found.${NC}" >&2
|
echo -e "${RED}Error: VERSION file not found in $REPO_ROOT.${NC}" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
VERSION=$(cat VERSION | tr -d '\n\r ')
|
VERSION=$(cat VERSION | tr -d '\n\r ')
|
||||||
ISO_NAME="Sovran_SystemsOS-${VERSION}.iso"
|
ISO_NAME="Sovran_SystemsOS-${VERSION}.iso"
|
||||||
SHA_NAME="${ISO_NAME}.sha256"
|
SHA_NAME="${ISO_NAME}.sha256"
|
||||||
|
|
||||||
|
# Output directory OUTSIDE the repo (override with ISO_OUT_DIR)
|
||||||
|
OUT_DIR="${ISO_OUT_DIR:-$HOME/Sovran-builds}"
|
||||||
|
mkdir -p "$OUT_DIR"
|
||||||
|
OUT_DIR="$(cd "$OUT_DIR" && pwd)"
|
||||||
|
ISO_PATH="$OUT_DIR/${ISO_NAME}"
|
||||||
|
SHA_PATH="$OUT_DIR/${SHA_NAME}"
|
||||||
|
RESULT_LINK="$OUT_DIR/result"
|
||||||
|
|
||||||
echo -e "${BLUE}╔════════════════════════════════════════════════════════════╗${NC}"
|
echo -e "${BLUE}╔════════════════════════════════════════════════════════════╗${NC}"
|
||||||
echo -e "${BLUE}║ Sovran_SystemsOS CDN ISO Packaging & Upload Tool ║${NC}"
|
echo -e "${BLUE}║ Sovran_SystemsOS CDN ISO Packaging & Upload Tool ║${NC}"
|
||||||
echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}"
|
echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}"
|
||||||
echo -e " Version : ${CYAN}${VERSION}${NC}"
|
echo -e " Version : ${CYAN}${VERSION}${NC}"
|
||||||
echo -e " ISO : ${CYAN}${ISO_NAME}${NC}"
|
echo -e " ISO : ${CYAN}${ISO_NAME}${NC}"
|
||||||
|
echo -e " Repo : ${CYAN}${REPO_ROOT}${NC} (left untouched)"
|
||||||
|
echo -e " Output : ${CYAN}${OUT_DIR}${NC} (outside the repo)"
|
||||||
echo
|
echo
|
||||||
|
|
||||||
# Locate or build ISO
|
# Locate or build ISO
|
||||||
SRC_ISO=""
|
SRC_ISO=""
|
||||||
if [ -f "result/iso/${ISO_NAME}" ]; then
|
if [ -f "$RESULT_LINK/iso/${ISO_NAME}" ]; then
|
||||||
SRC_ISO="result/iso/${ISO_NAME}"
|
SRC_ISO="$RESULT_LINK/iso/${ISO_NAME}"
|
||||||
|
elif [ -f "$RESULT_LINK/iso/Sovran_SystemsOS.iso" ]; then
|
||||||
|
SRC_ISO="$RESULT_LINK/iso/Sovran_SystemsOS.iso"
|
||||||
|
elif [ -f "result/iso/${ISO_NAME}" ]; then
|
||||||
|
SRC_ISO="result/iso/${ISO_NAME}" # legacy: from an older in-repo build
|
||||||
elif [ -f "result/iso/Sovran_SystemsOS.iso" ]; then
|
elif [ -f "result/iso/Sovran_SystemsOS.iso" ]; then
|
||||||
SRC_ISO="result/iso/Sovran_SystemsOS.iso"
|
SRC_ISO="result/iso/Sovran_SystemsOS.iso"
|
||||||
else
|
else
|
||||||
FOUND=$(find result -name "*.iso" 2>/dev/null | head -n 1 || true)
|
FOUND=$(find "$RESULT_LINK" result -name "*.iso" 2>/dev/null | head -n 1 || true)
|
||||||
if [ -n "$FOUND" ]; then
|
if [ -n "$FOUND" ]; then
|
||||||
SRC_ISO="$FOUND"
|
SRC_ISO="$FOUND"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$SRC_ISO" ] || [ ! -f "$SRC_ISO" ]; then
|
if [ -z "$SRC_ISO" ] || [ ! -f "$SRC_ISO" ]; then
|
||||||
echo -e "${YELLOW}Built ISO not found in result/. Building ISO via Nix...${NC}"
|
echo -e "${YELLOW}Built ISO not found. Building via Nix (result link goes outside the repo)...${NC}"
|
||||||
nix build .#nixosConfigurations.sovran_systemsos-iso.config.system.build.isoImage
|
rm -f "$RESULT_LINK"
|
||||||
|
nix build .#nixosConfigurations.sovran_systemsos-iso.config.system.build.isoImage \
|
||||||
|
--out-link "$RESULT_LINK"
|
||||||
|
|
||||||
if [ -f "result/iso/${ISO_NAME}" ]; then
|
if [ -f "$RESULT_LINK/iso/${ISO_NAME}" ]; then
|
||||||
SRC_ISO="result/iso/${ISO_NAME}"
|
SRC_ISO="$RESULT_LINK/iso/${ISO_NAME}"
|
||||||
else
|
else
|
||||||
FOUND=$(find result -name "*.iso" 2>/dev/null | head -n 1 || true)
|
FOUND=$(find "$RESULT_LINK" -name "*.iso" 2>/dev/null | head -n 1 || true)
|
||||||
if [ -n "$FOUND" ]; then
|
if [ -n "$FOUND" ]; then
|
||||||
SRC_ISO="$FOUND"
|
SRC_ISO="$FOUND"
|
||||||
else
|
|
||||||
echo -e "${RED}Error: Failed to locate built ISO in result/.${NC}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e "${BLUE}Step 1: Copying built ISO out of Nix store...${NC}"
|
if [ -z "$SRC_ISO" ] || [ ! -f "$SRC_ISO" ]; then
|
||||||
cp -v "$SRC_ISO" "./${ISO_NAME}"
|
echo -e "${RED}Error: Failed to locate built ISO.${NC}" >&2
|
||||||
echo -e " ${GREEN}✓${NC} Copied to ./${ISO_NAME}"
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${BLUE}Step 1: Copying built ISO to output dir (outside repo)...${NC}"
|
||||||
|
cp -v "$SRC_ISO" "$ISO_PATH"
|
||||||
|
echo -e " ${GREEN}✓${NC} Copied to $ISO_PATH"
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo -e "${BLUE}Step 2: Generating versioned SHA-256 checksum...${NC}"
|
echo -e "${BLUE}Step 2: Generating versioned SHA-256 checksum...${NC}"
|
||||||
sha256sum "${ISO_NAME}" > "${SHA_NAME}"
|
sha256sum "$ISO_PATH" > "$SHA_PATH"
|
||||||
echo -e " ${GREEN}✓${NC} Generated ${SHA_NAME}"
|
echo -e " ${GREEN}✓${NC} Generated $SHA_PATH"
|
||||||
cat "${SHA_NAME}"
|
cat "$SHA_PATH"
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo -e "${BLUE}Step 3: Verifying checksum...${NC}"
|
echo -e "${BLUE}Step 3: Verifying checksum...${NC}"
|
||||||
sha256sum --check "${SHA_NAME}"
|
sha256sum --check "$SHA_PATH"
|
||||||
echo -e " ${GREEN}✓${NC} Checksum verified successfully"
|
echo -e " ${GREEN}✓${NC} Checksum verified successfully"
|
||||||
|
|
||||||
# Parse arguments for upload
|
# Parse arguments for upload
|
||||||
@@ -107,13 +138,13 @@ if [ "$DO_UPLOAD" -eq 1 ]; then
|
|||||||
|
|
||||||
if [ -n "${CDN_RSYNC_TARGET:-}" ]; then
|
if [ -n "${CDN_RSYNC_TARGET:-}" ]; then
|
||||||
echo -e " Uploading via rsync to ${CDN_RSYNC_TARGET}..."
|
echo -e " Uploading via rsync to ${CDN_RSYNC_TARGET}..."
|
||||||
rsync -avP "${ISO_NAME}" "${SHA_NAME}" "${CDN_RSYNC_TARGET}"
|
rsync -avP "$ISO_PATH" "$SHA_PATH" "${CDN_RSYNC_TARGET}"
|
||||||
UPLOADED=1
|
UPLOADED=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "${CDN_RCLONE_REMOTE:-}" ]; then
|
if [ -n "${CDN_RCLONE_REMOTE:-}" ]; then
|
||||||
echo -e " Uploading via rclone to ${CDN_RCLONE_REMOTE}..."
|
echo -e " Uploading via rclone to ${CDN_RCLONE_REMOTE}..."
|
||||||
rclone copy "${ISO_NAME}" "${SHA_NAME}" "${CDN_RCLONE_REMOTE}"
|
rclone copy "$ISO_PATH" "$SHA_PATH" "${CDN_RCLONE_REMOTE}"
|
||||||
UPLOADED=1
|
UPLOADED=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -128,8 +159,8 @@ else
|
|||||||
echo -e "${GREEN}╔════════════════════════════════════════════════════════════╗${NC}"
|
echo -e "${GREEN}╔════════════════════════════════════════════════════════════╗${NC}"
|
||||||
echo -e "${GREEN}║ ✅ ISO packaging & verification complete! ║${NC}"
|
echo -e "${GREEN}║ ✅ ISO packaging & verification complete! ║${NC}"
|
||||||
echo -e "${GREEN}╚════════════════════════════════════════════════════════════╝${NC}"
|
echo -e "${GREEN}╚════════════════════════════════════════════════════════════╝${NC}"
|
||||||
echo -e "Ready files:"
|
echo -e "Ready files (in ${CYAN}${OUT_DIR}${NC}, outside the repo):"
|
||||||
ls -lh "${ISO_NAME}" "${SHA_NAME}"
|
ls -lh "$ISO_PATH" "$SHA_PATH"
|
||||||
echo
|
echo
|
||||||
echo "To upload to CDN, run:"
|
echo "To upload to CDN, run:"
|
||||||
echo " ./scripts/upload-cdn.sh --upload"
|
echo " ./scripts/upload-cdn.sh --upload"
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
{ nixpkgs, overlay-stable, system ? "x86_64-linux" }:
|
||||||
|
|
||||||
|
let
|
||||||
|
lib = nixpkgs.lib;
|
||||||
|
pkgs = import nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
overlays = [ overlay-stable ];
|
||||||
|
};
|
||||||
|
|
||||||
|
normalize = s:
|
||||||
|
lib.replaceStrings [ "\n" "\\" " " ] [ " " "" " " ] s;
|
||||||
|
|
||||||
|
extractAfter = prefix: str:
|
||||||
|
let
|
||||||
|
match = builtins.match ".*${prefix} ([^ ]+).*" (normalize str);
|
||||||
|
in
|
||||||
|
if match == null then
|
||||||
|
throw "Unable to extract ${prefix} from: ${normalize str}"
|
||||||
|
else
|
||||||
|
builtins.head match;
|
||||||
|
|
||||||
|
extractFlagValue = flag: str:
|
||||||
|
let
|
||||||
|
match = builtins.match ".*${flag}=([^ ]+).*" (normalize str);
|
||||||
|
in
|
||||||
|
if match == null then
|
||||||
|
throw "Unable to extract ${flag} from: ${normalize str}"
|
||||||
|
else
|
||||||
|
builtins.head match;
|
||||||
|
|
||||||
|
config = (lib.nixosSystem {
|
||||||
|
inherit system;
|
||||||
|
modules = [
|
||||||
|
{ nixpkgs.hostPlatform = system; nixpkgs.overlays = [ overlay-stable ]; }
|
||||||
|
../modules/bitcoin
|
||||||
|
{
|
||||||
|
nix-bitcoin.generateSecrets = true;
|
||||||
|
nix-bitcoin.secretsDir = "/build/secrets";
|
||||||
|
|
||||||
|
services.btcpayserver.enable = true;
|
||||||
|
services.btcpayserver.lightningBackend = "lnd";
|
||||||
|
services.nbxplorer.dataDir = "/build/nbxplorer";
|
||||||
|
services.btcpayserver.dataDir = "/build/btcpayserver";
|
||||||
|
services.lnd.dataDir = "/build/lnd";
|
||||||
|
services.bitcoind.dataDir = "/build/bitcoind";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}).config;
|
||||||
|
|
||||||
|
nbxplorerPreStart = config.systemd.services.nbxplorer.preStart;
|
||||||
|
bitcoindPreStart = config.systemd.services.bitcoind.preStart;
|
||||||
|
btcpayExecStart = config.systemd.services.btcpayserver.serviceConfig.ExecStart;
|
||||||
|
|
||||||
|
nbxplorerConfigPath = extractAfter "install -m 600" nbxplorerPreStart;
|
||||||
|
btcpayConfigPath = extractFlagValue "--conf" btcpayExecStart;
|
||||||
|
|
||||||
|
nbxplorerConfig = builtins.readFile nbxplorerConfigPath;
|
||||||
|
btcpayConfig = builtins.readFile btcpayConfigPath;
|
||||||
|
in
|
||||||
|
assert lib.assertMsg
|
||||||
|
(config.nix-bitcoin.secrets.bitcoin-HMAC-btcpayserver.user == config.services.bitcoind.user)
|
||||||
|
"bitcoin-HMAC-btcpayserver must be owned by bitcoind";
|
||||||
|
assert lib.assertMsg
|
||||||
|
(config.nix-bitcoin.secrets.bitcoin-rpcpassword-btcpayserver.user == config.services.bitcoind.user)
|
||||||
|
"bitcoin-rpcpassword-btcpayserver must be owned by bitcoind";
|
||||||
|
assert lib.assertMsg
|
||||||
|
(config.nix-bitcoin.secrets.bitcoin-rpcpassword-btcpayserver.group == config.services.nbxplorer.group)
|
||||||
|
"bitcoin-rpcpassword-btcpayserver must be group-readable by nbxplorer";
|
||||||
|
assert lib.assertMsg
|
||||||
|
(!(lib.elem config.services.nbxplorer.group config.users.users.${config.services.btcpayserver.user}.extraGroups))
|
||||||
|
"btcpayserver must not receive the nbxplorer group";
|
||||||
|
assert lib.assertMsg
|
||||||
|
(lib.elem "nix-bitcoin-secrets.target" config.systemd.services.nbxplorer.after)
|
||||||
|
"nbxplorer must wait for nix-bitcoin-secrets.target";
|
||||||
|
assert lib.assertMsg
|
||||||
|
(config.systemd.services.nbxplorer.serviceConfig.MemoryDenyWriteExecute == false)
|
||||||
|
"nbxplorer needs MemoryDenyWriteExecute = false";
|
||||||
|
assert lib.assertMsg
|
||||||
|
(config.systemd.services.btcpayserver.serviceConfig.MemoryDenyWriteExecute == false)
|
||||||
|
"btcpayserver needs MemoryDenyWriteExecute = false";
|
||||||
|
assert lib.assertMsg
|
||||||
|
(lib.hasInfix "network=mainnet" nbxplorerConfig
|
||||||
|
&& lib.hasInfix "btcrpcuser=btcpayserver" nbxplorerConfig
|
||||||
|
&& lib.hasInfix "btcnodeendpoint=127.0.0.1:8335" nbxplorerConfig
|
||||||
|
&& lib.hasInfix "bind=127.0.0.1" nbxplorerConfig
|
||||||
|
&& lib.hasInfix "port=24444" nbxplorerConfig
|
||||||
|
&& lib.hasInfix "postgres=User ID=nbxplorer;Host=/run/postgresql;Database=nbxplorer" nbxplorerConfig)
|
||||||
|
"nbxplorer base config must contain the expected non-secret settings";
|
||||||
|
assert lib.assertMsg
|
||||||
|
(!lib.hasInfix "/build/btcpayserver/settings.config" btcpayExecStart
|
||||||
|
&& lib.hasInfix "--datadir='/build/btcpayserver'" btcpayExecStart)
|
||||||
|
"btcpayserver must use a deterministic config file plus --datadir";
|
||||||
|
assert lib.assertMsg
|
||||||
|
(lib.hasInfix "network=mainnet" btcpayConfig
|
||||||
|
&& lib.hasInfix "bind=127.0.0.1" btcpayConfig
|
||||||
|
&& lib.hasInfix "port=23000" btcpayConfig
|
||||||
|
&& lib.hasInfix "btcexplorerurl=http://127.0.0.1:24444/" btcpayConfig
|
||||||
|
&& lib.hasInfix "explorer.postgres=User ID=nbxplorer;Host=/run/postgresql;Database=nbxplorer" btcpayConfig
|
||||||
|
&& lib.hasInfix "postgres=User ID=btcpayserver;Host=/run/postgresql;Database=btcpayserver" btcpayConfig
|
||||||
|
&& lib.hasInfix "btclightning=type=lnd-rest;server=https://127.0.0.1:8080/;macaroonfilepath=/run/lnd/btcpayserver.macaroon;certfilepath=/build/secrets/lnd-cert" btcpayConfig)
|
||||||
|
"btcpayserver config must preserve BTCPay, NBXplorer, database, and LND settings";
|
||||||
|
assert lib.assertMsg
|
||||||
|
(lib.hasInfix "readValidatedRpcHmac()" bitcoindPreStart
|
||||||
|
&& lib.hasInfix ''if [[ ! -e "$hmacFile" ]]; then'' bitcoindPreStart
|
||||||
|
&& lib.hasInfix ''if [[ ! -r "$hmacFile" ]]; then'' bitcoindPreStart
|
||||||
|
&& lib.hasInfix ''if [[ -z "$hmacPayload" ]]; then'' bitcoindPreStart
|
||||||
|
&& lib.hasInfix ''^[[:xdigit:]]+\$[[:xdigit:]]+$'' bitcoindPreStart
|
||||||
|
&& lib.hasInfix ''Bitcoin RPC HMAC file has invalid format'' bitcoindPreStart
|
||||||
|
&& lib.hasInfix ''hmacPayload="$(readValidatedRpcHmac '/build/secrets/bitcoin-HMAC-btcpayserver')" || exit 1'' bitcoindPreStart)
|
||||||
|
"bitcoind preStart must validate missing, unreadable, empty, and malformed HMAC files";
|
||||||
|
pkgs.runCommand "bitcoin-btcpay-hardening" {} ''
|
||||||
|
mkdir -p /build/secrets /build/nbxplorer
|
||||||
|
|
||||||
|
printf '%s' 'first-password' > /build/secrets/bitcoin-rpcpassword-btcpayserver
|
||||||
|
bash -euo pipefail -c ${lib.escapeShellArg nbxplorerPreStart}
|
||||||
|
|
||||||
|
test "$(stat -c '%a' /build/nbxplorer/settings.config)" = "600"
|
||||||
|
test "$(grep -c '^btcrpcuser=' /build/nbxplorer/settings.config)" = "1"
|
||||||
|
test "$(grep -c '^btcrpcpassword=' /build/nbxplorer/settings.config)" = "1"
|
||||||
|
test "$(grep -c '^postgres=' /build/nbxplorer/settings.config)" = "1"
|
||||||
|
|
||||||
|
printf '%s' 'rotated-password' > /build/secrets/bitcoin-rpcpassword-btcpayserver
|
||||||
|
bash -euo pipefail -c ${lib.escapeShellArg nbxplorerPreStart}
|
||||||
|
|
||||||
|
test "$(grep -c '^btcrpcuser=' /build/nbxplorer/settings.config)" = "1"
|
||||||
|
test "$(grep -c '^btcrpcpassword=' /build/nbxplorer/settings.config)" = "1"
|
||||||
|
test "$(grep -c '^postgres=' /build/nbxplorer/settings.config)" = "1"
|
||||||
|
! grep -q 'first-password' /build/nbxplorer/settings.config
|
||||||
|
grep -q 'rotated-password' /build/nbxplorer/settings.config
|
||||||
|
|
||||||
|
touch "$out"
|
||||||
|
''
|
||||||
Reference in new issue
Block a user