Skip to content

Commit 0d5b3d7

Browse files
nielstroncffls
andauthored
Implement new default constr ID (#262)
* Implement new default constr ID * Formatting * Fix plutus data hash * Introduce Unit default empty constructor * Formatting much * Add test for constructor id uniqueness * Add test for determinism of constructor id * Remove unused imports * Fix integration test * Cache CONSTR_ID * Avoid using _CONSTR_ID from parent class by adding class name to _CONSTR_ID * Add a flag to enable/disable auto CONSTR_ID * Revert "Add a flag to enable/disable auto CONSTR_ID" This reverts commit 787e160. --------- Co-authored-by: Jerry <[email protected]>
1 parent a88d725 commit 0d5b3d7

File tree

4 files changed

+123
-12
lines changed

4 files changed

+123
-12
lines changed

integration-test/test/test_plutus.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def test_plutus_v1(self):
2626

2727
builder = TransactionBuilder(self.chain_context)
2828
builder.add_input_address(giver_address)
29-
datum = PlutusData() # A Unit type "()" in Haskell
29+
datum = Unit() # A Unit type "()" in Haskell
3030
builder.add_output(
3131
TransactionOutput(script_address, 50000000, datum_hash=datum_hash(datum))
3232
)

pycardano/plutus.py

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
import json
77
from dataclasses import dataclass, field, fields
88
from enum import Enum
9-
from typing import Any, ClassVar, Optional, Type, Union
9+
from hashlib import sha256
10+
from typing import Any, Optional, Type, Union
1011

1112
import cbor2
1213
from cbor2 import CBORTag
@@ -44,9 +45,16 @@
4445
"datum_hash",
4546
"plutus_script_hash",
4647
"script_hash",
48+
"Unit",
4749
]
4850

4951

52+
# taken from https://stackoverflow.com/a/13624858
53+
class classproperty(property):
54+
def __get__(self, owner_self, owner_cls):
55+
return self.fget(owner_cls)
56+
57+
5058
class CostModels(DictCBORSerializable):
5159
KEY_TYPE = int
5260
VALUE_TYPE = dict
@@ -460,9 +468,25 @@ class will reduce the complexity of serialization and deserialization tremendous
460468
>>> assert test == Test.from_cbor("d87a9f187b43333231ff")
461469
"""
462470

463-
CONSTR_ID: ClassVar[int] = 0
464-
"""Constructor ID of this plutus data.
465-
It is primarily used by Plutus core to reconstruct a data structure from serialized CBOR bytes."""
471+
@classproperty
472+
def CONSTR_ID(cls):
473+
"""
474+
Constructor ID of this plutus data.
475+
It is primarily used by Plutus core to reconstruct a data structure from serialized CBOR bytes.
476+
The default implementation is an almost unique, deterministic constructor ID in the range 1 - 2^32 based
477+
on class attributes, types and class name.
478+
"""
479+
k = f"_CONSTR_ID_{cls.__name__}"
480+
if not hasattr(cls, k):
481+
det_string = (
482+
cls.__name__
483+
+ "*"
484+
+ "*".join([f"{f.name}~{f.type}" for f in fields(cls)])
485+
)
486+
det_hash = sha256(det_string.encode("utf8")).hexdigest()
487+
setattr(cls, k, int(det_hash, 16) % 2**32)
488+
489+
return getattr(cls, k)
466490

467491
def __post_init__(self):
468492
valid_types = (PlutusData, dict, IndefiniteList, int, bytes)
@@ -820,3 +844,10 @@ def script_hash(script: ScriptType) -> ScriptHash:
820844
)
821845
else:
822846
raise TypeError(f"Unexpected script type: {type(script)}")
847+
848+
849+
@dataclass
850+
class Unit(PlutusData):
851+
"""The default "Unit type" with a 0 constructor ID"""
852+
853+
CONSTR_ID = 0

test/pycardano/test_plutus.py

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import copy
2-
import unittest
2+
import subprocess
3+
import sys
4+
import tempfile
35
from dataclasses import dataclass
46
from test.pycardano.util import check_two_way_cbor
57
from typing import Dict, List, Union
68

79
import pytest
810
from cbor2 import CBORTag
911

10-
from pycardano.exception import DeserializeException, SerializeException
12+
from pycardano.exception import DeserializeException
1113
from pycardano.plutus import (
1214
COST_MODELS,
1315
ExecutionUnits,
@@ -51,6 +53,7 @@ class DictTest(PlutusData):
5153

5254
@dataclass
5355
class ListTest(PlutusData):
56+
CONSTR_ID = 0
5457
a: List[LargestTest]
5558

5659

@@ -204,7 +207,7 @@ def test_plutus_data_from_json_wrong_data_structure_type():
204207
def test_plutus_data_hash():
205208
assert (
206209
bytes.fromhex(
207-
"923918e403bf43c34b4ef6b48eb2ee04babed17320d8d1b9ff9ad086e86f44ec"
210+
"19d31e4f3aa9b03ad93b64c8dd2cc822d247c21e2c22762b7b08e6cadfeddb47"
208211
)
209212
== PlutusData().hash().payload
210213
)
@@ -316,3 +319,80 @@ def test_clone_plutus_data():
316319
my_vesting.deadline = 1643235300001
317320

318321
assert cloned_vesting != my_vesting
322+
323+
324+
def test_unique_constr_ids():
325+
@dataclass
326+
class A(PlutusData):
327+
pass
328+
329+
@dataclass
330+
class B(PlutusData):
331+
pass
332+
333+
assert (
334+
A.CONSTR_ID != B.CONSTR_ID
335+
), "Different classes (different names) have same default constructor ID"
336+
B_tmp = B
337+
338+
@dataclass
339+
class B(PlutusData):
340+
a: int
341+
b: bytes
342+
343+
assert (
344+
B_tmp.CONSTR_ID != B.CONSTR_ID
345+
), "Different classes (different fields) have same default constructor ID"
346+
347+
B_tmp = B
348+
349+
@dataclass
350+
class B(PlutusData):
351+
a: bytes
352+
b: bytes
353+
354+
assert (
355+
B_tmp.CONSTR_ID != B.CONSTR_ID
356+
), "Different classes (different field types) have same default constructor ID"
357+
358+
359+
def test_deterministic_constr_ids_local():
360+
@dataclass
361+
class A(PlutusData):
362+
a: int
363+
b: bytes
364+
365+
A_tmp = A
366+
367+
@dataclass
368+
class A(PlutusData):
369+
a: int
370+
b: bytes
371+
372+
assert (
373+
A_tmp.CONSTR_ID == A.CONSTR_ID
374+
), "Same class has different default constructor ID"
375+
376+
377+
def test_deterministic_constr_ids_global():
378+
code = """
379+
from dataclasses import dataclass
380+
from pycardano import PlutusData
381+
382+
@dataclass
383+
class A(PlutusData):
384+
a: int
385+
b: bytes
386+
387+
print(A.CONSTR_ID)
388+
"""
389+
tmpfile = tempfile.TemporaryFile()
390+
tmpfile.write(code.encode("utf8"))
391+
tmpfile.seek(0)
392+
res = subprocess.run([sys.executable], stdin=tmpfile, capture_output=True).stdout
393+
tmpfile.seek(0)
394+
res2 = subprocess.run([sys.executable], stdin=tmpfile, capture_output=True).stdout
395+
396+
assert (
397+
res == res2
398+
), "Same class has different default constructor id in two consecutive runs"

test/pycardano/test_util.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from test.pycardano.util import chain_context
22

33
from pycardano.hash import SCRIPT_HASH_SIZE, ScriptDataHash
4-
from pycardano.plutus import ExecutionUnits, PlutusData, Redeemer, RedeemerTag
4+
from pycardano.plutus import ExecutionUnits, PlutusData, Redeemer, RedeemerTag, Unit
55
from pycardano.transaction import Value
66
from pycardano.utils import min_lovelace_pre_alonzo, script_data_hash
77

@@ -145,7 +145,7 @@ def test_min_lovelace_multi_asset_9(self, chain_context):
145145

146146

147147
def test_script_data_hash():
148-
unit = PlutusData()
148+
unit = Unit()
149149
redeemers = [Redeemer(unit, ExecutionUnits(1000000, 1000000))]
150150
redeemers[0].tag = RedeemerTag.SPEND
151151
assert ScriptDataHash.from_primitive(
@@ -154,14 +154,14 @@ def test_script_data_hash():
154154

155155

156156
def test_script_data_hash_datum_only():
157-
unit = PlutusData()
157+
unit = Unit()
158158
assert ScriptDataHash.from_primitive(
159159
"2f50ea2546f8ce020ca45bfcf2abeb02ff18af2283466f888ae489184b3d2d39"
160160
) == script_data_hash(redeemers=[], datums=[unit])
161161

162162

163163
def test_script_data_hash_redeemer_only():
164-
unit = PlutusData()
164+
unit = Unit()
165165
redeemers = []
166166
assert ScriptDataHash.from_primitive(
167167
"a88fe2947b8d45d1f8b798e52174202579ecf847b8f17038c7398103df2d27b0"

0 commit comments

Comments
 (0)