From af8afdc4a6c4c93c1738a7c0fb1c9265616a3b77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Mon, 23 Oct 2023 17:28:50 +0200 Subject: [PATCH] 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 --- pycardano/plutus.py | 46 +++++++++++++++++++++++++++++---- test/pycardano/test_plutus.py | 48 +++++++++++++++++++++++++++-------- 2 files changed, 79 insertions(+), 15 deletions(-) diff --git a/pycardano/plutus.py b/pycardano/plutus.py index 3ee15800..d515118d 100644 --- a/pycardano/plutus.py +++ b/pycardano/plutus.py @@ -4,6 +4,7 @@ import inspect import json +import typing from dataclasses import dataclass, field, fields from enum import Enum from hashlib import sha256 @@ -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): """ @@ -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) diff --git a/test/pycardano/test_plutus.py b/test/pycardano/test_plutus.py index ba3cd992..f16df8fe 100644 --- a/test/pycardano/test_plutus.py +++ b/test/pycardano/test_plutus.py @@ -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 @@ -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() ) @@ -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),d:map,e:union),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()))