Skip to content

Commit

Permalink
Reconstructable deterministic constrids (#272)
Browse files Browse the repository at this point in the history
* Change the constructor id computation to be reproducible by third
parties

* Add test to check that id_map supports complex types

* dict -> map and support indefinitelist, Datum

* QA

* Fix hash of unit

* Add support for ByteString type and fix constructor id for test

* Add support for ByteString type in PlutusData
  • Loading branch information
nielstron authored Oct 23, 2023
1 parent 2f17b9a commit af8afdc
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 15 deletions.
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

0 comments on commit af8afdc

Please sign in to comment.