-
-
Notifications
You must be signed in to change notification settings - Fork 80
Expand Bech32 usage #473
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
Merged
Merged
Expand Bech32 usage #473
Changes from 7 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
b2ba5f0
Add CIP129_PAYLOAD_SIZE constant to hash.py
d0931ad
Add PoolOperator class for handling Bech32 pool key hashes
467a60d
Add GovernanceCredential and DRepCredential classes with Bech32 encod…
38b1983
Add CommitteeHotCredential class and Bech32 encoding/decoding for gov…
c54dda4
Add tests for new encoding/decoding functionality
c874db6
Merge branch 'Python-Cardano:main' into expand-bech32-usage
KINGH242 cd67dd6
style: fix black check issues
b8a1a66
refactor: simplify PoolOperator ID methods and update tests
a38239b
refactor: enhance GovActionId encoding/decoding and improve tests
371c5c8
refactor: streamline governance credential encoding and decoding methods
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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, | ||
|
|
@@ -36,6 +48,8 @@ | |
| "RegDRepCert", | ||
| "UnregDRepCertificate", | ||
| "UpdateDRepCertificate", | ||
| "GovernanceCredential", | ||
| "GovernanceKeyType", | ||
| ] | ||
|
|
||
| from pycardano.pool_params import PoolParams | ||
|
|
@@ -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 | ||
|
|
@@ -135,13 +305,28 @@ class DRep(ArrayCBORSerializable): | |
| ) | ||
| """The credential associated with this DRep, if applicable""" | ||
|
|
||
| def id(self, id_format: IdFormat = IdFormat.CIP129) -> str: | ||
| """ | ||
| DRep ID. | ||
| """ | ||
| return self.encode(id_format) | ||
|
|
||
| def id_hex(self, id_format: IdFormat = IdFormat.CIP129) -> str: | ||
|
||
| """ | ||
| 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])) | ||
|
|
@@ -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): | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would move
id_formatto an internal field of DRep. Doing so would lead to a better alignment with howAddresstype work today.For example, an address has the following usage pattern:
There are two essential calls:
str(addr)andbytes(addr).str(addr)is implemented by__repr__, andbytes(addr)is implemented by__bytes__.Similarly, it would be great if DRep could stick to this usage pattern.
Something like this: