Skip to content

calldata.decode() returns memoryview for TYPE_BYTES, breaking consumers that expect bytes (crashes to_str()) #108

Description

@midasbal

calldata.decode() is annotated to return CalldataEncodable, whose union includes bytes. But for a TYPE_BYTES field it returns a memoryview, not bytes, and several consumers depend on the value actually being bytes.

In genlayer_py/abi/calldata/decoder.py, mem is a memoryview, and slicing a memoryview yields another memoryview:

elif typ == consts.TYPE_BYTES:
    ret_bytes = mem[:code]
    mem = mem[code:]
    return ret_bytes
>>> isinstance(memoryview(b"hello")[:5], bytes)
False

The most visible consequence is in abi/calldata/string.py, to_str(), which dispatches on isinstance(d, bytes) and raises on anything unmatched:

elif isinstance(d, bytes):
    buf.append("b#")
    buf.append(d.hex())
...
else:
    raise GenLayerError(f"can't encode {d} to calldata")

A memoryview misses the bytes branch and falls through to the final else. So any calldata containing a raw bytes field raises GenLayerError when rendered, e.g. via _decode_pending_transactions() in types/transactions.py, which calls to_str(decode(...)) directly.

contracts/actions.py's read_contract() path is also affected: it returns calldata.decode(...) directly to the caller, so a contract that returns a bytes field hands back a memoryview. .decode() and json.dumps() fail on it, while == and .hex() happen to work via the buffer protocol, so it's a partial, operation-dependent break rather than a guaranteed crash there.

Reproduction

from genlayer_py.abi import calldata

encoded = calldata.encode(b"hello")
decoded = calldata.decode(encoded)
print(type(decoded))               # <class 'memoryview'>
print(isinstance(decoded, bytes))  # False
calldata.to_str(decoded)           # raises GenLayerError: can't encode <memory at ...>

Possible fix, and a question

The direct fix is return bytes(ret_bytes) in the TYPE_BYTES branch, which restores the documented type and fixes to_str(). But if memoryview is intentional here for zero-copy reasons, the alternative is to widen the type contract and make consumers (to_str, and anything downstream of read_contract) accept the buffer protocol instead. Copying has a cost on large byte fields, so I didn't want to assume.

Which direction would you prefer? Happy to open a PR with a regression test (encode a bytes value, decode it, assert the type and that to_str() round-trips) once you confirm the approach.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions