diff --git a/cardano-cli/cardano-cli.cabal b/cardano-cli/cardano-cli.cabal index 0f1fec824..e944a9548 100644 --- a/cardano-cli/cardano-cli.cabal +++ b/cardano-cli/cardano-cli.cabal @@ -80,6 +80,7 @@ library Cardano.CLI.EraBased.Commands.StakePool Cardano.CLI.EraBased.Commands.TextView Cardano.CLI.EraBased.Commands.Transaction + Cardano.CLI.EraBased.HashChecking Cardano.CLI.EraBased.Options.Common Cardano.CLI.EraBased.Options.Genesis Cardano.CLI.EraBased.Options.Governance @@ -203,6 +204,7 @@ library cardano-crypto-wrapper ^>=1.5.1, cardano-data >=1.1, cardano-git-rev ^>=0.2.2, + cardano-ledger-api, cardano-ping ^>=0.5, cardano-prelude, cardano-slotting ^>=0.2.0.0, diff --git a/cardano-cli/src/Cardano/CLI/EraBased/HashChecking.hs b/cardano-cli/src/Cardano/CLI/EraBased/HashChecking.hs new file mode 100644 index 000000000..97202b254 --- /dev/null +++ b/cardano-cli/src/Cardano/CLI/EraBased/HashChecking.hs @@ -0,0 +1,123 @@ +{-# LANGUAGE ScopedTypeVariables #-} + +module Cardano.CLI.EraBased.HashChecking + ( checkCertificateHashes + , checkVotingProcedureHashes + , checkProposalHashes + ) +where + +import Cardano.Api (Certificate (..), ExceptT, firstExceptT) +import qualified Cardano.Api.Ledger as L +import qualified Cardano.Api.Shelley as Shelley + +import Cardano.CLI.Run.Hash (carryHashChecks) +import Cardano.CLI.Types.Common (MustCheckHash (..), PotentiallyCheckedAnchor (..)) +import Cardano.CLI.Types.Errors.TxCmdError (TxCmdError (..)) +import qualified Cardano.Ledger.Api.Governance as L + +import Control.Monad (forM_) +import Control.Monad.Trans.Except.Extra (left) + +-- | Check the hash of the anchor data against the hash in the anchor +checkAnchorMetadataHash :: L.Anchor L.StandardCrypto -> ExceptT TxCmdError IO () +checkAnchorMetadataHash anchor = + firstExceptT (TxCmdHashCheckError $ L.anchorUrl anchor) $ + carryHashChecks + ( PotentiallyCheckedAnchor + { pcaMustCheck = CheckHash + , pcaAnchor = anchor + } + ) + +-- | Find references to anchor data and check the hashes are valid +-- and they match the linked data. +checkCertificateHashes :: Certificate era -> ExceptT TxCmdError IO () +checkCertificateHashes c = + case c of + ShelleyRelatedCertificate _ shelleyCert -> + case shelleyCert of + L.ShelleyTxCertDelegCert shelleyDelegCert -> + case shelleyDelegCert of + L.ShelleyRegCert _ -> return () + L.ShelleyUnRegCert _ -> return () + L.ShelleyDelegCert _ _ -> return () + L.ShelleyTxCertPool shelleyPoolCert -> + case shelleyPoolCert of + L.RegPool poolParams -> forM_ (L.ppMetadata poolParams) checkPoolMetadataHash + L.RetirePool _ _ -> return () + L.ShelleyTxCertGenesisDeleg _ -> return () + L.ShelleyTxCertMir _ -> return () + ConwayCertificate ceo conwayCert -> + Shelley.conwayEraOnwardsConstraints ceo $ + case conwayCert of + L.ConwayTxCertDeleg _ -> return () + L.ConwayTxCertPool conwayPoolCert -> + case conwayPoolCert of + L.RegPool poolParams -> forM_ (L.ppMetadata poolParams) checkPoolMetadataHash + L.RetirePool _ _ -> return () + L.ConwayTxCertGov govCert -> + case govCert of + L.ConwayRegDRep _ _ mAnchor -> forM_ mAnchor checkAnchorMetadataHash + L.ConwayUnRegDRep _ _ -> return () + L.ConwayUpdateDRep _ mAnchor -> forM_ mAnchor checkAnchorMetadataHash + L.ConwayAuthCommitteeHotKey _ _ -> return () + L.ConwayResignCommitteeColdKey _ mAnchor -> forM_ mAnchor checkAnchorMetadataHash + where + checkPoolMetadataHash :: L.PoolMetadata -> ExceptT TxCmdError IO () + checkPoolMetadataHash (L.PoolMetadata{L.pmUrl = url, L.pmHash = hashBytes}) = do + let mHash = L.hashFromBytes hashBytes + hash <- maybe (left $ TxCmdPoolMetadataHashError url) return mHash + let safeHash = L.unsafeMakeSafeHash hash + checkAnchorMetadataHash + ( L.Anchor + { L.anchorUrl = url + , L.anchorDataHash = safeHash + } + ) + +-- | Find references to anchor data in voting procedures and check the hashes are valid +-- and they match the linked data. +checkVotingProcedureHashes + :: Shelley.ShelleyBasedEra era -> Shelley.VotingProcedures era -> ExceptT TxCmdError IO () +checkVotingProcedureHashes eon (Shelley.VotingProcedures (L.VotingProcedures voterMap)) = + Shelley.shelleyBasedEraConstraints eon $ + forM_ + voterMap + ( \vpMap -> + forM_ + vpMap + ( \(L.VotingProcedure _ mAnchor) -> + forM_ mAnchor checkAnchorMetadataHash + ) + ) + +-- | Find references to anchor data in proposals and check the hashes are valid +-- and they match the linked data. +checkProposalHashes + :: forall era. Shelley.ShelleyBasedEra era -> Shelley.Proposal era -> ExceptT TxCmdError IO () +checkProposalHashes + eon + ( Shelley.Proposal + ( L.ProposalProcedure + { L.pProcGovAction = govAction + , L.pProcAnchor = anchor + } + ) + ) = + Shelley.shelleyBasedEraConstraints eon $ do + checkAnchorMetadataHash anchor + checkGovActionHashes govAction + where + checkGovActionHashes + :: L.GovAction (Shelley.ShelleyLedgerEra era) -> ExceptT TxCmdError IO () + checkGovActionHashes govAction' = + Shelley.shelleyBasedEraConstraints eon $ + case govAction' of + L.ParameterChange{} -> return () + L.HardForkInitiation _ _ -> return () + L.TreasuryWithdrawals _ _ -> return () + L.NoConfidence _ -> return () + L.UpdateCommittee{} -> return () + L.NewConstitution _ constitution -> checkAnchorMetadataHash $ L.constitutionAnchor constitution + L.InfoAction -> return () diff --git a/cardano-cli/src/Cardano/CLI/EraBased/Run/Transaction.hs b/cardano-cli/src/Cardano/CLI/EraBased/Run/Transaction.hs index e0527663f..a30a3f6f1 100644 --- a/cardano-cli/src/Cardano/CLI/EraBased/Run/Transaction.hs +++ b/cardano-cli/src/Cardano/CLI/EraBased/Run/Transaction.hs @@ -41,6 +41,8 @@ import Cardano.Api.Shelley import qualified Cardano.Binary as CBOR import qualified Cardano.CLI.EraBased.Commands.Transaction as Cmd +import Cardano.CLI.EraBased.HashChecking (checkCertificateHashes, checkProposalHashes, + checkVotingProcedureHashes) import Cardano.CLI.EraBased.Run.Genesis.Common (readProtocolParameters) import Cardano.CLI.EraBased.Run.Query import Cardano.CLI.Read @@ -64,6 +66,7 @@ import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Lazy.Char8 as LBS import Data.Containers.ListUtils (nubOrd) import Data.Data ((:~:) (..)) +import Data.Foldable (forM_) import qualified Data.Foldable as Foldable import Data.Function ((&)) import qualified Data.List as List @@ -160,6 +163,9 @@ runTransactionBuildCmd ) | (CertificateFile certFile, mSwit) <- certFilesAndMaybeScriptWits ] + + forM_ certsAndMaybeScriptWits (checkCertificateHashes . fst) + withdrawalsAndMaybeScriptWits <- firstExceptT TxCmdScriptWitnessError $ readScriptWitnessFilesTuple eon withdrawals @@ -191,11 +197,15 @@ runTransactionBuildCmd (\w -> firstExceptT TxCmdVoteError $ ExceptT (readVotingProceduresFiles w voteFiles)) era' + forM_ votingProceduresAndMaybeScriptWits (checkVotingProcedureHashes eon . fst) + proposals <- newExceptT $ first TxCmdProposalError <$> readTxGovernanceActions eon proposalFiles + forM_ proposals (checkProposalHashes eon . fst) + -- the same collateral input can be used for several plutus scripts let filteredTxinsc = nubOrd txinsc diff --git a/cardano-cli/src/Cardano/CLI/Types/Errors/TxCmdError.hs b/cardano-cli/src/Cardano/CLI/Types/Errors/TxCmdError.hs index e0b9d0134..ee1bf87e8 100644 --- a/cardano-cli/src/Cardano/CLI/Types/Errors/TxCmdError.hs +++ b/cardano-cli/src/Cardano/CLI/Types/Errors/TxCmdError.hs @@ -15,11 +15,13 @@ module Cardano.CLI.Types.Errors.TxCmdError where import Cardano.Api +import qualified Cardano.Api.Ledger as L import Cardano.Api.Shelley import Cardano.CLI.Read import Cardano.CLI.Types.Common import Cardano.CLI.Types.Errors.BootstrapWitnessError +import Cardano.CLI.Types.Errors.HashCmdError (HashCheckError) import Cardano.CLI.Types.Errors.NodeEraMismatchError import qualified Cardano.CLI.Types.Errors.NodeEraMismatchError as NEM import Cardano.CLI.Types.Errors.ProtocolParamsError @@ -29,6 +31,7 @@ import Cardano.CLI.Types.TxFeature import qualified Cardano.Prelude as List import Ouroboros.Consensus.Cardano.Block (EraMismatch (..)) +import Control.Exception (displayException) import Data.Text (Text) {- HLINT ignore "Use let" -} @@ -84,6 +87,8 @@ data TxCmdError | TxCmdProtocolParamsConverstionError ProtocolParametersConversionError | forall era. TxCmdTxGovDuplicateVotes (TxGovDuplicateVotes era) | forall era. TxCmdFeeEstimationError (TxFeeEstimationError era) + | TxCmdPoolMetadataHashError L.Url + | TxCmdHashCheckError L.Url HashCheckError renderTxCmdError :: TxCmdError -> Doc ann renderTxCmdError = \case @@ -217,6 +222,10 @@ renderTxCmdError = \case prettyError e TxCmdFeeEstimationError e -> prettyError e + TxCmdPoolMetadataHashError url -> + "Hash of the pool metadata file is not valid. Url:" <+> pretty (L.urlToText url) + TxCmdHashCheckError url e -> + "Hash of the file is not valid. Url:" <+> pretty (L.urlToText url) <+> pretty (displayException e) prettyPolicyIdList :: [PolicyId] -> Doc ann prettyPolicyIdList =