Skip to content
256 changes: 249 additions & 7 deletions pycardano/certificate.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,20 @@
from enum import Enum, unique
from typing import Optional, Tuple, Type, Union

from pycardano.exception import DeserializeException
from pycardano.hash import AnchorDataHash, PoolKeyHash, ScriptHash, VerificationKeyHash
from pycardano.crypto.bech32 import bech32_decode, convertbits, encode
from pycardano.exception import (
DecodingException,
DeserializeException,
SerializeException,
)
from pycardano.hash import (
CIP129_PAYLOAD_SIZE,
VERIFICATION_KEY_HASH_SIZE,
AnchorDataHash,
PoolKeyHash,
ScriptHash,
VerificationKeyHash,
)
from pycardano.serialization import (
ArrayCBORSerializable,
CodedSerializable,
Expand Down Expand Up @@ -36,6 +48,8 @@
"RegDRepCert",
"UnregDRepCertificate",
"UpdateDRepCertificate",
"GovernanceCredential",
"GovernanceKeyType",
]

from pycardano.pool_params import PoolParams
Expand Down Expand Up @@ -92,15 +106,171 @@ def __hash__(self):
return hash(self.to_cbor())


class IdFormat(Enum):
"""
Id format definition.
"""

CIP129 = "cip129"
CIP105 = "cip105"


class CredentialType(Enum):
"""
Credential type definition.
"""

KEY_HASH = 0b0010
"""Key hash"""

SCRIPT_HASH = 0b0011
"""Script hash"""


class GovernanceKeyType(Enum):
"""
Governance key type definition.
"""

CC_HOT = 0b0000
"""Committee cold hot key"""

CC_COLD = 0b0001
"""Committee cold key"""

DREP = 0b0010
"""DRep key"""


@dataclass(repr=False)
class DRepCredential(StakeCredential):
class GovernanceCredential(StakeCredential):
"""Represents a governance credential."""

def __repr__(self):
return f"{self.encode()}"

def __bytes__(self):
return self._compute_header_byte() + bytes(self.credential.payload)

governance_key_type: GovernanceKeyType = field(init=False)
"""Governance key type."""

def id(self, id_format: IdFormat = IdFormat.CIP129) -> str:
"""
Governance credential ID.
"""
return self.encode(id_format)

def id_hex(self, id_format: IdFormat = IdFormat.CIP129) -> str:
"""
Governance credential ID in hexadecimal format.
"""
if id_format == IdFormat.CIP129:
return bytes(self).hex()
else:
return bytes(self)[1:].hex()

@property
def credential_type(self) -> CredentialType:
"""Credential type."""
if isinstance(self.credential, VerificationKeyHash):
return CredentialType.KEY_HASH
else:
return CredentialType.SCRIPT_HASH

def _compute_header_byte(self) -> bytes:
"""Compute the header byte."""
return (
self.governance_key_type.value << 4 | self.credential_type.value
).to_bytes(1, byteorder="big")

def _compute_hrp(self, id_format: IdFormat = IdFormat.CIP129) -> str:
"""Compute human-readable prefix for bech32 encoder.

Based on
`miscellaneous section <https://github.com/cardano-foundation/CIPs/tree/master/CIP-0005#miscellaneous>`_
in CIP-5.
"""
prefix = ""
if self.governance_key_type == GovernanceKeyType.CC_HOT:
prefix = "cc_hot"
elif self.governance_key_type == GovernanceKeyType.CC_COLD:
prefix = "cc_cold"
elif self.governance_key_type == GovernanceKeyType.DREP:
prefix = "drep"

suffix = ""
if isinstance(self.credential, VerificationKeyHash):
suffix = ""
elif isinstance(self.credential, ScriptHash):
suffix = "_script"

return prefix + suffix if id_format == IdFormat.CIP105 else prefix

def encode(self, id_format: IdFormat = IdFormat.CIP129) -> str:
"""Encode the governance credential in Bech32 format.

More info about Bech32 `here <https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#Bech32>`_.

Returns:
str: Encoded governance credential in Bech32 format.
"""
data = bytes(self) if id_format == IdFormat.CIP129 else bytes(self)[1:]
return encode(self._compute_hrp(id_format), data)

@classmethod
def decode(cls: Type[GovernanceCredential], data: str) -> GovernanceCredential:
"""Decode a bech32 string into a governance credential object.

Args:
data (str): Bech32-encoded string.

Returns:
GovernanceCredential: Decoded governance credential.

Raises:
DecodingException: When the input string is not a valid governance credential.
"""
hrp, checksum, _ = bech32_decode(data)
value = bytes(convertbits(checksum, 5, 8, False))
if len(value) == VERIFICATION_KEY_HASH_SIZE:
# CIP-105
if "script" in hrp:
return cls(ScriptHash(value))
else:
return cls(VerificationKeyHash(value))
elif len(value) == CIP129_PAYLOAD_SIZE:
header = value[0]
payload = value[1:]

key_type = GovernanceKeyType((header & 0xF0) >> 4)
credential_type = CredentialType(header & 0x0F)

if key_type != cls.governance_key_type:
raise DecodingException(f"Invalid key type: {key_type}")

if credential_type == CredentialType.KEY_HASH:
return cls(VerificationKeyHash(payload))
elif credential_type == CredentialType.SCRIPT_HASH:
return cls(ScriptHash(payload))
else:
raise DecodingException(f"Invalid credential type: {credential_type}")
else:
raise DecodingException(f"Invalid data length: {len(data)}")

def to_primitive(self):
return [self._CODE, self.credential.to_primitive()]


@dataclass(repr=False)
class DRepCredential(GovernanceCredential):
"""Represents a Delegate Representative (DRep) credential.

This credential type is specifically used for DReps in the governance system,
inheriting from StakeCredential.
inheriting from GovernanceCredential.
"""

pass
governance_key_type: GovernanceKeyType = GovernanceKeyType.DREP


@unique
Expand Down Expand Up @@ -135,13 +305,28 @@ class DRep(ArrayCBORSerializable):
)
"""The credential associated with this DRep, if applicable"""

def id(self, id_format: IdFormat = IdFormat.CIP129) -> str:
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would move id_format to an internal field of DRep. Doing so would lead to a better alignment with how Address type work today.

For example, an address has the following usage pattern:

>>> addr = Address.from_primitive("addr_test1vr2p8st5t5cxqglyjky7vk98k7jtfhdpvhl4e97cezuhn0cqcexl7")
addr
addr_test1vr2p8st5t5cxqglyjky7vk98k7jtfhdpvhl4e97cezuhn0cqcexl7
>>> bytes(addr)
b'`\xd4\x13\xc1t]0`#\xe4\x95\x89\xe6X\xa7\xb7\xa4\xb4\xdd\xa1e\xff\\\x97\xd8\xc8\xb9y\xbf'
>>> bytes(addr).hex()
'60d413c1745d306023e49589e658a7b7a4b4dda165ff5c97d8c8b979bf'
>>> str(addr)
'addr_test1vr2p8st5t5cxqglyjky7vk98k7jtfhdpvhl4e97cezuhn0cqcexl7'

There are two essential calls: str(addr) and bytes(addr).

str(addr) is implemented by __repr__, and bytes(addr) is implemented by __bytes__.

Similarly, it would be great if DRep could stick to this usage pattern.

Something like this:

>>> drep = DRep(..., IdFormat.CIP105)
>>> bytes(drep).hex()
'4828a2dadba97ca9fd0cdc99975899470c219bdc0d828cfa6ddf6d69'
>>> str(drep)
'drep1fq529kkm4972nlgvmjvewkyeguxzrx7upkpge7ndmakkjnstaxx'

"""
DRep ID.
"""
return self.encode(id_format)

def id_hex(self, id_format: IdFormat = IdFormat.CIP129) -> str:
Copy link
Collaborator

Choose a reason for hiding this comment

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

As mentioned above, this seems a bit redundant, as user could simply call bytes(drep).hex() instead.

"""
DRep ID in hexadecimal format.
"""
if self.credential is not None:
drep_credential = DRepCredential(self.credential)
return drep_credential.id_hex(id_format)
return ""

@classmethod
@limit_primitive_type(list)
def from_primitive(cls: Type[DRep], values: Union[list, tuple]) -> DRep:
try:
kind = DRepKind(values[0])
except ValueError:
raise DeserializeException(f"Invalid DRep type {values[0]}")
except ValueError as e:
raise DeserializeException(f"Invalid DRep type {values[0]}") from e

if kind == DRepKind.VERIFICATION_KEY_HASH:
return cls(kind=kind, credential=VerificationKeyHash(values[1]))
Expand All @@ -159,6 +344,63 @@ def to_primitive(self):
return [self.kind.value, self.credential.to_primitive()]
return [self.kind.value]

def encode(self, id_format: IdFormat = IdFormat.CIP129) -> str:
"""Encode the DRep in Bech32 format.

More info about Bech32 `here <https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#Bech32>`_.

Returns:
str: Encoded DRep in Bech32 format.

Examples:
>>> vkey_bytes = bytes.fromhex("00000000000000000000000000000000000000000000000000000000")
>>> credential = VerificationKeyHash(vkey_bytes)
>>> print(DRep(kind=DRepKind.VERIFICATION_KEY_HASH, credential=credential).encode())
drep1ygqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq7vlc9n
"""
if self.kind == DRepKind.ALWAYS_ABSTAIN:
return "drep_always_abstain"
elif self.kind == DRepKind.ALWAYS_NO_CONFIDENCE:
return "drep_always_no_confidence"
elif self.credential is not None:
drep_credential = DRepCredential(self.credential)
return drep_credential.encode(id_format)
else:
raise SerializeException("DRep credential is None")

@classmethod
def decode(cls: Type[DRep], data: str) -> DRep:
"""Decode a bech32 string into a DRep object.

Args:
data (str): Bech32-encoded string.

Returns:
DRep: Decoded DRep.

Raises:
DecodingException: When the input string is not a valid DRep.

Examples:
>>> credential = DRep.decode("drep1ygqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq7vlc9n")
>>> khash = VerificationKeyHash(bytes.fromhex("00000000000000000000000000000000000000000000000000000000"))
>>> assert credential == DRep(DRepKind.VERIFICATION_KEY_HASH, khash)
"""
if data == "drep_always_abstain":
return cls(kind=DRepKind.ALWAYS_ABSTAIN)
elif data == "drep_always_no_confidence":
return cls(kind=DRepKind.ALWAYS_NO_CONFIDENCE)
else:
drep_credential = DRepCredential.decode(data)
return cls(
kind=(
DRepKind.VERIFICATION_KEY_HASH
if isinstance(drep_credential.credential, VerificationKeyHash)
else DRepKind.SCRIPT_HASH
),
credential=drep_credential.credential,
)


@dataclass(repr=False)
class StakeRegistration(CodedSerializable):
Expand Down
Loading