Skip to content

Reconstructable deterministic constrids #272

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 8 commits into from
Oct 23, 2023
46 changes: 41 additions & 5 deletions pycardano/plutus.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import inspect
import json
import typing
from dataclasses import dataclass, field, fields
from enum import Enum
from hashlib import sha256
Expand Down Expand Up @@ -448,6 +449,45 @@ def get_tag(constr_id: int) -> Optional[int]:
return None


def id_map(cls, skip_constructor=False):
"""
Constructs a unique representation of a PlutusData type definition.
Intended for automatic constructor generation.
"""
if cls == bytes or cls == ByteString:
return "bytes"
if cls == int:
return "int"
if cls == RawCBOR or cls == RawPlutusData or cls == Datum:
return "any"
if cls == IndefiniteList:
return "list"
if hasattr(cls, "__origin__"):
origin = getattr(cls, "__origin__")
if origin == list:
prefix = "list"
elif origin == dict:
prefix = "map"
elif origin == typing.Union:
prefix = "union"
else:
raise TypeError(
f"Unexpected parameterized type for automatic constructor generation: {cls}"
)
return prefix + "<" + ",".join(id_map(a) for a in cls.__args__) + ">"
if issubclass(cls, PlutusData):
return (
"cons["
+ cls.__name__
+ "]("
+ (str(cls.CONSTR_ID) if not skip_constructor else "_")
+ ";"
+ ",".join(f.name + ":" + id_map(f.type) for f in fields(cls))
+ ")"
)
raise TypeError(f"Unexpected type for automatic constructor generation: {cls}")


@dataclass(repr=False)
class PlutusData(ArrayCBORSerializable):
"""
Expand Down Expand Up @@ -481,11 +521,7 @@ def CONSTR_ID(cls):
"""
k = f"_CONSTR_ID_{cls.__name__}"
if not hasattr(cls, k):
det_string = (
cls.__name__
+ "*"
+ "*".join([f"{f.name}~{f.type}" for f in fields(cls)])
)
det_string = id_map(cls, skip_constructor=True)
det_hash = sha256(det_string.encode("utf8")).hexdigest()
setattr(cls, k, int(det_hash, 16) % 2**32)

Expand Down
48 changes: 38 additions & 10 deletions test/pycardano/test_plutus.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@
Redeemer,
RedeemerTag,
plutus_script_hash,
id_map,
Datum,
Unit,
)
from pycardano.serialization import ByteString, IndefiniteList
from pycardano.serialization import IndefiniteList, RawCBOR, ByteString


@dataclass
Expand Down Expand Up @@ -206,10 +209,8 @@ def test_plutus_data_from_json_wrong_data_structure_type():

def test_plutus_data_hash():
assert (
bytes.fromhex(
"19d31e4f3aa9b03ad93b64c8dd2cc822d247c21e2c22762b7b08e6cadfeddb47"
)
== PlutusData().hash().payload
"923918e403bf43c34b4ef6b48eb2ee04babed17320d8d1b9ff9ad086e86f44ec"
== Unit().hash().payload.hex()
)


Expand Down Expand Up @@ -398,20 +399,47 @@ class A(PlutusData):
), "Same class has different default constructor id in two consecutive runs"


def test_id_map_supports_all():
@dataclass
class A(PlutusData):
CONSTR_ID = 0
a: int
b: bytes
c: ByteString
d: List[int]

@dataclass
class C(PlutusData):
x: RawPlutusData
y: RawCBOR
z: Datum
w: IndefiniteList

@dataclass
class B(PlutusData):
a: int
c: A
d: Dict[bytes, C]
e: Union[A, C]

s = id_map(B)
assert (
s
== "cons[B](3809077817;a:int,c:cons[A](0;a:int,b:bytes,c:bytes,d:list<int>),d:map<bytes,cons[C](892310804;x:any,y:any,z:any,w:list)>,e:union<cons[A](0;a:int,b:bytes,c:bytes,d:list<int>),cons[C](892310804;x:any,y:any,z:any,w:list)>)"
)


def test_plutus_data_long_bytes():
@dataclass
class A(PlutusData):
CONSTR_ID = 0
a: ByteString

quote = (
"The line separating good and evil passes ... right through every human heart."
)

quote_hex = (
"d866821a8e5890cf9f5f5840546865206c696e652073657061726174696e6720676f6f6420616"
"e64206576696c20706173736573202e2e2e207269676874207468726f7567682065766572794d"
"2068756d616e2068656172742effff"
)
quote_hex = "d8799f5f5840546865206c696e652073657061726174696e6720676f6f6420616e64206576696c20706173736573202e2e2e207269676874207468726f7567682065766572794d2068756d616e2068656172742effff"

A_tmp = A(ByteString(quote.encode()))

Expand Down