diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index ccdc39eecd8d..c460e7164bf6 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -692,6 +692,7 @@ ./services/networking/prosody.nix ./services/networking/quagga.nix ./services/networking/quassel.nix + ./services/networking/quorum.nix ./services/networking/quicktun.nix ./services/networking/racoon.nix ./services/networking/radicale.nix diff --git a/nixos/modules/services/networking/quorum.nix b/nixos/modules/services/networking/quorum.nix new file mode 100644 index 000000000000..2f612c9db686 --- /dev/null +++ b/nixos/modules/services/networking/quorum.nix @@ -0,0 +1,229 @@ +{ config, pkgs, lib, ... }: +let + + inherit (lib) mkEnableOption mkIf mkOption literalExample types optionalString; + + cfg = config.services.quorum; + dataDir = "/var/lib/quorum"; + genesisFile = pkgs.writeText "genesis.json" (builtins.toJSON cfg.genesis); + staticNodesFile = pkgs.writeText "static-nodes.json" (builtins.toJSON cfg.staticNodes); + +in { + options = { + + services.quorum = { + enable = mkEnableOption "Quorum blockchain daemon"; + + user = mkOption { + type = types.str; + default = "quorum"; + description = "The user as which to run quorum."; + }; + + group = mkOption { + type = types.str; + default = cfg.user; + description = "The group as which to run quorum."; + }; + + port = mkOption { + type = types.port; + default = 21000; + description = "Override the default port on which to listen for connections."; + }; + + nodekeyFile = mkOption { + type = types.path; + default = "${dataDir}/nodekey"; + description = "Path to the nodekey."; + }; + + staticNodes = mkOption { + type = types.listOf types.str; + default = []; + example = [ "enode://dd333ec28f0a8910c92eb4d336461eea1c20803eed9cf2c056557f986e720f8e693605bba2f4e8f289b1162e5ac7c80c914c7178130711e393ca76abc1d92f57@0.0.0.0:30303?discport=0" ]; + description = "List of validator nodes."; + }; + + privateconfig = mkOption { + type = types.str; + default = "ignore"; + description = "Configuration of privacy transaction manager."; + }; + + syncmode = mkOption { + type = types.enum [ "fast" "full" "light" ]; + default = "full"; + description = "Blockchain sync mode."; + }; + + blockperiod = mkOption { + type = types.int; + default = 5; + description = "Default minimum difference between two consecutive block's timestamps in seconds."; + }; + + permissioned = mkOption { + type = types.bool; + default = true; + description = "Allow only a defined list of nodes to connect."; + }; + + rpc = { + enable = mkOption { + type = types.bool; + default = true; + description = "Enable RPC interface."; + }; + + address = mkOption { + type = types.str; + default = "0.0.0.0"; + description = "Listening address for RPC connections."; + }; + + port = mkOption { + type = types.port; + default = 22004; + description = "Override the default port on which to listen for RPC connections."; + }; + + api = mkOption { + type = types.str; + default = "admin,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,istanbul"; + description = "API's offered over the HTTP-RPC interface."; + }; + }; + + ws = { + enable = mkOption { + type = types.bool; + default = true; + description = "Enable WS-RPC interface."; + }; + + address = mkOption { + type = types.str; + default = "0.0.0.0"; + description = "Listening address for WS-RPC connections."; + }; + + port = mkOption { + type = types.port; + default = 8546; + description = "Override the default port on which to listen for WS-RPC connections."; + }; + + api = mkOption { + type = types.str; + default = "admin,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,istanbul"; + description = "API's offered over the WS-RPC interface."; + }; + + origins = mkOption { + type = types.str; + default = "*"; + description = "Origins from which to accept websockets requests"; + }; + }; + + genesis = mkOption { + type = types.nullOr types.attrs; + default = null; + example = literalExample '' { + alloc = { + a47385db68718bdcbddc2d2bb7c54018066ec111 = { + balance = "1000000000000000000000000000"; + }; + }; + coinbase = "0x0000000000000000000000000000000000000000"; + config = { + byzantiumBlock = 4; + chainId = 494702925; + eip150Block = 2; + eip155Block = 3; + eip158Block = 3; + homesteadBlock = 1; + isQuorum = true; + istanbul = { + epoch = 30000; + policy = 0; + }; + }; + difficulty = "0x1"; + extraData = "0x0000000000000000000000000000000000000000000000000000000000000000f85ad59438f0508111273d8e482f49410ca4078afc86a961b8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0"; + gasLimit = "0x2FEFD800"; + mixHash = "0x63746963616c2062797a616e74696e65201111756c7420746f6c6572616e6365"; + nonce = "0x0"; + parentHash = "0x0000000000000000000000000000000000000000000000000000000000000000"; + timestamp = "0x00"; + }''; + description = "Blockchain genesis settings."; + }; + }; + }; + + config = mkIf cfg.enable { + environment.systemPackages = [ pkgs.quorum ]; + systemd.tmpfiles.rules = [ + "d '${dataDir}' 0770 '${cfg.user}' '${cfg.group}' - -" + ]; + systemd.services.quorum = { + description = "Quorum daemon"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + environment = { + PRIVATE_CONFIG = "${cfg.privateconfig}"; + }; + preStart = '' + if [ ! -d ${dataDir}/geth ]; then + if [ ! -d ${dataDir}/keystore ]; then + echo ERROR: You need to create a wallet before initializing your genesis file, run: + echo # su -s /bin/sh - quorum + echo $ geth --datadir ${dataDir} account new + echo and configure your genesis file accordingly. + exit 1; + fi + ln -s ${staticNodesFile} ${dataDir}/static-nodes.json + ${pkgs.quorum}/bin/geth --datadir ${dataDir} init ${genesisFile} + fi + ''; + serviceConfig = { + User = cfg.user; + Group = cfg.group; + ExecStart = ''${pkgs.quorum}/bin/geth \ + --nodiscover \ + --verbosity 5 \ + --nodekey ${cfg.nodekeyFile} \ + --istanbul.blockperiod ${toString cfg.blockperiod} \ + --syncmode ${cfg.syncmode} \ + ${optionalString (cfg.permissioned) + "--permissioned"} \ + --mine --minerthreads 1 \ + ${optionalString (cfg.rpc.enable) + "--rpc --rpcaddr ${cfg.rpc.address} --rpcport ${toString cfg.rpc.port} --rpcapi ${cfg.rpc.api}"} \ + ${optionalString (cfg.ws.enable) + "--ws --wsaddr ${cfg.ws.address} --wsport ${toString cfg.ws.port} --wsapi ${cfg.ws.api} --wsorigins ${cfg.ws.origins}"} \ + --emitcheckpoints \ + --datadir ${dataDir} \ + --port ${toString cfg.port}''; + Restart = "on-failure"; + + # Hardening measures + PrivateTmp = "true"; + ProtectSystem = "full"; + NoNewPrivileges = "true"; + PrivateDevices = "true"; + MemoryDenyWriteExecute = "true"; + }; + }; + users.users.${cfg.user} = { + name = cfg.user; + group = cfg.group; + description = "Quorum daemon user"; + home = dataDir; + isSystemUser = true; + }; + users.groups.${cfg.group} = {}; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 51b463747b0e..63770e0460c2 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -250,6 +250,7 @@ in prosodyMysql = handleTest ./xmpp/prosody-mysql.nix {}; proxy = handleTest ./proxy.nix {}; quagga = handleTest ./quagga.nix {}; + quorum = handleTest ./quorum.nix {}; rabbitmq = handleTest ./rabbitmq.nix {}; radarr = handleTest ./radarr.nix {}; radicale = handleTest ./radicale.nix {}; diff --git a/nixos/tests/quorum.nix b/nixos/tests/quorum.nix new file mode 100644 index 000000000000..846d2a930188 --- /dev/null +++ b/nixos/tests/quorum.nix @@ -0,0 +1,79 @@ +import ./make-test-python.nix ({ pkgs, ... }: { + name = "quorum"; + meta = with pkgs.stdenv.lib.maintainers; { + maintainers = [ mmahut ]; + }; + + nodes = { + machine = { ... }: { + services.quorum = { + enable = true; + permissioned = false; + staticNodes = [ "enode://dd333ec28f0a8910c92eb4d336461eea1c20803eed9cf2c056557f986e720f8e693605bba2f4e8f289b1162e5ac7c80c914c7178130711e393ca76abc1d92f57@0.0.0.0:30303?discport=0" ]; + genesis = { + alloc = { + "189d23d201b03ae1cf9113672df29a5d672aefa3" = { + balance = "0x446c3b15f9926687d2c40534fdb564000000000000"; + }; + "44b07d2c28b8ed8f02b45bd84ac7d9051b3349e6" = { + balance = "0x446c3b15f9926687d2c40534fdb564000000000000"; + }; + "4c1ccd426833b9782729a212c857f2f03b7b4c0d" = { + balance = "0x446c3b15f9926687d2c40534fdb564000000000000"; + }; + "7ae555d0f6faad7930434abdaac2274fd86ab516" = { + balance = "0x446c3b15f9926687d2c40534fdb564000000000000"; + }; + c1056df7c02b6f1a353052eaf0533cc7cb743b52 = { + balance = "0x446c3b15f9926687d2c40534fdb564000000000000"; + }; + }; + coinbase = "0x0000000000000000000000000000000000000000"; + config = { + byzantiumBlock = 1; + chainId = 10; + eip150Block = 1; + eip150Hash = + "0x0000000000000000000000000000000000000000000000000000000000000000"; + eip155Block = 1; + eip158Block = 1; + isQuorum = true; + istanbul = { + epoch = 30000; + policy = 0; + }; + }; + difficulty = "0x1"; + extraData = + "0x0000000000000000000000000000000000000000000000000000000000000000f8aff869944c1ccd426833b9782729a212c857f2f03b7b4c0d94189d23d201b03ae1cf9113672df29a5d672aefa39444b07d2c28b8ed8f02b45bd84ac7d9051b3349e694c1056df7c02b6f1a353052eaf0533cc7cb743b52947ae555d0f6faad7930434abdaac2274fd86ab516b8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0"; + gasLimit = "0xe0000000"; + gasUsed = "0x0"; + mixHash = + "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365"; + nonce = "0x0"; + number = "0x0"; + parentHash = + "0x0000000000000000000000000000000000000000000000000000000000000000"; + timestamp = "0x5cffc201"; + }; + }; + }; + }; + + testScript = '' + start_all() + machine.wait_until_succeeds("mkdir -p /var/lib/quorum/keystore") + machine.wait_until_succeeds( + 'echo \{\\"address\\":\\"9377bc3936de934c497e22917b81aa8774ac3bb0\\",\\"crypto\\":\{\\"cipher\\":\\"aes-128-ctr\\",\\"ciphertext\\":\\"ad8341d8ef225650403fd366c955f41095e438dd966a3c84b3d406818c1e366c\\",\\"cipherparams\\":\{\\"iv\\":\\"2a09f7a72fd6dff7c43150ff437e6ac2\\"\},\\"kdf\\":\\"scrypt\\",\\"kdfparams\\":\{\\"dklen\\":32,\\"n\\":262144,\\"p\\":1,\\"r\\":8,\\"salt\\":\\"d1a153845bb80cd6274c87c5bac8ac09fdfac5ff131a6f41b5ed319667f12027\\"\},\\"mac\\":\\"a9621ad88fa1d042acca6fc2fcd711f7e05bfbadea3f30f379235570c8e270d3\\"\},\\"id\\":\\"89e847a3-1527-42f6-a321-77de0a14ce02\\",\\"version\\":3\}\\" > /var/lib/quorum/keystore/UTC--2020-03-23T11-08-34.144812212Z--9377bc3936de934c497e22917b81aa8774ac3bb0' + ) + machine.wait_until_succeeds( + "echo fe2725c4e8f7617764b845e8d939a65c664e7956eb47ed7d934573f16488efc1 > /var/lib/quorum/nodekey" + ) + machine.wait_until_succeeds("systemctl restart quorum") + machine.wait_for_unit("quorum.service") + machine.sleep(15) + machine.wait_until_succeeds( + 'geth attach /var/lib/quorum/geth.ipc --exec "eth.accounts" | grep 0x9377bc3936de934c497e22917b81aa8774ac3bb0' + ) + ''; +})