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
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -447,6 +448,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:
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}")
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is nice. Is there a language-agnostic documentation for it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No. Basically, this is the reference implementation for this kind of constructor IDs. I could write up a documentation, either in this document (where?), OpShin or in a dedicated CIP

Copy link
Collaborator

Choose a reason for hiding this comment

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

I will leave you to decide. For me, because this can potentially get adopted by other languages, I think a dedicated CIP is the best choice.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have added a CIP here: cardano-foundation/CIPs#608



@dataclass(repr=False)
class PlutusData(ArrayCBORSerializable):
"""
Expand Down Expand Up @@ -478,11 +518,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
40 changes: 35 additions & 5 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 IndefiniteList
from pycardano.serialization import IndefiniteList, RawCBOR


@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 @@ -396,3 +397,32 @@ class A(PlutusData):
assert (
res == res2
), "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: 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](1013743048;a:int,c:cons[A](0;a:int,b:bytes,c: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:list<int>),cons[C](892310804;x:any,y:any,z:any,w:list)>)"
)