diff --git a/ssz/constants.py b/ssz/constants.py index fb0d8d0d..caebc0f3 100644 --- a/ssz/constants.py +++ b/ssz/constants.py @@ -5,9 +5,6 @@ CHUNK_SIZE = 32 # named BYTES_PER_CHUNK in the spec EMPTY_CHUNK = Hash32(b"\x00" * CHUNK_SIZE) -SIZE_PREFIX_SIZE = 4 # named BYTES_PER_LENGTH_PREFIX in the spec -MAX_CONTENT_SIZE = 2 ** (SIZE_PREFIX_SIZE * 8) - 1 - SIGNATURE_FIELD_NAME = "signature" # number of bytes for a serialized offset diff --git a/ssz/sedes/base.py b/ssz/sedes/base.py index f711e63f..8fabae70 100644 --- a/ssz/sedes/base.py +++ b/ssz/sedes/base.py @@ -3,7 +3,6 @@ abstractmethod, ) import io -import itertools import operator from typing import ( IO, @@ -176,17 +175,3 @@ def deserialize(self, data: bytes) -> TDeserialized: @abstractmethod def _deserialize_stream(self, stream: IO[bytes]) -> TDeserialized: pass - - -TSerializableElement = TypeVar("TSerializableElement") -TDeserializedElement = TypeVar("TDeserializedElement") - - -class HomogenousSequence(CompositeSedes[Sequence[Any], Tuple[Any]]): - element_sedes: TSedes - - # - # Serialization - # - def _get_item_sedes_pairs(self, value: Sequence[Any]) -> Tuple[Tuple[Any, TSedes], ...]: - return tuple(zip(value, itertools.repeat(self.element_sedes))) diff --git a/ssz/sedes/container.py b/ssz/sedes/container.py index 400af7a7..e3c14ae3 100644 --- a/ssz/sedes/container.py +++ b/ssz/sedes/container.py @@ -44,8 +44,6 @@ class Container(CompositeSedes[Sequence[Any], Tuple[Any, ...]]): def __init__(self, field_sedes: Sequence[TSedes]) -> None: if len(field_sedes) == 0: raise ValidationError("Cannot define container without any fields") - from ssz.sedes.base import BaseSedes - assert all(isinstance(sedes, BaseSedes) for sedes in field_sedes) self.field_sedes = tuple(field_sedes) # diff --git a/ssz/sedes/list.py b/ssz/sedes/list.py index 61199e95..262b5758 100644 --- a/ssz/sedes/list.py +++ b/ssz/sedes/list.py @@ -40,7 +40,7 @@ TSerializable = TypeVar("TSerializable") TDeserialized = TypeVar("TDeserialized") -EMPTY_LIST_HASH_TRIES_ROOT = mix_in_length(merkleize(pack([])), 0) +EMPTY_LIST_HASH_TREE_ROOT = mix_in_length(merkleize(pack([])), 0) class EmptyList(BaseCompositeSedes[Sequence[TSerializable], Tuple[TSerializable, ...]]): @@ -61,8 +61,8 @@ def deserialize(self, data: bytes) -> Tuple[TDeserialized, ...]: def hash_tree_root(self, value: Sequence[TSerializable]) -> bytes: if len(value): - raise ValueError("Cannot compute trie hash for non-empty value using `EmptyList` sedes") - return EMPTY_LIST_HASH_TRIES_ROOT + raise ValueError("Cannot compute tree hash for non-empty value using `EmptyList` sedes") + return EMPTY_LIST_HASH_TREE_ROOT empty_list = EmptyList() @@ -98,15 +98,24 @@ def _deserialize_stream(self, stream: IO[bytes]) -> Iterable[TDeserialized]: element_size = self.element_sedes.get_fixed_size() data = stream.read() if len(data) % element_size != 0: - raise DeserializationError("TODO: INVALID LENGTH") + raise DeserializationError( + f"Invalid length. List is comprised of a fixed size sedes " + f"but total serialized data is not an even multiple of the " + f"element size. data length: {len(data)} element size: " + f"{element_size}" + ) for segment in partition(element_size, data): yield self.element_sedes.deserialize(segment) else: + stream_zero_loc = stream.tell() try: first_offset = s_decode_offset(stream) except DeserializationError: - # Empty list - return + if stream.tell() == stream_zero_loc: + # Empty list + return + else: + raise num_remaining_offset_bytes = first_offset - stream.tell() if num_remaining_offset_bytes % OFFSET_SIZE != 0: @@ -134,7 +143,7 @@ def _deserialize_stream(self, stream: IO[bytes]) -> Iterable[TDeserialized]: # def hash_tree_root(self, value: Iterable[TSerializable]) -> bytes: if len(value) == 0: - return EMPTY_LIST_HASH_TRIES_ROOT + return EMPTY_LIST_HASH_TREE_ROOT elif isinstance(self.element_sedes, BasicSedes): serialized_items = tuple( self.element_sedes.serialize(element) diff --git a/ssz/utils.py b/ssz/utils.py index cd538b0e..48b1ad23 100644 --- a/ssz/utils.py +++ b/ssz/utils.py @@ -55,7 +55,7 @@ def decode_offset(data: bytes) -> int: def s_decode_offset(stream: IO[bytes]) -> int: - data = read_exact(4, stream) + data = read_exact(OFFSET_SIZE, stream) return decode_offset(data) diff --git a/tests/sedes/test_composite_sedes.py b/tests/sedes/test_composite_sedes.py index a358c527..b9e4010b 100644 --- a/tests/sedes/test_composite_sedes.py +++ b/tests/sedes/test_composite_sedes.py @@ -7,6 +7,9 @@ import pytest import ssz +from ssz.exceptions import ( + DeserializationError, +) from ssz.sedes import ( Container, List, @@ -31,6 +34,16 @@ def test_list(value, serialized): assert ssz.decode(decode_hex(serialized), sedes) == value +def test_invalid_serialized_list(): + # ensure that an improperly read offset (not enough bytes) does not + # incorrectly register as an empty list due to mis-interpreting the failed + # stream read as the stream having been empty. + data = decode_hex("0x0001") + sedes = List(List(uint8)) + with pytest.raises(DeserializationError): + ssz.decode(data, sedes=sedes) + + @pytest.mark.parametrize( ("value", "serialized"), ( @@ -58,7 +71,7 @@ def test_tuple_of_static_sized_entries(value, serialized): ), ) ) -def test_tuple_of_dynamic_sized_entries(value, serialized): +def test_list_of_dynamic_sized_entries(value, serialized): sedes = Vector(List(uint8), len(value)) assert encode_hex(ssz.encode(value, sedes)) == serialized assert ssz.decode(decode_hex(serialized), sedes) == value