diff --git a/flake.lock b/flake.lock index f78c13ea8..43b02ae98 100644 --- a/flake.lock +++ b/flake.lock @@ -17,6 +17,23 @@ "type": "github" } }, + "CHaP_2": { + "flake": false, + "locked": { + "lastModified": 1777742585, + "narHash": "sha256-ZzXz2vOhqethlqPgBExPXEnKWvaTbidsIxh5MGv+pwE=", + "owner": "intersectmbo", + "repo": "cardano-haskell-packages", + "rev": "e8a483522ee73c8c9493ea6055553e5c2532e66b", + "type": "github" + }, + "original": { + "owner": "intersectmbo", + "ref": "repo", + "repo": "cardano-haskell-packages", + "type": "github" + } + }, "HTTP": { "flake": false, "locked": { @@ -33,6 +50,22 @@ "type": "github" } }, + "HTTP_2": { + "flake": false, + "locked": { + "lastModified": 1451647621, + "narHash": "sha256-oHIyw3x0iKBexEo49YeUDV1k74ZtyYKGR2gNJXXRxts=", + "owner": "phadej", + "repo": "HTTP", + "rev": "9bc0996d412fef1787449d841277ef663ad9a915", + "type": "github" + }, + "original": { + "owner": "phadej", + "repo": "HTTP", + "type": "github" + } + }, "blst": { "flake": false, "locked": { @@ -50,6 +83,40 @@ "type": "github" } }, + "blst_2": { + "flake": false, + "locked": { + "lastModified": 1749204514, + "narHash": "sha256-Q9/zGN93TnJt2c8YvSaURstoxT02ts3nVkO5V08m4TI=", + "owner": "supranational", + "repo": "blst", + "rev": "6d960cd05d6fe2b5bc9ba161edf0c1a131b87c4c", + "type": "github" + }, + "original": { + "owner": "supranational", + "ref": "v0.3.15", + "repo": "blst", + "type": "github" + } + }, + "cabal-32": { + "flake": false, + "locked": { + "lastModified": 1603716527, + "narHash": "sha256-X0TFfdD4KZpwl0Zr6x+PLxUt/VyKQfX7ylXHdmZIL+w=", + "owner": "haskell", + "repo": "cabal", + "rev": "48bf10787e27364730dd37a42b603cee8d6af7ee", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "3.2", + "repo": "cabal", + "type": "github" + } + }, "cabal-34": { "flake": false, "locked": { @@ -67,6 +134,23 @@ "type": "github" } }, + "cabal-34_2": { + "flake": false, + "locked": { + "lastModified": 1645834128, + "narHash": "sha256-wG3d+dOt14z8+ydz4SL7pwGfe7SiimxcD/LOuPCV6xM=", + "owner": "haskell", + "repo": "cabal", + "rev": "5ff598c67f53f7c4f48e31d722ba37172230c462", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "3.4", + "repo": "cabal", + "type": "github" + } + }, "cabal-36": { "flake": false, "locked": { @@ -84,6 +168,83 @@ "type": "github" } }, + "cabal-36_2": { + "flake": false, + "locked": { + "lastModified": 1669081697, + "narHash": "sha256-I5or+V7LZvMxfbYgZATU4awzkicBwwok4mVoje+sGmU=", + "owner": "haskell", + "repo": "cabal", + "rev": "8fd619e33d34924a94e691c5fea2c42f0fc7f144", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "3.6", + "repo": "cabal", + "type": "github" + } + }, + "cardano-automation": { + "inputs": { + "flake-utils": "flake-utils", + "hackageNix": "hackageNix", + "haskellNix": [ + "cardano-node", + "haskellNix" + ], + "nixpkgs": [ + "cardano-node", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1764682512, + "narHash": "sha256-yY3yIBmiCKsZ7YN++ttEKZiVMIHjjlAngFWaTGvBBvg=", + "owner": "input-output-hk", + "repo": "cardano-automation", + "rev": "9a91636c94317bff98ebf6b913f8c38beef0b374", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "cardano-automation", + "type": "github" + } + }, + "cardano-node": { + "inputs": { + "CHaP": "CHaP_2", + "cardano-automation": "cardano-automation", + "customConfig": "customConfig", + "empty-flake": "empty-flake", + "flake-compat": "flake-compat", + "hackageNix": "hackageNix_2", + "haskellNix": "haskellNix", + "incl": "incl", + "iohkNix": "iohkNix", + "nixpkgs": [ + "cardano-node", + "haskellNix", + "nixpkgs-unstable" + ], + "utils": "utils" + }, + "locked": { + "lastModified": 1777953209, + "narHash": "sha256-+dod+EL73MICgbgflyJnwDlxbCeaBbBLrr2tLX59hAA=", + "owner": "IntersectMBO", + "repo": "cardano-node", + "rev": "97036a66bcf8c89f687ae57a048eecc0389977ef", + "type": "github" + }, + "original": { + "owner": "IntersectMBO", + "ref": "11.0.1", + "repo": "cardano-node", + "type": "github" + } + }, "cardano-shell": { "flake": false, "locked": { @@ -100,7 +261,87 @@ "type": "github" } }, + "cardano-shell_2": { + "flake": false, + "locked": { + "lastModified": 1608537748, + "narHash": "sha256-PulY1GfiMgKVnBci3ex4ptk2UNYMXqGjJOxcPy2KYT4=", + "owner": "input-output-hk", + "repo": "cardano-shell", + "rev": "9392c75087cb9a3d453998f4230930dea3a95725", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "cardano-shell", + "type": "github" + } + }, + "customConfig": { + "locked": { + "lastModified": 1630400035, + "narHash": "sha256-MWaVOCzuFwp09wZIW9iHq5wWen5C69I940N1swZLEQ0=", + "owner": "input-output-hk", + "repo": "empty-flake", + "rev": "2040a05b67bf9a669ce17eca56beb14b4206a99a", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "empty-flake", + "type": "github" + } + }, + "empty-flake": { + "locked": { + "lastModified": 1630400035, + "narHash": "sha256-MWaVOCzuFwp09wZIW9iHq5wWen5C69I940N1swZLEQ0=", + "owner": "input-output-hk", + "repo": "empty-flake", + "rev": "2040a05b67bf9a669ce17eca56beb14b4206a99a", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "empty-flake", + "type": "github" + } + }, "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1647532380, + "narHash": "sha256-wswAxyO8AJTH7d5oU8VK82yBCpqwA+p6kLgpb1f1PAY=", + "owner": "input-output-hk", + "repo": "flake-compat", + "rev": "7da118186435255a30b5ffeabba9629c344c0bec", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "ref": "fixes", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-compat_2": { + "flake": false, + "locked": { + "lastModified": 1672831974, + "narHash": "sha256-z9k3MfslLjWQfnjBtEtJZdq3H7kyi2kQtUThfTgdRk0=", + "owner": "input-output-hk", + "repo": "flake-compat", + "rev": "45f2638735f8cdc40fe302742b79f248d23eb368", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "ref": "hkm/gitlab-fix", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-compat_3": { "flake": false, "locked": { "lastModified": 1672831974, @@ -135,7 +376,39 @@ "type": "github" } }, + "flake-utils": { + "locked": { + "lastModified": 1667395993, + "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, "hackage-for-stackage": { + "flake": false, + "locked": { + "lastModified": 1755649550, + "narHash": "sha256-YNKeqYIezur2MvPmfVI/aHjcVRwOdBW7Du3jg6iXjKs=", + "owner": "input-output-hk", + "repo": "hackage.nix", + "rev": "5e56db8bc478dfb7466ea83744c3ab928aff0329", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "ref": "for-stackage", + "repo": "hackage.nix", + "type": "github" + } + }, + "hackage-for-stackage_2": { "flake": false, "locked": { "lastModified": 1778287713, @@ -168,7 +441,55 @@ "type": "github" } }, + "hackage-internal_2": { + "flake": false, + "locked": { + "lastModified": 1750307553, + "narHash": "sha256-iiafNoeLHwlSLQTyvy8nPe2t6g5AV4PPcpMeH/2/DLs=", + "owner": "input-output-hk", + "repo": "hackage.nix", + "rev": "f7867baa8817fab296528f4a4ec39d1c7c4da4f3", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "hackage.nix", + "type": "github" + } + }, "hackageNix": { + "flake": false, + "locked": { + "lastModified": 1759154585, + "narHash": "sha256-OC5Y3E20bwkfMVlB2uhf7eF/FcuC1JD/BXkxR8rMjR4=", + "owner": "input-output-hk", + "repo": "hackage.nix", + "rev": "7e61ac3eb4cc042b37c6511b65984f59fe6d40de", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "hackage.nix", + "type": "github" + } + }, + "hackageNix_2": { + "flake": false, + "locked": { + "lastModified": 1774557316, + "narHash": "sha256-AErDLAypo/a4gNSKjExpNUM7xdlsTdTf68cWnbr1bOA=", + "owner": "input-output-hk", + "repo": "hackage.nix", + "rev": "06fa3e96f4d7ced3496ec984c8016aad5282db67", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "hackage.nix", + "type": "github" + } + }, + "hackageNix_3": { "flake": false, "locked": { "lastModified": 1779824738, @@ -187,11 +508,13 @@ "haskellNix": { "inputs": { "HTTP": "HTTP", + "cabal-32": "cabal-32", "cabal-34": "cabal-34", "cabal-36": "cabal-36", "cardano-shell": "cardano-shell", - "flake-compat": "flake-compat", + "flake-compat": "flake-compat_2", "hackage": [ + "cardano-node", "hackageNix" ], "hackage-for-stackage": "hackage-for-stackage", @@ -201,7 +524,6 @@ "hls-2.0": "hls-2.0", "hls-2.10": "hls-2.10", "hls-2.11": "hls-2.11", - "hls-2.12": "hls-2.12", "hls-2.2": "hls-2.2", "hls-2.3": "hls-2.3", "hls-2.4": "hls-2.4", @@ -213,6 +535,7 @@ "hpc-coveralls": "hpc-coveralls", "iserv-proxy": "iserv-proxy", "nixpkgs": [ + "cardano-node", "nixpkgs" ], "nixpkgs-2305": "nixpkgs-2305", @@ -220,11 +543,65 @@ "nixpkgs-2405": "nixpkgs-2405", "nixpkgs-2411": "nixpkgs-2411", "nixpkgs-2505": "nixpkgs-2505", - "nixpkgs-2511": "nixpkgs-2511", "nixpkgs-unstable": "nixpkgs-unstable", "old-ghc-nix": "old-ghc-nix", "stackage": "stackage" }, + "locked": { + "lastModified": 1762315551, + "narHash": "sha256-7uaB/UpiFn/+gf7s5NMpSTTUv5Ws30DjsmmqZry+1cY=", + "owner": "input-output-hk", + "repo": "haskell.nix", + "rev": "ef52c36b9835c77a255befe2a20075ba71e3bfab", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "haskell.nix", + "type": "github" + } + }, + "haskellNix_2": { + "inputs": { + "HTTP": "HTTP_2", + "cabal-34": "cabal-34_2", + "cabal-36": "cabal-36_2", + "cardano-shell": "cardano-shell_2", + "flake-compat": "flake-compat_3", + "hackage": [ + "hackageNix" + ], + "hackage-for-stackage": "hackage-for-stackage_2", + "hackage-internal": "hackage-internal_2", + "hls": "hls_2", + "hls-1.10": "hls-1.10_2", + "hls-2.0": "hls-2.0_2", + "hls-2.10": "hls-2.10_2", + "hls-2.11": "hls-2.11_2", + "hls-2.12": "hls-2.12", + "hls-2.2": "hls-2.2_2", + "hls-2.3": "hls-2.3_2", + "hls-2.4": "hls-2.4_2", + "hls-2.5": "hls-2.5_2", + "hls-2.6": "hls-2.6_2", + "hls-2.7": "hls-2.7_2", + "hls-2.8": "hls-2.8_2", + "hls-2.9": "hls-2.9_2", + "hpc-coveralls": "hpc-coveralls_2", + "iserv-proxy": "iserv-proxy_2", + "nixpkgs": [ + "nixpkgs" + ], + "nixpkgs-2305": "nixpkgs-2305_2", + "nixpkgs-2311": "nixpkgs-2311_2", + "nixpkgs-2405": "nixpkgs-2405_2", + "nixpkgs-2411": "nixpkgs-2411_2", + "nixpkgs-2505": "nixpkgs-2505_2", + "nixpkgs-2511": "nixpkgs-2511", + "nixpkgs-unstable": "nixpkgs-unstable_2", + "old-ghc-nix": "old-ghc-nix_2", + "stackage": "stackage_2" + }, "locked": { "lastModified": 1778289300, "narHash": "sha256-3r+uzTzU/BcepGz7fn8ZQ+n/BMUsMXroh1LesvVNjdg=", @@ -272,6 +649,23 @@ "type": "github" } }, + "hls-1.10_2": { + "flake": false, + "locked": { + "lastModified": 1680000865, + "narHash": "sha256-rc7iiUAcrHxwRM/s0ErEsSPxOR3u8t7DvFeWlMycWgo=", + "owner": "haskell", + "repo": "haskell-language-server", + "rev": "b08691db779f7a35ff322b71e72a12f6e3376fd9", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "1.10.0.0", + "repo": "haskell-language-server", + "type": "github" + } + }, "hls-2.0": { "flake": false, "locked": { @@ -289,6 +683,23 @@ "type": "github" } }, + "hls-2.0_2": { + "flake": false, + "locked": { + "lastModified": 1687698105, + "narHash": "sha256-OHXlgRzs/kuJH8q7Sxh507H+0Rb8b7VOiPAjcY9sM1k=", + "owner": "haskell", + "repo": "haskell-language-server", + "rev": "783905f211ac63edf982dd1889c671653327e441", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "2.0.0.1", + "repo": "haskell-language-server", + "type": "github" + } + }, "hls-2.10": { "flake": false, "locked": { @@ -306,6 +717,23 @@ "type": "github" } }, + "hls-2.10_2": { + "flake": false, + "locked": { + "lastModified": 1743069404, + "narHash": "sha256-q4kDFyJDDeoGqfEtrZRx4iqMVEC2MOzCToWsFY+TOzY=", + "owner": "haskell", + "repo": "haskell-language-server", + "rev": "2318c61db3a01e03700bd4b05665662929b7fe8b", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "2.10.0.0", + "repo": "haskell-language-server", + "type": "github" + } + }, "hls-2.11": { "flake": false, "locked": { @@ -323,6 +751,23 @@ "type": "github" } }, + "hls-2.11_2": { + "flake": false, + "locked": { + "lastModified": 1747306193, + "narHash": "sha256-/MmtpF8+FyQlwfKHqHK05BdsxC9LHV70d/FiMM7pzBM=", + "owner": "haskell", + "repo": "haskell-language-server", + "rev": "46ef4523ea4949f47f6d2752476239f1c6d806fe", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "2.11.0.0", + "repo": "haskell-language-server", + "type": "github" + } + }, "hls-2.12": { "flake": false, "locked": { @@ -357,6 +802,23 @@ "type": "github" } }, + "hls-2.2_2": { + "flake": false, + "locked": { + "lastModified": 1693064058, + "narHash": "sha256-8DGIyz5GjuCFmohY6Fa79hHA/p1iIqubfJUTGQElbNk=", + "owner": "haskell", + "repo": "haskell-language-server", + "rev": "b30f4b6cf5822f3112c35d14a0cba51f3fe23b85", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "2.2.0.0", + "repo": "haskell-language-server", + "type": "github" + } + }, "hls-2.3": { "flake": false, "locked": { @@ -374,6 +836,23 @@ "type": "github" } }, + "hls-2.3_2": { + "flake": false, + "locked": { + "lastModified": 1695910642, + "narHash": "sha256-tR58doOs3DncFehHwCLczJgntyG/zlsSd7DgDgMPOkI=", + "owner": "haskell", + "repo": "haskell-language-server", + "rev": "458ccdb55c9ea22cd5d13ec3051aaefb295321be", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "2.3.0.0", + "repo": "haskell-language-server", + "type": "github" + } + }, "hls-2.4": { "flake": false, "locked": { @@ -391,6 +870,23 @@ "type": "github" } }, + "hls-2.4_2": { + "flake": false, + "locked": { + "lastModified": 1699862708, + "narHash": "sha256-YHXSkdz53zd0fYGIYOgLt6HrA0eaRJi9mXVqDgmvrjk=", + "owner": "haskell", + "repo": "haskell-language-server", + "rev": "54507ef7e85fa8e9d0eb9a669832a3287ffccd57", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "2.4.0.1", + "repo": "haskell-language-server", + "type": "github" + } + }, "hls-2.5": { "flake": false, "locked": { @@ -408,6 +904,23 @@ "type": "github" } }, + "hls-2.5_2": { + "flake": false, + "locked": { + "lastModified": 1701080174, + "narHash": "sha256-fyiR9TaHGJIIR0UmcCb73Xv9TJq3ht2ioxQ2mT7kVdc=", + "owner": "haskell", + "repo": "haskell-language-server", + "rev": "27f8c3d3892e38edaef5bea3870161815c4d014c", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "2.5.0.0", + "repo": "haskell-language-server", + "type": "github" + } + }, "hls-2.6": { "flake": false, "locked": { @@ -415,68 +928,168 @@ "narHash": "sha256-+P87oLdlPyMw8Mgoul7HMWdEvWP/fNlo8jyNtwME8E8=", "owner": "haskell", "repo": "haskell-language-server", - "rev": "6e0b342fa0327e628610f2711f8c3e4eaaa08b1e", + "rev": "6e0b342fa0327e628610f2711f8c3e4eaaa08b1e", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "2.6.0.0", + "repo": "haskell-language-server", + "type": "github" + } + }, + "hls-2.6_2": { + "flake": false, + "locked": { + "lastModified": 1705325287, + "narHash": "sha256-+P87oLdlPyMw8Mgoul7HMWdEvWP/fNlo8jyNtwME8E8=", + "owner": "haskell", + "repo": "haskell-language-server", + "rev": "6e0b342fa0327e628610f2711f8c3e4eaaa08b1e", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "2.6.0.0", + "repo": "haskell-language-server", + "type": "github" + } + }, + "hls-2.7": { + "flake": false, + "locked": { + "lastModified": 1708965829, + "narHash": "sha256-LfJ+TBcBFq/XKoiNI7pc4VoHg4WmuzsFxYJ3Fu+Jf+M=", + "owner": "haskell", + "repo": "haskell-language-server", + "rev": "50322b0a4aefb27adc5ec42f5055aaa8f8e38001", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "2.7.0.0", + "repo": "haskell-language-server", + "type": "github" + } + }, + "hls-2.7_2": { + "flake": false, + "locked": { + "lastModified": 1708965829, + "narHash": "sha256-LfJ+TBcBFq/XKoiNI7pc4VoHg4WmuzsFxYJ3Fu+Jf+M=", + "owner": "haskell", + "repo": "haskell-language-server", + "rev": "50322b0a4aefb27adc5ec42f5055aaa8f8e38001", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "2.7.0.0", + "repo": "haskell-language-server", + "type": "github" + } + }, + "hls-2.8": { + "flake": false, + "locked": { + "lastModified": 1715153580, + "narHash": "sha256-Vi/iUt2pWyUJlo9VrYgTcbRviWE0cFO6rmGi9rmALw0=", + "owner": "haskell", + "repo": "haskell-language-server", + "rev": "dd1be1beb16700de59e0d6801957290bcf956a0a", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "2.8.0.0", + "repo": "haskell-language-server", + "type": "github" + } + }, + "hls-2.8_2": { + "flake": false, + "locked": { + "lastModified": 1715153580, + "narHash": "sha256-Vi/iUt2pWyUJlo9VrYgTcbRviWE0cFO6rmGi9rmALw0=", + "owner": "haskell", + "repo": "haskell-language-server", + "rev": "dd1be1beb16700de59e0d6801957290bcf956a0a", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "2.8.0.0", + "repo": "haskell-language-server", + "type": "github" + } + }, + "hls-2.9": { + "flake": false, + "locked": { + "lastModified": 1719993701, + "narHash": "sha256-wy348++MiMm/xwtI9M3vVpqj2qfGgnDcZIGXw8sF1sA=", + "owner": "haskell", + "repo": "haskell-language-server", + "rev": "90319a7e62ab93ab65a95f8f2bcf537e34dae76a", "type": "github" }, "original": { "owner": "haskell", - "ref": "2.6.0.0", + "ref": "2.9.0.1", "repo": "haskell-language-server", "type": "github" } }, - "hls-2.7": { + "hls-2.9_2": { "flake": false, "locked": { - "lastModified": 1708965829, - "narHash": "sha256-LfJ+TBcBFq/XKoiNI7pc4VoHg4WmuzsFxYJ3Fu+Jf+M=", + "lastModified": 1719993701, + "narHash": "sha256-wy348++MiMm/xwtI9M3vVpqj2qfGgnDcZIGXw8sF1sA=", "owner": "haskell", "repo": "haskell-language-server", - "rev": "50322b0a4aefb27adc5ec42f5055aaa8f8e38001", + "rev": "90319a7e62ab93ab65a95f8f2bcf537e34dae76a", "type": "github" }, "original": { "owner": "haskell", - "ref": "2.7.0.0", + "ref": "2.9.0.1", "repo": "haskell-language-server", "type": "github" } }, - "hls-2.8": { + "hls_2": { "flake": false, "locked": { - "lastModified": 1715153580, - "narHash": "sha256-Vi/iUt2pWyUJlo9VrYgTcbRviWE0cFO6rmGi9rmALw0=", + "lastModified": 1741604408, + "narHash": "sha256-tuq3+Ip70yu89GswZ7DSINBpwRprnWnl6xDYnS4GOsc=", "owner": "haskell", "repo": "haskell-language-server", - "rev": "dd1be1beb16700de59e0d6801957290bcf956a0a", + "rev": "682d6894c94087da5e566771f25311c47e145359", "type": "github" }, "original": { "owner": "haskell", - "ref": "2.8.0.0", "repo": "haskell-language-server", "type": "github" } }, - "hls-2.9": { + "hpc-coveralls": { "flake": false, "locked": { - "lastModified": 1719993701, - "narHash": "sha256-wy348++MiMm/xwtI9M3vVpqj2qfGgnDcZIGXw8sF1sA=", - "owner": "haskell", - "repo": "haskell-language-server", - "rev": "90319a7e62ab93ab65a95f8f2bcf537e34dae76a", + "lastModified": 1607498076, + "narHash": "sha256-8uqsEtivphgZWYeUo5RDUhp6bO9j2vaaProQxHBltQk=", + "owner": "sevanspowell", + "repo": "hpc-coveralls", + "rev": "14df0f7d229f4cd2e79f8eabb1a740097fdfa430", "type": "github" }, "original": { - "owner": "haskell", - "ref": "2.9.0.1", - "repo": "haskell-language-server", + "owner": "sevanspowell", + "repo": "hpc-coveralls", "type": "github" } }, - "hpc-coveralls": { + "hpc-coveralls_2": { "flake": false, "locked": { "lastModified": 1607498076, @@ -492,10 +1105,31 @@ "type": "github" } }, + "incl": { + "inputs": { + "nixlib": "nixlib" + }, + "locked": { + "lastModified": 1693483555, + "narHash": "sha256-Beq4WhSeH3jRTZgC1XopTSU10yLpK1nmMcnGoXO0XYo=", + "owner": "divnix", + "repo": "incl", + "rev": "526751ad3d1e23b07944b14e3f6b7a5948d3007b", + "type": "github" + }, + "original": { + "owner": "divnix", + "repo": "incl", + "type": "github" + } + }, "iohkNix": { "inputs": { "blst": "blst", - "nixpkgs": "nixpkgs", + "nixpkgs": [ + "cardano-node", + "nixpkgs" + ], "secp256k1": "secp256k1", "sodium": "sodium" }, @@ -513,7 +1147,45 @@ "type": "github" } }, + "iohkNix_2": { + "inputs": { + "blst": "blst_2", + "nixpkgs": "nixpkgs", + "secp256k1": "secp256k1_2", + "sodium": "sodium_2" + }, + "locked": { + "lastModified": 1777941182, + "narHash": "sha256-FX3+8GIrB2z4akmcYTStELDKVJWgqy9yFt0mxwpU3Qc=", + "owner": "input-output-hk", + "repo": "iohk-nix", + "rev": "9de00113c11ba8cac908a63acf34b193cda7475b", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "iohk-nix", + "type": "github" + } + }, "iserv-proxy": { + "flake": false, + "locked": { + "lastModified": 1755040634, + "narHash": "sha256-8W7uHpAIG8HhO3ig5OGHqvwduoye6q6dlrea1IrP2eI=", + "owner": "stable-haskell", + "repo": "iserv-proxy", + "rev": "1383d199a2c64f522979005d112b4fbdee38dd92", + "type": "github" + }, + "original": { + "owner": "stable-haskell", + "ref": "iserv-syms", + "repo": "iserv-proxy", + "type": "github" + } + }, + "iserv-proxy_2": { "flake": false, "locked": { "lastModified": 1775620557, @@ -530,6 +1202,21 @@ "type": "github" } }, + "nixlib": { + "locked": { + "lastModified": 1667696192, + "narHash": "sha256-hOdbIhnpWvtmVynKcsj10nxz9WROjZja+1wRAJ/C9+s=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "babd9cd2ca6e413372ed59fbb1ecc3c3a5fd3e5b", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, "nixpkgs": { "locked": { "lastModified": 1751071626, @@ -562,6 +1249,22 @@ "type": "github" } }, + "nixpkgs-2305_2": { + "locked": { + "lastModified": 1705033721, + "narHash": "sha256-K5eJHmL1/kev6WuqyqqbS1cdNnSidIZ3jeqJ7GbrYnQ=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a1982c92d8980a0114372973cbdfe0a307f1bdea", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-23.05-darwin", + "repo": "nixpkgs", + "type": "github" + } + }, "nixpkgs-2311": { "locked": { "lastModified": 1719957072, @@ -578,6 +1281,22 @@ "type": "github" } }, + "nixpkgs-2311_2": { + "locked": { + "lastModified": 1719957072, + "narHash": "sha256-gvFhEf5nszouwLAkT9nWsDzocUTqLWHuL++dvNjMp9I=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "7144d6241f02d171d25fba3edeaf15e0f2592105", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-23.11-darwin", + "repo": "nixpkgs", + "type": "github" + } + }, "nixpkgs-2405": { "locked": { "lastModified": 1735564410, @@ -594,7 +1313,39 @@ "type": "github" } }, + "nixpkgs-2405_2": { + "locked": { + "lastModified": 1735564410, + "narHash": "sha256-HB/FA0+1gpSs8+/boEavrGJH+Eq08/R2wWNph1sM1Dg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "1e7a8f391f1a490460760065fa0630b5520f9cf8", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-24.05-darwin", + "repo": "nixpkgs", + "type": "github" + } + }, "nixpkgs-2411": { + "locked": { + "lastModified": 1748037224, + "narHash": "sha256-92vihpZr6dwEMV6g98M5kHZIttrWahb9iRPBm1atcPk=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "f09dede81861f3a83f7f06641ead34f02f37597f", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-24.11-darwin", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-2411_2": { "locked": { "lastModified": 1751290243, "narHash": "sha256-kNf+obkpJZWar7HZymXZbW+Rlk3HTEIMlpc6FCNz0Ds=", @@ -611,6 +1362,22 @@ } }, "nixpkgs-2505": { + "locked": { + "lastModified": 1748852332, + "narHash": "sha256-r/wVJWmLYEqvrJKnL48r90Wn9HWX9SHFt6s4LhuTh7k=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a8167f3cc2f991dd4d0055746df53dae5fd0c953", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-25.05-darwin", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-2505_2": { "locked": { "lastModified": 1764560356, "narHash": "sha256-M5aFEFPppI4UhdOxwdmceJ9bDJC4T6C6CzCK1E2FZyo=", @@ -658,6 +1425,22 @@ } }, "nixpkgs-unstable": { + "locked": { + "lastModified": 1748856973, + "narHash": "sha256-RlTsJUvvr8ErjPBsiwrGbbHYW8XbB/oek0Gi78XdWKg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e4b09e47ace7d87de083786b404bf232eb6c89d8", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-unstable_2": { "locked": { "lastModified": 1775888245, "narHash": "sha256-nwASzrRDD1JBEu/o8ekKYEXm/oJW6EMCzCRdrwcLe90=", @@ -690,13 +1473,31 @@ "type": "github" } }, + "old-ghc-nix_2": { + "flake": false, + "locked": { + "lastModified": 1631092763, + "narHash": "sha256-sIKgO+z7tj4lw3u6oBZxqIhDrzSkvpHtv0Kki+lh9Fg=", + "owner": "angerman", + "repo": "old-ghc-nix", + "rev": "af48a7a7353e418119b6dfe3cd1463a657f342b8", + "type": "github" + }, + "original": { + "owner": "angerman", + "ref": "master", + "repo": "old-ghc-nix", + "type": "github" + } + }, "root": { "inputs": { "CHaP": "CHaP", + "cardano-node": "cardano-node", "flake-parts": "flake-parts", - "hackageNix": "hackageNix", - "haskellNix": "haskellNix", - "iohkNix": "iohkNix", + "hackageNix": "hackageNix_3", + "haskellNix": "haskellNix_2", + "iohkNix": "iohkNix_2", "nixpkgs": [ "haskellNix", "nixpkgs-unstable" @@ -720,6 +1521,23 @@ "type": "github" } }, + "secp256k1_2": { + "flake": false, + "locked": { + "lastModified": 1683999695, + "narHash": "sha256-9nJJVENMXjXEJZzw8DHzin1DkFkF8h9m/c6PuM7Uk4s=", + "owner": "bitcoin-core", + "repo": "secp256k1", + "rev": "acf5c55ae6a94e5ca847e07def40427547876101", + "type": "github" + }, + "original": { + "owner": "bitcoin-core", + "ref": "v0.3.2", + "repo": "secp256k1", + "type": "github" + } + }, "sodium": { "flake": false, "locked": { @@ -737,7 +1555,40 @@ "type": "github" } }, + "sodium_2": { + "flake": false, + "locked": { + "lastModified": 1675156279, + "narHash": "sha256-0uRcN5gvMwO7MCXVYnoqG/OmeBFi8qRVnDWJLnBb9+Y=", + "owner": "input-output-hk", + "repo": "libsodium", + "rev": "dbb48cce5429cb6585c9034f002568964f1ce567", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "libsodium", + "rev": "dbb48cce5429cb6585c9034f002568964f1ce567", + "type": "github" + } + }, "stackage": { + "flake": false, + "locked": { + "lastModified": 1755648773, + "narHash": "sha256-NhcOu6GwYal+awBQLoMT4vf7L7Ar1DectDjK2mF653I=", + "owner": "input-output-hk", + "repo": "stackage.nix", + "rev": "1a0ea16d99761b93456460c255a8b723647b2c77", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "stackage.nix", + "type": "github" + } + }, + "stackage_2": { "flake": false, "locked": { "lastModified": 1778113854, @@ -752,6 +1603,39 @@ "repo": "stackage.nix", "type": "github" } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 5dab645d4..3f9d475c2 100644 --- a/flake.nix +++ b/flake.nix @@ -14,6 +14,7 @@ inputs.nixpkgs.follows = "nixpkgs"; inputs.hackage.follows = "hackageNix"; }; + cardano-node.url = "github:IntersectMBO/cardano-node/11.0.1"; iohkNix.url = "github:input-output-hk/iohk-nix"; nixpkgs.follows = "haskellNix/nixpkgs-unstable"; self.submodules = true; @@ -27,7 +28,7 @@ systems = [ "x86_64-linux" # "aarch64-linux" - # "aarch64-darwin" + "aarch64-darwin" ]; perSystem = {system, ...}: { _module.args.pkgs = import inputs.nixpkgs { diff --git a/perSystem/devShells.nix b/perSystem/devShells.nix index 88fcffa93..5f5385dee 100644 --- a/perSystem/devShells.nix +++ b/perSystem/devShells.nix @@ -1,5 +1,5 @@ { inputs, ... }: { - perSystem = { shellFor, pkgs, ... }: { + perSystem = { shellFor, hsPkgs, pkgs, system, ... }: { devShells.default = shellFor { packages = p: [ p.ogmios ]; @@ -29,5 +29,28 @@ withHoogle = true; }; + + devShells.integration = let + cn = inputs.cardano-node.packages.${system}; + in shellFor { + packages = p: [ p.ogmios p.ogmios-integration-tests ]; + + nativeBuildInputs = [ + pkgs.jq + hsPkgs.ogmios.components.exes.ogmios + cn.cardano-node + cn.cardano-cli + cn.cardano-testnet + cn.tx-generator + ]; + + tools = { + cabal = "latest"; + }; + + shellHook = '' + export LANG="en_US.UTF-8" + ''; + }; }; } diff --git a/perSystem/packages.nix b/perSystem/packages.nix index ef27c9bb1..b75cab968 100644 --- a/perSystem/packages.nix +++ b/perSystem/packages.nix @@ -8,6 +8,7 @@ default = ogmios.components.exes.ogmios; ogmios = ogmios.components.exes.ogmios; ogmios-lib = ogmios.components.library; + ogmios-integration-tests = hsPkgs.ogmios-integration-tests.components.exes.ogmios-integration-tests; }; checks.ogmios-unit = ogmios.checks.unit; diff --git a/server/cabal.project b/server/cabal.project index 66168b150..c85ca6fae 100644 --- a/server/cabal.project +++ b/server/cabal.project @@ -46,6 +46,7 @@ packages: modules/hjsonschema modules/hspec-json-schema modules/json-rpc + test/integration tests: False diff --git a/server/test/integration/ogmios-integration-tests.cabal b/server/test/integration/ogmios-integration-tests.cabal new file mode 100644 index 000000000..f162dfc54 --- /dev/null +++ b/server/test/integration/ogmios-integration-tests.cabal @@ -0,0 +1,39 @@ +cabal-version: 3.0 +name: ogmios-integration-tests +version: 0.1.0.0 +license: MPL-2.0 +build-type: Simple + +executable ogmios-integration-tests + main-is: Main.hs + hs-source-dirs: src + default-language: Haskell2010 + ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N + + other-modules: + Test.Integration.Constitution + Test.Integration.DelegateRepresentatives + Test.Integration.Env + Test.Integration.Epoch + Test.Integration.LedgerTip + Test.Integration.LiveStakeDistribution + Test.Integration.NetworkTip + Test.Integration.ProtocolParameters + Test.Integration.RewardAccountSummaries + Test.Integration.StakePools + Test.Integration.Utxo + + build-depends: + , base >= 4.14 && < 5 + , aeson + , bytestring + , containers + , directory + , filepath + , network + , process + , tasty + , tasty-hunit + , temporary + , text + , websockets diff --git a/server/test/integration/src/Main.hs b/server/test/integration/src/Main.hs new file mode 100644 index 000000000..7ee0dc2b6 --- /dev/null +++ b/server/test/integration/src/Main.hs @@ -0,0 +1,30 @@ +module Main (main) where + +import Test.Tasty (defaultMain, testGroup) +import Test.Integration.Constitution (constitutionTests) +import Test.Integration.DelegateRepresentatives (delegateRepresentativesTests) +import Test.Integration.Env (withTestEnv) +import Test.Integration.Epoch (epochTests) +import Test.Integration.LedgerTip (ledgerTipTests) +import Test.Integration.LiveStakeDistribution (liveStakeDistributionTests) +import Test.Integration.NetworkTip (networkTipTests) +import Test.Integration.ProtocolParameters (protocolParametersTests) +import Test.Integration.RewardAccountSummaries (rewardAccountSummariesTests) +import Test.Integration.StakePools (stakePoolsTests) +import Test.Integration.Utxo (utxoTests) + +main :: IO () +main = defaultMain $ + withTestEnv $ \getEnv -> + testGroup "Ogmios Integration Tests" + [ utxoTests getEnv + , protocolParametersTests getEnv + , ledgerTipTests getEnv + , networkTipTests getEnv + , epochTests getEnv + , stakePoolsTests getEnv + , rewardAccountSummariesTests getEnv + , liveStakeDistributionTests getEnv + , constitutionTests getEnv + , delegateRepresentativesTests getEnv + ] diff --git a/server/test/integration/src/Test/Integration/Constitution.hs b/server/test/integration/src/Test/Integration/Constitution.hs new file mode 100644 index 000000000..fe0a16fee --- /dev/null +++ b/server/test/integration/src/Test/Integration/Constitution.hs @@ -0,0 +1,100 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} + +module Test.Integration.Constitution + ( constitutionTests + ) where + +import Data.Aeson + ( Value(..) + , (.:) + , eitherDecode + , encode + , object + , (.=) + ) +import Data.Aeson.Types (parseEither, withObject) +import Data.Text (Text) +import System.FilePath (()) +import System.Process (callProcess) +import Test.Tasty (TestTree, testGroup) +import Test.Tasty.HUnit (assertEqual, assertFailure, testCase) + +import qualified Data.Aeson.KeyMap as KM +import qualified Data.ByteString.Lazy as LBS +import qualified Network.WebSockets as WS + +import Test.Integration.Env (TestEnv(..), queryOgmiosRetry) + +constitutionTests :: IO TestEnv -> TestTree +constitutionTests getEnv = testGroup "Constitution" + [ testCase "constitution matches cardano-cli" $ do + env <- getEnv + + ogmiosResp <- queryOgmiosRetry (envOgmiosPort env) queryOgmios + ogmiosResult <- case ogmiosResp of + Object o + | Just result <- KM.lookup "result" o -> pure result + | Just err <- KM.lookup "error" o -> + assertFailure $ "Ogmios returned error: " <> show err + _ -> assertFailure $ "Unexpected ogmios response: " <> show ogmiosResp + + cliResult <- queryCli (envWorkDir env) (envNodeSocket env) (envTestnetMagic env) + + oHash <- parseOgmiosHash ogmiosResult + cHash <- parseCliHash cliResult + assertEqual "constitution anchor hash" oHash cHash + ] + +-- --------------------------------------------------------------------------- +-- Queries +-- --------------------------------------------------------------------------- + +queryOgmios :: Int -> IO Value +queryOgmios port = + WS.runClient "127.0.0.1" port "/" $ \conn -> do + WS.sendTextData conn $ encode $ object + [ "jsonrpc" .= ("2.0" :: Text) + , "method" .= ("queryLedgerState/constitution" :: Text) + , "id" .= Null + ] + resp <- WS.receiveData conn + case eitherDecode resp of + Left err -> fail $ "Failed to decode ogmios response: " <> err + Right val -> pure val + +queryCli :: FilePath -> FilePath -> Int -> IO Value +queryCli workDir socketPath magic = do + let outFile = workDir "cli-constitution.json" + callProcess "cardano-cli" + [ "conway", "query", "constitution" + , "--testnet-magic", show magic + , "--socket-path", socketPath + , "--out-file", outFile + ] + contents <- LBS.readFile outFile + case eitherDecode contents of + Left err -> fail $ "Failed to decode cardano-cli output: " <> err + Right val -> pure val + +-- --------------------------------------------------------------------------- +-- Parsers +-- --------------------------------------------------------------------------- + +parseOgmiosHash :: Value -> IO Text +parseOgmiosHash val = case parseEither parser val of + Left err -> assertFailure $ "Failed to parse ogmios constitution: " <> err + Right v -> pure v + where + parser = withObject "constitution" $ \o -> do + metadata <- o .: "metadata" + metadata .: "hash" + +parseCliHash :: Value -> IO Text +parseCliHash val = case parseEither parser val of + Left err -> assertFailure $ "Failed to parse cli constitution: " <> err + Right v -> pure v + where + parser = withObject "constitution" $ \o -> do + anchor <- o .: "anchor" + anchor .: "dataHash" diff --git a/server/test/integration/src/Test/Integration/DelegateRepresentatives.hs b/server/test/integration/src/Test/Integration/DelegateRepresentatives.hs new file mode 100644 index 000000000..756d72604 --- /dev/null +++ b/server/test/integration/src/Test/Integration/DelegateRepresentatives.hs @@ -0,0 +1,126 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} + +module Test.Integration.DelegateRepresentatives + ( delegateRepresentativesTests + ) where + +import Control.Monad (when) +import Data.Aeson + ( Value(..) + , eitherDecode + , encode + , object + , (.=) + ) +import Data.Aeson.Types (parseEither, withArray, withObject) +import Data.Foldable (toList) +import Data.Set (Set) +import Data.Text (Text) +import System.FilePath (()) +import System.Process (callProcess) +import Test.Tasty (TestTree, testGroup) +import Test.Tasty.HUnit (assertFailure, testCase) + +import qualified Data.Aeson.KeyMap as KM +import qualified Data.ByteString.Lazy as LBS +import qualified Data.Set as Set +import qualified Network.WebSockets as WS + +import Test.Integration.Env (TestEnv(..), queryOgmiosRetry) + +delegateRepresentativesTests :: IO TestEnv -> TestTree +delegateRepresentativesTests getEnv = testGroup "DelegateRepresentatives" + [ testCase "DRep IDs match cardano-cli" $ do + env <- getEnv + + ogmiosResp <- queryOgmiosRetry (envOgmiosPort env) queryOgmios + ogmiosResult <- case ogmiosResp of + Object o + | Just result <- KM.lookup "result" o -> pure result + | Just err <- KM.lookup "error" o -> + assertFailure $ "Ogmios returned error: " <> show err + _ -> assertFailure $ "Unexpected ogmios response: " <> show ogmiosResp + + cliResult <- queryCli (envWorkDir env) (envNodeSocket env) (envTestnetMagic env) + + ogmiosDreps <- case parseOgmiosDrepIds ogmiosResult of + Left err -> assertFailure $ "Failed to parse ogmios DRep IDs: " <> err + Right s -> pure s + cliDreps <- case parseCliDrepIds cliResult of + Left err -> assertFailure $ "Failed to parse cli DRep IDs: " <> err + Right s -> pure s + + let ogmiosOnly = Set.difference ogmiosDreps cliDreps + cliOnly = Set.difference cliDreps ogmiosDreps + when (not (Set.null ogmiosOnly) || not (Set.null cliOnly)) $ + assertFailure $ unlines + [ "DRep ID sets differ:" + , " In Ogmios only: " <> show (Set.toList ogmiosOnly) + , " In cardano-cli only: " <> show (Set.toList cliOnly) + ] + ] + +-- --------------------------------------------------------------------------- +-- Queries +-- --------------------------------------------------------------------------- + +queryOgmios :: Int -> IO Value +queryOgmios port = + WS.runClient "127.0.0.1" port "/" $ \conn -> do + WS.sendTextData conn $ encode $ object + [ "jsonrpc" .= ("2.0" :: Text) + , "method" .= ("queryLedgerState/delegateRepresentatives" :: Text) + , "id" .= Null + ] + resp <- WS.receiveData conn + case eitherDecode resp of + Left err -> fail $ "Failed to decode ogmios response: " <> err + Right val -> pure val + +queryCli :: FilePath -> FilePath -> Int -> IO Value +queryCli workDir socketPath magic = do + let outFile = workDir "cli-drep-state.json" + callProcess "cardano-cli" + [ "conway", "query", "drep-state" + , "--all-dreps" + , "--testnet-magic", show magic + , "--socket-path", socketPath + , "--out-file", outFile + ] + contents <- LBS.readFile outFile + case eitherDecode contents of + Left err -> fail $ "Failed to decode cardano-cli output: " <> err + Right val -> pure val + +-- --------------------------------------------------------------------------- +-- Parsers +-- --------------------------------------------------------------------------- + +-- Ogmios returns an array of objects with "id" and "type" fields. +-- Special entries "abstain" and "noConfidence" have no "id". +parseOgmiosDrepIds :: Value -> Either String (Set Text) +parseOgmiosDrepIds = parseEither $ withArray "dreps" $ \arr -> do + ids <- mapM extractId (toList arr) + pure $ Set.fromList [i | Just i <- ids] + where + extractId = withObject "drep" $ \o -> + case KM.lookup "id" o of + Just (String drepId) -> pure (Just drepId) + _ -> pure Nothing + +-- cardano-cli drep-state --all-dreps returns an array of [drepId, drepState] pairs +parseCliDrepIds :: Value -> Either String (Set Text) +parseCliDrepIds = parseEither $ withArray "dreps" $ \arr -> do + ids <- mapM extractId (toList arr) + pure (Set.fromList ids) + where + extractId = withArray "pair" $ \pair -> case toList pair of + (drepId:_) -> case drepId of + Object o -> case KM.lookup "keyHash" o of + Just (String h) -> pure h + _ -> case KM.lookup "scriptHash" o of + Just (String h) -> pure h + _ -> fail $ "No keyHash or scriptHash in DRep ID: " <> show o + _ -> fail $ "Expected object for DRep ID, got: " <> show drepId + _ -> fail "Empty DRep pair" diff --git a/server/test/integration/src/Test/Integration/Env.hs b/server/test/integration/src/Test/Integration/Env.hs new file mode 100644 index 000000000..a8906f8a9 --- /dev/null +++ b/server/test/integration/src/Test/Integration/Env.hs @@ -0,0 +1,290 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} + +module Test.Integration.Env + ( TestEnv(..) + , withTestEnv + , queryOgmiosRetry + ) where + +import Control.Concurrent (threadDelay) +import Control.Exception (SomeException, try) +import Control.Monad (filterM) +import Data.Foldable (toList) +import Data.Maybe (catMaybes, listToMaybe) +import Network.Socket + ( AddrInfo(..) + , SocketType(..) + , close + , connect + , defaultHints + , getAddrInfo + , openSocket + ) +import System.Directory + ( createDirectoryIfMissing + , doesDirectoryExist + , doesFileExist + , findExecutable + , listDirectory + ) +import System.Environment (getEnvironment) +import System.FilePath ((), takeFileName) +import System.IO (Handle, IOMode(..), hClose, openFile) +import System.IO.Temp (createTempDirectory, getCanonicalTemporaryDirectory) +import System.Exit (ExitCode(..)) +import System.Process + ( CreateProcess(..) + , ProcessHandle + , StdStream(..) + , createProcess + , proc + , terminateProcess + , waitForProcess + ) +import Test.Tasty (TestTree, withResource) + +data TestEnv = TestEnv + { envWorkDir :: !FilePath + , envTestnetDir :: !FilePath + , envNodeSocket :: !FilePath + , envNodeConfig :: !FilePath + , envOgmiosPort :: !Int + , envTestnetMagic :: !Int + } + +data ManagedState = ManagedState + { msProcesses :: [ProcessHandle] + , msLogHandles :: [Handle] + , msWorkDir :: FilePath + } + +ogmiosPort :: Int +ogmiosPort = 11337 + +testnetMagic :: Int +testnetMagic = 42 + +withTestEnv :: (IO TestEnv -> TestTree) -> TestTree +withTestEnv f = withResource setup' teardown' (f . fmap fst) + where + setup' = setup + teardown' (_, managed) = teardown managed + +setup :: IO (TestEnv, ManagedState) +setup = do + tmpBase <- getCanonicalTemporaryDirectory + workDir <- createTempDirectory tmpBase "ogmios-integration" + let logsDir = workDir "logs" + testnetDir = workDir "testnet" + createDirectoryIfMissing True logsDir + + putStrLn $ "[integration] work directory: " <> workDir + + -- Resolve tool paths for cardano-testnet's env vars + toolEnv <- resolveToolEnv + + -- Start cardano-testnet + testnetStdout <- openFile (logsDir "cardano-testnet.stdout") WriteMode + testnetStderr <- openFile (logsDir "cardano-testnet.stderr") WriteMode + (_, _, _, testnetPh) <- createProcess + (proc "cardano-testnet" + [ "cardano" + , "--testnet-magic", show testnetMagic + , "--output-dir", testnetDir + ]) + { env = Just toolEnv + , std_out = UseHandle testnetStdout + , std_err = UseHandle testnetStderr + } + + putStrLn "[integration] waiting for cardano-testnet node socket..." + socketPath <- waitForFile testnetDir ["sock"] 120 + putStrLn $ "[integration] found node socket: " <> socketPath + + configPath <- findNodeConfig testnetDir + putStrLn $ "[integration] found node config: " <> configPath + + -- Start ogmios + ogmiosStdout <- openFile (logsDir "ogmios.stdout") WriteMode + ogmiosStderr <- openFile (logsDir "ogmios.stderr") WriteMode + (_, _, _, ogmiosPh) <- createProcess + (proc "ogmios" + [ "--node-socket", socketPath + , "--node-config", configPath + , "--port", show ogmiosPort + , "--log-level", "error" + ]) + { std_out = UseHandle ogmiosStdout + , std_err = UseHandle ogmiosStderr + } + + putStrLn "[integration] waiting for ogmios..." + waitForTcpPort ogmiosPort 60 + putStrLn "[integration] ogmios is ready." + + -- Run tx-generator to populate the UTxO set + runTxGenerator workDir logsDir testnetDir + + let testEnv = TestEnv + { envWorkDir = workDir + , envTestnetDir = testnetDir + , envNodeSocket = socketPath + , envNodeConfig = configPath + , envOgmiosPort = ogmiosPort + , envTestnetMagic = testnetMagic + } + managed = ManagedState + { msProcesses = [ogmiosPh, testnetPh] + , msLogHandles = [testnetStdout, testnetStderr, ogmiosStdout, ogmiosStderr] + , msWorkDir = workDir + } + pure (testEnv, managed) + +teardown :: ManagedState -> IO () +teardown ms = do + mapM_ (\ph -> terminateProcess ph >> waitForProcess ph) (msProcesses ms) + mapM_ hClose (msLogHandles ms) + putStrLn $ "[integration] logs available at: " <> msWorkDir ms "logs" + +-- --------------------------------------------------------------------------- +-- Helpers +-- --------------------------------------------------------------------------- + +-- cardano-testnet needs CARDANO_CLI and CARDANO_NODE env vars +-- pointing at the executables (it won't find them via PATH alone). +resolveToolEnv :: IO [(String, String)] +resolveToolEnv = do + cliPath <- requireExe "cardano-cli" + nodePath <- requireExe "cardano-node" + baseEnv <- getEnvironment + pure $ baseEnv + <> [ ("CARDANO_CLI", cliPath) + , ("CARDANO_NODE", nodePath) + ] + where + requireExe name = do + mPath <- findExecutable name + case mPath of + Just p -> pure p + Nothing -> fail $ name <> " not found on PATH" + +waitForFile :: FilePath -> [String] -> Int -> IO FilePath +waitForFile dir names maxSeconds = go maxSeconds + where + go 0 = fail $ + "File(s) " <> show names <> " not found under " <> dir + <> " after " <> show maxSeconds <> "s" + go n = do + result <- tryNames names + case result of + Just path -> pure path + Nothing -> threadDelay 1000000 >> go (n - 1) + + tryNames [] = pure Nothing + tryNames (name:rest) = do + result <- findRecursive dir name + case result of + Just path -> pure (Just path) + Nothing -> tryNames rest + +findRecursive :: FilePath -> String -> IO (Maybe FilePath) +findRecursive dir name = do + exists <- doesDirectoryExist dir + if not exists + then pure Nothing + else do + entries <- listDirectory dir + let fullPaths = map (dir ) entries + files <- filterM doesFileExist fullPaths + case filter (\p -> takeFileName p == name) files of + (f:_) -> pure (Just f) + [] -> do + dirs <- filterM doesDirectoryExist fullPaths + results <- mapM (\d -> findRecursive d name) dirs + pure . listToMaybe . catMaybes $ toList results + +findNodeConfig :: FilePath -> IO FilePath +findNodeConfig dir = tryNames configNames + where + configNames = ["configuration.yaml", "configuration.json", "config.json"] + + tryNames [] = fail $ + "No node configuration found under " <> dir + <> " (tried: " <> show configNames <> ")" + tryNames (name:rest) = do + result <- findRecursive dir name + case result of + Just path -> pure path + Nothing -> tryNames rest + +runTxGenerator :: FilePath -> FilePath -> FilePath -> IO () +runTxGenerator workDir logsDir testnetDir = do + let configFile = workDir "tx-generator-config.json" + sigKeyPath = testnetDir "utxo-keys" "utxo1" "utxo.skey" + writeFile configFile $ unlines + [ "{" + , " \"tx_count\": 30," + , " \"tps\": 10," + , " \"inputs_per_tx\": 2," + , " \"outputs_per_tx\": 2," + , " \"tx_fee\": 212345," + , " \"min_utxo_value\": 1000000," + , " \"add_tx_size\": 39," + , " \"init_cooldown\": 5," + , " \"era\": \"Conway\"," + , " \"keepalive\": 30," + , " \"debugMode\": false," + , " \"plutus\": null," + , " \"sigKey\": " <> show sigKeyPath + , "}" + ] + putStrLn "[integration] running tx-generator..." + txgenStdout <- openFile (logsDir "tx-generator.stdout") WriteMode + txgenStderr <- openFile (logsDir "tx-generator.stderr") WriteMode + (_, _, _, ph) <- createProcess + (proc "tx-generator" + [ "json_highlevel", configFile + , "--testnet-config-dir", testnetDir + ]) + { cwd = Just workDir + , std_out = UseHandle txgenStdout + , std_err = UseHandle txgenStderr + } + exitCode <- waitForProcess ph + hClose txgenStdout + hClose txgenStderr + case exitCode of + ExitSuccess -> putStrLn "[integration] tx-generator finished." + ExitFailure c -> fail $ "tx-generator exited with code " <> show c + +waitForTcpPort :: Int -> Int -> IO () +waitForTcpPort port maxSeconds = go maxSeconds + where + go 0 = fail $ + "Port " <> show port <> " not accepting connections after " + <> show maxSeconds <> "s" + go n = do + result <- try @SomeException $ do + let hints = defaultHints { addrSocketType = Stream } + addr:_ <- getAddrInfo (Just hints) (Just "127.0.0.1") (Just (show port)) + sock <- openSocket addr + connect sock (addrAddress addr) + close sock + case result of + Right () -> pure () + Left _ -> threadDelay 1000000 >> go (n - 1) + +-- | Query ogmios via WebSocket with retries. +-- Ogmios may temporarily lose its node connection (especially after +-- tx-generator load), so we retry on any exception. +queryOgmiosRetry :: Int -> (Int -> IO a) -> IO a +queryOgmiosRetry port action = go (5 :: Int) + where + go 0 = action port + go n = do + result <- try @SomeException (action port) + case result of + Right val -> pure val + Left _ -> threadDelay 2000000 >> go (n - 1) diff --git a/server/test/integration/src/Test/Integration/Epoch.hs b/server/test/integration/src/Test/Integration/Epoch.hs new file mode 100644 index 000000000..8b6a32fcc --- /dev/null +++ b/server/test/integration/src/Test/Integration/Epoch.hs @@ -0,0 +1,79 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} + +module Test.Integration.Epoch + ( epochTests + ) where + +import Data.Aeson + ( Value(..) + , (.:) + , eitherDecode + , encode + , object + , (.=) + ) +import Data.Aeson.Types (parseEither, withObject) +import Data.Text (Text) +import System.FilePath (()) +import System.Process (callProcess) +import Test.Tasty (TestTree, testGroup) +import Test.Tasty.HUnit (assertEqual, assertFailure, testCase) + +import qualified Data.Aeson.KeyMap as KM +import qualified Data.ByteString.Lazy as LBS +import qualified Network.WebSockets as WS + +import Test.Integration.Env (TestEnv(..), queryOgmiosRetry) + +epochTests :: IO TestEnv -> TestTree +epochTests getEnv = testGroup "Epoch" + [ testCase "epoch matches cardano-cli tip epoch" $ do + env <- getEnv + + ogmiosResp <- queryOgmiosRetry (envOgmiosPort env) queryOgmios + ogmiosEpoch <- case ogmiosResp of + Object o + | Just (Number n) <- KM.lookup "result" o -> pure (round n :: Integer) + | Just err <- KM.lookup "error" o -> + assertFailure $ "Ogmios returned error: " <> show err + _ -> assertFailure $ "Unexpected ogmios response: " <> show ogmiosResp + + cliResult <- queryCli (envWorkDir env) (envNodeSocket env) (envTestnetMagic env) + cliEpoch <- case parseEither (withObject "tip" (.: "epoch")) cliResult of + Left err -> assertFailure $ "Missing epoch in cli tip: " <> err + Right v -> pure (v :: Integer) + + assertEqual "epoch" ogmiosEpoch cliEpoch + ] + +-- --------------------------------------------------------------------------- +-- Queries +-- --------------------------------------------------------------------------- + +queryOgmios :: Int -> IO Value +queryOgmios port = + WS.runClient "127.0.0.1" port "/" $ \conn -> do + WS.sendTextData conn $ encode $ object + [ "jsonrpc" .= ("2.0" :: Text) + , "method" .= ("queryLedgerState/epoch" :: Text) + , "id" .= Null + ] + resp <- WS.receiveData conn + case eitherDecode resp of + Left err -> fail $ "Failed to decode ogmios response: " <> err + Right val -> pure val + +queryCli :: FilePath -> FilePath -> Int -> IO Value +queryCli workDir socketPath magic = do + let outFile = workDir "cli-epoch-tip.json" + callProcess "cardano-cli" + [ "conway", "query", "tip" + , "--testnet-magic", show magic + , "--socket-path", socketPath + , "--out-file", outFile + ] + contents <- LBS.readFile outFile + case eitherDecode contents of + Left err -> fail $ "Failed to decode cardano-cli output: " <> err + Right val -> pure val diff --git a/server/test/integration/src/Test/Integration/LedgerTip.hs b/server/test/integration/src/Test/Integration/LedgerTip.hs new file mode 100644 index 000000000..4e9a2368e --- /dev/null +++ b/server/test/integration/src/Test/Integration/LedgerTip.hs @@ -0,0 +1,94 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} + +module Test.Integration.LedgerTip + ( ledgerTipTests + ) where + +import Data.Aeson + ( Value(..) + , (.:) + , eitherDecode + , encode + , object + , (.=) + ) +import Data.Aeson.Key (Key) +import Data.Aeson.Types (parseEither, withObject) +import Data.Text (Text) +import System.FilePath (()) +import System.Process (callProcess) +import Test.Tasty (TestTree, testGroup) +import Test.Tasty.HUnit (assertBool, assertFailure, testCase) + +import qualified Data.Aeson.KeyMap as KM +import qualified Data.ByteString.Lazy as LBS +import qualified Network.WebSockets as WS + +import Test.Integration.Env (TestEnv(..), queryOgmiosRetry) + +ledgerTipTests :: IO TestEnv -> TestTree +ledgerTipTests getEnv = testGroup "LedgerTip" + [ testCase "ledgerState/tip slot is bracketed by cardano-cli tip" $ do + env <- getEnv + + cliBefore <- queryCli (envWorkDir env) (envNodeSocket env) (envTestnetMagic env) "cli-tip-before.json" + cSlotBefore <- parseField cliBefore "slot" + + ogmiosResp <- queryOgmiosRetry (envOgmiosPort env) queryOgmios + ogmiosResult <- case ogmiosResp of + Object o + | Just result <- KM.lookup "result" o -> pure result + | Just err <- KM.lookup "error" o -> + assertFailure $ "Ogmios returned error: " <> show err + _ -> assertFailure $ "Unexpected ogmios response: " <> show ogmiosResp + oSlot <- parseField ogmiosResult "slot" + + cliAfter <- queryCli (envWorkDir env) (envNodeSocket env) (envTestnetMagic env) "cli-tip-after.json" + cSlotAfter <- parseField cliAfter "slot" + + assertBool + ("Expected cli_before <= ogmios <= cli_after, got: " + <> show cSlotBefore <> " <= " <> show oSlot <> " <= " <> show cSlotAfter) + (cSlotBefore <= oSlot && oSlot <= cSlotAfter) + ] + +-- --------------------------------------------------------------------------- +-- Queries +-- --------------------------------------------------------------------------- + +queryOgmios :: Int -> IO Value +queryOgmios port = + WS.runClient "127.0.0.1" port "/" $ \conn -> do + WS.sendTextData conn $ encode $ object + [ "jsonrpc" .= ("2.0" :: Text) + , "method" .= ("queryLedgerState/tip" :: Text) + , "id" .= Null + ] + resp <- WS.receiveData conn + case eitherDecode resp of + Left err -> fail $ "Failed to decode ogmios response: " <> err + Right val -> pure val + +queryCli :: FilePath -> FilePath -> Int -> FilePath -> IO Value +queryCli workDir socketPath magic outName = do + let outFile = workDir outName + callProcess "cardano-cli" + [ "conway", "query", "tip" + , "--testnet-magic", show magic + , "--socket-path", socketPath + , "--out-file", outFile + ] + contents <- LBS.readFile outFile + case eitherDecode contents of + Left err -> fail $ "Failed to decode cardano-cli output: " <> err + Right val -> pure val + +-- --------------------------------------------------------------------------- +-- Helpers +-- --------------------------------------------------------------------------- + +parseField :: Value -> Key -> IO Integer +parseField val key = case parseEither (withObject "obj" (.: key)) val of + Left err -> assertFailure $ "Missing field " <> show key <> ": " <> err + Right v -> pure v diff --git a/server/test/integration/src/Test/Integration/LiveStakeDistribution.hs b/server/test/integration/src/Test/Integration/LiveStakeDistribution.hs new file mode 100644 index 000000000..0b758a738 --- /dev/null +++ b/server/test/integration/src/Test/Integration/LiveStakeDistribution.hs @@ -0,0 +1,107 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} + +module Test.Integration.LiveStakeDistribution + ( liveStakeDistributionTests + ) where + +import Control.Monad (when) +import Data.Aeson + ( Value(..) + , eitherDecode + , encode + , object + , (.=) + ) +import Data.Aeson.Types (parseEither, withObject) +import Data.Set (Set) +import Data.Text (Text) +import System.Process (readProcess) +import Test.Tasty (TestTree, testGroup) +import Test.Tasty.HUnit (assertFailure, testCase) + +import qualified Data.Aeson.Key as Key +import qualified Data.Aeson.KeyMap as KM +import qualified Data.ByteString.Lazy as LBS +import qualified Data.Set as Set +import qualified Network.WebSockets as WS + +import Test.Integration.Env (TestEnv(..), queryOgmiosRetry) + +liveStakeDistributionTests :: IO TestEnv -> TestTree +liveStakeDistributionTests getEnv = testGroup "LiveStakeDistribution" + [ testCase "pool IDs in stake-distribution match" $ do + env <- getEnv + + ogmiosResp <- queryOgmiosRetry (envOgmiosPort env) queryOgmios + ogmiosResult <- case ogmiosResp of + Object o + | Just result <- KM.lookup "result" o -> pure result + | Just err <- KM.lookup "error" o -> + assertFailure $ "Ogmios returned error: " <> show err + _ -> assertFailure $ "Unexpected ogmios response: " <> show ogmiosResp + + cliOutput <- queryCli (envNodeSocket env) (envTestnetMagic env) + + ogmiosPoolIds <- case parseOgmiosPoolIds ogmiosResult of + Left err -> assertFailure $ "Failed to parse ogmios pools: " <> err + Right s -> pure s + cliPoolIds <- case parseCliPoolIds cliOutput of + Left err -> assertFailure $ "Failed to parse cli pools: " <> err + Right s -> pure s + + let ogmiosOnly = Set.difference ogmiosPoolIds cliPoolIds + cliOnly = Set.difference cliPoolIds ogmiosPoolIds + when (not (Set.null ogmiosOnly) || not (Set.null cliOnly)) $ + assertFailure $ unlines + [ "Stake distribution pool ID sets differ:" + , " In Ogmios only: " <> show (Set.toList ogmiosOnly) + , " In cardano-cli only: " <> show (Set.toList cliOnly) + ] + ] + +-- --------------------------------------------------------------------------- +-- Queries +-- --------------------------------------------------------------------------- + +queryOgmios :: Int -> IO Value +queryOgmios port = + WS.runClient "127.0.0.1" port "/" $ \conn -> do + WS.sendTextData conn $ encode $ object + [ "jsonrpc" .= ("2.0" :: Text) + , "method" .= ("queryLedgerState/liveStakeDistribution" :: Text) + , "id" .= Null + ] + resp <- WS.receiveData conn + case eitherDecode resp of + Left err -> fail $ "Failed to decode ogmios response: " <> err + Right val -> pure val + +-- cardano-cli stake-distribution outputs a text table, use --output-json +queryCli :: FilePath -> Int -> IO Value +queryCli socketPath magic = do + output <- readProcess "cardano-cli" + [ "conway", "query", "stake-distribution" + , "--testnet-magic", show magic + , "--socket-path", socketPath + , "--output-json" + ] "" + case eitherDecode (LBS.fromStrict $ encodeUtf8 output) of + Left err -> fail $ "Failed to decode cardano-cli output: " <> err + Right val -> pure val + where + encodeUtf8 = LBS.toStrict . LBS.pack . map (fromIntegral . fromEnum) + +-- --------------------------------------------------------------------------- +-- Parsers +-- --------------------------------------------------------------------------- + +-- Ogmios returns an object keyed by pool ID +parseOgmiosPoolIds :: Value -> Either String (Set Text) +parseOgmiosPoolIds = parseEither $ withObject "distribution" $ \o -> + pure $ Set.fromList $ map Key.toText (KM.keys o) + +-- cardano-cli --output-json returns an object keyed by pool ID +parseCliPoolIds :: Value -> Either String (Set Text) +parseCliPoolIds = parseEither $ withObject "distribution" $ \o -> + pure $ Set.fromList $ map Key.toText (KM.keys o) diff --git a/server/test/integration/src/Test/Integration/NetworkTip.hs b/server/test/integration/src/Test/Integration/NetworkTip.hs new file mode 100644 index 000000000..35c5ef67e --- /dev/null +++ b/server/test/integration/src/Test/Integration/NetworkTip.hs @@ -0,0 +1,94 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} + +module Test.Integration.NetworkTip + ( networkTipTests + ) where + +import Data.Aeson + ( Value(..) + , (.:) + , eitherDecode + , encode + , object + , (.=) + ) +import Data.Aeson.Key (Key) +import Data.Aeson.Types (parseEither, withObject) +import Data.Text (Text) +import System.FilePath (()) +import System.Process (callProcess) +import Test.Tasty (TestTree, testGroup) +import Test.Tasty.HUnit (assertBool, assertFailure, testCase) + +import qualified Data.Aeson.KeyMap as KM +import qualified Data.ByteString.Lazy as LBS +import qualified Network.WebSockets as WS + +import Test.Integration.Env (TestEnv(..), queryOgmiosRetry) + +networkTipTests :: IO TestEnv -> TestTree +networkTipTests getEnv = testGroup "NetworkTip" + [ testCase "network/tip slot is bracketed by cardano-cli tip" $ do + env <- getEnv + + cliBefore <- queryCli (envWorkDir env) (envNodeSocket env) (envTestnetMagic env) "cli-network-tip-before.json" + cSlotBefore <- parseField cliBefore "slot" + + ogmiosResp <- queryOgmiosRetry (envOgmiosPort env) queryOgmios + ogmiosResult <- case ogmiosResp of + Object o + | Just result <- KM.lookup "result" o -> pure result + | Just err <- KM.lookup "error" o -> + assertFailure $ "Ogmios returned error: " <> show err + _ -> assertFailure $ "Unexpected ogmios response: " <> show ogmiosResp + oSlot <- parseField ogmiosResult "slot" + + cliAfter <- queryCli (envWorkDir env) (envNodeSocket env) (envTestnetMagic env) "cli-network-tip-after.json" + cSlotAfter <- parseField cliAfter "slot" + + assertBool + ("Expected cli_before <= ogmios <= cli_after, got: " + <> show cSlotBefore <> " <= " <> show oSlot <> " <= " <> show cSlotAfter) + (cSlotBefore <= oSlot && oSlot <= cSlotAfter) + ] + +-- --------------------------------------------------------------------------- +-- Queries +-- --------------------------------------------------------------------------- + +queryOgmios :: Int -> IO Value +queryOgmios port = + WS.runClient "127.0.0.1" port "/" $ \conn -> do + WS.sendTextData conn $ encode $ object + [ "jsonrpc" .= ("2.0" :: Text) + , "method" .= ("queryNetwork/tip" :: Text) + , "id" .= Null + ] + resp <- WS.receiveData conn + case eitherDecode resp of + Left err -> fail $ "Failed to decode ogmios response: " <> err + Right val -> pure val + +queryCli :: FilePath -> FilePath -> Int -> FilePath -> IO Value +queryCli workDir socketPath magic outName = do + let outFile = workDir outName + callProcess "cardano-cli" + [ "conway", "query", "tip" + , "--testnet-magic", show magic + , "--socket-path", socketPath + , "--out-file", outFile + ] + contents <- LBS.readFile outFile + case eitherDecode contents of + Left err -> fail $ "Failed to decode cardano-cli output: " <> err + Right val -> pure val + +-- --------------------------------------------------------------------------- +-- Helpers +-- --------------------------------------------------------------------------- + +parseField :: Value -> Key -> IO Integer +parseField val key = case parseEither (withObject "obj" (.: key)) val of + Left err -> assertFailure $ "Missing field " <> show key <> ": " <> err + Right v -> pure v diff --git a/server/test/integration/src/Test/Integration/ProtocolParameters.hs b/server/test/integration/src/Test/Integration/ProtocolParameters.hs new file mode 100644 index 000000000..1761eae0b --- /dev/null +++ b/server/test/integration/src/Test/Integration/ProtocolParameters.hs @@ -0,0 +1,114 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} + +module Test.Integration.ProtocolParameters + ( protocolParametersTests + ) where + +import Data.Aeson + ( Value(..) + , (.:) + , eitherDecode + , encode + , object + , (.=) + ) +import Data.Aeson.Key (Key) +import Data.Aeson.Types (parseEither, withObject) +import Data.Text (Text) +import System.FilePath (()) +import System.Process (callProcess) +import Test.Tasty (TestTree, testGroup) +import Test.Tasty.HUnit (assertEqual, assertFailure, testCase) + +import qualified Data.Aeson.KeyMap as KM +import qualified Data.ByteString.Lazy as LBS +import qualified Network.WebSockets as WS + +import Test.Integration.Env (TestEnv(..), queryOgmiosRetry) + +protocolParametersTests :: IO TestEnv -> TestTree +protocolParametersTests getEnv = testGroup "ProtocolParameters" + [ testCase "protocolParameters match cardano-cli" $ do + env <- getEnv + + ogmiosResp <- queryOgmiosRetry (envOgmiosPort env) queryOgmios + ogmiosResult <- case ogmiosResp of + Object o + | Just result <- KM.lookup "result" o -> pure result + | Just err <- KM.lookup "error" o -> + assertFailure $ "Ogmios returned error: " <> show err + _ -> assertFailure $ "Unexpected ogmios response: " <> show ogmiosResp + + cliResult <- queryCli (envWorkDir env) (envNodeSocket env) (envTestnetMagic env) + + oMinFeeCoeff <- field1 ogmiosResult "minFeeCoefficient" + cMinFeeCoeff <- field1 cliResult "txFeePerByte" + assertEqual "minFeeCoefficient" oMinFeeCoeff cMinFeeCoeff + + oMinFeeConst <- field3 ogmiosResult "minFeeConstant" "ada" "lovelace" + cMinFeeConst <- field1 cliResult "txFeeFixed" + assertEqual "minFeeConstant" oMinFeeConst cMinFeeConst + + oMaxBlockBody <- field2 ogmiosResult "maxBlockBodySize" "bytes" + cMaxBlockBody <- field1 cliResult "maxBlockBodySize" + assertEqual "maxBlockBodySize" oMaxBlockBody cMaxBlockBody + + oMaxTxSize <- field2 ogmiosResult "maxTransactionSize" "bytes" + cMaxTxSize <- field1 cliResult "maxTxSize" + assertEqual "maxTxSize" oMaxTxSize cMaxTxSize + + oStakeDeposit <- field3 ogmiosResult "stakeCredentialDeposit" "ada" "lovelace" + cStakeDeposit <- field1 cliResult "stakeAddressDeposit" + assertEqual "stakeCredentialDeposit" oStakeDeposit cStakeDeposit + + oPoolDeposit <- field3 ogmiosResult "stakePoolDeposit" "ada" "lovelace" + cPoolDeposit <- field1 cliResult "stakePoolDeposit" + assertEqual "stakePoolDeposit" oPoolDeposit cPoolDeposit + ] + +-- --------------------------------------------------------------------------- +-- Queries +-- --------------------------------------------------------------------------- + +queryOgmios :: Int -> IO Value +queryOgmios port = + WS.runClient "127.0.0.1" port "/" $ \conn -> do + WS.sendTextData conn $ encode $ object + [ "jsonrpc" .= ("2.0" :: Text) + , "method" .= ("queryLedgerState/protocolParameters" :: Text) + , "id" .= Null + ] + resp <- WS.receiveData conn + case eitherDecode resp of + Left err -> fail $ "Failed to decode ogmios response: " <> err + Right val -> pure val + +queryCli :: FilePath -> FilePath -> Int -> IO Value +queryCli workDir socketPath magic = do + let outFile = workDir "cli-protocol-parameters.json" + callProcess "cardano-cli" + [ "conway", "query", "protocol-parameters" + , "--testnet-magic", show magic + , "--socket-path", socketPath + , "--out-file", outFile + ] + contents <- LBS.readFile outFile + case eitherDecode contents of + Left err -> fail $ "Failed to decode cardano-cli output: " <> err + Right val -> pure val + +-- --------------------------------------------------------------------------- +-- Helpers +-- --------------------------------------------------------------------------- + +field1 :: Value -> Key -> IO Value +field1 val k = case parseEither (withObject "obj" (.: k)) val of + Left err -> assertFailure $ "Missing field " <> show k <> ": " <> err + Right v -> pure v + +field2 :: Value -> Key -> Key -> IO Value +field2 val k1 k2 = field1 val k1 >>= \v -> field1 v k2 + +field3 :: Value -> Key -> Key -> Key -> IO Value +field3 val k1 k2 k3 = field2 val k1 k2 >>= \v -> field1 v k3 diff --git a/server/test/integration/src/Test/Integration/RewardAccountSummaries.hs b/server/test/integration/src/Test/Integration/RewardAccountSummaries.hs new file mode 100644 index 000000000..7eb251ef3 --- /dev/null +++ b/server/test/integration/src/Test/Integration/RewardAccountSummaries.hs @@ -0,0 +1,126 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} + +module Test.Integration.RewardAccountSummaries + ( rewardAccountSummariesTests + ) where + +import Data.Aeson + ( Value(..) + , (.:) + , eitherDecode + , encode + , object + , (.=) + ) +import Data.Aeson.Types (parseEither, withArray, withObject) +import Data.Foldable (toList) +import Data.Text (Text) +import System.FilePath (()) +import System.Process (readProcess) +import Test.Tasty (TestTree, testGroup) +import Test.Tasty.HUnit (assertEqual, assertFailure, testCase) + +import qualified Data.Aeson.KeyMap as KM +import qualified Data.ByteString.Lazy as LBS +import qualified Network.WebSockets as WS + +import Test.Integration.Env (TestEnv(..), queryOgmiosRetry) + +rewardAccountSummariesTests :: IO TestEnv -> TestTree +rewardAccountSummariesTests getEnv = testGroup "RewardAccountSummaries" + [ testCase "rewardAccountSummaries match cardano-cli stake-address-info" $ do + env <- getEnv + + -- Derive stake address from the first delegator's staking key + let stakingVkey = envTestnetDir env "stake-delegators" + "delegator1" "staking.vkey" + output <- readProcess "cardano-cli" + [ "conway", "stake-address", "build" + , "--stake-verification-key-file", stakingVkey + , "--testnet-magic", show (envTestnetMagic env) + ] "" + stakeAddr <- case lines output of + (addr:_) -> pure addr + [] -> assertFailure "cardano-cli stake-address build returned empty output" + + -- Query ogmios + ogmiosResp <- queryOgmiosRetry (envOgmiosPort env) (\p -> queryOgmios p stakeAddr) + ogmiosResult <- case ogmiosResp of + Object o + | Just result <- KM.lookup "result" o -> pure result + | Just err <- KM.lookup "error" o -> + assertFailure $ "Ogmios returned error: " <> show err + _ -> assertFailure $ "Unexpected ogmios response: " <> show ogmiosResp + + -- Query cardano-cli + cliResult <- queryCli (envWorkDir env) (envNodeSocket env) (envTestnetMagic env) stakeAddr + + ogmiosRewards <- case parseOgmiosRewards ogmiosResult of + Left err -> assertFailure $ "Failed to parse ogmios rewards: " <> err + Right v -> pure v + + -- Parse rewards from cardano-cli: array of objects + cliRewards <- case parseCliRewards cliResult of + Left err -> assertFailure $ "Failed to parse cli rewards: " <> err + Right v -> pure v + + assertEqual "reward balance (lovelace)" ogmiosRewards cliRewards + ] + +-- --------------------------------------------------------------------------- +-- Queries +-- --------------------------------------------------------------------------- + +queryOgmios :: Int -> String -> IO Value +queryOgmios port stakeAddr = + WS.runClient "127.0.0.1" port "/" $ \conn -> do + WS.sendTextData conn $ encode $ object + [ "jsonrpc" .= ("2.0" :: Text) + , "method" .= ("queryLedgerState/rewardAccountSummaries" :: Text) + , "params" .= object [ "keys" .= [ stakeAddr ] ] + , "id" .= Null + ] + resp <- WS.receiveData conn + case eitherDecode resp of + Left err -> fail $ "Failed to decode ogmios response: " <> err + Right val -> pure val + +queryCli :: FilePath -> FilePath -> Int -> String -> IO Value +queryCli workDir socketPath magic stakeAddr = do + let outFile = workDir "cli-stake-address-info.json" + output <- readProcess "cardano-cli" + [ "conway", "query", "stake-address-info" + , "--address", stakeAddr + , "--testnet-magic", show magic + , "--socket-path", socketPath + , "--out-file", outFile + ] "" + contents <- LBS.readFile outFile + case eitherDecode contents of + Left err -> fail $ "Failed to decode cardano-cli output: " <> err <> "\nraw: " <> output + Right val -> pure val + +-- --------------------------------------------------------------------------- +-- Parsers +-- --------------------------------------------------------------------------- + +-- Ogmios returns an array of objects: [{ "credential": "...", "rewards": { "ada": { "lovelace": N } }, ... }] +parseOgmiosRewards :: Value -> Either String Integer +parseOgmiosRewards val = parseEither parser val + where + parser = withArray "summaries" $ \arr -> case toList arr of + [] -> fail "No reward account summaries returned" + (x:_) -> flip (withObject "entry") x $ \o -> do + rewards <- o .: "rewards" + ada <- rewards .: "ada" + ada .: "lovelace" + +-- cardano-cli returns: [ { "address": "...", "rewardAccountBalance": N, ... } ] +parseCliRewards :: Value -> Either String Integer +parseCliRewards val = parseEither parser val + where + parser = withArray "info" $ \arr -> case toList arr of + [] -> fail "No stake address info returned" + (x:_) -> flip (withObject "entry") x $ \o -> + o .: "rewardAccountBalance" diff --git a/server/test/integration/src/Test/Integration/StakePools.hs b/server/test/integration/src/Test/Integration/StakePools.hs new file mode 100644 index 000000000..c15bfbb5c --- /dev/null +++ b/server/test/integration/src/Test/Integration/StakePools.hs @@ -0,0 +1,114 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} + +module Test.Integration.StakePools + ( stakePoolsTests + ) where + +import Control.Monad (when) +import Data.Aeson + ( Value(..) + , eitherDecode + , encode + , object + , (.=) + ) +import Data.Aeson.Types (parseEither, withArray, withObject) +import Data.Foldable (toList) +import Data.Set (Set) +import Data.Text (Text) +import System.FilePath (()) +import System.Process (callProcess) +import Test.Tasty (TestTree, testGroup) +import Test.Tasty.HUnit (assertFailure, testCase) + +import qualified Data.Aeson.Key as Key +import qualified Data.Aeson.KeyMap as KM +import qualified Data.ByteString.Lazy as LBS +import qualified Data.Set as Set +import qualified Network.WebSockets as WS + +import Test.Integration.Env (TestEnv(..), queryOgmiosRetry) + +stakePoolsTests :: IO TestEnv -> TestTree +stakePoolsTests getEnv = testGroup "StakePools" + [ testCase "stakePool IDs match cardano-cli" $ do + env <- getEnv + + ogmiosResp <- queryOgmiosRetry (envOgmiosPort env) queryOgmios + ogmiosResult <- case ogmiosResp of + Object o + | Just result <- KM.lookup "result" o -> pure result + | Just err <- KM.lookup "error" o -> + assertFailure $ "Ogmios returned error: " <> show err + _ -> assertFailure $ "Unexpected ogmios response: " <> show ogmiosResp + + cliResult <- queryCli (envWorkDir env) (envNodeSocket env) (envTestnetMagic env) + + let ogmiosPoolIds = parseOgmiosPoolIds ogmiosResult + cliPoolIds = parseCliPoolIds cliResult + + ogmiosSet <- case ogmiosPoolIds of + Left err -> assertFailure $ "Failed to parse ogmios pool IDs: " <> err + Right s -> pure s + cliSet <- case cliPoolIds of + Left err -> assertFailure $ "Failed to parse cli pool IDs: " <> err + Right s -> pure s + + let ogmiosOnly = Set.difference ogmiosSet cliSet + cliOnly = Set.difference cliSet ogmiosSet + when (not (Set.null ogmiosOnly) || not (Set.null cliOnly)) $ + assertFailure $ unlines + [ "Stake pool ID sets differ:" + , " In Ogmios only: " <> show (Set.toList ogmiosOnly) + , " In cardano-cli only: " <> show (Set.toList cliOnly) + ] + ] + +-- --------------------------------------------------------------------------- +-- Queries +-- --------------------------------------------------------------------------- + +queryOgmios :: Int -> IO Value +queryOgmios port = + WS.runClient "127.0.0.1" port "/" $ \conn -> do + WS.sendTextData conn $ encode $ object + [ "jsonrpc" .= ("2.0" :: Text) + , "method" .= ("queryLedgerState/stakePools" :: Text) + , "id" .= Null + ] + resp <- WS.receiveData conn + case eitherDecode resp of + Left err -> fail $ "Failed to decode ogmios response: " <> err + Right val -> pure val + +queryCli :: FilePath -> FilePath -> Int -> IO Value +queryCli workDir socketPath magic = do + let outFile = workDir "cli-stake-pools.json" + callProcess "cardano-cli" + [ "conway", "query", "stake-pools" + , "--testnet-magic", show magic + , "--socket-path", socketPath + , "--out-file", outFile + ] + contents <- LBS.readFile outFile + case eitherDecode contents of + Left err -> fail $ "Failed to decode cardano-cli output: " <> err + Right val -> pure val + +-- --------------------------------------------------------------------------- +-- Parsers +-- --------------------------------------------------------------------------- + +-- Ogmios returns an object keyed by pool ID (bech32) +parseOgmiosPoolIds :: Value -> Either String (Set Text) +parseOgmiosPoolIds = parseEither $ withObject "pools" $ \o -> + pure $ Set.fromList $ map Key.toText (KM.keys o) + +-- cardano-cli stake-pools returns an array of bech32 pool IDs +parseCliPoolIds :: Value -> Either String (Set Text) +parseCliPoolIds = parseEither $ withArray "pools" $ \arr -> + fmap Set.fromList $ mapM parseString (toList arr) + where + parseString (String s) = pure s + parseString v = fail $ "Expected string, got: " <> show v diff --git a/server/test/integration/src/Test/Integration/Utxo.hs b/server/test/integration/src/Test/Integration/Utxo.hs new file mode 100644 index 000000000..7841e8bc6 --- /dev/null +++ b/server/test/integration/src/Test/Integration/Utxo.hs @@ -0,0 +1,173 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} + +module Test.Integration.Utxo + ( utxoTests + ) where + +import Control.Monad (forM, when) +import Data.Aeson + ( Value(..) + , (.:) + , eitherDecode + , encode + , object + , (.=) + ) +import Data.Aeson.Types (Parser, parseEither, withArray, withObject) +import Data.Foldable (toList) +import Data.Set (Set) +import Data.Text (Text) +import System.FilePath (()) +import System.Process (callProcess) +import Test.Tasty (TestTree, testGroup) +import Test.Tasty.HUnit (assertFailure, testCase) +import Text.Read (readMaybe) + +import qualified Data.Aeson.Key as Key +import qualified Data.Aeson.KeyMap as KM +import qualified Data.ByteString.Lazy as LBS +import qualified Data.Set as Set +import qualified Data.Text as T +import qualified Network.WebSockets as WS + +import Test.Integration.Env (TestEnv(..), queryOgmiosRetry) + +-- --------------------------------------------------------------------------- +-- Normalized UTxO representation +-- --------------------------------------------------------------------------- + +data NormalizedUtxo = NormalizedUtxo + { nuTxId :: !Text + , nuTxIndex :: !Int + , nuAddress :: !Text + , nuLovelace :: !Integer + } deriving (Eq, Ord, Show) + +-- --------------------------------------------------------------------------- +-- Tests +-- --------------------------------------------------------------------------- + +utxoTests :: IO TestEnv -> TestTree +utxoTests getEnv = testGroup "UTxO" + [ testCase "WholeUtxo matches cardano-cli" $ do + env <- getEnv + + -- Query ogmios via WebSocket + ogmiosResp <- queryOgmiosRetry (envOgmiosPort env) queryOgmiosWholeUtxo + ogmiosResult <- case ogmiosResp of + Object o + | Just result <- KM.lookup "result" o -> pure result + | Just err <- KM.lookup "error" o -> + assertFailure $ "Ogmios returned error: " <> show err + _ -> assertFailure $ "Unexpected ogmios response shape: " <> show ogmiosResp + + ogmiosUtxos <- case parseOgmiosUtxo ogmiosResult of + Left err -> assertFailure $ "Failed to parse ogmios UTxO: " <> err + Right utxos -> pure utxos + + -- Query cardano-cli + cliOutput <- queryCardanoCliWholeUtxo + (envWorkDir env) (envNodeSocket env) (envTestnetMagic env) + cliUtxos <- case parseCardanoCliUtxo cliOutput of + Left err -> assertFailure $ "Failed to parse cardano-cli UTxO: " <> err + Right utxos -> pure utxos + + -- Compare + let ogmiosOnly = Set.difference ogmiosUtxos cliUtxos + cliOnly = Set.difference cliUtxos ogmiosUtxos + when (not (Set.null ogmiosOnly) || not (Set.null cliOnly)) $ + assertFailure $ unlines + [ "UTxO sets differ:" + , " In Ogmios only (" <> show (Set.size ogmiosOnly) <> "):" + , concatMap (\e -> " " <> show e <> "\n") (Set.toList ogmiosOnly) + , " In cardano-cli only (" <> show (Set.size cliOnly) <> "):" + , concatMap (\e -> " " <> show e <> "\n") (Set.toList cliOnly) + ] + ] + +-- --------------------------------------------------------------------------- +-- Ogmios query +-- --------------------------------------------------------------------------- + +queryOgmiosWholeUtxo :: Int -> IO Value +queryOgmiosWholeUtxo port = + WS.runClient "127.0.0.1" port "/" $ \conn -> do + let req = encode $ object + [ "jsonrpc" .= ("2.0" :: Text) + , "method" .= ("queryLedgerState/utxo" :: Text) + , "params" .= object [] + , "id" .= Null + ] + WS.sendTextData conn req + resp <- WS.receiveData conn + case eitherDecode resp of + Left err -> fail $ "Failed to decode ogmios response: " <> err + Right val -> pure val + +-- --------------------------------------------------------------------------- +-- cardano-cli query +-- --------------------------------------------------------------------------- + +queryCardanoCliWholeUtxo :: FilePath -> FilePath -> Int -> IO Value +queryCardanoCliWholeUtxo workDir socketPath magic = do + let outFile = workDir "cli-utxo.json" + callProcess "cardano-cli" + [ "conway", "query", "utxo" + , "--whole-utxo" + , "--testnet-magic", show magic + , "--socket-path", socketPath + , "--out-file", outFile + ] + contents <- LBS.readFile outFile + case eitherDecode contents of + Left err -> fail $ "Failed to decode cardano-cli output: " <> err + Right val -> pure val + +-- --------------------------------------------------------------------------- +-- Parsers +-- --------------------------------------------------------------------------- + +parseOgmiosUtxo :: Value -> Either String (Set NormalizedUtxo) +parseOgmiosUtxo = parseEither $ withArray "utxo" $ \arr -> do + entries <- mapM parseEntry (toList arr) + pure (Set.fromList entries) + where + parseEntry = withObject "utxo entry" $ \o -> do + tx <- o .: "transaction" + txId <- tx .: "id" + idx <- o .: "index" + addr <- o .: "address" + val <- o .: "value" + ada <- val .: "ada" + lv <- ada .: "lovelace" + pure NormalizedUtxo + { nuTxId = txId + , nuTxIndex = idx + , nuAddress = addr + , nuLovelace = lv + } + +parseCardanoCliUtxo :: Value -> Either String (Set NormalizedUtxo) +parseCardanoCliUtxo = parseEither $ withObject "utxo set" $ \o -> do + entries <- forM (KM.toList o) $ \(key, val) -> do + let keyText = Key.toText key + (txId, rest) = T.breakOn "#" keyText + idxText = T.drop 1 rest + idx <- case readMaybe (T.unpack idxText) of + Just n -> pure n + Nothing -> fail $ "Invalid UTxO key (expected txid#index): " <> T.unpack keyText + parseOutput txId idx val + pure (Set.fromList entries) + +parseOutput :: Text -> Int -> Value -> Parser NormalizedUtxo +parseOutput txId idx = withObject "utxo output" $ \o -> do + addr <- o .: "address" + val <- o .: "value" + lv <- val .: "lovelace" + pure NormalizedUtxo + { nuTxId = txId + , nuTxIndex = idx + , nuAddress = addr + , nuLovelace = lv + }