Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add metadata validation, hash checking, and URL support to stake-pool registration-certificate, and hash checking and URL support to stake-pool metadata-hash #932

Merged
merged 13 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from 8 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
1 change: 1 addition & 0 deletions cardano-cli/cardano-cli.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ test-suite cardano-cli-test
Test.Cli.Pioneers.Exercise5
Test.Cli.Pioneers.Exercise6
Test.Cli.Pipes
Test.Cli.Shelley.Certificates.StakePool
Test.Cli.Shelley.Run.Hash
Test.Cli.Shelley.Run.Query
Test.Cli.Shelley.Transaction.Build
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ data GovernanceDRepRegistrationCertificateCmdArgs era
:: !( Maybe
( PotentiallyCheckedAnchor
DRepMetadataUrl
(L.Anchor L.StandardCrypto)
)
)
, outFile :: !(File () Out)
Expand All @@ -77,6 +78,7 @@ data GovernanceDRepUpdateCertificateCmdArgs era
:: Maybe
( PotentiallyCheckedAnchor
DRepMetadataUrl
(L.Anchor L.StandardCrypto)
)
, outFile :: !(File () Out)
}
Expand Down
24 changes: 21 additions & 3 deletions cardano-cli/src/Cardano/CLI/EraBased/Commands/StakePool.hs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ module Cardano.CLI.EraBased.Commands.StakePool
, StakePoolIdCmdArgs (..)
, StakePoolMetadataHashCmdArgs (..)
, StakePoolRegistrationCertificateCmdArgs (..)
, StakePoolMetadataSource (..)
, StakePoolMetadataHashGoal (..)
)
where

import Cardano.Api.Ledger (Coin)
import qualified Cardano.Api.Ledger as L
import Cardano.Api.Shelley hiding (QueryInShelleyBasedEra (..))

import Cardano.CLI.Types.Common
Expand Down Expand Up @@ -48,11 +51,25 @@ data StakePoolIdCmdArgs era

data StakePoolMetadataHashCmdArgs era
= StakePoolMetadataHashCmdArgs
{ poolMetadataFile :: !(StakePoolMetadataFile In)
, mOutFile :: !(Maybe (File () Out))
{ poolMetadataSource :: !StakePoolMetadataSource
, hashGoal :: !StakePoolMetadataHashGoal
}
deriving Show

data StakePoolMetadataSource
= StakePoolMetadataFileIn !(StakePoolMetadataFile In)
| StakePoolMetadataURL !L.Url
deriving Show

data StakePoolMetadataHashGoal
palas marked this conversation as resolved.
Show resolved Hide resolved
= -- | The hash is written to stdout
StakePoolMetadataHashToStdout
| -- | The hash to check against
CheckStakePoolMetadataHash !(Hash StakePoolMetadata)
| -- | The output file to which the hash is written
StakePoolMetadataHashToFile !(File () Out)
deriving Show

data StakePoolRegistrationCertificateCmdArgs era
= StakePoolRegistrationCertificateCmdArgs
{ sbe :: !(ShelleyBasedEra era)
Expand All @@ -73,7 +90,8 @@ data StakePoolRegistrationCertificateCmdArgs era
-- ^ Pool owner verification staking key(s).
, relays :: ![StakePoolRelay]
-- ^ Stake pool relays.
, mMetadata :: !(Maybe StakePoolMetadataReference)
, mMetadata
:: !(Maybe (PotentiallyCheckedAnchor StakePoolMetadataReference StakePoolMetadataReference))
-- ^ Stake pool metadata.
, network :: !NetworkId
, outFile :: !(File () Out)
Expand Down
24 changes: 15 additions & 9 deletions cardano-cli/src/Cardano/CLI/EraBased/Options/Common.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2683,12 +2683,11 @@ pPort =
, Opt.help "The stake pool relay's port"
]

pStakePoolMetadataReference :: Parser (Maybe StakePoolMetadataReference)
pStakePoolMetadataReference :: Parser StakePoolMetadataReference
pStakePoolMetadataReference =
optional $
StakePoolMetadataReference
<$> pStakePoolMetadataUrl
<*> pStakePoolMetadataHash
StakePoolMetadataReference
<$> pStakePoolMetadataUrl
<*> pStakePoolMetadataHash

pStakePoolMetadataUrl :: Parser Text
pStakePoolMetadataUrl =
Expand Down Expand Up @@ -2724,7 +2723,11 @@ pStakePoolRegistrationParserRequirements envCli =
<*> pRewardAcctVerificationKeyOrFile
<*> some pPoolOwnerVerificationKeyOrFile
<*> many pPoolRelay
<*> pStakePoolMetadataReference
<*> optional
( pPotentiallyCheckedAnchorData
pMustCheckStakeMetadataHash
pStakePoolMetadataReference
)
<*> pNetworkId envCli

pProtocolParametersUpdate :: Parser ProtocolParametersUpdate
Expand Down Expand Up @@ -3586,9 +3589,9 @@ pMustCheckHash flagSuffix' dataName' hashParamName' urlParamName' =
]

pPotentiallyCheckedAnchorData
:: Parser (MustCheckHash anchorDataType)
-> Parser (L.Anchor L.StandardCrypto)
-> Parser (PotentiallyCheckedAnchor anchorDataType)
:: Parser (MustCheckHash anchorType)
-> Parser anchor
-> Parser (PotentiallyCheckedAnchor anchorType anchor)
pPotentiallyCheckedAnchorData mustCheckHash anchorData =
PotentiallyCheckedAnchor
<$> anchorData
Expand All @@ -3603,6 +3606,9 @@ pMustCheckConstitutionHash = pMustCheckHash "constitution-hash" "constitution" "
pMustCheckMetadataHash :: Parser (MustCheckHash DRepMetadataUrl)
pMustCheckMetadataHash = pMustCheckHash "drep-metadata-hash" "DRep metadata" "--drep-metadata-hash" "--drep-metadata-url"

pMustCheckStakeMetadataHash :: Parser (MustCheckHash StakePoolMetadataReference)
pMustCheckStakeMetadataHash = pMustCheckHash "metadata-hash" "stake pool metadata" "--metadata-hash" "--metadata-url"

pPreviousGovernanceAction :: Parser (Maybe (TxId, Word16))
pPreviousGovernanceAction =
optional $
Expand Down
48 changes: 44 additions & 4 deletions cardano-cli/src/Cardano/CLI/EraBased/Options/StakePool.hs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@ module Cardano.CLI.EraBased.Options.StakePool
where

import Cardano.Api
import qualified Cardano.Api.Ledger as L
import Cardano.Api.Shelley (Hash (StakePoolMetadataHash))

import Cardano.CLI.Environment (EnvCli (..))
import qualified Cardano.CLI.EraBased.Commands.StakePool as Cmd
import Cardano.CLI.EraBased.Options.Common
import Cardano.CLI.Read (readSafeHash)
import qualified Cardano.Crypto.Hash as L

import Options.Applicative hiding (help, str)
import qualified Options.Applicative as Opt
Expand All @@ -42,7 +46,10 @@ pStakePoolCmds era envCli =
, Just $
subParser "metadata-hash" $
Opt.info pStakePoolMetadataHashCmd $
Opt.progDesc "Print the hash of pool metadata."
Opt.progDesc
( "Calculate the hash of a stake pool metadata file,"
<> " optionally checking the obtained hash against an expected value."
)
]

pStakePoolId
Expand All @@ -61,8 +68,37 @@ pStakePoolMetadataHashCmd
pStakePoolMetadataHashCmd =
fmap Cmd.StakePoolMetadataHashCmd $
Cmd.StakePoolMetadataHashCmdArgs
<$> pPoolMetadataFile
<*> pMaybeOutputFile
<$> pPoolMetadataSource
<*> pPoolMetadataHashGoal

pPoolMetadataSource :: Parser Cmd.StakePoolMetadataSource
pPoolMetadataSource =
asum
[ Cmd.StakePoolMetadataFileIn <$> pPoolMetadataFile
, Cmd.StakePoolMetadataURL
<$> pUrl "pool-metadata-url" "URL pointing to the JSON Metadata file to hash."
]

pPoolMetadataHashGoal :: Parser Cmd.StakePoolMetadataHashGoal
pPoolMetadataHashGoal =
asum
[ Cmd.CheckStakePoolMetadataHash <$> pExpectedStakePoolMetadataHash
, Cmd.StakePoolMetadataHashToFile <$> pOutputFile
]
<|> pure Cmd.StakePoolMetadataHashToStdout

pExpectedStakePoolMetadataHash :: Parser (Hash StakePoolMetadata)
palas marked this conversation as resolved.
Show resolved Hide resolved
pExpectedStakePoolMetadataHash =
Opt.option (StakePoolMetadataHash . L.castHash . L.extractHash <$> readSafeHash) $
mconcat
[ Opt.long "expected-hash"
, Opt.metavar "HASH"
, Opt.help $
mconcat
[ "Expected hash for the stake pool metadata for verification purposes. "
, "If provided, the hash of the stake pool metadata will be compared to this value."
]
]

pStakePoolRegistrationCertificateCmd
:: ()
Expand All @@ -84,7 +120,11 @@ pStakePoolRegistrationCertificateCmd era envCli = do
<*> pRewardAcctVerificationKeyOrFile
<*> some pPoolOwnerVerificationKeyOrFile
<*> many pPoolRelay
<*> pStakePoolMetadataReference
<*> optional
( pPotentiallyCheckedAnchorData
pMustCheckStakeMetadataHash
pStakePoolMetadataReference
)
<*> pNetworkId envCli
<*> pOutputFile
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ runGovernanceDRepMetadataHashCmd
-- | Check the hash of the anchor data against the hash in the anchor if
-- checkHash is set to CheckHash.
carryHashChecks
:: PotentiallyCheckedAnchor DRepMetadataUrl
:: PotentiallyCheckedAnchor DRepMetadataUrl (L.Anchor L.StandardCrypto)
-- ^ The information about anchor data and whether to check the hash (see 'PotentiallyCheckedAnchor')
-> ExceptT HashCheckError IO ()
carryHashChecks potentiallyCheckedAnchor =
Expand Down
84 changes: 72 additions & 12 deletions cardano-cli/src/Cardano/CLI/EraBased/Run/StakePool.hs
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,22 @@ module Cardano.CLI.EraBased.Run.StakePool
)
where

import Cardano.Api
import qualified Cardano.Api.Ledger as L
import Cardano.Api.Shelley

import Cardano.CLI.EraBased.Commands.StakePool
import qualified Cardano.CLI.EraBased.Commands.StakePool as Cmd
import Cardano.CLI.Run.Hash (allSchemas, getByteStringFromURL, httpsAndIpfsSchemas)
import Cardano.CLI.Types.Common
import Cardano.CLI.Types.Errors.HashCmdError (FetchURLError (..))
import Cardano.CLI.Types.Errors.StakePoolCmdError
import Cardano.CLI.Types.Key (readVerificationKeyOrFile)

import Control.Monad (when)
import qualified Data.ByteString.Char8 as BS
import Data.Function ((&))
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Data.Text.Encoding (encodeUtf8)

runStakePoolCmds
:: ()
Expand Down Expand Up @@ -102,7 +106,7 @@ runStakePoolRegistrationCertificateCmd
, stakePoolPledge = poolPledge
, stakePoolOwners = stakePoolOwners'
, stakePoolRelays = relays
, stakePoolMetadata = mMetadata
, stakePoolMetadata = pcaAnchor <$> mMetadata
}

let ledgerStakePoolParams = toShelleyPoolParams stakePoolParams
Expand All @@ -111,6 +115,8 @@ runStakePoolRegistrationCertificateCmd
shelleyBasedEraConstraints sbe ledgerStakePoolParams
registrationCert = makeStakePoolRegistrationCertificate req

mapM_ carryHashChecks mMetadata

firstExceptT StakePoolCmdWriteFileError
. newExceptT
$ writeLazyByteStringFile outFile
Expand Down Expand Up @@ -221,19 +227,73 @@ runStakePoolMetadataHashCmd
-> ExceptT StakePoolCmdError IO ()
runStakePoolMetadataHashCmd
Cmd.StakePoolMetadataHashCmdArgs
{ poolMetadataFile
, mOutFile
{ poolMetadataSource
, hashGoal
} = do
metadataBytes <-
lift (readByteStringFile poolMetadataFile)
& onLeft (left . StakePoolCmdReadFileError)
case poolMetadataSource of
StakePoolMetadataFileIn poolMetadataFile ->
firstExceptT StakePoolCmdReadFileError
. newExceptT
$ readByteStringFile poolMetadataFile
StakePoolMetadataURL urlText ->
fetchURLToStakePoolCmdError $ getByteStringFromURL allSchemas urlText

(_metadata, metadataHash) <-
firstExceptT StakePoolCmdMetadataValidationError
. hoistEither
$ validateAndHashStakePoolMetadata metadataBytes
case mOutFile of
Nothing -> liftIO $ BS.putStrLn (serialiseToRawBytesHex metadataHash)
Just (File fpath) ->
handleIOExceptT (StakePoolCmdWriteFileError . FileIOError fpath) $
BS.writeFile fpath (serialiseToRawBytesHex metadataHash)

case hashGoal of
CheckStakePoolMetadataHash expectedHash
| metadataHash /= expectedHash ->
left $ StakePoolCmdHashMismatchError expectedHash metadataHash
| otherwise -> liftIO $ putStrLn "Hashes match!"
StakePoolMetadataHashToFile outFile -> writeOutput (Just outFile) metadataHash
StakePoolMetadataHashToStdout -> writeOutput Nothing metadataHash
where
writeOutput :: Maybe (File () Out) -> Hash StakePoolMetadata -> ExceptT StakePoolCmdError IO ()
writeOutput mOutFile metadataHash =
case mOutFile of
Nothing -> liftIO $ BS.putStrLn (serialiseToRawBytesHex metadataHash)
Just (File fpath) ->
handleIOExceptT (StakePoolCmdWriteFileError . FileIOError fpath) $
BS.writeFile fpath (serialiseToRawBytesHex metadataHash)

fetchURLToStakePoolCmdError
:: ExceptT FetchURLError IO BS.ByteString -> ExceptT StakePoolCmdError IO BS.ByteString
fetchURLToStakePoolCmdError = withExceptT StakePoolCmdFetchURLError

-- | Check the hash of the anchor data against the hash in the anchor if
-- checkHash is set to CheckHash.
carryHashChecks
:: PotentiallyCheckedAnchor StakePoolMetadataReference StakePoolMetadataReference
-- ^ The information about anchor data and whether to check the hash (see 'PotentiallyCheckedAnchor')
-> ExceptT StakePoolCmdError IO ()
carryHashChecks potentiallyCheckedAnchor =
case pcaMustCheck potentiallyCheckedAnchor of
CheckHash -> do
let url = toUrl $ stakePoolMetadataURL anchor
metadataBytes <-
withExceptT
StakePoolCmdFetchURLError
(getByteStringFromURL httpsAndIpfsSchemas url)

let expectedHash = stakePoolMetadataHash anchor

(_metadata, metadataHash) <-
firstExceptT StakePoolCmdMetadataValidationError
. hoistEither
$ validateAndHashStakePoolMetadata metadataBytes

when (metadataHash /= expectedHash) $
left $
StakePoolCmdHashMismatchError expectedHash metadataHash
TrustHash -> pure ()
where
anchor = pcaAnchor potentiallyCheckedAnchor

toUrl :: Text -> L.Url
toUrl t =
let l = BS.length (encodeUtf8 t)
in fromMaybe (error "Internal Error: length of URL was miscalculated") $ L.textToUrl l t
palas marked this conversation as resolved.
Show resolved Hide resolved
5 changes: 2 additions & 3 deletions cardano-cli/src/Cardano/CLI/Types/Common.hs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ module Cardano.CLI.Types.Common
where

import Cardano.Api hiding (Script)
import Cardano.Api.Ledger (Anchor)
import qualified Cardano.Api.Ledger as L

import qualified Cardano.Chain.Slotting as Byron
Expand Down Expand Up @@ -653,9 +652,9 @@ data MustCheckHash a
| TrustHash
deriving (Eq, Show)

data PotentiallyCheckedAnchor anchorType
data PotentiallyCheckedAnchor anchorType anchor
= PotentiallyCheckedAnchor
{ pcaAnchor :: Anchor L.StandardCrypto
{ pcaAnchor :: anchor
-- ^ The anchor data whose hash is to be checked
, pcaMustCheck :: MustCheckHash anchorType
-- ^ Whether to check the hash or not (CheckHash for checking or TrustHash for not checking)
Expand Down
19 changes: 19 additions & 0 deletions cardano-cli/src/Cardano/CLI/Types/Errors/StakePoolCmdError.hs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,21 @@ module Cardano.CLI.Types.Errors.StakePoolCmdError
where

import Cardano.Api
import Cardano.Api.Shelley (Hash (StakePoolMetadataHash))

import Cardano.CLI.Types.Errors.HashCmdError (FetchURLError)

data StakePoolCmdError
= StakePoolCmdReadFileError !(FileError TextEnvelopeError)
| StakePoolCmdReadKeyFileError !(FileError InputDecodeError)
| StakePoolCmdWriteFileError !(FileError ())
| StakePoolCmdMetadataValidationError !StakePoolMetadataValidationError
| StakePoolCmdHashMismatchError
!(Hash StakePoolMetadata)
-- ^ Expected hash
!(Hash StakePoolMetadata)
-- ^ Actual hash
| StakePoolCmdFetchURLError !FetchURLError
deriving Show

renderStakePoolCmdError :: StakePoolCmdError -> Doc ann
Expand All @@ -28,3 +37,13 @@ renderStakePoolCmdError = \case
prettyError fileErr
StakePoolCmdWriteFileError fileErr ->
prettyError fileErr
StakePoolCmdHashMismatchError
(StakePoolMetadataHash expectedHash)
(StakePoolMetadataHash actualHash) ->
"Hashes do not match!"
<> "\nExpected:"
<+> pretty (show expectedHash)
<> "\n Actual:"
<+> pretty (show actualHash)
StakePoolCmdFetchURLError fetchErr ->
"Error fetching stake pool metadata: " <> prettyException fetchErr
Loading
Loading