packages feed

cardano-addresses-4.0.2: lib/Cardano/Address/KeyHash.hs

{-# LANGUAGE BinaryLiterals #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE UndecidableInstances #-}

{-# OPTIONS_HADDOCK prune #-}

-- |
-- Copyright: 2020 Input Output (Hong Kong) Ltd., 2021-2022 Input Output Global Inc. (IOG), 2023-2025 Intersect
-- License: Apache-2.0

module Cardano.Address.KeyHash
    (
    -- * Types
      KeyHash (..)
    , KeyRole (..)
    , GovernanceType (..)

    -- * Conversions
    , keyHashFromBytes
    , keyHashFromText
    , keyHashToText

    -- * Errors
    , ErrKeyHashFromText
    , prettyErrKeyHashFromText
    ) where

import Prelude

import Cardano.Address.Derivation
    ( credentialHashSize, hashCredential )
import Codec.Binary.Encoding
    ( AbstractEncoding (..), encode, fromBase16 )
import Control.DeepSeq
    ( NFData )
import Data.Aeson
    ( ToJSON (..), Value (..) )
import Data.Bifunctor
    ( first )
import Data.ByteString
    ( ByteString )
import Data.Either.Combinators
    ( maybeToRight )
import Data.Text
    ( Text )
import GHC.Generics
    ( Generic )

import qualified Cardano.Codec.Bech32.Prefixes as CIP5
import qualified Codec.Binary.Bech32 as Bech32
import qualified Data.ByteString as BS
import qualified Data.Text.Encoding as T

-- | Determines if one asks for deprecated HRP prefixes, __*_vkh__ and __*_script__
-- in accordance to [CIP-0105](https://github.com/cardano-foundation/CIPs/tree/master/CIP-0105) (on demand when flag 'cip-0105' is used) or uses default format
-- specified in [CIP-0129](https://github.com/cardano-foundation/CIPs/tree/master/CIP-0129) (where additional byte is prepended to 28-byte hash).
data GovernanceType = NoGovernance | CIP0129 | CIP0105
    deriving (Eq, Show)

-- | Determines the role a given key plays.
--
-- The role basically can be mapped into derivation path
-- which was used to derive it from the parent. Also it has a dedicated user facing HRP
-- when presented in bech32 format - see 'keyHashToText' for more details.
--
-- Take notice that purpose/role (except 'Policy') are as defined below in derivation path:
--
-- @
-- m / purpose' / coin_type' / account_ix' / role / index
-- @
--
-- 'Policy' has a dedicated derivation path as follows:
--
-- @
-- m / purpose' / coin_type' / policy_ix'
-- @
--
--
-- +------------------+------------+--------+------------------------------------------------------------------------------+
-- |    KeyRole       |   purpose  |  role  |                                  CIP                                         |
-- +==================+============+========+==============================================================================+
-- | PaymentShared    |   1854H    |   0,1  | [CIP-1854](https://github.com/cardano-foundation/CIPs/tree/master/CIP-1854)  |
-- +------------------+------------+--------+------------------------------------------------------------------------------+
-- | DelegationShared |   1854H    |   2    | [CIP-1854](https://github.com/cardano-foundation/CIPs/tree/master/CIP-1854)  |
-- +------------------+------------+--------+------------------------------------------------------------------------------+
-- | Payment          |   1852H    |   0,1  | [CIP-1852](https://github.com/cardano-foundation/CIPs/tree/master/CIP-1852)  |
-- +------------------+------------+--------+------------------------------------------------------------------------------+
-- | Delegation       |   1852H    |   2    | [CIP-0011](https://github.com/cardano-foundation/CIPs/tree/master/CIP-0011)  |
-- +------------------+------------+--------+------------------------------------------------------------------------------+
-- | Representative   |   1852H    |   3    | [CIP-0105](https://github.com/cardano-foundation/CIPs/tree/master/CIP-0105)  |
-- +------------------+------------+--------+------------------------------------------------------------------------------+
-- | CommitteeCold    |   1852H    |   4    | [CIP-0105](https://github.com/cardano-foundation/CIPs/tree/master/CIP-0105)  |
-- +------------------+------------+--------+------------------------------------------------------------------------------+
-- | CommitteeHot     |   1852H    |   5    | [CIP-0105](https://github.com/cardano-foundation/CIPs/tree/master/CIP-0105)  |
-- +------------------+------------+--------+------------------------------------------------------------------------------+
-- | Policy           |   1855H    |   -    | [CIP-1855](https://github.com/cardano-foundation/CIPs/tree/master/CIP-1855)  |
-- +------------------+------------+--------+------------------------------------------------------------------------------+
--
data KeyRole =
      PaymentShared
    | DelegationShared
    | Payment
    | Delegation
    | Policy
    | Representative
    | CommitteeCold
    | CommitteeHot
    | Unknown
    deriving (Generic, Show, Ord, Eq)
instance NFData KeyRole

-- | A 'KeyHash' type represents verification key hash that participate in building
-- multi-signature script. The hash is expected to have size of 28-byte.
--
-- @since 3.0.0
data KeyHash = KeyHash
    { role :: KeyRole
    , digest :: ByteString }
    deriving (Generic, Show, Ord, Eq)
instance NFData KeyHash

-- | Construct an 'KeyHash' from raw 'ByteString' (28 bytes).
--
-- @since 3.0.0
keyHashFromBytes :: (KeyRole, ByteString) -> Maybe KeyHash
keyHashFromBytes (cred, bytes)
    | BS.length bytes /= credentialHashSize = Nothing
    | otherwise = Just $ KeyHash cred bytes

-- | Encode a 'KeyHash' to bech32 'Text' or hex is key role unknown.
--  If one wants to include, valid in governance roles only, additional byte
--  as specified in CIP-0129, the function needs to be called with withByte=true.
--
-- @since 3.0.0
keyHashToText :: KeyHash -> GovernanceType -> Text
keyHashToText (KeyHash cred keyHash) govType = case cred of
    PaymentShared ->
        T.decodeUtf8 $ encode (EBech32 CIP5.addr_shared_vkh) keyHash
    DelegationShared ->
        T.decodeUtf8 $ encode (EBech32 CIP5.stake_shared_vkh) keyHash
    Payment ->
        T.decodeUtf8 $ encode (EBech32 CIP5.addr_vkh) keyHash
    Delegation ->
        T.decodeUtf8 $ encode (EBech32 CIP5.stake_vkh) keyHash
    Policy ->
        T.decodeUtf8 $ encode (EBech32 CIP5.policy_vkh) keyHash
    Representative -> case govType of
        CIP0105 ->
            T.decodeUtf8 $ encode (EBech32 CIP5.drep_vkh) keyHash
        _ ->
            T.decodeUtf8 $ encode (EBech32 CIP5.drep) $ keyHashAppendByteCIP0129 keyHash cred
    CommitteeCold -> case govType of
        CIP0105 ->
            T.decodeUtf8 $ encode (EBech32 CIP5.cc_cold_vkh) keyHash
        _ ->
            T.decodeUtf8 $ encode (EBech32 CIP5.cc_cold) $ keyHashAppendByteCIP0129 keyHash cred
    CommitteeHot -> case govType of
        CIP0105 ->
            T.decodeUtf8 $ encode (EBech32 CIP5.cc_hot_vkh) keyHash
        _ ->
            T.decodeUtf8 $ encode (EBech32 CIP5.cc_hot) $ keyHashAppendByteCIP0129 keyHash cred
    Unknown ->
        T.decodeUtf8 $ encode EBase16 keyHash

-- | In accordance to CIP-0129 (https://github.com/cardano-foundation/CIPs/tree/master/CIP-0129)
--   one byte is prepended to vkh only in governance context. The rules how to contruct it are summarized
--   below
--
--   drep       0010....
--   hot        0000....    key type
--   cold       0001....
--
--   keyhash    ....0010
--   This is on top of X_vkh, where X={drep, cc_hot, cc_hot}, which lacks the additional byte.
--   In `keyHashFromText` we additionally
--   support reading legacy X which also lacks the additional byte, and has the same payload as
--   as the corresponding X_vkh.
keyHashAppendByteCIP0129 :: ByteString -> KeyRole -> ByteString
keyHashAppendByteCIP0129 payload cred =
    maybe payload (`BS.cons` payload) bytePrefix
  where
    bytePrefix = case cred of
        Representative -> Just 0b00100010
        CommitteeCold -> Just 0b00010010
        CommitteeHot -> Just 0b00000010
        _ -> Nothing

-- | Construct a 'KeyHash' from 'Text'. It should be
-- Bech32 encoded text with one of following hrp:
--
-- - `addr_shared_vkh`
-- - `stake_shared_vkh`
-- - `addr_vkh`
-- - `stake_vkh`
-- - `policy_vkh`
-- - `drep`
-- - `cc_cold`
-- - `cc_hot`
-- - `drep_vkh`
-- - `cc_cold_vkh`
-- - `cc_hot_vkh`
-- - `addr_shared_vk`
-- - `stake_shared_vk`
-- - `addr_vk`
-- - `stake_vk`
-- - `policy_vk`
-- - `cc_cold_vk`
-- - `cc_hot_vk`
-- - `addr_shared_xvk`
-- - `stake_shared_xvk`
-- - `addr_xvk`
-- - `stake_xvk`
-- - `policy_xvk`
-- - `drep_xvk`
-- - `cc_cold_xvk`
-- - `cc_hot_xvk`
--
-- Raw keys will be hashed on the fly, whereas hash that are directly
-- provided will remain as such.
-- If if hex is encountered 'Unknown' policy key is assumed.
--
-- @since 3.1.0
keyHashFromText :: Text -> Either ErrKeyHashFromText KeyHash
keyHashFromText txt =
    case (fromBase16 $ T.encodeUtf8 txt) of
        Right bs ->
            if checkBSLength bs 28 then
                pure $ KeyHash Unknown bs
            else if checkBSLength bs 32 then
                pure $ KeyHash Unknown (hashCredential bs)
            else if checkBSLength bs 64 then
                pure $ KeyHash Unknown (hashCredential $ BS.take 32 bs)
            else
                Left (ErrKeyHashFromTextInvalidHex $ BS.length bs)
        Left _ -> do
            (hrp, dp) <- first (const ErrKeyHashFromTextInvalidString) $
                Bech32.decodeLenient txt

            maybeToRight ErrKeyHashFromTextWrongDataPart (Bech32.dataPartToBytes dp)
                >>= maybeToRight ErrKeyHashFromTextWrongHrp . convertBytes hrp
                >>= maybeToRight ErrKeyHashFromTextWrongPayload . keyHashFromBytes
 where
    convertBytes hrp bytes
        | hrp == CIP5.addr_shared_vkh && checkBSLength bytes 28 =
              Just (PaymentShared, bytes)
        | hrp == CIP5.stake_shared_vkh && checkBSLength bytes 28 =
              Just (DelegationShared, bytes)
        | hrp == CIP5.addr_vkh && checkBSLength bytes 28 =
              Just (Payment, bytes)
        | hrp == CIP5.stake_vkh && checkBSLength bytes 28 =
              Just (Delegation, bytes)
        | hrp == CIP5.policy_vkh && checkBSLength bytes 28 =
              Just (Policy, bytes)
        | hrp == CIP5.drep && checkBSLength bytes 29 =
              let (fstByte, payload) = first BS.head $ BS.splitAt 1 bytes
              --   drep          0010....
              --   keyhash       ....0010
              in if fstByte == 0b00100010 then
                  Just (Representative, payload)
                 else
                  Nothing
        | hrp == CIP5.drep && checkBSLength bytes 28 =
              Just (Representative, bytes)
        | hrp == CIP5.drep_vkh && checkBSLength bytes 28 =
              Just (Representative, bytes)
        | hrp == CIP5.cc_cold && checkBSLength bytes 29 =
              let (fstByte, payload) = first BS.head $ BS.splitAt 1 bytes
              --   cold          0001....
              --   keyhash       ....0010
              in if fstByte == 0b00010010 then
                  Just (CommitteeCold, payload)
                 else
                  Nothing
        | hrp == CIP5.cc_cold && checkBSLength bytes 28 =
              Just (CommitteeCold, bytes)
        | hrp == CIP5.cc_cold_vkh && checkBSLength bytes 28 =
              Just (CommitteeCold, bytes)
        | hrp == CIP5.cc_hot && checkBSLength bytes 29 =
              let (fstByte, payload) = first BS.head $ BS.splitAt 1 bytes
              --   hot           0000....
              --   keyhash       ....0010
              in if fstByte == 0b00000010 then
                  Just (CommitteeHot, payload)
                 else
                  Nothing
        | hrp == CIP5.cc_hot && checkBSLength bytes 28 =
              Just (CommitteeHot, bytes)
        | hrp == CIP5.cc_hot_vkh && checkBSLength bytes 28 =
              Just (CommitteeHot, bytes)
        | hrp == CIP5.addr_shared_vk && checkBSLength bytes 32 =
              Just (PaymentShared, hashCredential bytes)
        | hrp == CIP5.addr_vk && checkBSLength bytes 32 =
              Just (Payment, hashCredential bytes)
        | hrp == CIP5.addr_shared_xvk && checkBSLength bytes 64 =
              Just (PaymentShared, hashCredential $ BS.take 32 bytes)
        | hrp == CIP5.addr_xvk && checkBSLength bytes 64 =
              Just (Payment, hashCredential $ BS.take 32 bytes)
        | hrp == CIP5.stake_shared_vk && checkBSLength bytes 32 =
              Just (DelegationShared, hashCredential bytes)
        | hrp == CIP5.stake_vk && checkBSLength bytes 32 =
              Just (Delegation, hashCredential bytes)
        | hrp == CIP5.stake_shared_xvk && checkBSLength bytes 64 =
              Just (DelegationShared, hashCredential $ BS.take 32 bytes)
        | hrp == CIP5.stake_xvk && checkBSLength bytes 64 =
              Just (Delegation, hashCredential $ BS.take 32 bytes)
        | hrp == CIP5.policy_vk && checkBSLength bytes 32 =
              Just (Policy, hashCredential bytes)
        | hrp == CIP5.policy_xvk && checkBSLength bytes 64 =
              Just (Policy, hashCredential $ BS.take 32 bytes)
        | hrp == CIP5.drep_vk && checkBSLength bytes 32 =
              Just (Representative, hashCredential bytes)
        | hrp == CIP5.drep_xvk && checkBSLength bytes 64 =
              Just (Representative, hashCredential $ BS.take 32 bytes)
        | hrp == CIP5.cc_cold_vk && checkBSLength bytes 32 =
              Just (CommitteeCold, hashCredential bytes)
        | hrp == CIP5.cc_cold_xvk && checkBSLength bytes 64 =
              Just (CommitteeCold, hashCredential $ BS.take 32 bytes)
        | hrp == CIP5.cc_hot_vk && checkBSLength bytes 32 =
              Just (CommitteeHot, hashCredential bytes)
        | hrp == CIP5.cc_hot_xvk && checkBSLength bytes 64 =
              Just (CommitteeHot, hashCredential $ BS.take 32 bytes)
        | otherwise = Nothing
    checkBSLength :: ByteString -> Int -> Bool
    checkBSLength bytes expLength =
        BS.length bytes == expLength

-- | Possible errors when deserializing a key hash from text.
--
-- @since 3.0.0
data ErrKeyHashFromText
    = ErrKeyHashFromTextInvalidString
    | ErrKeyHashFromTextWrongPayload
    | ErrKeyHashFromTextWrongHrp
    | ErrKeyHashFromTextWrongDataPart
    | ErrKeyHashFromTextInvalidHex Int
    deriving (Show, Eq)

-- | Possible errors when deserializing a key hash from text.
--
-- @since 3.0.0
prettyErrKeyHashFromText :: ErrKeyHashFromText -> String
prettyErrKeyHashFromText = \case
    ErrKeyHashFromTextInvalidString ->
        "Invalid encoded string: must be either bech32 or hex-encoded."
    ErrKeyHashFromTextWrongPayload ->
        "Verification key hash must contain exactly 28 bytes."
    ErrKeyHashFromTextWrongHrp ->
        "Invalid human-readable prefix: must be 'X_vkh', 'X_vk', 'X_xvk' where X is 'addr_shared', 'stake_shared' or 'policy'."
    ErrKeyHashFromTextWrongDataPart ->
        "Verification key hash is Bech32-encoded but has an invalid data part."
    ErrKeyHashFromTextInvalidHex size->
        "Invalid hex-encoded string: must be either 28, 32 or 64 bytes, but has " <> show size <> " bytes."

instance ToJSON KeyHash where
    toJSON = String . flip keyHashToText CIP0129