Compare commits

...
29 Commits
Author SHA1 Message Date
Sovran Systems 5d5e369411 Merge pull request #408 from naturallaw777/copilot/vendor-mempool-packages
Vendor mempool packages and make `services.mempool` self-contained
2026-08-10 10:09:33 -05:00
copilot-swe-agent[bot]andnaturallaw777 425a1845b4 chore: clean mempool module comment typo
Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-08-10 15:07:42 +00:00
copilot-swe-agent[bot]andnaturallaw777 546dacf396 vendor mempool packages and wire mempool module to vendored pkgs
Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-08-10 15:06:50 +00:00
copilot-swe-agent[bot] 17c0db8290 Initial plan 2026-08-10 15:03:18 +00:00
Sovran Systems dac733de9a Refactor onion service configuration for LND 2026-08-10 09:55:59 -05:00
Sovran Systems 3baf504c60 Fix typo in onion-addresses service check 2026-08-10 09:52:55 -05:00
Sovran Systems f220b93b12 Add missing bitcoind-rpc-public-whitelist.nix
This file is imported by bitcoind.nix line 375 but was missing from the repo.
Vendored from nix-bitcoin to make the module self-contained.
2026-08-10 09:45:23 -05:00
Sovran Systems b2a5233653 Merge pull request #407 from naturallaw777/copilot/fixnix-bitcoin-vendoring
Complete nix-bitcoin vendoring for fully self-contained Bitcoin modules
2026-08-10 09:41:26 -05:00
copilot-swe-agent[bot] 2bb5c50196 Initial plan 2026-08-10 14:30:27 +00:00
Sovran Systems 80c4b052a0 Merge pull request #406 from naturallaw777/fix/onion-services-remove-joinmarket-ob-watcher
Remove joinmarket-ob-watcher from onion-services defaults
2026-08-10 09:15:26 -05:00
Sovran Systems 2accada021 Remove joinmarket-ob-watcher from onion-services defaults
The joinmarket-ob-watcher module was not imported from nix-bitcoin,
so referencing it in the onion-services defaults caused an
"attribute 'joinmarket-ob-watcher' missing" evaluation error.
2026-08-10 09:13:33 -05:00
Sovran Systems a6296b9473 Fix formatting of extraGroups in btcpayserver.nix 2026-08-10 08:55:12 -05:00
Sovran Systems 5173fb3df6 Merge pull request #404 from naturallaw777/fix/syntax-final
fix: add missing semicolon after extraGroups in btcpayserver.nix
2026-08-10 08:53:36 -05:00
Syntax Fix 147afcc4ac fix: add missing semicolon after extraGroups in btcpayserver.nix
- Previous lnd-only refactor removed the '++ optional clightning' line
  but left 'extraGroups = [ cfg.nbxplorer.group ]' without trailing ';'
  -> syntax error: unexpected '=', expecting ';' at home = ...
- Add ';' to fix build on both f13ff45 and 8b8c811
2026-08-10 08:51:39 -05:00
Sovran Systems 2e35539ae6 Merge pull request #403 from naturallaw777/tailor/lnd-only-final
tailor: make bitcoin modules truly Sovran-only (lnd-only) and delete …
2026-08-10 08:40:08 -05:00
Sovran Tailor 31cd5f96f2 tailor: make bitcoin modules truly Sovran-only (lnd-only) and delete stubs.nix
- lndconnect.nix: rewrite to lnd-only (320 lines removed) - remove
  services.clightning.plugins.clnrest.lnconnect and
  services.clightning-rest.lndconnect (Sovran only uses services.lnd.lndconnect for Zeus)
  Fixes: 'services.clightning.plugins.clnrest.address does not exist' on f13ff45
  and 'attribute enable missing' at lndconnect.nix:216

- btcpayserver.nix: remove liquidd (lbtc) dead branch - Sovran uses lbtc=false
  * remove inherit (config.services) bitcoind liquidd -> just bitcoind
  * lbtc chains/rpc handling is dead code, keep guards but no need for liquidd service

- mempool.nix: remove fulcrum branch - Sovran uses electrs only
- rtl.nix: remove lightning-loop block and clightning service block (lnd only)
- Delete modules/bitcoin/stubs.nix entirely - no dead service references left
  * liquidd, fulcrum, lightning-loop, joinmarket now not referenced at all
  * clightning already handled via guards, not stubs
- Result: modules/bitcoin/ is now 100% tailored to Sovran (bitcoind+knots, lnd, electrs, rtl, btcpayserver, mempool)
  No more stubs whack-a-mole on any nixos-unstable
2026-08-10 08:37:29 -05:00
Sovran Systems 90262e672e Merge pull request #402 from naturallaw777/refactor/clean-bitcoin
refactor: move vendor/nix-bitcoin to modules/bitcoin, remove overlays
2026-08-10 08:24:07 -05:00
Sovran Clean 1fbeafd02f refactor: move vendor/nix-bitcoin to modules/bitcoin, remove overlays
- Move modules/vendor/nix-bitcoin/* -> modules/bitcoin/* (clean, Sovran-owned)
  * modules/bitcoin/default.nix imports the 6 tailored services
  * modules/bitcoin/common.nix bundles secrets/onion/lib
  * modules/bitcoin/stubs.nix kept minimal (no clightning)
  * packages/lndinit/default.nix replaces pkgs/sovran-overlay.nix
    (lnd.nix now uses pkgs.callPackage ../../packages/lndinit {})
- Remove pkgs/sovran-overlay.nix, pkgs/nbxplorer.nix, pkgs/README.md
  * No global overlay - lndinit is a normal package in packages/
- Remove modules/vendor/ entirely
- Update flake.nix: drop overlay-sovran, import ./modules/bitcoin
  instead of ./modules/vendor/nix-bitcoin/modules.nix
- No more random vendor/ or pkgs/ dirs - follows Sovran convention:
  modules/ for NixOS modules, packages/ for packages
2026-08-10 08:20:40 -05:00
Sovran Systems 953cedce3f Merge pull request #401 from naturallaw777/fix/final-btcpayserver
fix: remove services.clightning.enable assignment that fails on f13ff45
2026-08-10 04:45:38 -05:00
Sovran Fix 58d7e21d28 fix: remove services.clightning.enable assignment that fails on f13ff45
- f13ff45 HAS services.clightning but only as { plugins = ... }, no enable
  -> 'option does not exist, did you mean plugins' (your error)
- Sovran never uses lightningBackend == clightning (uses lnd),
  so just don't set services.clightning.enable at all
- Also make lnd port conflict check always true for clightning case
- Fixes both f13ff45 (has clightning/plugins) and 8b8c811 (removed)
2026-08-10 04:43:20 -05:00
Sovran Systems 9f7dcfa6de Merge pull request #400 from naturallaw777/fix/final-clightning-both
fix: remove clightning and clightning-rest from stubs to avoid duplic…
2026-08-10 04:35:03 -05:00
Sovran Fix 6c2c69d18a fix: remove clightning and clightning-rest from stubs to avoid duplicate on f13ff45
- f13ff45 (staging-dev) HAS both services -> unconditional stubs duplicate
- Remove both from stubs.nix, keep only liquidd/fulcrum/etc.
- lndconnect.nix left as is (defines clightning-rest.lndconnect) - safe on f13ff45 where base exists
- On 8b8c811 where clightning is removed, Sovran doesn't use it anyway (lnd only), so guards in btcpayserver/lnd prevent use
2026-08-10 04:31:23 -05:00
Sovran Systems 6b9c68e3b2 Merge pull request #399 from naturallaw777/fix/nixpkgs-unstable-clightning-2026-08
fix: add stubs for nixpkgs-unstable 2026-08 where services.clightning…
2026-08-09 21:23:01 -05:00
Sovran Fix 4b511348ae fix: add stubs for nixpkgs-unstable 2026-08 where services.clightning removed
nixpkgs 8b8c811 (2026-08-08) removed services.clightning.enable,
causing btcpayserver.nix:124 to throw 'option does not exist' on
nixos-rebuild (your error). Sovran uses lnd only, never clightning,
but evaluation still throws.

- Add modules/vendor/nix-bitcoin/stubs.nix to provide missing options
  as false stubs: clightning, clightning-rest, liquidd, fulcrum,
  lightning-loop/pool, joinmarket
- Guard btcpayserver/rtl/lnd/mempool clightning/liquidd references
  with config.services ? X checks
- Trim enable-tor.nix onionServices for removed services
2026-08-09 21:21:19 -05:00
Sovran Systems 81946895db Merge pull request #398 from naturallaw777/vendor/nixpkgs-only
Vendor/nixpkgs only
2026-08-09 20:24:11 -05:00
naturallaw777 a10ef95b5c chore: nix flake update - drop nix-bitcoin 2026-08-09 20:19:35 -05:00
Sovran PR Bot 278d480653 vendor: replace nix-bitcoin flake input with minimal vendored modules (nixpkgs-only)
- Remove inputs.nix-bitcoin (fort-nix/nix-bitcoin/release) from flake.nix
- Vendor only 6 services actually used by Sovran: bitcoind, electrs,
  lnd (+lndconnect), rtl, btcpayserver, mempool + supporting infra:
  secrets, onion-services/addresses, operator, nodeinfo, security,
  versioning
- All packages now from nixpkgs directly (pkgs.*) — no pinned pkgs
- Keep nix-bitcoin.* option namespace for compatibility
- backups.nix removed: Sovran uses rsnapshot to Second_Drive
  (configuration.nix: hourly/daily to BTCEcoandBackup) — duplicity
  remote backup not needed
- netns-isolation.nix replaced with stub (5 lines): original 365-line
  bridge/iptables/ip-netns broke Caddy/AlbyHub/RTL a year ago and
  is incompatible with nwc-wallets (requires enable=false). Stub
  keeps option valid but warns if enabled.
- Add pkgs/sovran-overlay.nix for gaps only: lndinit + netns-exec stub
2026-08-09 20:16:34 -05:00
naturallaw777 b64061135a fix(scripts): keep ISO artifacts out of the repo and auto-update README on release 2026-08-07 18:31:20 -05:00
Sovran Systems 825c3fbb4b Remove duplicate 1.0.6 release notes from CHANGELOG
Remove duplicate version 1.0.6 details from CHANGELOG.md.
2026-08-07 16:17:24 -05:00
32 changed files with 3280 additions and 223 deletions

No files matched your search

-44
View File
@@ -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
[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
### Added
Generated
+10 -141
View File
@@ -18,32 +18,6 @@
"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": {
"inputs": {
"nixpkgs-lib": [
@@ -65,47 +39,6 @@
"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": {
"locked": {
"lastModified": 1785590095,
@@ -121,29 +54,13 @@
"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": {
"locked": {
"lastModified": 1785734586,
"narHash": "sha256-ODZkEK9Gy50yg6h98u7KkitZ3oc/uuTFK00bh1CRdNA=",
"lastModified": 1786201459,
"narHash": "sha256-CiOTEjmwAmG2AWnaIno9YaCJJmpca2FXPhMAsnrolCg=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "531670d871c0e29724a02f3cbcac170adc65b58c",
"rev": "8b8c811c7c2541c30382c5de7ed26be055569c60",
"type": "github"
},
"original": {
@@ -153,45 +70,13 @@
"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": {
"locked": {
"lastModified": 1782847189,
"narHash": "sha256-twXPFqFsrrY5r28Zh7Homgcp2gUMBgQ6WDS98Q/3xFI=",
"lastModified": 1786106723,
"narHash": "sha256-zDSUbpoeo/9ZmD2+wXnzxoo1+uhL8vxc0b8yuYMKYq0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b6018f87da91d19d0ab4cf979885689b469cdd41",
"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",
"rev": "f13ff45afd1bb73e640eaa08a7066dbed07e3238",
"type": "github"
},
"original": {
@@ -201,7 +86,7 @@
"type": "github"
}
},
"nixpkgs_4": {
"nixpkgs_3": {
"locked": {
"lastModified": 1784555310,
"narHash": "sha256-/FCliTPgiuV1owejZFNx3Ch9irdvkOfOFl+HHZ+DrtM=",
@@ -220,8 +105,8 @@
"nixvim": {
"inputs": {
"flake-parts": "flake-parts",
"nixpkgs": "nixpkgs_4",
"systems": "systems_2"
"nixpkgs": "nixpkgs_3",
"systems": "systems"
},
"locked": {
"lastModified": 1785763201,
@@ -240,28 +125,12 @@
"root": {
"inputs": {
"btc-clients": "btc-clients",
"nix-bitcoin": "nix-bitcoin",
"nixpkgs": "nixpkgs_3",
"nixpkgs": "nixpkgs_2",
"nixpkgs-stable": "nixpkgs-stable",
"nixvim": "nixvim"
}
},
"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": {
"lastModified": 1774449309,
"narHash": "sha256-brhZ8DmuGtzkCYHJg4HEd602amKm89Y9ytsFZ5uWD1w=",
+5 -7
View File
@@ -3,13 +3,12 @@
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
nix-bitcoin.url = "github:fort-nix/nix-bitcoin/release";
nixvim.url = "github:nix-community/nixvim";
btc-clients.url = "github:emmanuelrosa/btc-clients-nix";
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
overlay-stable = final: prev: {
@@ -22,7 +21,7 @@
{
nixosConfigurations.nixos = nixpkgs.lib.nixosSystem {
modules = [
{ nixpkgs.hostPlatform = "x86_64-linux"; }
{ nixpkgs.hostPlatform = "x86_64-linux"; nixpkgs.overlays = [ overlay-stable ]; }
self.nixosModules.Sovran_SystemsOS
./hardware-configuration.nix
./role-state.nix
@@ -32,10 +31,9 @@
nixosConfigurations.sovran_systemsos-iso = nixpkgs.lib.nixosSystem {
modules = [
{ nixpkgs.hostPlatform = "x86_64-linux"; }
({ config, pkgs, ... }: { nixpkgs.overlays = [ overlay-stable ]; })
{ nixpkgs.hostPlatform = "x86_64-linux"; nixpkgs.overlays = [ overlay-stable ]; }
./iso/common.nix
nix-bitcoin.nixosModules.default
./modules/bitcoin
nixvim.nixosModules.nixvim
];
};
@@ -46,7 +44,7 @@
nixpkgs.overlays = [ overlay-stable ];
})
./configuration.nix
nix-bitcoin.nixosModules.default
./modules/bitcoin
nixvim.nixosModules.nixvim
];
config = {
+3 -3
View File
@@ -1,7 +1,7 @@
{ config, pkgs, lib, ... }:
lib.mkIf config.sovran_systemsOS.features.bitcoin-core {
services.bitcoind.package = lib.mkForce config.nix-bitcoin.pkgs.bitcoind;
# Vendored nix-bitcoin now uses nixpkgs directly; no nix-bitcoin.pkgs indirection.
# 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"
]
+479
View File
@@ -0,0 +1,479 @@
{ 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 ''
echo "rpcauth=${user.name}:$(cat ${secretsDir}/bitcoin-HMAC-${name})"
''
) (builtins.attrNames cfg.rpc.users);
in ''
${optionalString cfg.dataDirReadableByGroup ''
if [[ -e '${cfg.dataDir}/blocks' ]]; then
chmod -R g+rX '${cfg.dataDir}/blocks'
fi
''}
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
'';
};
}
+267
View File
@@ -0,0 +1,267 @@
{ config, lib, pkgs, ... }:
with lib;
let
options.services = {
btcpayserver = {
enable = mkEnableOption "btcpayserver, a self-hosted Bitcoin payment processor";
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.";
};
package = mkOption {
type = types.package;
default = if cfg.btcpayserver.lbtc then
pkgs.btcpayserver.override { altcoinSupport = true; }
else
pkgs.btcpayserver;
defaultText = "(See source)";
description = "The package providing btcpayserver binaries.";
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/btcpayserver";
description = "The data directory for btcpayserver.";
};
lightningBackend = mkOption {
type = types.nullOr (types.enum [ "lnd" ]); # Sovran uses LND only
default = null;
description = "The lightning node implementation to use.";
};
lbtc = mkOption {
type = types.bool;
default = false;
description = "Enable liquid support in btcpayserver.";
};
rootpath = mkOption {
type = types.nullOr types.str;
default = null;
example = "btcpayserver";
description = "The prefix for root-relative btcpayserver URLs.";
};
user = mkOption {
type = types.str;
default = "btcpayserver";
description = "The user as which to run btcpayserver.";
};
group = mkOption {
type = types.str;
default = cfg.btcpayserver.user;
description = "The group as which to run btcpayserver.";
};
tor.enforce = nbLib.tor.enforce;
};
nbxplorer = {
enable = mkOption {
# This option is only used by netns-isolation
internal = true;
default = cfg.btcpayserver.enable;
description = ''
nbxplorer is always enabled when btcpayserver is enabled.
'';
};
package = mkOption {
type = types.package;
default = pkgs.nbxplorer;
defaultText = "pkgs.nbxplorer";
description = "The package providing nbxplorer binaries.";
};
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.";
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/nbxplorer";
description = "The data directory for nbxplorer.";
};
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.enforce = nbLib.tor.enforce;
};
};
cfg = config.services;
nbLib = config.nix-bitcoin.lib;
inherit (config.services) bitcoind;
# liquidd removed - Sovran uses lbtc=false, not needed
in {
inherit options;
config = mkIf cfg.btcpayserver.enable {
services.bitcoind = {
enable = true;
rpc.users.btcpayserver = {
passwordHMACFromFile = true;
rpcwhitelist = cfg.bitcoind.rpc.users.public.rpcwhitelist ++ [
"setban"
"generatetoaddress"
];
};
listenWhitelisted = true;
};
# vendored fix: only enable clightning if module exists (nixpkgs unstable may have renamed/removed it)
# Sovran uses lnd only, so this is never true in practice
# clightning removed/has no enable on this nixpkgs - not used by Sovran (lnd only), intentionally not set
services.lnd = mkIf (cfg.btcpayserver.lightningBackend == "lnd") {
enable = true;
macaroons.btcpayserver = {
inherit (cfg.btcpayserver) user;
permissions = ''{"entity":"info","action":"read"},{"entity":"onchain","action":"read"},{"entity":"offchain","action":"read"},{"entity":"address","action":"read"},{"entity":"message","action":"read"},{"entity":"peers","action":"read"},{"entity":"signer","action":"read"},{"entity":"invoices","action":"read"},{"entity":"invoices","action":"write"},{"entity":"address","action":"write"}'';
};
};
# vendored fix: liquidd may not exist in nixpkgs with this name
services.postgresql = {
enable = true;
ensureDatabases = [
"btcpaydb" # This name is kept for backwards compatibility
"nbxplorer"
];
ensureUsers = [
{ name = cfg.btcpayserver.user; }
{ name = cfg.nbxplorer.user; }
];
};
systemd.services.postgresql-setup.postStart = ''
psql -tAc '
ALTER DATABASE "btcpaydb" OWNER TO "${cfg.btcpayserver.user}";
ALTER DATABASE "nbxplorer" OWNER TO "${cfg.nbxplorer.user}";
'
'';
systemd.tmpfiles.rules = [
"d '${cfg.nbxplorer.dataDir}' 0770 ${cfg.nbxplorer.user} ${cfg.nbxplorer.group} - -"
"d '${cfg.btcpayserver.dataDir}' 0770 ${cfg.btcpayserver.user} ${cfg.btcpayserver.group} - -"
];
systemd.services.nbxplorer = let
configFile = builtins.toFile "config" ''
network=${bitcoind.network}
btcrpcuser=${cfg.bitcoind.rpc.users.btcpayserver.name}
btcrpcurl=http://${nbLib.addressWithPort bitcoind.rpc.address cfg.bitcoind.rpc.port}
btcnodeendpoint=${nbLib.addressWithPort bitcoind.address 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'
{
echo "btcrpcpassword=$(cat ${config.nix-bitcoin.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}
'';
User = cfg.nbxplorer.user;
Restart = "on-failure";
RestartSec = "10s";
ReadWritePaths = [ cfg.nbxplorer.dataDir ];
MemoryDenyWriteExecute = false;
} // nbLib.allowedIPAddresses cfg.nbxplorer.tor.enforce;
};
systemd.services.btcpayserver = let
nbExplorerUrl = "http://${nbLib.addressWithPort cfg.nbxplorer.address cfg.nbxplorer.port}/";
nbExplorerCookie = "${cfg.nbxplorer.dataDir}/${bitcoind.makeNetworkName "Main" "RegTest"}/.cookie";
configFile = builtins.toFile "btcpayserver-config" (''
network=${bitcoind.network}
bind=${cfg.btcpayserver.address}
port=${toString cfg.btcpayserver.port}
socksendpoint=${config.nix-bitcoin.torClientAddressWithPort}
btcexplorerurl=${nbExplorerUrl}
btcexplorercookiefile=${nbExplorerCookie}
postgres=User ID=${cfg.btcpayserver.user};Host=/run/postgresql;Database=btcpaydb
'' + optionalString (cfg.btcpayserver.rootpath != null) ''
rootpath=${cfg.btcpayserver.rootpath}
'' + optionalString (cfg.btcpayserver.lightningBackend == "lnd")
(
"btclightning=type=lnd-rest;" +
"server=https://${nbLib.address cfg.lnd.restAddress}:${toString cfg.lnd.restPort}/;" +
"macaroonfilepath=/run/lnd/btcpayserver.macaroon;" +
"certfilepath=${config.services.lnd.certPath}" +
"\n"
)
+ optionalString cfg.btcpayserver.lbtc ''
chains=btc,lbtc
lbtcexplorerurl=${nbExplorerUrl}
lbtcexplorercookiefile=${nbExplorerCookie}
'');
in rec {
wantedBy = [ "multi-user.target" ];
requires = [ "postgresql.target" ];
wants = [ "nbxplorer.service" ]
++ optional (cfg.btcpayserver.lightningBackend != null) "${cfg.btcpayserver.lightningBackend}.service";
after = requires ++ wants;
serviceConfig = nbLib.defaultHardening // {
ExecStart = ''
${cfg.btcpayserver.package}/bin/btcpayserver --conf=${configFile} \
--datadir='${cfg.btcpayserver.dataDir}'
'';
User = cfg.btcpayserver.user;
# Also restart after the program has exited successfully.
# This is required to support restarting from the web interface after
# interactive plugin installation.
# Restart rate limiting is implemented via the `startLimit*` options below.
Restart = "always";
ReadWritePaths = [ cfg.btcpayserver.dataDir ];
MemoryDenyWriteExecute = false;
} // nbLib.allowedIPAddresses cfg.btcpayserver.tor.enforce;
startLimitIntervalSec = 30;
startLimitBurst = 10;
};
users.users.${cfg.nbxplorer.user} = {
isSystemUser = true;
group = cfg.nbxplorer.group;
extraGroups = [ "bitcoinrpc-public" ];
home = cfg.nbxplorer.dataDir;
};
users.groups.${cfg.nbxplorer.group} = {};
users.users.${cfg.btcpayserver.user} = {
isSystemUser = true;
group = cfg.btcpayserver.group;
extraGroups = [ cfg.nbxplorer.group ];
home = cfg.btcpayserver.dataDir;
};
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
'';
};
}
+15
View File
@@ -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
];
}
+16
View File
@@ -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" ];
}
+106
View File
@@ -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} = {};
};
}
+132
View File
@@ -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
+313
View File
@@ -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}'
'';
};
}
+63
View File
@@ -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" ];
})
]);
}
+347
View File
@@ -0,0 +1,347 @@
{ 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 {}).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 {}).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
mempoolPkgs = pkgs.callPackage ../../packages/mempool {};
# 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} = {};
};
}
+60
View File
@@ -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)";
};
};
};
}
+154
View File
@@ -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})""")
'');
};
};
}
+106
View File
@@ -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}
'';
};
};
}
+119
View File
@@ -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;
};
};
}
];
}
+58
View File
@@ -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
'';
};
};
}
+236
View File
@@ -0,0 +1,236 @@
{ config, lib, pkgs, ... }:
with lib;
let
options.services.rtl = {
enable = mkEnableOption "Ride The Lightning, a web interface for lnd and clightning";
address = mkOption {
type = types.str;
default = "127.0.0.1";
description = "HTTP server address.";
};
port = mkOption {
type = types.port;
default = 3000;
description = "HTTP server port.";
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/rtl";
description = "The data directory for RTL.";
};
nodes = {
clightning = {
enable = mkOption {
type = types.bool;
default = false;
description = "Enable the clightning node interface.";
};
extraConfig = mkOption {
type = with types; attrsOf anything;
default = {};
example = {
Settings.userPersona = "MERCHANT";
Settings.logLevel = "DEBUG";
};
description = ''
Extra clightning node configuration.
See here for all available options:
https://github.com/Ride-The-Lightning/RTL/blob/master/.github/docs/Application_configurations.md
'';
};
};
lnd = {
enable = mkOption {
type = types.bool;
default = false;
description = "Enable the lnd node interface.";
};
loop = mkOption {
type = types.bool;
default = false;
description = "Enable swaps with lightning-loop.";
};
extraConfig = mkOption {
type = with types; attrsOf anything;
default = {};
example = {
Settings.userPersona = "MERCHANT";
Settings.logLevel = "DEBUG";
};
description = ''
Extra lnd node configuration.
See here for all available options:
https://github.com/Ride-The-Lightning/RTL/blob/master/.github/docs/Application_configurations.md
'';
};
};
reverseOrder = mkOption {
type = types.bool;
default = false;
description = ''
Reverse the order of nodes shown in the UI.
By default, clightning is shown before lnd.
'';
};
};
nightTheme = mkOption {
type = types.bool;
default = false;
description = "Enable the Night UI Theme.";
};
extraCurrency = mkOption {
type = with types; nullOr str;
default = null;
example = "USD";
description = ''
Currency code (ISO 4217) of the extra currency used for displaying balances.
When set, this option enables online currency rate fetching.
Warning: Rate fetching requires outgoing clearnet connections, so option
{option}`tor.enforce` is automatically disabled.
'';
};
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.enforce = nbLib.tor.enforce;
};
cfg = config.services.rtl;
nbLib = config.nix-bitcoin.lib;
nbPkgs = pkgs;
secretsDir = config.nix-bitcoin.secretsDir;
runePath = "${cfg.dataDir}/clightning-admin-rune";
inherit (nbLib) optionalAttr;
node = { isLnd, index }: {
inherit index;
lnNode = "Node";
lnImplementation = if isLnd then "LND" else "CLT";
Authentication = {
${optionalAttr (isLnd && lndLoopEnabled) "swapMacaroonPath"} = "${(lightning-loop.dataDir or "/var/lib/lightning-loop")}/${bitcoind.network}";
${optionalAttr (isLnd) "macaroonPath"} = "${cfg.dataDir}/macaroons";
${optionalAttr (!isLnd) "runePath"} = runePath;
};
Settings = {
userPersona = "OPERATOR";
themeMode = if cfg.nightTheme then "NIGHT" else "DAY";
themeColor = "PURPLE";
${optionalAttr isLnd "channelBackupPath"} = "${cfg.dataDir}/backup/lnd";
logLevel = "INFO";
fiatConversion = cfg.extraCurrency != null;
${optionalAttr (cfg.extraCurrency != null) "currencyUnit"} = cfg.extraCurrency;
${optionalAttr (isLnd && lndLoopEnabled) "swapServerUrl"} =
"https://${nbLib.addressWithPort (lightning-loop.restAddress or "127.0.0.1") (lightning-loop.restPort or 8081)}";
lnServerUrl = "https://${
if isLnd
then nbLib.addressWithPort lnd.restAddress lnd.restPort
else nbLib.addressWithPort clightning.plugins.clnrest.address clightning.plugins.clnrest.port
}";
};
};
nodes' =
optional cfg.nodes.clightning.enable
(recursiveUpdate (node { isLnd = false; index = 1; }) cfg.nodes.clightning.extraConfig) ++
optional cfg.nodes.lnd.enable
(recursiveUpdate (node { isLnd = true; index = 2; }) cfg.nodes.lnd.extraConfig);
nodes = if cfg.nodes.reverseOrder then reverseList nodes' else nodes';
rtlConfig = {
multiPass = "@multiPass@";
host = cfg.address;
port = cfg.port;
SSO.rtlSSO = 0;
inherit nodes;
};
configFile = builtins.toFile "config" (builtins.toJSON rtlConfig);
inherit (config.services)
bitcoind
lnd
clightning
lightning-loop;
lndLoopEnabled = cfg.nodes.lnd.enable && cfg.nodes.lnd.loop;
in {
inherit options;
config = mkIf cfg.enable {
assertions = [
{ assertion = cfg.nodes.clightning.enable || cfg.nodes.lnd.enable;
message = ''
RTL: At least one of `nodes.lnd.enable` or `nodes.clightning.enable` must be `true`.
'';
}
];
services.lnd.enable = mkIf cfg.nodes.lnd.enable true; # lightning-loop removed - not used
# vendored fix: clightning may not exist in this nixpkgs
# clightning removed - Sovran uses lnd only
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.clightning.enable "clightning.service" ++
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'
'')
++ optional cfg.nodes.clightning.enable
(nbLib.rootScript "rtl-create-clnrest-rune-file" ''
rune=$(cat '${clightning.networkDir}/admin-rune')
install --compare -m 640 -o ${cfg.user} -g ${cfg.group} <(printf 'LIGHTNING_RUNE="%s"\n' "$rune") '${runePath}'
'');
ExecStart = "${pkgs.rtl}/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
'';
};
}
+235
View File
@@ -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"
'';
};
};
}
+48
View File
@@ -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>
'')
];
};
}
+12
View File
@@ -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 = {};
}
+1
View File
@@ -68,6 +68,7 @@ lib.mkIf config.sovran_systemsOS.services.bitcoin {
name = "free";
};
# vendored: now no-op (always uses nixpkgs)
nix-bitcoin.useVersionLockedPkgs = false;
systemd.services.bitcoind = {
+3 -3
View File
@@ -3,9 +3,9 @@
{
config = lib.mkMerge [
# nix-bitcoin is globally imported by the flake (nixosModules.Sovran_SystemsOS).
# This default satisfies nix-bitcoin's generateSecrets assertion so that Desktop
# Only systems can evaluate without enabling any Bitcoin services.
# Vendored nix-bitcoin is always imported via modules/vendor/nix-bitcoin/modules.nix.
# This default satisfies the secrets assertion so Desktop-Only systems evaluate
# without enabling any Bitcoin services.
{
nix-bitcoin.generateSecrets = lib.mkDefault true;
}
+19
View File
@@ -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
+201
View File
@@ -0,0 +1,201 @@
{ 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;
};
}
+28
View File
@@ -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;
}
+30
View File
@@ -283,6 +283,36 @@ git add VERSION
git commit -m "chore: bump VERSION to ${TAG} for ISO naming" || true
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
# ─────────────────────────────────────────────────────────────────────────────
+56 -25
View File
@@ -7,7 +7,15 @@
# Usage:
# ./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):
# 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_RCLONE_REMOTE - rclone remote target (e.g. s3:my-bucket/downloads/)
# CDN_UPLOAD_CMD - custom upload command
@@ -22,65 +30,88 @@ BLUE='\033[0;34m'
CYAN='\033[0;36m'
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
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
fi
VERSION=$(cat VERSION | tr -d '\n\r ')
ISO_NAME="Sovran_SystemsOS-${VERSION}.iso"
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}║ Sovran_SystemsOS CDN ISO Packaging & Upload Tool ║${NC}"
echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}"
echo -e " Version : ${CYAN}${VERSION}${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
# Locate or build ISO
SRC_ISO=""
if [ -f "result/iso/${ISO_NAME}" ]; then
SRC_ISO="result/iso/${ISO_NAME}"
if [ -f "$RESULT_LINK/iso/${ISO_NAME}" ]; then
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
SRC_ISO="result/iso/Sovran_SystemsOS.iso"
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
SRC_ISO="$FOUND"
fi
fi
if [ -z "$SRC_ISO" ] || [ ! -f "$SRC_ISO" ]; then
echo -e "${YELLOW}Built ISO not found in result/. Building ISO via Nix...${NC}"
nix build .#nixosConfigurations.sovran_systemsos-iso.config.system.build.isoImage
if [ -f "result/iso/${ISO_NAME}" ]; then
SRC_ISO="result/iso/${ISO_NAME}"
echo -e "${YELLOW}Built ISO not found. Building via Nix (result link goes outside the repo)...${NC}"
rm -f "$RESULT_LINK"
nix build .#nixosConfigurations.sovran_systemsos-iso.config.system.build.isoImage \
--out-link "$RESULT_LINK"
if [ -f "$RESULT_LINK/iso/${ISO_NAME}" ]; then
SRC_ISO="$RESULT_LINK/iso/${ISO_NAME}"
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
SRC_ISO="$FOUND"
else
echo -e "${RED}Error: Failed to locate built ISO in result/.${NC}" >&2
exit 1
fi
fi
fi
echo -e "${BLUE}Step 1: Copying built ISO out of Nix store...${NC}"
cp -v "$SRC_ISO" "./${ISO_NAME}"
echo -e " ${GREEN}✓${NC} Copied to ./${ISO_NAME}"
if [ -z "$SRC_ISO" ] || [ ! -f "$SRC_ISO" ]; then
echo -e "${RED}Error: Failed to locate built ISO.${NC}" >&2
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 -e "${BLUE}Step 2: Generating versioned SHA-256 checksum...${NC}"
sha256sum "${ISO_NAME}" > "${SHA_NAME}"
echo -e " ${GREEN}✓${NC} Generated ${SHA_NAME}"
cat "${SHA_NAME}"
sha256sum "$ISO_PATH" > "$SHA_PATH"
echo -e " ${GREEN}✓${NC} Generated $SHA_PATH"
cat "$SHA_PATH"
echo
echo -e "${BLUE}Step 3: Verifying checksum...${NC}"
sha256sum --check "${SHA_NAME}"
sha256sum --check "$SHA_PATH"
echo -e " ${GREEN}✓${NC} Checksum verified successfully"
# Parse arguments for upload
@@ -97,7 +128,7 @@ done
if [ "$DO_UPLOAD" -eq 1 ]; then
echo
echo -e "${BLUE}Step 4: Uploading to CDN...${NC}"
UPLOADED=0
if [ -n "${CDN_UPLOAD_CMD:-}" ]; then
echo -e " Running custom CDN_UPLOAD_CMD..."
@@ -107,13 +138,13 @@ if [ "$DO_UPLOAD" -eq 1 ]; then
if [ -n "${CDN_RSYNC_TARGET:-}" ]; then
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
fi
if [ -n "${CDN_RCLONE_REMOTE:-}" ]; then
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
fi
@@ -128,8 +159,8 @@ else
echo -e "${GREEN}╔════════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ ✅ ISO packaging & verification complete! ║${NC}"
echo -e "${GREEN}╚════════════════════════════════════════════════════════════╝${NC}"
echo -e "Ready files:"
ls -lh "${ISO_NAME}" "${SHA_NAME}"
echo -e "Ready files (in ${CYAN}${OUT_DIR}${NC}, outside the repo):"
ls -lh "$ISO_PATH" "$SHA_PATH"
echo
echo "To upload to CDN, run:"
echo " ./scripts/upload-cdn.sh --upload"