Skip to content

Commit af8afdc

Browse files
authored
Reconstructable deterministic constrids (#272)
* 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
1 parent 2f17b9a commit af8afdc

File tree

2 files changed

+79
-15
lines changed

2 files changed

+79
-15
lines changed

pycardano/plutus.py

+41-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import inspect
66
import json
7+
import typing
78
from dataclasses import dataclass, field, fields
89
from enum import Enum
910
from hashlib import sha256
@@ -448,6 +449,45 @@ def get_tag(constr_id: int) -> Optional[int]:
448449
return None
449450

450451

452+
def id_map(cls, skip_constructor=False):
453+
"""
454+
Constructs a unique representation of a PlutusData type definition.
455+
Intended for automatic constructor generation.
456+
"""
457+
if cls == bytes or cls == ByteString:
458+
return "bytes"
459+
if cls == int:
460+
return "int"
461+
if cls == RawCBOR or cls == RawPlutusData or cls == Datum:
462+
return "any"
463+
if cls == IndefiniteList:
464+
return "list"
465+
if hasattr(cls, "__origin__"):
466+
origin = getattr(cls, "__origin__")
467+
if origin == list:
468+
prefix = "list"
469+
elif origin == dict:
470+
prefix = "map"
471+
elif origin == typing.Union:
472+
prefix = "union"
473+
else:
474+
raise TypeError(
475+
f"Unexpected parameterized type for automatic constructor generation: {cls}"
476+
)
477+
return prefix + "<" + ",".join(id_map(a) for a in cls.__args__) + ">"
478+
if issubclass(cls, PlutusData):
479+
return (
480+
"cons["
481+
+ cls.__name__
482+
+ "]("
483+
+ (str(cls.CONSTR_ID) if not skip_constructor else "_")
484+
+ ";"
485+
+ ",".join(f.name + ":" + id_map(f.type) for f in fields(cls))
486+
+ ")"
487+
)
488+
raise TypeError(f"Unexpected type for automatic constructor generation: {cls}")
489+
490+
451491
@dataclass(repr=False)
452492
class PlutusData(ArrayCBORSerializable):
453493
"""
@@ -481,11 +521,7 @@ def CONSTR_ID(cls):
481521
"""
482522
k = f"_CONSTR_ID_{cls.__name__}"
483523
if not hasattr(cls, k):
484-
det_string = (
485-
cls.__name__
486-
+ "*"
487-
+ "*".join([f"{f.name}~{f.type}" for f in fields(cls)])
488-
)
524+
det_string = id_map(cls, skip_constructor=True)
489525
det_hash = sha256(det_string.encode("utf8")).hexdigest()
490526
setattr(cls, k, int(det_hash, 16) % 2**32)
491527

test/pycardano/test_plutus.py

+38-10
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@
1818
Redeemer,
1919
RedeemerTag,
2020
plutus_script_hash,
21+
id_map,
22+
Datum,
23+
Unit,
2124
)
22-
from pycardano.serialization import ByteString, IndefiniteList
25+
from pycardano.serialization import IndefiniteList, RawCBOR, ByteString
2326

2427

2528
@dataclass
@@ -206,10 +209,8 @@ def test_plutus_data_from_json_wrong_data_structure_type():
206209

207210
def test_plutus_data_hash():
208211
assert (
209-
bytes.fromhex(
210-
"19d31e4f3aa9b03ad93b64c8dd2cc822d247c21e2c22762b7b08e6cadfeddb47"
211-
)
212-
== PlutusData().hash().payload
212+
"923918e403bf43c34b4ef6b48eb2ee04babed17320d8d1b9ff9ad086e86f44ec"
213+
== Unit().hash().payload.hex()
213214
)
214215

215216

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

400401

402+
def test_id_map_supports_all():
403+
@dataclass
404+
class A(PlutusData):
405+
CONSTR_ID = 0
406+
a: int
407+
b: bytes
408+
c: ByteString
409+
d: List[int]
410+
411+
@dataclass
412+
class C(PlutusData):
413+
x: RawPlutusData
414+
y: RawCBOR
415+
z: Datum
416+
w: IndefiniteList
417+
418+
@dataclass
419+
class B(PlutusData):
420+
a: int
421+
c: A
422+
d: Dict[bytes, C]
423+
e: Union[A, C]
424+
425+
s = id_map(B)
426+
assert (
427+
s
428+
== "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)>)"
429+
)
430+
431+
401432
def test_plutus_data_long_bytes():
402433
@dataclass
403434
class A(PlutusData):
435+
CONSTR_ID = 0
404436
a: ByteString
405437

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

410-
quote_hex = (
411-
"d866821a8e5890cf9f5f5840546865206c696e652073657061726174696e6720676f6f6420616"
412-
"e64206576696c20706173736573202e2e2e207269676874207468726f7567682065766572794d"
413-
"2068756d616e2068656172742effff"
414-
)
442+
quote_hex = "d8799f5f5840546865206c696e652073657061726174696e6720676f6f6420616e64206576696c20706173736573202e2e2e207269676874207468726f7567682065766572794d2068756d616e2068656172742effff"
415443

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

0 commit comments

Comments
 (0)