From a6566a7f2fc2e69c2aab12affca1fb632ae79088 Mon Sep 17 00:00:00 2001 From: Mateusz Galazyn Date: Thu, 20 Aug 2026 14:47:05 +0200 Subject: [PATCH] cardano-testnet: strengthen RPC ReadGenesis test coverage This reverts the temporary relaxation from 764a27fbfa and extends the test. cardano-rpc now resolves initial funds from the genesis sgExtraConfig, so the ReadGenesis response carries the wallets that create-testnet-data provisions. The test now asserts the exact initialFunds map from the genesis file (extraConfig.initialFunds.data preferred, legacy top-level initialFunds as fallback), verifies the returned genesis hash equals the Blake2b-256 hash of the raw Shelley genesis file bytes, and covers the FAILED_PRECONDITION error for a genesis file that changed since the node started. --- ...rpc_readgenesis_initialfunds_assertion.yml | 5 ++ .../Cardano/Testnet/Test/Rpc/Genesis.hs | 70 ++++++++++++++++--- 2 files changed, 67 insertions(+), 8 deletions(-) create mode 100644 cardano-testnet/.changes/20260820_124721_mgalazyn_reenable_rpc_readgenesis_initialfunds_assertion.yml diff --git a/cardano-testnet/.changes/20260820_124721_mgalazyn_reenable_rpc_readgenesis_initialfunds_assertion.yml b/cardano-testnet/.changes/20260820_124721_mgalazyn_reenable_rpc_readgenesis_initialfunds_assertion.yml new file mode 100644 index 00000000000..13dca038a8b --- /dev/null +++ b/cardano-testnet/.changes/20260820_124721_mgalazyn_reenable_rpc_readgenesis_initialfunds_assertion.yml @@ -0,0 +1,5 @@ +pr: 6657 +kind: + - test +description: | + Strengthened the UTxO RPC `ReadGenesis` test: re-enabled the `initialFunds` assertion and upgraded it to an exact comparison with the funds embedded in the genesis file, checked the returned genesis hash against the Blake2b-256 hash of the Shelley genesis file, and added coverage for the `FAILED_PRECONDITION` error when the genesis file changes after node startup. diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Rpc/Genesis.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Rpc/Genesis.hs index 0e41f577581..2ba03c76f5b 100644 --- a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Rpc/Genesis.hs +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Rpc/Genesis.hs @@ -2,6 +2,7 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} module Cardano.Testnet.Test.Rpc.Genesis @@ -12,13 +13,21 @@ where import Cardano.Api import qualified Cardano.Api.Experimental as Exp +import qualified Cardano.Crypto.Hash.Blake2b as Crypto +import qualified Cardano.Crypto.Hash.Class as Crypto import qualified Cardano.Rpc.Client as Rpc import qualified Cardano.Rpc.Proto.Api.UtxoRpc.Query as U5c import Cardano.Testnet import Prelude +import Control.Applicative ((<|>)) import Control.Monad (void) +import Control.Monad.Catch (try) +import qualified Data.Aeson as Aeson +import qualified Data.Aeson.Key as Aeson +import qualified Data.Aeson.KeyMap as Aeson +import qualified Data.Aeson.Lens as Aeson import qualified Data.ByteString as BS import Data.Default.Class import Data.List.NonEmpty (NonEmpty ((:|))) @@ -26,6 +35,7 @@ import qualified Data.Map.Strict as Map import qualified Data.Text as Text import Data.Word (Word32) import Lens.Micro +import Network.GRPC.Spec (GrpcError (..), GrpcException (..)) import Testnet.Property.Util (integrationRetryWorkspace) @@ -44,7 +54,8 @@ hprop_rpc_read_genesis = integrationRetryWorkspace 2 "rpc-read-genesis" $ \tempA runtimeOptions = def{runtimeEnableRpc = RpcEnabled} TestnetRuntime - { testnetMagic + { shelleyGenesisFile + , testnetMagic , testnetNodes = node0 :| _ } <- createAndRunTestnet creationOptions runtimeOptions conf @@ -52,12 +63,34 @@ hprop_rpc_read_genesis = integrationRetryWorkspace 2 "rpc-read-genesis" $ \tempA rpcSocket <- H.note . unFile $ nodeRpcSocketPath node0 let rpcServer = Rpc.ServerUnix rpcSocket + originalGenesisBytes <- H.evalIO $ BS.readFile shelleyGenesisFile + + H.note_ "The handler caches the parsed genesis only on a successful read (TimedCache.hs), so this hash-mismatch check must run before any successful ReadGenesis call: a successful call first would warm the cache and hide the corruption for up to five idle minutes" + H.evalIO $ BS.writeFile shelleyGenesisFile (originalGenesisBytes <> " ") + + H.note_ "ReadGenesis fails with FAILED_PRECONDITION when the genesis file's bytes no longer match the hash the node computed at startup" + readGenesisHashMismatchResult <- + H.evalIO . try . Rpc.withConnection def rpcServer $ \conn -> + Rpc.nonStreaming conn (Rpc.rpc @(Rpc.Protobuf U5c.QueryService "readGenesis")) def + case readGenesisHashMismatchResult of + Left GrpcException{grpcError} + | grpcError == GrpcFailedPrecondition -> pure () + | otherwise -> do + H.note_ $ "expected " <> show GrpcFailedPrecondition <> ", got: " <> show grpcError + H.failure + Right (_ :: Rpc.Proto U5c.ReadGenesisResponse) -> do + H.note_ $ "expected " <> show GrpcFailedPrecondition <> ", but the call succeeded" + H.failure + + H.evalIO $ BS.writeFile shelleyGenesisFile originalGenesisBytes + response <- H.evalIO . Rpc.withConnection def rpcServer $ \conn -> Rpc.nonStreaming conn (Rpc.rpc @(Rpc.Protobuf U5c.QueryService "readGenesis")) def - H.note_ "genesis is the 32-byte Shelley genesis hash" - H.assertWith (response ^. U5c.genesis) $ (== 32) . BS.length + H.note_ "genesis is the Blake2b-256 hash of the raw Shelley genesis file bytes, exactly as the node computed it at startup" + response ^. U5c.genesis + H.=== Crypto.hashToBytes (Crypto.hashWith id originalGenesisBytes :: Crypto.Hash Crypto.Blake2b_256 BS.ByteString) H.note_ "caip2 is derived from the testnet's own network magic" response ^. U5c.caip2 H.=== networkMagicToCaip2 (fromIntegral testnetMagic) @@ -71,11 +104,32 @@ hprop_rpc_read_genesis = integrationRetryWorkspace 2 "rpc-read-genesis" $ \tempA H.assertWith (cardanoGenesis ^. U5c.systemStart) $ not . Text.null void $ H.nothingFail (cardanoGenesis ^. U5c.maybe'protocolParams) - -- TODO: re-enable once cardano-rpc resolves initial funds from sgExtraConfig. - -- cardano-cli create-testnet-data funds wallets via sgExtraConfig.secInitialFunds and leaves the legacy sgInitialFunds field empty, so the RPC response's initialFunds map is currently always empty for testnet genesis. - -- Handler fix pending on cardano-api branch mgalazyn/fix/rpc-initial-funds-extraconfig. - -- H.note_ "initialFunds is non-empty: only the uncompacted boot-time genesis carries it, and cardano-testnet funds its wallets there" - -- H.assertWith (cardanoGenesis ^. U5c.initialFunds) $ not . Map.null + H.note_ "initialFunds matches exactly the funds embedded in the genesis file: only the uncompacted boot-time genesis carries them, and cardano-testnet funds its wallets there. extraConfig.initialFunds.data wins when present; the legacy top-level initialFunds field is the fallback, so the test survives the pending cardano-cli revert (PR #1420) that moves the funds back to the top level" + genesisJson <- H.leftFail (Aeson.eitherDecodeStrict' originalGenesisBytes :: Either String Aeson.Value) + -- Mirrors the preference order of ledger's own 'resolveInjectionSource': extraConfig wins. + initialFundsObject <- + H.nothingFail $ + (genesisJson ^? Aeson.key "extraConfig" . Aeson.key "initialFunds" . Aeson.key "data" . Aeson._Object) + <|> (genesisJson ^? Aeson.key "initialFunds" . Aeson._Object) + + expectedInitialFunds <- + Map.fromList + <$> H.nothingFail + ( traverse + (\(addressKey, amount) -> (,) (Aeson.toText addressKey) <$> amount ^? Aeson._Integer) + (Aeson.toList initialFundsObject) + ) + + H.note_ "initialFunds is non-empty (regression guard for #6655: cardano-testnet must always fund its wallets)" + H.assertWith expectedInitialFunds $ not . Map.null + + actualInitialFunds <- + Map.fromList + <$> traverse + (\(addressHex, amount) -> (,) addressHex . toInteger <$> H.nothingFail (amount ^. U5c.maybe'int)) + (Map.toList (cardanoGenesis ^. U5c.initialFunds)) + + actualInitialFunds H.=== expectedInitialFunds H.note_ "Byron: protocolConsts, startTime, bootStakeholders" protocolConsts <- H.nothingFail (cardanoGenesis ^. U5c.maybe'protocolConsts)