1{ config, pkgs, lib, ... }:
2let
3
4 inherit (lib) mkEnableOption mkIf mkOption literalExample types optionalString;
5
6 cfg = config.services.quorum;
7 dataDir = "/var/lib/quorum";
8 genesisFile = pkgs.writeText "genesis.json" (builtins.toJSON cfg.genesis);
9 staticNodesFile = pkgs.writeText "static-nodes.json" (builtins.toJSON cfg.staticNodes);
10
11in {
12 options = {
13
14 services.quorum = {
15 enable = mkEnableOption "Quorum blockchain daemon";
16
17 user = mkOption {
18 type = types.str;
19 default = "quorum";
20 description = "The user as which to run quorum.";
21 };
22
23 group = mkOption {
24 type = types.str;
25 default = cfg.user;
26 description = "The group as which to run quorum.";
27 };
28
29 port = mkOption {
30 type = types.port;
31 default = 21000;
32 description = "Override the default port on which to listen for connections.";
33 };
34
35 nodekeyFile = mkOption {
36 type = types.path;
37 default = "${dataDir}/nodekey";
38 description = "Path to the nodekey.";
39 };
40
41 staticNodes = mkOption {
42 type = types.listOf types.str;
43 default = [];
44 example = [ "enode://dd333ec28f0a8910c92eb4d336461eea1c20803eed9cf2c056557f986e720f8e693605bba2f4e8f289b1162e5ac7c80c914c7178130711e393ca76abc1d92f57@0.0.0.0:30303?discport=0" ];
45 description = "List of validator nodes.";
46 };
47
48 privateconfig = mkOption {
49 type = types.str;
50 default = "ignore";
51 description = "Configuration of privacy transaction manager.";
52 };
53
54 syncmode = mkOption {
55 type = types.enum [ "fast" "full" "light" ];
56 default = "full";
57 description = "Blockchain sync mode.";
58 };
59
60 blockperiod = mkOption {
61 type = types.int;
62 default = 5;
63 description = "Default minimum difference between two consecutive block's timestamps in seconds.";
64 };
65
66 permissioned = mkOption {
67 type = types.bool;
68 default = true;
69 description = "Allow only a defined list of nodes to connect.";
70 };
71
72 rpc = {
73 enable = mkOption {
74 type = types.bool;
75 default = true;
76 description = "Enable RPC interface.";
77 };
78
79 address = mkOption {
80 type = types.str;
81 default = "0.0.0.0";
82 description = "Listening address for RPC connections.";
83 };
84
85 port = mkOption {
86 type = types.port;
87 default = 22004;
88 description = "Override the default port on which to listen for RPC connections.";
89 };
90
91 api = mkOption {
92 type = types.str;
93 default = "admin,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,istanbul";
94 description = "API's offered over the HTTP-RPC interface.";
95 };
96 };
97
98 ws = {
99 enable = mkOption {
100 type = types.bool;
101 default = true;
102 description = "Enable WS-RPC interface.";
103 };
104
105 address = mkOption {
106 type = types.str;
107 default = "0.0.0.0";
108 description = "Listening address for WS-RPC connections.";
109 };
110
111 port = mkOption {
112 type = types.port;
113 default = 8546;
114 description = "Override the default port on which to listen for WS-RPC connections.";
115 };
116
117 api = mkOption {
118 type = types.str;
119 default = "admin,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,istanbul";
120 description = "API's offered over the WS-RPC interface.";
121 };
122
123 origins = mkOption {
124 type = types.str;
125 default = "*";
126 description = "Origins from which to accept websockets requests";
127 };
128 };
129
130 genesis = mkOption {
131 type = types.nullOr types.attrs;
132 default = null;
133 example = literalExample '' {
134 alloc = {
135 a47385db68718bdcbddc2d2bb7c54018066ec111 = {
136 balance = "1000000000000000000000000000";
137 };
138 };
139 coinbase = "0x0000000000000000000000000000000000000000";
140 config = {
141 byzantiumBlock = 4;
142 chainId = 494702925;
143 eip150Block = 2;
144 eip155Block = 3;
145 eip158Block = 3;
146 homesteadBlock = 1;
147 isQuorum = true;
148 istanbul = {
149 epoch = 30000;
150 policy = 0;
151 };
152 };
153 difficulty = "0x1";
154 extraData = "0x0000000000000000000000000000000000000000000000000000000000000000f85ad59438f0508111273d8e482f49410ca4078afc86a961b8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0";
155 gasLimit = "0x2FEFD800";
156 mixHash = "0x63746963616c2062797a616e74696e65201111756c7420746f6c6572616e6365";
157 nonce = "0x0";
158 parentHash = "0x0000000000000000000000000000000000000000000000000000000000000000";
159 timestamp = "0x00";
160 }'';
161 description = "Blockchain genesis settings.";
162 };
163 };
164 };
165
166 config = mkIf cfg.enable {
167 environment.systemPackages = [ pkgs.quorum ];
168 systemd.tmpfiles.rules = [
169 "d '${dataDir}' 0770 '${cfg.user}' '${cfg.group}' - -"
170 ];
171 systemd.services.quorum = {
172 description = "Quorum daemon";
173 after = [ "network.target" ];
174 wantedBy = [ "multi-user.target" ];
175 environment = {
176 PRIVATE_CONFIG = "${cfg.privateconfig}";
177 };
178 preStart = ''
179 if [ ! -d ${dataDir}/geth ]; then
180 if [ ! -d ${dataDir}/keystore ]; then
181 echo ERROR: You need to create a wallet before initializing your genesis file, run:
182 echo # su -s /bin/sh - quorum
183 echo $ geth --datadir ${dataDir} account new
184 echo and configure your genesis file accordingly.
185 exit 1;
186 fi
187 ln -s ${staticNodesFile} ${dataDir}/static-nodes.json
188 ${pkgs.quorum}/bin/geth --datadir ${dataDir} init ${genesisFile}
189 fi
190 '';
191 serviceConfig = {
192 User = cfg.user;
193 Group = cfg.group;
194 ExecStart = ''${pkgs.quorum}/bin/geth \
195 --nodiscover \
196 --verbosity 5 \
197 --nodekey ${cfg.nodekeyFile} \
198 --istanbul.blockperiod ${toString cfg.blockperiod} \
199 --syncmode ${cfg.syncmode} \
200 ${optionalString (cfg.permissioned)
201 "--permissioned"} \
202 --mine --minerthreads 1 \
203 ${optionalString (cfg.rpc.enable)
204 "--rpc --rpcaddr ${cfg.rpc.address} --rpcport ${toString cfg.rpc.port} --rpcapi ${cfg.rpc.api}"} \
205 ${optionalString (cfg.ws.enable)
206 "--ws --wsaddr ${cfg.ws.address} --wsport ${toString cfg.ws.port} --wsapi ${cfg.ws.api} --wsorigins ${cfg.ws.origins}"} \
207 --emitcheckpoints \
208 --datadir ${dataDir} \
209 --port ${toString cfg.port}'';
210 Restart = "on-failure";
211
212 # Hardening measures
213 PrivateTmp = "true";
214 ProtectSystem = "full";
215 NoNewPrivileges = "true";
216 PrivateDevices = "true";
217 MemoryDenyWriteExecute = "true";
218 };
219 };
220 users.users.${cfg.user} = {
221 name = cfg.user;
222 group = cfg.group;
223 description = "Quorum daemon user";
224 home = dataDir;
225 isSystemUser = true;
226 };
227 users.groups.${cfg.group} = {};
228 };
229}