Skip to content

Commit a6f76ea

Browse files
bugfix: fixed incorrect bytestring encoding PlutusData (#269)
* bugfix: fixed incorrect bytestring encoding for bytestring longer than 64 bytes * Removed extraneous test encoder * Removed metadata dummy class * Added dummy ByteString class * Added ByteString equality test for bytes * Updated test reference hash * Updated byte encoding errors, added ByteString to as valid PlutusData type * Removed debug print statements * Fixed mypy error for equality checks on ByteString
1 parent 0a95536 commit a6f76ea

File tree

4 files changed

+68
-4
lines changed

4 files changed

+68
-4
lines changed

pycardano/metadata.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def _validate_type_and_size(data):
4141
if len(data) > self.MAX_ITEM_SIZE:
4242
raise InvalidArgumentException(
4343
f"The size of {data} exceeds {self.MAX_ITEM_SIZE} bytes."
44+
"Use pycardano.serialization.ByteString for long bytes."
4445
)
4546
elif isinstance(data, str):
4647
if len(data.encode("utf-8")) > self.MAX_ITEM_SIZE:

pycardano/plutus.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@
1414
from nacl.encoding import RawEncoder
1515
from nacl.hash import blake2b
1616

17-
from pycardano.exception import DeserializeException
17+
from pycardano.exception import DeserializeException, InvalidArgumentException
1818
from pycardano.hash import DATUM_HASH_SIZE, SCRIPT_HASH_SIZE, DatumHash, ScriptHash
1919
from pycardano.nativescript import NativeScript
2020
from pycardano.serialization import (
2121
ArrayCBORSerializable,
22+
ByteString,
2223
CBORSerializable,
2324
DictCBORSerializable,
2425
IndefiniteList,
@@ -468,6 +469,8 @@ class will reduce the complexity of serialization and deserialization tremendous
468469
>>> assert test == Test.from_cbor("d87a9f187b43333231ff")
469470
"""
470471

472+
MAX_BYTES_SIZE = 64
473+
471474
@classproperty
472475
def CONSTR_ID(cls):
473476
"""
@@ -489,13 +492,20 @@ def CONSTR_ID(cls):
489492
return getattr(cls, k)
490493

491494
def __post_init__(self):
492-
valid_types = (PlutusData, dict, IndefiniteList, int, bytes)
495+
valid_types = (PlutusData, dict, IndefiniteList, int, ByteString, bytes)
493496
for f in fields(self):
494497
if inspect.isclass(f.type) and not issubclass(f.type, valid_types):
495498
raise TypeError(
496499
f"Invalid field type: {f.type}. A field in PlutusData should be one of {valid_types}"
497500
)
498501

502+
data = getattr(self, f.name)
503+
if isinstance(data, bytes) and len(data) > 64:
504+
raise InvalidArgumentException(
505+
f"The size of {data} exceeds {self.MAX_BYTES_SIZE} bytes. "
506+
"Use pycardano.serialization.ByteString for long bytes."
507+
)
508+
499509
def to_shallow_primitive(self) -> CBORTag:
500510
primitives: Primitive = super().to_shallow_primitive()
501511
if primitives:
@@ -553,6 +563,8 @@ def _dfs(obj):
553563
return {"int": obj}
554564
elif isinstance(obj, bytes):
555565
return {"bytes": obj.hex()}
566+
elif isinstance(obj, ByteString):
567+
return {"bytes": obj.value.hex()}
556568
elif isinstance(obj, IndefiniteList) or isinstance(obj, list):
557569
return {"list": [_dfs(item) for item in obj]}
558570
elif isinstance(obj, dict):
@@ -667,7 +679,10 @@ def _dfs(obj):
667679
elif "int" in obj:
668680
return obj["int"]
669681
elif "bytes" in obj:
670-
return bytes.fromhex(obj["bytes"])
682+
if len(obj["bytes"]) > 64:
683+
return ByteString(bytes.fromhex(obj["bytes"]))
684+
else:
685+
return bytes.fromhex(obj["bytes"])
671686
elif "list" in obj:
672687
return IndefiniteList([_dfs(item) for item in obj["list"]])
673688
else:

pycardano/serialization.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,22 @@ class IndefiniteFrozenList(FrozenList, IndefiniteList): # type: ignore
6060
pass
6161

6262

63+
@dataclass
64+
class ByteString:
65+
value: bytes
66+
67+
def __hash__(self):
68+
return hash(self.value)
69+
70+
def __eq__(self, other: object):
71+
if isinstance(other, ByteString):
72+
return self.value == other.value
73+
elif isinstance(other, bytes):
74+
return self.value == other
75+
else:
76+
return False
77+
78+
6379
@dataclass
6480
class RawCBOR:
6581
"""A wrapper class for bytes that represents a CBOR value."""
@@ -160,6 +176,7 @@ def default_encoder(
160176
assert isinstance(
161177
value,
162178
(
179+
ByteString,
163180
CBORSerializable,
164181
IndefiniteList,
165182
RawCBOR,
@@ -178,6 +195,15 @@ def default_encoder(
178195
for item in value:
179196
encoder.encode(item)
180197
encoder.write(b"\xff")
198+
elif isinstance(value, ByteString):
199+
if len(value.value) > 64:
200+
encoder.write(b"\x5f")
201+
for i in range(0, len(value.value), 64):
202+
imax = min(i + 64, len(value.value))
203+
encoder.encode(value.value[i:imax])
204+
encoder.write(b"\xff")
205+
else:
206+
encoder.encode(value.value)
181207
elif isinstance(value, RawCBOR):
182208
encoder.write(value.cbor)
183209
elif isinstance(value, FrozenList):

test/pycardano/test_plutus.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
RedeemerTag,
2020
plutus_script_hash,
2121
)
22-
from pycardano.serialization import IndefiniteList
22+
from pycardano.serialization import ByteString, IndefiniteList
2323

2424

2525
@dataclass
@@ -396,3 +396,25 @@ class A(PlutusData):
396396
assert (
397397
res == res2
398398
), "Same class has different default constructor id in two consecutive runs"
399+
400+
401+
def test_plutus_data_long_bytes():
402+
@dataclass
403+
class A(PlutusData):
404+
a: ByteString
405+
406+
quote = (
407+
"The line separating good and evil passes ... right through every human heart."
408+
)
409+
410+
quote_hex = (
411+
"d866821a8e5890cf9f5f5840546865206c696e652073657061726174696e6720676f6f6420616"
412+
"e64206576696c20706173736573202e2e2e207269676874207468726f7567682065766572794d"
413+
"2068756d616e2068656172742effff"
414+
)
415+
416+
A_tmp = A(ByteString(quote.encode()))
417+
418+
assert (
419+
A_tmp.to_cbor_hex() == quote_hex
420+
), "Long metadata bytestring is encoded incorrectly."

0 commit comments

Comments
 (0)