Skip to content

Commit 40e18d5

Browse files
KINGH242Hareem Adderley
andauthored
Expand Bech32 usage (#473)
* Add CIP129_PAYLOAD_SIZE constant to hash.py * Add PoolOperator class for handling Bech32 pool key hashes * Add GovernanceCredential and DRepCredential classes with Bech32 encoding/decoding support * Add CommitteeHotCredential class and Bech32 encoding/decoding for governance action IDs * Add tests for new encoding/decoding functionality * style: fix black check issues * refactor: simplify PoolOperator ID methods and update tests * refactor: enhance GovActionId encoding/decoding and improve tests * refactor: streamline governance credential encoding and decoding methods --------- Co-authored-by: Hareem Adderley <[email protected]>
1 parent ac5bc75 commit 40e18d5

File tree

7 files changed

+842
-15
lines changed

7 files changed

+842
-15
lines changed

pycardano/certificate.py

Lines changed: 240 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,20 @@
44
from enum import Enum, unique
55
from typing import Optional, Tuple, Type, Union
66

7-
from pycardano.exception import DeserializeException
8-
from pycardano.hash import AnchorDataHash, PoolKeyHash, ScriptHash, VerificationKeyHash
7+
from pycardano.crypto.bech32 import bech32_decode, convertbits, encode
8+
from pycardano.exception import (
9+
DecodingException,
10+
DeserializeException,
11+
SerializeException,
12+
)
13+
from pycardano.hash import (
14+
CIP129_PAYLOAD_SIZE,
15+
VERIFICATION_KEY_HASH_SIZE,
16+
AnchorDataHash,
17+
PoolKeyHash,
18+
ScriptHash,
19+
VerificationKeyHash,
20+
)
921
from pycardano.serialization import (
1022
ArrayCBORSerializable,
1123
CodedSerializable,
@@ -36,6 +48,8 @@
3648
"RegDRepCert",
3749
"UnregDRepCertificate",
3850
"UpdateDRepCertificate",
51+
"GovernanceCredential",
52+
"GovernanceKeyType",
3953
]
4054

4155
from pycardano.pool_params import PoolParams
@@ -92,15 +106,162 @@ def __hash__(self):
92106
return hash(self.to_cbor())
93107

94108

109+
class IdFormat(Enum):
110+
"""
111+
Id format definition.
112+
"""
113+
114+
CIP129 = "cip129"
115+
CIP105 = "cip105"
116+
117+
118+
class CredentialType(Enum):
119+
"""
120+
Credential type definition.
121+
"""
122+
123+
KEY_HASH = 0b0010
124+
"""Key hash"""
125+
126+
SCRIPT_HASH = 0b0011
127+
"""Script hash"""
128+
129+
130+
class GovernanceKeyType(Enum):
131+
"""
132+
Governance key type definition.
133+
"""
134+
135+
CC_HOT = 0b0000
136+
"""Committee cold hot key"""
137+
138+
CC_COLD = 0b0001
139+
"""Committee cold key"""
140+
141+
DREP = 0b0010
142+
"""DRep key"""
143+
144+
145+
@dataclass(repr=False)
146+
class GovernanceCredential(StakeCredential):
147+
"""Represents a governance credential."""
148+
149+
governance_key_type: GovernanceKeyType = field(init=False)
150+
"""Governance key type."""
151+
152+
id_format: IdFormat = field(default=IdFormat.CIP129, compare=False)
153+
"""Id format."""
154+
155+
def __repr__(self):
156+
return f"{self.encode()}"
157+
158+
def __bytes__(self):
159+
if self.id_format == IdFormat.CIP129:
160+
return self._compute_header_byte() + bytes(self.credential.payload)
161+
else:
162+
return bytes(self.credential.payload)
163+
164+
@property
165+
def credential_type(self) -> CredentialType:
166+
"""Credential type."""
167+
if isinstance(self.credential, VerificationKeyHash):
168+
return CredentialType.KEY_HASH
169+
else:
170+
return CredentialType.SCRIPT_HASH
171+
172+
def _compute_header_byte(self) -> bytes:
173+
"""Compute the header byte."""
174+
return (
175+
self.governance_key_type.value << 4 | self.credential_type.value
176+
).to_bytes(1, byteorder="big")
177+
178+
def _compute_hrp(self, id_format: IdFormat = IdFormat.CIP129) -> str:
179+
"""Compute human-readable prefix for bech32 encoder.
180+
181+
Based on
182+
`miscellaneous section <https://github.com/cardano-foundation/CIPs/tree/master/CIP-0005#miscellaneous>`_
183+
in CIP-5.
184+
"""
185+
prefix = ""
186+
if self.governance_key_type == GovernanceKeyType.CC_HOT:
187+
prefix = "cc_hot"
188+
elif self.governance_key_type == GovernanceKeyType.CC_COLD:
189+
prefix = "cc_cold"
190+
elif self.governance_key_type == GovernanceKeyType.DREP:
191+
prefix = "drep"
192+
193+
suffix = ""
194+
if isinstance(self.credential, VerificationKeyHash):
195+
suffix = ""
196+
elif isinstance(self.credential, ScriptHash):
197+
suffix = "_script"
198+
199+
return prefix + suffix if id_format == IdFormat.CIP105 else prefix
200+
201+
def encode(self) -> str:
202+
"""Encode the governance credential in Bech32 format.
203+
204+
More info about Bech32 `here <https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#Bech32>`_.
205+
206+
Returns:
207+
str: Encoded governance credential in Bech32 format.
208+
"""
209+
data = bytes(self)
210+
return encode(self._compute_hrp(self.id_format), data)
211+
212+
@classmethod
213+
def decode(cls: Type[GovernanceCredential], data: str) -> GovernanceCredential:
214+
"""Decode a bech32 string into a governance credential object.
215+
216+
Args:
217+
data (str): Bech32-encoded string.
218+
219+
Returns:
220+
GovernanceCredential: Decoded governance credential.
221+
222+
Raises:
223+
DecodingException: When the input string is not a valid governance credential.
224+
"""
225+
hrp, checksum, _ = bech32_decode(data)
226+
value = bytes(convertbits(checksum, 5, 8, False))
227+
if len(value) == VERIFICATION_KEY_HASH_SIZE:
228+
# CIP-105
229+
if "script" in hrp:
230+
return cls(credential=ScriptHash(value))
231+
else:
232+
return cls(credential=VerificationKeyHash(value))
233+
elif len(value) == CIP129_PAYLOAD_SIZE:
234+
header = value[0]
235+
payload = value[1:]
236+
237+
key_type = GovernanceKeyType((header & 0xF0) >> 4)
238+
credential_type = CredentialType(header & 0x0F)
239+
240+
if key_type != cls.governance_key_type:
241+
raise DecodingException(f"Invalid key type: {key_type}")
242+
243+
if credential_type == CredentialType.KEY_HASH:
244+
return cls(credential=VerificationKeyHash(payload))
245+
elif credential_type == CredentialType.SCRIPT_HASH:
246+
return cls(credential=ScriptHash(payload))
247+
else:
248+
raise DecodingException(f"Invalid credential type: {credential_type}")
249+
else:
250+
raise DecodingException(f"Invalid data length: {len(value)}")
251+
252+
def to_primitive(self):
253+
return [self._CODE, self.credential.to_primitive()]
254+
255+
95256
@dataclass(repr=False)
96-
class DRepCredential(StakeCredential):
257+
class DRepCredential(GovernanceCredential):
97258
"""Represents a Delegate Representative (DRep) credential.
98259
99260
This credential type is specifically used for DReps in the governance system,
100-
inheriting from StakeCredential.
261+
inheriting from GovernanceCredential.
101262
"""
102263

103-
pass
264+
governance_key_type: GovernanceKeyType = GovernanceKeyType.DREP
104265

105266

106267
@unique
@@ -135,13 +296,26 @@ class DRep(ArrayCBORSerializable):
135296
)
136297
"""The credential associated with this DRep, if applicable"""
137298

299+
id_format: IdFormat = field(default=IdFormat.CIP129, compare=False)
300+
301+
def __repr__(self):
302+
return f"{self.encode()}"
303+
304+
def __bytes__(self):
305+
if self.credential is not None:
306+
drep_credential = DRepCredential(
307+
credential=self.credential, id_format=self.id_format
308+
)
309+
return bytes(drep_credential)
310+
return b""
311+
138312
@classmethod
139313
@limit_primitive_type(list)
140314
def from_primitive(cls: Type[DRep], values: Union[list, tuple]) -> DRep:
141315
try:
142316
kind = DRepKind(values[0])
143-
except ValueError:
144-
raise DeserializeException(f"Invalid DRep type {values[0]}")
317+
except ValueError as e:
318+
raise DeserializeException(f"Invalid DRep type {values[0]}") from e
145319

146320
if kind == DRepKind.VERIFICATION_KEY_HASH:
147321
return cls(kind=kind, credential=VerificationKeyHash(values[1]))
@@ -159,6 +333,65 @@ def to_primitive(self):
159333
return [self.kind.value, self.credential.to_primitive()]
160334
return [self.kind.value]
161335

336+
def encode(self) -> str:
337+
"""Encode the DRep in Bech32 format.
338+
339+
More info about Bech32 `here <https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#Bech32>`_.
340+
341+
Returns:
342+
str: Encoded DRep in Bech32 format.
343+
344+
Examples:
345+
>>> vkey_bytes = bytes.fromhex("00000000000000000000000000000000000000000000000000000000")
346+
>>> credential = VerificationKeyHash(vkey_bytes)
347+
>>> print(DRep(kind=DRepKind.VERIFICATION_KEY_HASH, credential=credential).encode())
348+
drep1ygqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq7vlc9n
349+
"""
350+
if self.kind == DRepKind.ALWAYS_ABSTAIN:
351+
return "drep_always_abstain"
352+
elif self.kind == DRepKind.ALWAYS_NO_CONFIDENCE:
353+
return "drep_always_no_confidence"
354+
elif self.credential is not None:
355+
drep_credential = DRepCredential(
356+
credential=self.credential, id_format=self.id_format
357+
)
358+
return drep_credential.encode()
359+
else:
360+
raise SerializeException("DRep credential is None")
361+
362+
@classmethod
363+
def decode(cls: Type[DRep], data: str) -> DRep:
364+
"""Decode a bech32 string into a DRep object.
365+
366+
Args:
367+
data (str): Bech32-encoded string.
368+
369+
Returns:
370+
DRep: Decoded DRep.
371+
372+
Raises:
373+
DecodingException: When the input string is not a valid DRep.
374+
375+
Examples:
376+
>>> credential = DRep.decode("drep1ygqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq7vlc9n")
377+
>>> khash = VerificationKeyHash(bytes.fromhex("00000000000000000000000000000000000000000000000000000000"))
378+
>>> assert credential == DRep(DRepKind.VERIFICATION_KEY_HASH, khash)
379+
"""
380+
if data == "drep_always_abstain":
381+
return cls(kind=DRepKind.ALWAYS_ABSTAIN)
382+
elif data == "drep_always_no_confidence":
383+
return cls(kind=DRepKind.ALWAYS_NO_CONFIDENCE)
384+
else:
385+
drep_credential = DRepCredential.decode(data)
386+
return cls(
387+
kind=(
388+
DRepKind.VERIFICATION_KEY_HASH
389+
if isinstance(drep_credential.credential, VerificationKeyHash)
390+
else DRepKind.SCRIPT_HASH
391+
),
392+
credential=drep_credential.credential,
393+
)
394+
162395

163396
@dataclass(repr=False)
164397
class StakeRegistration(CodedSerializable):

0 commit comments

Comments
 (0)