Skip to content

Commit b6b2949

Browse files
authored
Update EIP-7916: Add remerkleable implementation and tests
Merged by EIP-Bot.
1 parent d97232b commit b6b2949

File tree

4 files changed

+1123
-1
lines changed

4 files changed

+1123
-1
lines changed

Diff for: EIPS/eip-7916.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
eip: 7916
33
title: SSZ ProgressiveList
44
description: New SSZ type to improve efficiency for short lists
5-
author: Zsolt Felföldi (@zsfelfoldi), Cayman (@wemeetagain)
5+
author: Zsolt Felföldi (@zsfelfoldi), Cayman (@wemeetagain), Etan Kissling (@etan-status)
66
discussions-to: https://ethereum-magicians.org/t/eip-7916-ssz-progressivebytelist/23254
77
status: Draft
88
type: Standards Track
@@ -165,6 +165,14 @@ Mixing in successor subtrees ensures predictable gindices and proof sizes.
165165

166166
`ProgressiveList[T]` is a new SSZ type, coexisting with `List[T, N]` and other types without conflict. Its `List`-equivalent serialization ensures compatibility with existing serializers.
167167

168+
## Test Cases
169+
170+
See [EIP assets](../assets/eip-7916/tests.patch).
171+
172+
## Reference Implementation
173+
174+
See [EIP assets](../assets/eip-7916/progressive.py), based on `protolambda/remerkleable`.
175+
168176
## Security Considerations
169177

170178
- Resource limits: The `uint32` limit for variable-length offsets essentially introduces a ~4GB cap when including a `ProgressiveList[T]` within another complex type, but practical limits (e.g., 10MB libp2p messages) apply. Implementations SHOULD enforce context-specific bounds.

Diff for: assets/eip-7916/progressive.py

+227
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
# This file implements `ProgressiveList` according to https://eips.ethereum.org/EIPS/eip-7916
2+
# The EIP is still under review, functionality may change or go away without deprecation.
3+
4+
from itertools import chain
5+
from typing import List as PyList, Optional, Type, cast
6+
from types import GeneratorType
7+
from remerkleable.basic import uint8, uint256
8+
from remerkleable.core import BasicView, ObjType, View, ViewHook, OFFSET_BYTE_LENGTH
9+
from remerkleable.complex import MonoSubtreeView, create_readonly_iter, append_view, pop_and_summarize
10+
from remerkleable.tree import Gindex, Node, PairNode, subtree_fill_to_contents, zero_node
11+
12+
13+
def subtree_fill_progressive(nodes: PyList[Node], depth=0) -> Node:
14+
if len(nodes) == 0:
15+
return zero_node(0)
16+
base_size = 1 << depth
17+
return PairNode(
18+
subtree_fill_to_contents(nodes[:base_size], depth),
19+
subtree_fill_progressive(nodes[base_size:], depth + 2),
20+
)
21+
22+
23+
def readonly_iter_progressive(backing: Node, length: int, elem_type: Type[View], is_packed: bool, depth=0):
24+
if length == 0:
25+
assert uint256.view_from_backing(backing) == uint256(0)
26+
27+
class EmptyIter(object):
28+
def __iter__(self):
29+
return self
30+
31+
def __next__(self):
32+
raise StopIteration
33+
return EmptyIter()
34+
35+
base_size = 1 << depth
36+
elems_per_chunk = 32 // elem_type.type_byte_length() if is_packed else 1
37+
38+
subtree_len = min(base_size * elems_per_chunk, length)
39+
return chain(
40+
create_readonly_iter(backing.get_left(), depth, subtree_len, elem_type, is_packed),
41+
readonly_iter_progressive(backing.get_right(), length - subtree_len, elem_type, is_packed, depth + 2),
42+
)
43+
44+
45+
def to_gindex_progressive(chunk_i: int) -> tuple[Gindex, int, int]:
46+
depth = 0
47+
gindex = 2
48+
while True:
49+
base_size = 1 << depth
50+
if chunk_i < base_size:
51+
return (gindex << 1 + depth) + chunk_i, depth, chunk_i
52+
chunk_i -= base_size
53+
depth += 2
54+
gindex = (gindex << 1) + 1
55+
56+
57+
def to_target_progressive(elem_type: Type[View], is_packed: bool, i: int) -> tuple[Gindex, int, int]:
58+
if is_packed:
59+
elems_per_chunk = 32 // elem_type.type_byte_length()
60+
chunk_i, offset_i = divmod(i, elems_per_chunk)
61+
else:
62+
elems_per_chunk = 1
63+
chunk_i, offset_i = i, 0
64+
65+
_, depth, chunk_i = to_gindex_progressive(chunk_i)
66+
i = chunk_i * elems_per_chunk + offset_i
67+
68+
target = 2
69+
d = 0
70+
while d < depth:
71+
target = (target << 1) + 1
72+
d += 2
73+
74+
return target, d, i
75+
76+
77+
class ProgressiveList(MonoSubtreeView):
78+
__slots__ = ()
79+
80+
def __new__(cls, *args, backing: Optional[Node] = None, hook: Optional[ViewHook] = None, **kwargs):
81+
if backing is not None:
82+
if len(args) != 0:
83+
raise Exception('cannot have both a backing and elements to init ProgressiveList')
84+
return super().__new__(cls, backing=backing, hook=hook, **kwargs)
85+
86+
elem_cls = cls.element_cls()
87+
vals = list(args)
88+
if len(vals) == 1:
89+
val = vals[0]
90+
if isinstance(val, (GeneratorType, list, tuple)):
91+
vals = list(val)
92+
if issubclass(elem_cls, uint8):
93+
if isinstance(val, bytes):
94+
vals = list(val)
95+
if isinstance(val, str):
96+
if val[:2] == '0x':
97+
val = val[2:]
98+
vals = list(bytes.fromhex(val))
99+
input_views = []
100+
if len(vals) > 0:
101+
for el in vals:
102+
if isinstance(el, View):
103+
input_views.append(el)
104+
else:
105+
input_views.append(elem_cls.coerce_view(el))
106+
input_nodes = cls.views_into_chunks(input_views)
107+
contents = subtree_fill_progressive(input_nodes)
108+
else:
109+
contents = zero_node(0)
110+
backing = PairNode(contents, uint256(len(input_views)).get_backing())
111+
return super().__new__(cls, backing=backing, hook=hook, **kwargs)
112+
113+
def __class_getitem__(cls, element_type) -> Type['ProgressiveList']:
114+
packed = isinstance(element_type, BasicView)
115+
116+
class ProgressiveListView(ProgressiveList):
117+
@classmethod
118+
def is_packed(cls) -> bool:
119+
return packed
120+
121+
@classmethod
122+
def element_cls(cls) -> Type[View]:
123+
return element_type
124+
125+
ProgressiveListView.__name__ = ProgressiveListView.type_repr()
126+
return ProgressiveListView
127+
128+
def length(self) -> int:
129+
return int(uint256.view_from_backing(self.get_backing().get_right()))
130+
131+
def value_byte_length(self) -> int:
132+
elem_cls = self.__class__.element_cls()
133+
if elem_cls.is_fixed_byte_length():
134+
return elem_cls.type_byte_length() * self.length()
135+
else:
136+
return sum(OFFSET_BYTE_LENGTH + cast(View, el).value_byte_length() for el in iter(self))
137+
138+
@classmethod
139+
def chunk_to_gindex(cls, chunk_i: int) -> Gindex:
140+
gindex, _, _ = to_gindex_progressive(chunk_i)
141+
return gindex
142+
143+
def readonly_iter(self):
144+
length = self.length()
145+
backing = self.get_backing().get_left()
146+
147+
elem_type: Type[View] = self.element_cls()
148+
is_packed = self.is_packed()
149+
150+
return readonly_iter_progressive(backing, length, elem_type, is_packed)
151+
152+
def append(self, v: View):
153+
ll = self.length()
154+
i = ll
155+
156+
elem_type = self.__class__.element_cls()
157+
is_packed = self.__class__.is_packed()
158+
gindex, d, i = to_target_progressive(elem_type, is_packed, i)
159+
160+
if not isinstance(v, elem_type):
161+
v = elem_type.coerce_view(v)
162+
163+
next_backing = self.get_backing()
164+
if i == 0: # Create new subtree
165+
next_backing = next_backing.setter(gindex)(PairNode(zero_node(d), zero_node(0)))
166+
gindex = gindex << 1
167+
next_backing = next_backing.setter(gindex)(append_view(
168+
next_backing.getter(gindex), d, i, v, elem_type, is_packed))
169+
170+
next_backing = next_backing.setter(3)(uint256(ll + 1).get_backing())
171+
self.set_backing(next_backing)
172+
173+
def pop(self):
174+
ll = self.length()
175+
if ll == 0:
176+
raise Exception('progressive list is empty, cannot pop')
177+
i = ll - 1
178+
179+
if i == 0:
180+
self.set_backing(PairNode(zero_node(0), zero_node(0)))
181+
return
182+
183+
elem_type = self.__class__.element_cls()
184+
is_packed = self.__class__.is_packed()
185+
gindex, d, i = to_target_progressive(elem_type, is_packed, i)
186+
187+
next_backing = self.get_backing()
188+
if i == 0: # Delete entire subtree
189+
next_backing = next_backing.setter(gindex)(zero_node(0))
190+
else:
191+
gindex = gindex << 1
192+
next_backing = next_backing.setter(gindex)(pop_and_summarize(
193+
next_backing.getter(gindex), d, i, elem_type, is_packed))
194+
195+
next_backing = next_backing.setter(3)(uint256(ll - 1).get_backing())
196+
self.set_backing(next_backing)
197+
198+
def get(self, i: int) -> View:
199+
i = int(i)
200+
if i < 0 or i >= self.length():
201+
raise IndexError
202+
return super().get(i)
203+
204+
def set(self, i: int, v: View) -> None:
205+
i = int(i)
206+
if i < 0 or i >= self.length():
207+
raise IndexError
208+
super().set(i, v)
209+
210+
@classmethod
211+
def type_repr(cls) -> str:
212+
return f'ProgressiveList[{cls.element_cls().__name__}]'
213+
214+
@classmethod
215+
def is_valid_count(cls, count: int) -> bool:
216+
return 0 <= count
217+
218+
@classmethod
219+
def min_byte_length(cls) -> int:
220+
return 0
221+
222+
@classmethod
223+
def max_byte_length(cls) -> int:
224+
return 1 << 32 # Essentially unbounded, limited by offsets if nested
225+
226+
def to_obj(self) -> ObjType:
227+
return list(el.to_obj() for el in self.readonly_iter())

0 commit comments

Comments
 (0)