Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 32 additions & 6 deletions pycardano/plutus.py
Original file line number Diff line number Diff line change
Expand Up @@ -804,17 +804,43 @@ class RawPlutusData(CBORSerializable):
data: RawDatum

def to_primitive(self) -> Primitive:
def _dfs(obj):
if isinstance(obj, list) and obj:
def _dfs(obj, preserve=False):
# `preserve` tracks decoded structures: a CBOR round trip marks
# indefinite lists as IndefiniteList while definite lists stay
# plain lists, and re-encoding must keep that distinction or a
# restored datum no longer matches its original bytes. Inside an
# IndefiniteList, children are therefore visited in preserve
# mode; structures built by from_dict or by hand keep the
# historical behaviour of wrapping plain lists as indefinite.
if isinstance(obj, IndefiniteList) and obj:
return IndefiniteList([_dfs(item, True) for item in obj])
elif isinstance(obj, list) and obj:
if preserve:
return [_dfs(item, True) for item in obj]
return IndefiniteList([_dfs(item) for item in obj])
elif isinstance(obj, dict):
return {_dfs(k): _dfs(v) for k, v in obj.items()}
elif isinstance(obj, CBORTag) and isinstance(obj.value, list) and obj.value:
return {_dfs(k, preserve): _dfs(v, preserve) for k, v in obj.items()}
elif (
isinstance(obj, CBORTag)
and isinstance(obj.value, (list, IndefiniteList))
and obj.value
):
child_preserve = preserve or isinstance(obj.value, IndefiniteList)
if obj.tag != 102:
value = IndefiniteList([_dfs(item) for item in obj.value])
value = IndefiniteList(
[_dfs(item, child_preserve) for item in obj.value]
)
else:
value = [_dfs(item) for item in obj.value]
value = [_dfs(item, child_preserve) for item in obj.value]
return CBORTag(tag=obj.tag, value=value)
elif isinstance(obj, bytes) and len(obj) > 64:
# The ledger requires byte strings longer than 64 bytes in
# Plutus data to be chunked (indefinite-length encoding).
# from_dict wraps such bytes in ByteString, but a round trip
# through CBOR (e.g. deepcopy or from_cbor) decodes them back
# to plain bytes, so re-wrap them here to keep the chunked
# encoding stable across copies.
return ByteString(obj)
return obj

return _dfs(self.data)
Expand Down
31 changes: 31 additions & 0 deletions test/pycardano/test_plutus.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,37 @@ def test_clone_raw_plutus_data():
assert cloned_tag != tag


def test_raw_plutus_data_long_bytes_stay_chunked():
# Byte strings longer than 64 bytes must keep their chunked
# (indefinite-length) encoding through deepcopy and CBOR round trips,
# otherwise the ledger rejects the datum.
data = RawPlutusData.from_dict(
{"constructor": 0, "fields": [{"bytes": "ab" * 182}]}
)
assert bytes.fromhex("5f5840") in data.to_cbor()
assert copy.deepcopy(data).to_cbor() == data.to_cbor()
assert RawPlutusData.from_cbor(data.to_cbor()).to_cbor() == data.to_cbor()


def test_raw_plutus_data_long_bytes_nested_stay_chunked():
map_data = RawPlutusData.from_dict(
{"map": [{"k": {"bytes": "cd" * 100}, "v": {"bytes": "ef" * 100}}]}
)
list_data = RawPlutusData.from_dict({"list": [{"bytes": "ab" * 65}]})
for datum in (map_data, list_data):
assert RawPlutusData.from_cbor(datum.to_cbor()).to_cbor() == datum.to_cbor()
assert copy.deepcopy(datum).to_cbor() == datum.to_cbor()


def test_raw_plutus_data_short_bytes_stay_definite():
data = RawPlutusData.from_dict(
{"constructor": 1, "fields": [{"bytes": "ab" * 32}, {"int": 42}]}
)
assert b"\x5f" not in data.to_cbor()
assert RawPlutusData.from_cbor(data.to_cbor()).to_cbor() == data.to_cbor()
assert copy.deepcopy(data).to_cbor() == data.to_cbor()


def test_clone_plutus_data():
key_hash = bytes.fromhex("c2ff616e11299d9094ce0a7eb5b7284b705147a822f4ffbd471f971a")
deadline = 1643235300000
Expand Down