Skip to content

Commit 7b45933

Browse files
authored
Update EIP-7495: Extend tests and add deserialization to impl
Merged by EIP-Bot.
1 parent a6ee85b commit 7b45933

3 files changed

Lines changed: 148 additions & 12 deletions

File tree

EIPS/eip-7495.md

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -141,16 +141,7 @@ Additionally, every time that the number of fields reaches a new power of 2, the
141141

142142
## Test Cases
143143

144-
```python
145-
@dataclass
146-
class Foo:
147-
a: uint64
148-
b: Optional[uint32]
149-
c: Optional[uint16]
150-
151-
value = PartialContainer[Foo, 32](a=64, b=None, c=16)
152-
assert bytes(serialize(value)).hex() == "0500000040000000000000001000"
153-
```
144+
See [EIP assets](../assets/eip-7495/tests.py).
154145

155146
## Reference Implementation
156147

assets/eip-7495/partial_container.py

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
from dataclasses import fields, is_dataclass
22
import io
3-
from typing import BinaryIO, Dict, Optional, TypeVar, Type, Union as PyUnion, get_args, get_origin
3+
from typing import BinaryIO, Dict, List as PyList, Optional, TypeVar, Type, Union as PyUnion, \
4+
get_args, get_origin
45
from textwrap import indent
56
from remerkleable.bitfields import Bitvector
6-
from remerkleable.complex import ComplexView, encode_offset
7+
from remerkleable.complex import ComplexView, FieldOffset, decode_offset, encode_offset
78
from remerkleable.core import View, ViewHook, OFFSET_BYTE_LENGTH
89
from remerkleable.tree import NavigationError, Node, PairNode, \
910
get_depth, subtree_fill_to_contents, zero_node
1011

1112
T = TypeVar('T')
1213
N = TypeVar('N')
14+
P = TypeVar('P', bound="PartialContainer")
1315

1416
PartialFields = Dict[str, tuple[Type[View], bool]]
1517

@@ -86,6 +88,30 @@ def fields(cls) -> PartialFields:
8688
ret[fkey] = (ftyp, fopt)
8789
return ret
8890

91+
@classmethod
92+
def is_fixed_byte_length(cls) -> bool:
93+
return False
94+
95+
@classmethod
96+
def min_byte_length(cls) -> int:
97+
total = Bitvector[cls.N].type_byte_length()
98+
for _, (ftyp, fopt) in cls.fields().items():
99+
if fopt:
100+
continue
101+
if not ftyp.is_fixed_byte_length():
102+
total += OFFSET_BYTE_LENGTH
103+
total += ftyp.min_byte_length()
104+
return total
105+
106+
@classmethod
107+
def max_byte_length(cls) -> int:
108+
total = Bitvector[cls.N].type_byte_length()
109+
for _, (ftyp, _) in cls.fields().items():
110+
if not ftyp.is_fixed_byte_length():
111+
total += OFFSET_BYTE_LENGTH
112+
total += ftyp.max_byte_length()
113+
return total
114+
89115
def active_fields(self) -> Bitvector:
90116
active_fields_node = super().get_backing().get_right()
91117
return Bitvector[self.__class__.N].view_from_backing(active_fields_node)
@@ -129,6 +155,52 @@ def __repr__(self):
129155
def type_repr(cls) -> str:
130156
return f"PartialContainer[{cls.T.__name__}, {cls.N}]"
131157

158+
@classmethod
159+
def deserialize(cls: Type[P], stream: BinaryIO, scope: int) -> P:
160+
num_prefix_bytes = Bitvector[cls.N].type_byte_length()
161+
if scope < num_prefix_bytes:
162+
raise ValueError("scope too small, cannot read PartialContainer active fields")
163+
active_fields = Bitvector[cls.N].deserialize(stream, num_prefix_bytes)
164+
scope = scope - num_prefix_bytes
165+
166+
max_findex = 0
167+
field_values: Dict[str, Optional[View]] = {}
168+
dyn_fields: PyList[FieldOffset] = []
169+
fixed_size = 0
170+
for findex, (fkey, (ftyp, _)) in enumerate(cls.fields().items()):
171+
max_findex = findex
172+
if not active_fields.get(findex):
173+
field_values[fkey] = None
174+
continue
175+
if ftyp.is_fixed_byte_length():
176+
fsize = ftyp.type_byte_length()
177+
field_values[fkey] = ftyp.deserialize(stream, fsize)
178+
fixed_size += fsize
179+
else:
180+
dyn_fields.append(FieldOffset(
181+
key=fkey, typ=ftyp, offset=int(decode_offset(stream))))
182+
fixed_size += OFFSET_BYTE_LENGTH
183+
if len(dyn_fields) > 0:
184+
if dyn_fields[0].offset < fixed_size:
185+
raise Exception(f"first offset {dyn_fields[0].offset} is "
186+
f"smaller than expected fixed size {fixed_size}")
187+
for i, (fkey, ftyp, foffset) in enumerate(dyn_fields):
188+
next_offset = dyn_fields[i + 1].offset if i + 1 < len(dyn_fields) else scope
189+
if foffset > next_offset:
190+
raise Exception(f"offset {i} is invalid: {foffset} "
191+
f"larger than next offset {next_offset}")
192+
fsize = next_offset - foffset
193+
f_min_size, f_max_size = ftyp.min_byte_length(), ftyp.max_byte_length()
194+
if not (f_min_size <= fsize <= f_max_size):
195+
raise Exception(f"offset {i} is invalid, size out of bounds: "
196+
f"{foffset}, next {next_offset}, implied size: {fsize}, "
197+
f"size bounds: [{f_min_size}, {f_max_size}]")
198+
field_values[fkey] = ftyp.deserialize(stream, fsize)
199+
for findex in range(max_findex + 1, cls.N):
200+
if active_fields.get(findex):
201+
raise Exception(f"unknown field index {findex}")
202+
return cls(**field_values) # type: ignore
203+
132204
def serialize(self, stream: BinaryIO) -> int:
133205
active_fields = self.active_fields()
134206
num_prefix_bytes = active_fields.serialize(stream)

assets/eip-7495/tests.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from partial_container import PartialContainer
2+
from dataclasses import dataclass
3+
from typing import Optional
4+
from remerkleable.basic import uint16, uint32, uint64, uint256
5+
from remerkleable.bitfields import Bitvector
6+
from remerkleable.byte_arrays import ByteList
7+
from remerkleable.complex import Container
8+
9+
@dataclass
10+
class Foo:
11+
a: uint64
12+
b: Optional[uint32]
13+
c: Optional[uint16]
14+
d: Optional[ByteList[4]]
15+
e: ByteList[4]
16+
17+
class Wrapper(Container):
18+
x: PartialContainer[Foo, 32]
19+
20+
# Test serialization
21+
value = Wrapper(
22+
x=PartialContainer[Foo, 32](a=64, b=None, c=16, d=None, e=ByteList[4]([0x42]))
23+
)
24+
expected_bytes = bytes.fromhex("0400000015000000400000000000000010000e00000042")
25+
assert value.encode_bytes() == expected_bytes
26+
27+
# Test deserialization
28+
assert Wrapper.decode_bytes(expected_bytes) == value
29+
30+
# Test merkleization
31+
class Bar(Container):
32+
a: uint64
33+
ph01: uint256
34+
c: uint16
35+
ph03: uint256
36+
e: ByteList[4]
37+
ph05: uint256
38+
ph06: uint256
39+
ph07: uint256
40+
ph08: uint256
41+
ph09: uint256
42+
ph0a: uint256
43+
ph0b: uint256
44+
ph0c: uint256
45+
ph0d: uint256
46+
ph0e: uint256
47+
ph0f: uint256
48+
ph10: uint256
49+
ph11: uint256
50+
ph12: uint256
51+
ph13: uint256
52+
ph14: uint256
53+
ph15: uint256
54+
ph16: uint256
55+
ph17: uint256
56+
ph18: uint256
57+
ph19: uint256
58+
ph1a: uint256
59+
ph1b: uint256
60+
ph1c: uint256
61+
ph1d: uint256
62+
ph1e: uint256
63+
ph1f: uint256
64+
65+
class Baz(Container):
66+
data: Bar
67+
active_fields: Bitvector[32]
68+
69+
expected = Baz(
70+
data=Bar(a=value.x.a, c=value.x.c, e=value.x.e),
71+
active_fields=Bitvector[32]([True, False, True, False, True] + [False] * 27)
72+
)
73+
assert value.hash_tree_root() == expected.hash_tree_root()

0 commit comments

Comments
 (0)