Skip to content

Commit

Permalink
Add functions for validating gov actions
Browse files Browse the repository at this point in the history
  • Loading branch information
palas committed Jan 14, 2025
1 parent a429ac2 commit 9ddff13
Show file tree
Hide file tree
Showing 17 changed files with 942 additions and 3 deletions.
6 changes: 6 additions & 0 deletions cardano-api/cardano-api.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ library internal
Cardano.Api.GenesisParameters
Cardano.Api.Governance.Actions.ProposalProcedure
Cardano.Api.Governance.Actions.VotingProcedure
Cardano.Api.Governance.Metadata.DrepRegistration
Cardano.Api.Governance.Metadata.GovAction
Cardano.Api.Governance.Metadata.Parsers
Cardano.Api.Governance.Metadata.Validation
Cardano.Api.Governance.Poll
Cardano.Api.HasTypeProxy
Cardano.Api.Hash
Expand Down Expand Up @@ -359,7 +363,9 @@ test-suite cardano-api-test
Test.Cardano.Api.Envelope
Test.Cardano.Api.EpochLeadership
Test.Cardano.Api.Eras
Test.Cardano.Api.Experimental
Test.Cardano.Api.Genesis
Test.Cardano.Api.GovAnchorValidation
Test.Cardano.Api.IO
Test.Cardano.Api.Json
Test.Cardano.Api.KeysByron
Expand Down
14 changes: 14 additions & 0 deletions cardano-api/internal/Cardano/Api/Certificate.hs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ module Cardano.Api.Certificate
-- * Internal functions
, filterUnRegCreds
, filterUnRegDRepCreds
, isDRepRegOrUpdateCert
)
where

Expand Down Expand Up @@ -793,3 +794,16 @@ getAnchorDataFromCertificate c =
, Ledger.anchorDataHash = Ledger.unsafeMakeSafeHash hash
}
)

-- | Returns `True` if the certificate is a DRep registration or update certificate,
-- otherwise `False`. This is to see if the certificate needs to be compliant with
-- CIP-0119.
isDRepRegOrUpdateCert :: Certificate era -> Bool
isDRepRegOrUpdateCert = \case
ShelleyRelatedCertificate _ _ -> False
ConwayCertificate ceo ccert ->
conwayEraOnwardsConstraints ceo $
case ccert of
Ledger.RegDRepTxCert{} -> True
Ledger.UpdateDRepTxCert{} -> True
_ -> False
5 changes: 5 additions & 0 deletions cardano-api/internal/Cardano/Api/DRepMetadata.hs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE TypeFamilies #-}

-- | DRep off-chain metadata
Expand Down Expand Up @@ -43,11 +44,15 @@ newtype instance Hash DRepMetadata = DRepMetadataHash (Shelley.Hash StandardCryp

instance HasTypeProxy DRepMetadata where
data AsType DRepMetadata = AsDRepMetadata
proxyToAsType :: Proxy DRepMetadata -> AsType DRepMetadata
proxyToAsType _ = AsDRepMetadata

instance SerialiseAsRawBytes (Hash DRepMetadata) where
serialiseToRawBytes :: Hash DRepMetadata -> ByteString
serialiseToRawBytes (DRepMetadataHash h) = Crypto.hashToBytes h

deserialiseFromRawBytes
:: AsType (Hash DRepMetadata) -> ByteString -> Either SerialiseAsRawBytesError (Hash DRepMetadata)
deserialiseFromRawBytes (AsHash AsDRepMetadata) bs =
maybeToRight (SerialiseAsRawBytesError "Unable to deserialise Hash DRepMetadata") $
DRepMetadataHash <$> Crypto.hashFromBytes bs
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TypeFamilies #-}

module Cardano.Api.Governance.Metadata.DrepRegistration (CIP119 (..)) where
-- | DRep off-chain metadata
--
-- This module implements validation of metadata for DRep registration and
-- update actions, as specified bt the CIP-119 (https://cips.cardano.org/cip/CIP-0119).
--
-- The constraints implemented in this module can be tested against a JSON
-- 'ByteString' by using the function 'validateGovActionAnchorData' in
-- "Cardano.Api.Governance.Metadata.Validation" with the parameter 'DrepRegistrationMetadata'.

import Cardano.Api.Governance.Metadata.Parsers (textWithMaxLength)
import Cardano.Api.Governance.Metadata.Validation (Authors, Body, GovActionMetadata (..),
HashAlgorithm)

import Data.Aeson (FromJSON, withObject, (.:), (.:?))
import qualified Data.Aeson as Aeson
import Data.Aeson.Types (Parser)
import Data.Text (Text)
import GHC.Generics (Generic)

data CIP119 = DrepRegistrationMetadata

instance FromJSON (GovActionMetadata CIP119) where
parseJSON :: Aeson.Value -> Parser (GovActionMetadata CIP119)
parseJSON = withObject "CIP119Common" $ \v ->
GovActionMetadata
<$> v .: "hashAlgorithm"
<*> pure Absent
<*> v .: "body"

-- Hash Algorithm (Enum)
data instance HashAlgorithm CIP119 = Blake2b256
deriving (Show, Generic)

instance FromJSON (HashAlgorithm CIP119) where
parseJSON :: Aeson.Value -> Parser (HashAlgorithm CIP119)
parseJSON = Aeson.withText "HashAlgorithm" $
\case
"blake2b-256" -> return Blake2b256
_ -> fail "Invalid hashAlgorithm, it must be: blake2b-256"

-- Body of the metadata document
data instance Body CIP119 = Body
{ paymentAddress :: Maybe Text
, givenName :: Text
, image :: Maybe ImageObject
, objectives :: Maybe Text
, motivations :: Maybe Text
, qualifications :: Maybe Text
, doNotList :: Maybe DoNotList
, references :: Maybe [Reference]
}
deriving (Show, Generic)

instance FromJSON (Body CIP119) where
parseJSON :: Aeson.Value -> Parser (Body CIP119)
parseJSON = withObject "Body" $ \v ->
Body
<$> v .:? "paymentAddress"
<*> (v .: "givenName" >>= textWithMaxLength "givenName" 80)
<*> v .:? "image"
<*> (v .:? "objectives" >>= traverse (textWithMaxLength "objectives" 1000))
<*> (v .:? "motivations" >>= traverse (textWithMaxLength "motivations" 1000))
<*> (v .:? "qualifications" >>= traverse (textWithMaxLength "qualifications" 1000))
<*> v .:? "doNotList"
<*> v .:? "references"

-- Profile picture
data ImageObject = ImageObject
{ contentUrl :: Text -- Base64 encoded image or URL
, sha256 :: Maybe Text -- Only present for URL images
}
deriving (Show, Generic)

instance FromJSON ImageObject where
parseJSON :: Aeson.Value -> Parser ImageObject
parseJSON = withObject "ImageObject" $ \v ->
ImageObject
<$> v .: "contentUrl"
<*> v .:? "sha256"

-- DoNotList Enum
data DoNotList = DoNotListTrue | DoNotListFalse
deriving (Show, Generic)

instance FromJSON DoNotList where
parseJSON :: Aeson.Value -> Parser DoNotList
parseJSON = Aeson.withText "DoNotList" $
\case
"true" -> return DoNotListTrue
"false" -> return DoNotListFalse
_ -> fail "Invalid doNotList value, must be one of: true, false"

-- Reference type
data Reference = Reference
{ refType :: ReferenceType
, label :: Text
, uri :: Text
}
deriving (Show, Generic)

instance FromJSON Reference where
parseJSON :: Aeson.Value -> Parser Reference
parseJSON = withObject "Reference" $ \v ->
Reference
<$> v .: "@type"
<*> v .: "label"
<*> v .: "uri"

-- ReferenceType Enum
data ReferenceType = GovernanceMetadata | Other | Link | Identity
deriving (Show, Generic)

instance FromJSON ReferenceType where
parseJSON :: Aeson.Value -> Parser ReferenceType
parseJSON = Aeson.withText "ReferenceType" $
\case
"GovernanceMetadata" -> return GovernanceMetadata
"Other" -> return Other
"Link" -> return Link
"Identity" -> return Identity
_ ->
fail "Invalid reference type, must be one of: GovernanceMetadata, Other, Link, Identity"

-- We don't need to validate Authors because it is optional in CIP-119
data instance Authors CIP119 = Absent
164 changes: 164 additions & 0 deletions cardano-api/internal/Cardano/Api/Governance/Metadata/GovAction.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TypeFamilies #-}

module Cardano.Api.Governance.Metadata.GovAction (CIP108 (..)) where
-- | Government action metadata
--
-- This module implements validation of metadata for Government Actions in
-- general, as specified bt the CIP-108 (https://cips.cardano.org/cip/CIP-0108),
-- except for Government Actions covered by other CIPs.
--
-- The constraints implemented in this module can be tested against a JSON
-- 'ByteString' by using the function 'validateGovActionAnchorData' in
-- "Cardano.Api.Governance.Metadata.Validation" with the parameter 'BaseGovActionMetadata'.

import Cardano.Api.Governance.Metadata.Parsers (textWithMaxLength)
import Cardano.Api.Governance.Metadata.Validation (Authors, Body, GovActionMetadata (..),
HashAlgorithm)

import Data.Aeson (FromJSON, withArray, withObject, withText, (.:), (.:?))
import qualified Data.Aeson as Aeson
import Data.Aeson.Types (Parser, Value (..))
import Data.Text (Text)
import GHC.Generics (Generic)

data CIP108 = BaseGovActionMetadata

instance FromJSON (GovActionMetadata CIP108) where
parseJSON :: Value -> Parser (GovActionMetadata CIP108)
parseJSON = withObject "CIP108Common" $ \v ->
GovActionMetadata
<$> v .: "hashAlgorithm"
<*> v .: "authors"
<*> v .: "body"

-- Enum for HashAlgorithm

data instance HashAlgorithm CIP108 = Blake2b256
deriving (Show, Generic)

instance FromJSON (HashAlgorithm CIP108) where
parseJSON :: Value -> Parser (HashAlgorithm CIP108)
parseJSON = withText "HashAlgorithm" $
\case
"blake2b-256" -> return Blake2b256
_ -> fail "Invalid hashAlgorithm value, must be: blake2b-256"

-- Author object

newtype instance Authors CIP108 = Authors [Author]
deriving (Show, Generic)

instance FromJSON (Authors CIP108) where
parseJSON :: Value -> Parser (Authors CIP108)
parseJSON = withArray "Authors" $ \arr ->
Authors <$> Aeson.parseJSON (Array arr)

data Author = Author
{ name :: Maybe Text
, witness :: Witness
}
deriving (Show, Generic)

instance FromJSON Author where
parseJSON :: Value -> Parser Author
parseJSON = withObject "Author" $ \v ->
Author
<$> v .:? "name"
<*> v .: "witness"

-- Witness object
data Witness = Witness
{ witnessAlgorithm :: Maybe WitnessAlgorithm
, publicKey :: Maybe Text
, signature :: Maybe Text
}
deriving (Show, Generic)

instance FromJSON Witness where
parseJSON :: Value -> Parser Witness
parseJSON = withObject "Witness" $ \v ->
Witness
<$> v .:? "witnessAlgorithm"
<*> v .:? "publicKey"
<*> v .:? "signature"

-- Enum for WitnessAlgorithm
data WitnessAlgorithm = Ed25519 | CIP0008
deriving (Show, Generic)

instance FromJSON WitnessAlgorithm where
parseJSON :: Value -> Parser WitnessAlgorithm
parseJSON = withText "WitnessAlgorithm" $
\case
"ed25519" -> return Ed25519
"CIP-0008" -> return CIP0008
_ -> fail "Invalid witnessAlgorithm value, must be: ed25519 or CIP-0008"

-- Body of the metadata document

data instance Body CIP108 = Body
{ title :: Text
, abstract :: Text
, motivation :: Text
, rationale :: Text
, references :: Maybe [Reference]
}
deriving (Show, Generic)

instance FromJSON (Body CIP108) where
parseJSON :: Value -> Parser (Body CIP108)
parseJSON = withObject "Body" $ \v ->
Body
<$> (v .: "title" >>= textWithMaxLength "title" 80)
<*> (v .: "abstract" >>= textWithMaxLength "abstract" 2500)
<*> v .: "motivation"
<*> v .: "rationale"
<*> v .:? "references"

-- Reference object
data Reference = Reference
{ refType :: ReferenceType
, label :: Text
, uri :: Text
, referenceHash :: Maybe ReferenceHash
}
deriving (Show, Generic)

instance FromJSON Reference where
parseJSON :: Value -> Parser Reference
parseJSON = withObject "Reference" $ \v ->
Reference
<$> v .: "@type"
<*> v .: "label"
<*> v .: "uri"
<*> v .:? "referenceHash"

-- Enum for ReferenceType
data ReferenceType = GovernanceMetadata | Other
deriving (Show, Generic)

instance FromJSON ReferenceType where
parseJSON :: Value -> Parser ReferenceType
parseJSON = withText "ReferenceType" $
\case
"GovernanceMetadata" -> return GovernanceMetadata
"Other" -> return Other
_ -> fail "Invalid reference type, must be one of: GovernanceMetadata, Other"

-- ReferenceHash object
data ReferenceHash = ReferenceHash
{ referenceHashDigest :: Text
, referenceHashAlgorithm :: HashAlgorithm CIP108
}
deriving (Show, Generic)

instance FromJSON ReferenceHash where
parseJSON :: Value -> Parser ReferenceHash
parseJSON = withObject "ReferenceHash" $ \v ->
ReferenceHash
<$> v .: "hashDigest"
<*> v .: "hashAlgorithm"
22 changes: 22 additions & 0 deletions cardano-api/internal/Cardano/Api/Governance/Metadata/Parsers.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
module Cardano.Api.Governance.Metadata.Parsers (textWithMaxLength) where

import Data.Aeson.Types (Parser, Value, parseJSON)
import Data.Text (Text)
import qualified Data.Text as T

-- | Parser for 'Text' that validates that the number of characters is
-- under a given maximum. The 'String' parameter is meant to be the name
-- of the field in order to be able to give context in case of error.
textWithMaxLength :: String -> Int -> Value -> Parser Text
textWithMaxLength fieldName maxLen value = do
txt <- parseJSON value
if T.length txt <= maxLen
then pure txt
else
fail $
"key \""
++ fieldName
++ "\" exceeds maximum length of "
++ show maxLen
++ " characters. Got length: "
++ show (T.length txt)
Loading

0 comments on commit 9ddff13

Please sign in to comment.