Skip to content

Implement Cip129 class #778

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

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open

Implement Cip129 class #778

wants to merge 6 commits into from

Conversation

Jimbo4350
Copy link
Contributor

@Jimbo4350 Jimbo4350 commented Mar 13, 2025

Changelog

- description: |
    Implement Cip129 class. This type class captures the bech32 encoding modification that allows identification of various governance credentials and governance action ids.
  type:
  - feature      

Context

Additional context for the PR goes here. If the PR fixes a particular issue please provide a link to the issue.

How to trust this PR

Highlight important bits of the PR that will make the review faster. If there are commands the reviewer can run to observe the new behavior, describe them.

Checklist

  • Commit sequence broadly makes sense and commits have useful messages
  • New tests are added if needed and existing tests are updated. See Running tests for more details
  • Self-reviewed the diff

@smelc smelc mentioned this pull request Mar 14, 2025
3 tasks
@Jimbo4350 Jimbo4350 force-pushed the jordan/cip-129 branch 2 times, most recently from 5f2abfb to 6245452 Compare March 20, 2025 14:10
@Jimbo4350 Jimbo4350 force-pushed the jordan/cip-129 branch 2 times, most recently from 0834fcd to baacdf3 Compare April 4, 2025 17:33
@Jimbo4350 Jimbo4350 marked this pull request as ready for review April 14, 2025 15:52
@Jimbo4350 Jimbo4350 force-pushed the jordan/cip-129 branch 2 times, most recently from d90b999 to 26c2331 Compare April 14, 2025 16:08
@Jimbo4350 Jimbo4350 changed the title Implement CIP129 class Implement CIP129 class Apr 14, 2025
cip129HeaderHexByte c =
case c of
L.KeyHashObj{} -> BS.singleton 0x02 -- 0000 0010
L.ScriptHashObj{} -> BS.singleton 0x03 -- 0000 0011
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be a use case for Binary Literals.

Copy link
Contributor Author

@Jimbo4350 Jimbo4350 Apr 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For instance, the binary integer literal 0b11001001 will be desugared into fromInteger 201 when BinaryLiterals is enabled.

I find fromInteger + BinaryLiterals to be less explicit than 0x.

@@ -2045,6 +2045,7 @@ instance HasTextEnvelope (SigningKey DRepKey) where
---
--- Drep extended keys
---

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only whitespace change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, just adjusting to conform to the other headings in the module.

Comment on lines 125 to 126
error $
"serialiseToBech32: invalid prefix "
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

error here is a signal that prefix has an imprecise type. It should be HumanReadablePart.

If we're going to throw an exception at runtime it's better for it to be as early as possible i.e. at instance definition.

I guess we'd have to copy a helper function:

unsafeHumanReadablePartFromText :: Text -> HumanReadablePart
unsafeHumanReadablePartFromText
    = either (error . ("Error while parsing Bech32: " <>) . show) id
    . humanReadablePartFromText

and then

instance CIP129 (Credential L.ColdCommitteeRole) where
  cip129Bech32PrefixFor _ = unsafeHumanReadablePartFromText "cc_cold"
....

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

@carbolymer carbolymer Apr 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or type literals magic...

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}

module MyLib where

import Data.Typeable
import GHC.TypeLits
import Data.Type.Bool
import Data.Type.Equality


-- | A validated Bech32 prefix with constraints encoded in the type
data ValidatedBech32Prefix (chars :: [Char]) where
  ValidatedBech32Prefix ::
    ( AllValidChars chars ~ 'True
    , ValidLength (Length chars) ~ 'True
    , ConsistentCase chars ~ 'True
    ) => Proxy chars -> ValidatedBech32Prefix chars

-- Smart constructor
mkBech32Prefix :: 
  ( AllValidChars chars ~ 'True
  , ValidLength (Length chars) ~ 'True
  , ConsistentCase chars ~ 'True
  ) => Proxy chars -> ValidatedBech32Prefix chars
mkBech32Prefix = ValidatedBech32Prefix

-- ======================
-- Type-level validation
-- ======================

-- Check if all characters are valid (ASCII 33–126)
type family AllValidChars (cs :: [Char]) :: Bool where
  AllValidChars '[] = 'True
  AllValidChars (c ': cs) = IsValidChar c && AllValidChars cs

-- Check single character validity
type family IsValidChar (c :: Char) :: Bool where
  IsValidChar c = ((CmpChar c '\x21' == LT) == False)  -- 33 <= c
               && ((CmpChar c '\x7E' == GT) == False)  -- c <= 126

-- Validate length (1–83)
type family ValidLength (len :: Nat) :: Bool where
  ValidLength len = ((CmpNat len 1 == LT) == False)  -- len >= 1
                 && ((CmpNat len 83 == GT) == False) -- len <= 83

-- Calculate length of character list
type family Length (cs :: [Char]) :: Nat where
  Length '[] = 0
  Length (c ': cs) = 1 + Length cs

-- Enforce consistent case (all lowercase or all uppercase)
type family ConsistentCase (cs :: [Char]) :: Bool where
  ConsistentCase '[] = 'True
  ConsistentCase (c ': cs) = CaseMatches (FirstCase c cs) c && ConsistentCase cs

-- Determine expected case from first character
type family FirstCase (c :: Char) (cs :: [Char]) :: Bool where
  FirstCase c _ = IsLower c

-- Check if character matches expected case
type family CaseMatches (isLower :: Bool) (c :: Char) :: Bool where
  CaseMatches 'True c = IsLower c
  CaseMatches 'False c = IsUpper c

-- Character case predicates
type family IsLower (c :: Char) :: Bool where
  IsLower c = ((CmpChar c 'a' == LT) == False) && ((CmpChar c 'z' == GT) == False)

type family IsUpper (c :: Char) :: Bool where
  IsUpper c = ((CmpChar c 'A' == LT) == False) && ((CmpChar c 'Z' == GT) == False)

-- ======================
-- Example Usage
-- ======================

-- Valid prefix (Bitcoin mainnet)
validExample :: ValidatedBech32Prefix ['b', 'c', 't']
validExample = mkBech32Prefix Proxy

-- Valid prefix (Uppercase)
validUpper :: ValidatedBech32Prefix ['T', 'E', 'S', 'T']
validUpper = mkBech32Prefix Proxy

This is from deepseek. I'm quite surprised that it got it almost right at first try.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth double checking that all exported symbols have haddocks

@Jimbo4350 Jimbo4350 changed the title Implement CIP129 class Implement Cip129 class Apr 23, 2025
@@ -0,0 +1,269 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}

Check warning

Code scanning / HLint

Unused LANGUAGE pragma Warning

cardano-api/src/Cardano/Api/Internal/Orphans/Misc.hs:2:1-31: Warning: Unused LANGUAGE pragma
  
Found:
  {-# LANGUAGE DeriveAnyClass #-}
  
Perhaps you should remove it.
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}

Check warning

Code scanning / HLint

Unused LANGUAGE pragma Warning

cardano-api/src/Cardano/Api/Internal/Orphans/Misc.hs:4:1-30: Warning: Unused LANGUAGE pragma
  
Found:
  {-# LANGUAGE DeriveGeneric #-}
  
Perhaps you should remove it.
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NamedFieldPuns #-}

Check warning

Code scanning / HLint

Unused LANGUAGE pragma Warning

cardano-api/src/Cardano/Api/Internal/Orphans/Misc.hs:9:1-31: Warning: Unused LANGUAGE pragma
  
Found:
  {-# LANGUAGE NamedFieldPuns #-}
  
Perhaps you should remove it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants