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.
calldata.decode()is annotated to returnCalldataEncodable, whose union includesbytes. But for aTYPE_BYTESfield it returns amemoryview, notbytes, and several consumers depend on the value actually beingbytes.In
genlayer_py/abi/calldata/decoder.py,memis amemoryview, and slicing amemoryviewyields anothermemoryview:The most visible consequence is in
abi/calldata/string.py,to_str(), which dispatches onisinstance(d, bytes)and raises on anything unmatched:A
memoryviewmisses thebytesbranch and falls through to the finalelse. So any calldata containing a rawbytesfield raisesGenLayerErrorwhen rendered, e.g. via_decode_pending_transactions()intypes/transactions.py, which callsto_str(decode(...))directly.contracts/actions.py'sread_contract()path is also affected: it returnscalldata.decode(...)directly to the caller, so a contract that returns abytesfield hands back amemoryview..decode()andjson.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
Possible fix, and a question
The direct fix is
return bytes(ret_bytes)in theTYPE_BYTESbranch, which restores the documented type and fixesto_str(). But ifmemoryviewis intentional here for zero-copy reasons, the alternative is to widen the type contract and make consumers (to_str, and anything downstream ofread_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
bytesvalue, decode it, assert the type and thatto_str()round-trips) once you confirm the approach.