Skip to content

Commit

Permalink
Update EIP-7495: Add Variant[S] / OneOf[S] type safety layer
Browse files Browse the repository at this point in the history
Merged by EIP-Bot.
  • Loading branch information
etan-status authored Nov 22, 2023
1 parent 01ba9da commit bba648c
Show file tree
Hide file tree
Showing 3 changed files with 331 additions and 59 deletions.
42 changes: 42 additions & 0 deletions EIPS/eip-7495.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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())
Loading

0 comments on commit bba648c

Please sign in to comment.