Skip to content

bugfix: fixed incorrect bytestring encoding PlutusData #269

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Oct 13, 2023
1 change: 1 addition & 0 deletions pycardano/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def _validate_type_and_size(data):
if len(data) > self.MAX_ITEM_SIZE:
raise InvalidArgumentException(
f"The size of {data} exceeds {self.MAX_ITEM_SIZE} bytes."
"Use pycardano.serialization.ByteString for long bytes."
)
elif isinstance(data, str):
if len(data.encode("utf-8")) > self.MAX_ITEM_SIZE:
Expand Down
21 changes: 18 additions & 3 deletions pycardano/plutus.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@
from nacl.encoding import RawEncoder
from nacl.hash import blake2b

from pycardano.exception import DeserializeException
from pycardano.exception import DeserializeException, InvalidArgumentException
from pycardano.hash import DATUM_HASH_SIZE, SCRIPT_HASH_SIZE, DatumHash, ScriptHash
from pycardano.nativescript import NativeScript
from pycardano.serialization import (
ArrayCBORSerializable,
ByteString,
CBORSerializable,
DictCBORSerializable,
IndefiniteList,
Expand Down Expand Up @@ -468,6 +469,8 @@ class will reduce the complexity of serialization and deserialization tremendous
>>> assert test == Test.from_cbor("d87a9f187b43333231ff")
"""

MAX_BYTES_SIZE = 64

@classproperty
def CONSTR_ID(cls):
"""
Expand All @@ -489,13 +492,20 @@ def CONSTR_ID(cls):
return getattr(cls, k)

def __post_init__(self):
valid_types = (PlutusData, dict, IndefiniteList, int, bytes)
valid_types = (PlutusData, dict, IndefiniteList, int, ByteString, bytes)
for f in fields(self):
if inspect.isclass(f.type) and not issubclass(f.type, valid_types):
raise TypeError(
f"Invalid field type: {f.type}. A field in PlutusData should be one of {valid_types}"
)

data = getattr(self, f.name)
if isinstance(data, bytes) and len(data) > 64:
raise InvalidArgumentException(
f"The size of {data} exceeds {self.MAX_BYTES_SIZE} bytes. "
"Use pycardano.serialization.ByteString for long bytes."
)

def to_shallow_primitive(self) -> CBORTag:
primitives: Primitive = super().to_shallow_primitive()
if primitives:
Expand Down Expand Up @@ -553,6 +563,8 @@ def _dfs(obj):
return {"int": obj}
elif isinstance(obj, bytes):
return {"bytes": obj.hex()}
elif isinstance(obj, ByteString):
return {"bytes": obj.value.hex()}
elif isinstance(obj, IndefiniteList) or isinstance(obj, list):
return {"list": [_dfs(item) for item in obj]}
elif isinstance(obj, dict):
Expand Down Expand Up @@ -667,7 +679,10 @@ def _dfs(obj):
elif "int" in obj:
return obj["int"]
elif "bytes" in obj:
return bytes.fromhex(obj["bytes"])
if len(obj["bytes"]) > 64:
return ByteString(bytes.fromhex(obj["bytes"]))
else:
return bytes.fromhex(obj["bytes"])
elif "list" in obj:
return IndefiniteList([_dfs(item) for item in obj["list"]])
else:
Expand Down
26 changes: 26 additions & 0 deletions pycardano/serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,22 @@ class IndefiniteFrozenList(FrozenList, IndefiniteList): # type: ignore
pass


@dataclass
class ByteString:
value: bytes

def __hash__(self):
return hash(self.value)

def __eq__(self, other: object):
if isinstance(other, ByteString):
return self.value == other.value
elif isinstance(other, bytes):
return self.value == other
else:
return False


@dataclass
class RawCBOR:
"""A wrapper class for bytes that represents a CBOR value."""
Expand Down Expand Up @@ -160,6 +176,7 @@ def default_encoder(
assert isinstance(
value,
(
ByteString,
CBORSerializable,
IndefiniteList,
RawCBOR,
Expand All @@ -178,6 +195,15 @@ def default_encoder(
for item in value:
encoder.encode(item)
encoder.write(b"\xff")
elif isinstance(value, ByteString):
if len(value.value) > 64:
encoder.write(b"\x5f")
for i in range(0, len(value.value), 64):
imax = min(i + 64, len(value.value))
encoder.encode(value.value[i:imax])
encoder.write(b"\xff")
else:
encoder.encode(value.value)
elif isinstance(value, RawCBOR):
encoder.write(value.cbor)
elif isinstance(value, FrozenList):
Expand Down
24 changes: 23 additions & 1 deletion test/pycardano/test_plutus.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
RedeemerTag,
plutus_script_hash,
)
from pycardano.serialization import IndefiniteList
from pycardano.serialization import ByteString, IndefiniteList


@dataclass
Expand Down Expand Up @@ -396,3 +396,25 @@ class A(PlutusData):
assert (
res == res2
), "Same class has different default constructor id in two consecutive runs"


def test_plutus_data_long_bytes():
@dataclass
class A(PlutusData):
a: ByteString

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

quote_hex = (
"d866821a8e5890cf9f5f5840546865206c696e652073657061726174696e6720676f6f6420616"
"e64206576696c20706173736573202e2e2e207269676874207468726f7567682065766572794d"
"2068756d616e2068656172742effff"
)

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

assert (
A_tmp.to_cbor_hex() == quote_hex
), "Long metadata bytestring is encoded incorrectly."