From 928a4e4d8e76aa2c8820a721b35121d75b763644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Wed, 4 Oct 2023 22:40:28 +0200 Subject: [PATCH 1/7] Change the constructor id computation to be reproducible by third parties --- pycardano/plutus.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/pycardano/plutus.py b/pycardano/plutus.py index c8562874..79aedd27 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 @@ -446,6 +447,28 @@ def get_tag(constr_id: int) -> Optional[int]: else: 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 hasattr(cls, "__origin__"): + origin = getattr(cls, "__origin__") + if origin == list: + prefix = "list" + elif origin == dict: + prefix = "dict" + 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 "constructor:" + cls.__name__ + "(" + (str(cls.CONSTR_ID) if not skip_constructor else "_") + ";" + ",".join(f.name + ":" + id_map(f.type) for f in fields(cls)) + ")" + if cls == RawCBOR or cls == RawPlutusData: + return "any" + raise TypeError(f"Unexpected type for automatic constructor generation: {cls}") @dataclass(repr=False) class PlutusData(ArrayCBORSerializable): @@ -478,11 +501,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) From cb35ce5dc95fa18871c532cc80cc30ecb50781f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Wed, 4 Oct 2023 22:49:09 +0200 Subject: [PATCH 2/7] Add test to check that id_map supports complex types --- pycardano/plutus.py | 4 ++-- test/pycardano/test_plutus.py | 28 ++++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/pycardano/plutus.py b/pycardano/plutus.py index 79aedd27..687491bb 100644 --- a/pycardano/plutus.py +++ b/pycardano/plutus.py @@ -463,9 +463,9 @@ def id_map(cls, skip_constructor=False): 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__) + ")" + return prefix + "<" + ",".join(id_map(a) for a in cls.__args__) + ">" if issubclass(cls, PlutusData): - return "constructor:" + cls.__name__ + "(" + (str(cls.CONSTR_ID) if not skip_constructor else "_") + ";" + ",".join(f.name + ":" + id_map(f.type) for f in fields(cls)) + ")" + 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)) + ")" if cls == RawCBOR or cls == RawPlutusData: return "any" raise TypeError(f"Unexpected type for automatic constructor generation: {cls}") diff --git a/test/pycardano/test_plutus.py b/test/pycardano/test_plutus.py index f2aba0f9..24e7d62c 100644 --- a/test/pycardano/test_plutus.py +++ b/test/pycardano/test_plutus.py @@ -17,9 +17,9 @@ RawPlutusData, Redeemer, RedeemerTag, - plutus_script_hash, + plutus_script_hash, id_map, ) -from pycardano.serialization import IndefiniteList +from pycardano.serialization import IndefiniteList, RawCBOR @dataclass @@ -396,3 +396,27 @@ 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 + + @dataclass + class B(PlutusData): + a: int + c: A + d: Dict[bytes, C] + e: Union[A, C] + + s = id_map(B, skip_constructor=True) + assert s == "cons[B](_;a:int,c:cons[A](0;a:int,b:bytes,c:list),d:dict,e:union),cons[C](3081122523;x:any,y:any)>)" + assert B.CONSTR_ID == 2561434002 \ No newline at end of file From 29f99a28b822a3e08e3173e18609f2e54a5764ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Wed, 4 Oct 2023 22:56:09 +0200 Subject: [PATCH 3/7] dict -> map and support indefinitelist, Datum --- pycardano/plutus.py | 24 +++++++++++++++++++----- test/pycardano/test_plutus.py | 14 ++++++++++---- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/pycardano/plutus.py b/pycardano/plutus.py index 687491bb..4f24f9e3 100644 --- a/pycardano/plutus.py +++ b/pycardano/plutus.py @@ -447,29 +447,43 @@ def get_tag(constr_id: int) -> Optional[int]: else: 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 = "dict" + prefix = "map" elif origin == typing.Union: prefix = "union" else: - raise TypeError(f"Unexpected parameterized type for automatic constructor generation: {cls}") + 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)) + ")" - if cls == RawCBOR or cls == RawPlutusData: - return "any" + 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): """ diff --git a/test/pycardano/test_plutus.py b/test/pycardano/test_plutus.py index 24e7d62c..bf4ed035 100644 --- a/test/pycardano/test_plutus.py +++ b/test/pycardano/test_plutus.py @@ -17,7 +17,8 @@ RawPlutusData, Redeemer, RedeemerTag, - plutus_script_hash, id_map, + plutus_script_hash, + id_map, Datum, ) from pycardano.serialization import IndefiniteList, RawCBOR @@ -397,6 +398,7 @@ class A(PlutusData): res == res2 ), "Same class has different default constructor id in two consecutive runs" + def test_id_map_supports_all(): @dataclass class A(PlutusData): @@ -409,6 +411,8 @@ class A(PlutusData): class C(PlutusData): x: RawPlutusData y: RawCBOR + z: Datum + w: IndefiniteList @dataclass class B(PlutusData): @@ -417,6 +421,8 @@ class B(PlutusData): d: Dict[bytes, C] e: Union[A, C] - s = id_map(B, skip_constructor=True) - assert s == "cons[B](_;a:int,c:cons[A](0;a:int,b:bytes,c:list),d:dict,e:union),cons[C](3081122523;x:any,y:any)>)" - assert B.CONSTR_ID == 2561434002 \ No newline at end of file + s = id_map(B) + assert ( + s + == "cons[B](1013743048;a:int,c:cons[A](0;a:int,b:bytes,c:list),d:map,e:union),cons[C](892310804;x:any,y:any,z:any,w:list)>)" + ) From 00a9a70c9f7a3ac88ccdabd784a3b4fcb2c81dfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Wed, 4 Oct 2023 22:57:06 +0200 Subject: [PATCH 4/7] QA --- pycardano/plutus.py | 5 ++++- test/pycardano/test_plutus.py | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pycardano/plutus.py b/pycardano/plutus.py index 4f24f9e3..86e0d503 100644 --- a/pycardano/plutus.py +++ b/pycardano/plutus.py @@ -449,7 +449,10 @@ def get_tag(constr_id: int) -> Optional[int]: def id_map(cls, skip_constructor=False): - """Constructs a unique representation of a PlutusData type definition. Intended for automatic constructor generation.""" + """ + Constructs a unique representation of a PlutusData type definition. + Intended for automatic constructor generation. + """ if cls == bytes: return "bytes" if cls == int: diff --git a/test/pycardano/test_plutus.py b/test/pycardano/test_plutus.py index bf4ed035..842d0593 100644 --- a/test/pycardano/test_plutus.py +++ b/test/pycardano/test_plutus.py @@ -18,7 +18,8 @@ Redeemer, RedeemerTag, plutus_script_hash, - id_map, Datum, + id_map, + Datum, ) from pycardano.serialization import IndefiniteList, RawCBOR From 955bab87998356940888569403bde41868276b3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Wed, 4 Oct 2023 23:04:47 +0200 Subject: [PATCH 5/7] Fix hash of unit --- test/pycardano/test_plutus.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/pycardano/test_plutus.py b/test/pycardano/test_plutus.py index 842d0593..fb56724f 100644 --- a/test/pycardano/test_plutus.py +++ b/test/pycardano/test_plutus.py @@ -20,6 +20,7 @@ plutus_script_hash, id_map, Datum, + Unit, ) from pycardano.serialization import IndefiniteList, RawCBOR @@ -208,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() ) From 789b07763cb4396e47c9bba506582e58565a6e5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Mon, 23 Oct 2023 15:09:05 +0200 Subject: [PATCH 6/7] Add support for ByteString type and fix constructor id for test --- pycardano/plutus.py | 2 +- test/pycardano/test_plutus.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pycardano/plutus.py b/pycardano/plutus.py index 67547d09..d515118d 100644 --- a/pycardano/plutus.py +++ b/pycardano/plutus.py @@ -454,7 +454,7 @@ def id_map(cls, skip_constructor=False): Constructs a unique representation of a PlutusData type definition. Intended for automatic constructor generation. """ - if cls == bytes: + if cls == bytes or cls == ByteString: return "bytes" if cls == int: return "int" diff --git a/test/pycardano/test_plutus.py b/test/pycardano/test_plutus.py index 3342b04a..dd4a4d25 100644 --- a/test/pycardano/test_plutus.py +++ b/test/pycardano/test_plutus.py @@ -431,6 +431,7 @@ class B(PlutusData): def test_plutus_data_long_bytes(): @dataclass class A(PlutusData): + CONSTR_ID = 0 a: ByteString quote = ( @@ -438,9 +439,7 @@ class A(PlutusData): ) quote_hex = ( - "d866821a8e5890cf9f5f5840546865206c696e652073657061726174696e6720676f6f6420616" - "e64206576696c20706173736573202e2e2e207269676874207468726f7567682065766572794d" - "2068756d616e2068656172742effff" + "d8799f5f5840546865206c696e652073657061726174696e6720676f6f6420616e64206576696c20706173736573202e2e2e207269676874207468726f7567682065766572794d2068756d616e2068656172742effff" ) A_tmp = A(ByteString(quote.encode())) From 2809b342f685c832c887f91d58aacd16e502f5cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Mon, 23 Oct 2023 15:10:33 +0200 Subject: [PATCH 7/7] Add support for ByteString type in PlutusData --- test/pycardano/test_plutus.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test/pycardano/test_plutus.py b/test/pycardano/test_plutus.py index dd4a4d25..f16df8fe 100644 --- a/test/pycardano/test_plutus.py +++ b/test/pycardano/test_plutus.py @@ -405,7 +405,8 @@ class A(PlutusData): CONSTR_ID = 0 a: int b: bytes - c: List[int] + c: ByteString + d: List[int] @dataclass class C(PlutusData): @@ -424,7 +425,7 @@ class B(PlutusData): s = id_map(B) assert ( s - == "cons[B](1013743048;a:int,c:cons[A](0;a:int,b:bytes,c:list),d:map,e:union),cons[C](892310804;x:any,y:any,z:any,w:list)>)" + == "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)>)" ) @@ -438,9 +439,7 @@ class A(PlutusData): "The line separating good and evil passes ... right through every human heart." ) - quote_hex = ( - "d8799f5f5840546865206c696e652073657061726174696e6720676f6f6420616e64206576696c20706173736573202e2e2e207269676874207468726f7567682065766572794d2068756d616e2068656172742effff" - ) + quote_hex = "d8799f5f5840546865206c696e652073657061726174696e6720676f6f6420616e64206576696c20706173736573202e2e2e207269676874207468726f7567682065766572794d2068756d616e2068656172742effff" A_tmp = A(ByteString(quote.encode()))