Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

module Cardano.Testnet.Test.Rpc.Genesis
Expand All @@ -12,20 +13,29 @@ 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 ((:|)))
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)

Expand All @@ -44,20 +54,43 @@ hprop_rpc_read_genesis = integrationRetryWorkspace 2 "rpc-read-genesis" $ \tempA
runtimeOptions = def{runtimeEnableRpc = RpcEnabled}

TestnetRuntime
{ testnetMagic
{ shelleyGenesisFile
, testnetMagic
, testnetNodes = node0 :| _
} <-
createAndRunTestnet creationOptions runtimeOptions conf

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)
Expand All @@ -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)
Expand Down
Loading