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