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