1+ # Partner Chains Node NixOS module
2+ { self , ...} :
3+ {
4+ flake . nixosModules . partner-chains =
5+ { config , lib , pkgs , ... } :
6+
7+ with lib ;
8+
9+ let
10+ cfg = config . services . partner-chains ;
11+ in {
12+ options . services . partner-chains = {
13+ enable = mkEnableOption "Partner Chains Node" ;
14+
15+ # Common environment variables
16+ environment = mkOption {
17+ type = types . attrsOf types . str ;
18+ default = {
19+ CARDANO_SECURITY_PARAMETER = "432" ;
20+ CARDANO_ACTIVE_SLOTS_COEFF = "0.05" ;
21+ DB_SYNC_POSTGRES_CONNECTION_STRING = "postgresql://cexplorer:password@localhost:5432/cexplorer" ;
22+ MC__FIRST_EPOCH_TIMESTAMP_MILLIS = "1666656000000" ;
23+ MC__EPOCH_DURATION_MILLIS = "86400000" ;
24+ MC__SLOT_DURATION_MILLIS = "1000" ;
25+ MC__FIRST_EPOCH_NUMBER = "0" ;
26+ MC__FIRST_SLOT_NUMBER = "0" ;
27+ BLOCK_STABILITY_MARGIN = "0" ;
28+ } ;
29+ description = "Environment variables for the partner chains node" ;
30+ } ;
31+
32+ # Node-specific options
33+ nodeName = mkOption {
34+ type = types . nullOr types . str ;
35+ default = null ;
36+ example = "alice" ;
37+ description = "Node name flag (--alice, --bob, --charlie, etc.)" ;
38+ } ;
39+
40+ nodeKey = mkOption {
41+ type = types . str ;
42+ example = "0a04cb23cff606facb13ddd43655840e9f6f32bd7b432809620d461596e188e9" ;
43+ description = "Node key for identification" ;
44+ } ;
45+
46+ chainSpecPath = mkOption {
47+ type = types . path ;
48+ default = "/var/lib/partner-chains/chain-spec.json" ;
49+ description = "Path to the chain specification file" ;
50+ } ;
51+
52+ keystorePath = mkOption {
53+ type = types . path ;
54+ default = "/var/lib/partner-chains/keystore" ;
55+ description = "Path to the keystore directory" ;
56+ } ;
57+
58+ listenAddr = mkOption {
59+ type = types . str ;
60+ default = "/ip4/0.0.0.0/tcp/30333" ;
61+ description = "Address to listen on" ;
62+ } ;
63+
64+ prometheusPort = mkOption {
65+ type = types . int ;
66+ default = 9615 ;
67+ description = "Prometheus metrics port" ;
68+ } ;
69+
70+ rpcPort = mkOption {
71+ type = types . int ;
72+ default = 9944 ;
73+ description = "RPC server port" ;
74+ } ;
75+
76+ logLevel = mkOption {
77+ type = types . str ;
78+ default = "runtime=debug" ;
79+ description = "Log level configuration" ;
80+ } ;
81+
82+ reservedNodes = mkOption {
83+ type = types . listOf types . str ;
84+ default = [ ] ;
85+ example = [ "/dns/dave.node.sc.iog.io/tcp/30333/p2p/12D3KooWH4LhgJDUbYbXsksQef4jTpDjA64ecUBjBVJprNzF64hE" ] ;
86+ description = "List of reserved nodes" ;
87+ } ;
88+
89+ bootNodes = mkOption {
90+ type = types . listOf types . str ;
91+ default = [ ] ;
92+ example = [ "/dns/eve.node.sc.iog.io/tcp/30333/p2p/12D3KooWN3YiYbk9nMZJ2VG7uk9iKfFWb1Kwrj7PoMdadfnAsRJm" ] ;
93+ description = "List of boot nodes" ;
94+ } ;
95+
96+ extraArgs = mkOption {
97+ type = types . listOf types . str ;
98+ default = [ ] ;
99+ example = [ "--rpc-methods=unsafe" "--rpc-max-connections" "1000" ] ;
100+ description = "Additional command line arguments" ;
101+ } ;
102+
103+ enableRpc = mkOption {
104+ type = types . bool ;
105+ default = false ;
106+ description = "Enable RPC server" ;
107+ } ;
108+
109+ rpcCors = mkOption {
110+ type = types . str ;
111+ default = "all" ;
112+ description = "RPC CORS setting" ;
113+ } ;
114+
115+ blockBeneficiary = mkOption {
116+ type = types . str ;
117+ example = "0a04cb23cff606facb13ddd43655840e9f6f32bd7b432809620d461596e188e9" ;
118+ description = "Sidechain block beneficiary address" ;
119+ } ;
120+
121+ package = mkOption {
122+ type = types . package ;
123+ default = pkgs . partner-chains . packages . x86_64-linux . partner-chains ;
124+ description = "The partner-chains package to use" ;
125+ } ;
126+
127+ # New options
128+ enableValidator = mkOption {
129+ type = types . bool ;
130+ default = false ;
131+ description = "Enable validator mode" ;
132+ } ;
133+
134+ pruning = mkOption {
135+ type = types . enum [ "default" "archive" ] ;
136+ default = "default" ;
137+ description = "Pruning mode for the blockchain data" ;
138+ } ;
139+ } ;
140+
141+ config = mkIf cfg . enable {
142+ systemd . services . partner-chains = {
143+ description = "Partner Chains Node" ;
144+ wantedBy = [ "multi-user.target" ] ;
145+ after = [ "network.target" ] ;
146+
147+ # Combine the default environment with any overrides
148+ environment = cfg . environment // {
149+ SIDECHAIN_BLOCK_BENEFICIARY = cfg . blockBeneficiary ;
150+ } ;
151+
152+ preStart = ''
153+ # Ensure directories exist
154+ mkdir -p ${ dirOf cfg . chainSpecPath }
155+ mkdir -p ${ cfg . keystorePath }
156+
157+ # Create a default chain spec if it doesn't exist
158+ if [ ! -f "${ cfg . chainSpecPath } " ]; then
159+ echo "Chain spec file does not exist at ${ cfg . chainSpecPath } "
160+ echo "Creating a minimal placeholder. Please ensure a proper chain spec is configured."
161+ echo '{"name": "partner-chains", "id": "partner-chains"}' > ${ cfg . chainSpecPath }
162+ fi
163+
164+ # Set proper permissions
165+ chown -R partner-chains:partner-chains ${ dirOf cfg . chainSpecPath }
166+ chown -R partner-chains:partner-chains ${ cfg . keystorePath }
167+ chmod 750 ${ dirOf cfg . chainSpecPath }
168+ chmod 750 ${ cfg . keystorePath }
169+ chmod 640 ${ cfg . chainSpecPath }
170+
171+ # Create data directories with proper permissions
172+ DATA_DIR=$(dirname ${ cfg . keystorePath } )
173+ if [ ! -d "$DATA_DIR" ]; then
174+ mkdir -p $DATA_DIR
175+ chown -R partner-chains:partner-chains $DATA_DIR
176+ chmod -R 750 $DATA_DIR
177+ fi
178+ '' ;
179+
180+ serviceConfig = let
181+ # Node name flag (--alice, --bob, etc.)
182+ nodeNameFlag = if cfg . nodeName != null then "--${ cfg . nodeName } " else "" ;
183+
184+ # Reserved nodes flags
185+ reservedNodesFlags = if cfg . reservedNodes != [ ]
186+ then [ "--reserved-only" ] ++ ( flatten ( map ( node : [ "--reserved-nodes" node ] ) cfg . reservedNodes ) )
187+ else [ ] ;
188+
189+ # Boot nodes flags
190+ bootNodesFlags = flatten ( map ( node : [ "--bootnodes" node ] ) cfg . bootNodes ) ;
191+
192+ # RPC flags
193+ rpcFlags = if cfg . enableRpc
194+ then [ "--rpc-external" "--rpc-cors=${ cfg . rpcCors } " "--rpc-port" ( toString cfg . rpcPort ) ]
195+ else [ ] ;
196+
197+ # Pruning flags
198+ pruningFlags = if cfg . pruning == "archive"
199+ then [ "--state-pruning" "archive" "--blocks-pruning" "archive" ]
200+ else [ ] ;
201+
202+ # Combine all arguments
203+ allArgs =
204+ [ nodeNameFlag ]
205+ ++ ( optional cfg . enableValidator "--validator" )
206+ ++ [ "--node-key" cfg . nodeKey ]
207+ ++ [ "--chain" cfg . chainSpecPath ]
208+ ++ reservedNodesFlags
209+ ++ bootNodesFlags
210+ ++ [ "-llibp2p=debug" ]
211+ ++ [ "--listen-addr" cfg . listenAddr ]
212+ ++ [ "--keystore-path" cfg . keystorePath ]
213+ ++ [ "--log" cfg . logLevel ]
214+ ++ [ "--prometheus-port" ( toString cfg . prometheusPort ) ]
215+ ++ [ "--prometheus-external" ]
216+ ++ rpcFlags
217+ ++ pruningFlags
218+ ++ cfg . extraArgs ;
219+
220+ # Filter out empty strings (from nodeNameFlag if it's empty)
221+ cleanArgs = filter ( arg : arg != "" ) allArgs ;
222+ in {
223+ ExecStart = "${ cfg . package } /bin/partner-chains-node ${ concatStringsSep " " cleanArgs } " ;
224+
225+ # Important! Create a StateDirectory for persistent storage
226+ StateDirectory = "partner-chains" ;
227+
228+ # This ensures systemd captures all stdout/stderr
229+ StandardOutput = "journal" ;
230+ StandardError = "journal" ;
231+
232+ # Security hardening
233+ User = "partner-chains" ;
234+ Group = "partner-chains" ;
235+ Restart = "always" ;
236+ RestartSec = "10s" ;
237+ LimitNOFILE = 65535 ;
238+ } ;
239+ } ;
240+
241+ # Create the user/group
242+ users . users . partner-chains = {
243+ isSystemUser = true ;
244+ group = "partner-chains" ;
245+ home = "/var/lib/partner-chains" ;
246+ createHome = true ;
247+ } ;
248+ users . groups . partner-chains = { } ;
249+
250+ # Open firewall ports
251+ networking . firewall . allowedTCPPorts = [
252+ 30333 # P2P port
253+ cfg . prometheusPort
254+ ] ++ ( optionals cfg . enableRpc [ cfg . rpcPort ] ) ;
255+ } ;
256+ } ;
257+ }
0 commit comments