Skip to content
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

Update EIP-7495: Add Variant[S] / OneOf[S] type safety layer #7938

Merged
merged 2 commits into from Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
42 changes: 42 additions & 0 deletions EIPS/eip-7495.md
Expand Up @@ -116,6 +116,48 @@ Merkleization `hash_tree_root(value)` of an object `value` is extended with:

- `mix_in_aux(merkleize(([hash_tree_root(element) if is_active_field(element) else Bytes32() for element in value.data] + [Bytes32()] * N)[:N]), hash_tree_root(value.active_fields))` if `value` is a `StableContainer[N]`.

### `Variant[S]`

For the purpose of type safety, `Variant[S]` is defined to serve as a subset of `StableContainer` `S`. While `S` still determines how the `Variant[S]` is serialized and merkleized, `Variant[S]` MAY implement additional restrictions on valid combinations of fields.

- Fields in `Variant[S]` may have a different order than in `S`; the canonical order in `S` is always used for serialization and merkleization regardless of any alternative orders in `Variant[S]`
- Fields in `Variant[S]` may be required, despite being optional in `S`
- Fields in `Variant[S]` may be missing, despite being optional in `S`
- All fields that are required in `S` must be present in `Variant[S]`

```python
# Serialization and merkleization format
class Shape(StableContainer[4]):
side: Optional[uint16]
color: uint8
radius: Optional[uint16]

# Valid variants
class Square(Variant[Shape]):
side: uint16
color: uint8

class Circle(Variant[Shape]):
radius: uint16
color: uint8
```

In addition, `OneOf[S]` is defined to provide a `select_variant` helper function for determining the `Variant[S]` to use when parsing `S`. The `select_variant` helper function MAY incorporate environmental information, e.g., the fork schedule.

```python
class AnyShape(OneOf[Shape]):
@classmethod
def select_variant(cls, value: Shape, circle_allowed = True) -> Type[Shape]:
if value.radius is not None:
assert circle_allowed
return Circle
if value.side is not None:
return Square
assert False
```

The extent and syntax in which `Variant[S]` and `OneOf[S]` are supported MAY differ among underlying SSZ implementations. Where it supports clarity, specifications SHOULD use `Variant[S]` and `OneOf[S]` as defined here.

## Rationale

### What are the problems solved by `StableContainer[N]`?
Expand Down
83 changes: 81 additions & 2 deletions assets/eip-7495/stable_container.py
Expand Up @@ -3,13 +3,14 @@
get_args, get_origin
from textwrap import indent
from remerkleable.bitfields import Bitvector
from remerkleable.complex import ComplexView, FieldOffset, decode_offset, encode_offset
from remerkleable.complex import ComplexView, Container, FieldOffset, \
decode_offset, encode_offset
from remerkleable.core import View, ViewHook, OFFSET_BYTE_LENGTH
from remerkleable.tree import NavigationError, Node, PairNode, \
get_depth, subtree_fill_to_contents, zero_node

N = TypeVar('N')
S = TypeVar('S', bound="StableContainer")
S = TypeVar('S', bound="ComplexView")

class StableContainer(ComplexView):
_field_indices: Dict[str, tuple[int, Type[View], bool]]
Expand Down Expand Up @@ -216,3 +217,81 @@ def serialize(self, stream: BinaryIO) -> int:
stream.write(temp_dyn_stream.read(num_data_bytes))

return num_prefix_bytes + num_data_bytes

class Variant(ComplexView):
def __new__(cls, backing: Optional[Node] = None, hook: Optional[ViewHook] = None, **kwargs):
if backing is not None:
if len(kwargs) != 0:
raise Exception("cannot have both a backing and elements to init fields")
return super().__new__(cls, backing=backing, hook=hook, **kwargs)

extra_kwargs = kwargs.copy()
for fkey, (ftyp, fopt) in cls.fields().items():
if fkey in extra_kwargs:
extra_kwargs.pop(fkey)
elif not fopt:
raise AttributeError(f"Field '{fkey}' is required in {cls}")
else:
pass
if len(extra_kwargs) > 0:
raise AttributeError(f'The field names [{"".join(extra_kwargs.keys())}] are not defined in {cls}')

value = cls.S(backing, hook, **kwargs)
return cls(backing=value.get_backing())

def __class_getitem__(cls, s) -> Type["Variant"]:
if not issubclass(s, StableContainer):
raise Exception(f"invalid variant container: {s}")

class VariantView(Variant, s):
S = s

@classmethod
def fields(cls) -> Dict[str, tuple[Type[View], bool]]:
return s.fields()

VariantView.__name__ = VariantView.type_repr()
return VariantView

@classmethod
def type_repr(cls) -> str:
return f"Variant[{cls.S.__name__}]"

@classmethod
def deserialize(cls: Type[S], stream: BinaryIO, scope: int) -> S:
value = cls.S.deserialize(stream, scope)
return cls(backing=value.get_backing())

class OneOf(ComplexView):
def __class_getitem__(cls, s) -> Type["OneOf"]:
if not issubclass(s, StableContainer) and not issubclass(s, Container):
raise Exception(f"invalid oneof container: {s}")

class OneOfView(OneOf, s):
S = s

@classmethod
def fields(cls):
return s.fields()

OneOfView.__name__ = OneOfView.type_repr()
return OneOfView

@classmethod
def type_repr(cls) -> str:
return f"OneOf[{cls.S}]"

@classmethod
def decode_bytes(cls: Type[S], bytez: bytes, *args, **kwargs) -> S:
stream = io.BytesIO()
stream.write(bytez)
stream.seek(0)
return cls.deserialize(stream, len(bytez), *args, **kwargs)

@classmethod
def deserialize(cls: Type[S], stream: BinaryIO, scope: int, *args, **kwargs) -> S:
value = cls.S.deserialize(stream, scope)
v = cls.select_variant(value, *args, **kwargs)
if not issubclass(v.S, cls.S):
raise Exception(f"unsupported select_variant result: {v}")
return v(backing=value.get_backing())