eth-ssz-specs 0.0.1.dev1__tar.gz → 0.0.1.dev2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {eth_ssz_specs-0.0.1.dev1 → eth_ssz_specs-0.0.1.dev2}/CONTRIBUTING.md +1 -1
- {eth_ssz_specs-0.0.1.dev1 → eth_ssz_specs-0.0.1.dev2}/PKG-INFO +4 -4
- {eth_ssz_specs-0.0.1.dev1 → eth_ssz_specs-0.0.1.dev2}/README.md +3 -3
- {eth_ssz_specs-0.0.1.dev1 → eth_ssz_specs-0.0.1.dev2}/pyproject.toml +1 -1
- {eth_ssz_specs-0.0.1.dev1 → eth_ssz_specs-0.0.1.dev2}/src/ssz/__init__.py +2 -1
- {eth_ssz_specs-0.0.1.dev1 → eth_ssz_specs-0.0.1.dev2}/src/ssz/bitfields.py +5 -5
- {eth_ssz_specs-0.0.1.dev1 → eth_ssz_specs-0.0.1.dev2}/src/ssz/boolean.py +19 -5
- {eth_ssz_specs-0.0.1.dev1 → eth_ssz_specs-0.0.1.dev2}/src/ssz/byte_arrays.py +125 -28
- {eth_ssz_specs-0.0.1.dev1 → eth_ssz_specs-0.0.1.dev2}/src/ssz/collections.py +36 -13
- {eth_ssz_specs-0.0.1.dev1 → eth_ssz_specs-0.0.1.dev2}/src/ssz/container.py +19 -10
- {eth_ssz_specs-0.0.1.dev1 → eth_ssz_specs-0.0.1.dev2}/src/ssz/exceptions.py +2 -2
- {eth_ssz_specs-0.0.1.dev1 → eth_ssz_specs-0.0.1.dev2}/src/ssz/merkleization.py +367 -58
- {eth_ssz_specs-0.0.1.dev1 → eth_ssz_specs-0.0.1.dev2}/src/ssz/proofs.py +11 -4
- {eth_ssz_specs-0.0.1.dev1 → eth_ssz_specs-0.0.1.dev2}/src/ssz/ssz_base.py +215 -25
- {eth_ssz_specs-0.0.1.dev1 → eth_ssz_specs-0.0.1.dev2}/src/ssz/uint.py +232 -57
- {eth_ssz_specs-0.0.1.dev1 → eth_ssz_specs-0.0.1.dev2}/uv.lock +2 -2
- {eth_ssz_specs-0.0.1.dev1 → eth_ssz_specs-0.0.1.dev2}/.gitignore +0 -0
- {eth_ssz_specs-0.0.1.dev1 → eth_ssz_specs-0.0.1.dev2}/Justfile +0 -0
- {eth_ssz_specs-0.0.1.dev1 → eth_ssz_specs-0.0.1.dev2}/LICENSE +0 -0
- {eth_ssz_specs-0.0.1.dev1 → eth_ssz_specs-0.0.1.dev2}/src/ssz/base.py +0 -0
- {eth_ssz_specs-0.0.1.dev1 → eth_ssz_specs-0.0.1.dev2}/src/ssz/union.py +0 -0
|
@@ -46,7 +46,7 @@ If anything fails before the release job, no tag was created: fix and re-dispatc
|
|
|
46
46
|
only the PyPI publish fails, re-run the failed job on the same run instead.
|
|
47
47
|
|
|
48
48
|
Before the first release: add a PyPI trusted publisher (project `eth-ssz-specs`, owner
|
|
49
|
-
`
|
|
49
|
+
`ethereum`, repository `ssz-specs`, workflow `release.yaml`, environment `pypi`)
|
|
50
50
|
and create the `pypi` environment in the GitHub repository settings.
|
|
51
51
|
|
|
52
52
|
Reproduce the artifacts locally with `just build` and `just pack-fixtures <tag>`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: eth-ssz-specs
|
|
3
|
-
Version: 0.0.1.
|
|
3
|
+
Version: 0.0.1.dev2
|
|
4
4
|
Summary: Ethereum SSZ (Simple Serialize) specification and Merkleization
|
|
5
5
|
Project-URL: Homepage, https://github.com/ethereum/ssz-specs
|
|
6
6
|
Project-URL: Source, https://github.com/ethereum/ssz-specs
|
|
@@ -65,7 +65,7 @@ implementations can run to ensure compliance with the specifications.
|
|
|
65
65
|
## Releases
|
|
66
66
|
|
|
67
67
|
Each release ships the `eth-ssz-specs` package on PyPI and the reference tests on the
|
|
68
|
-
[releases page](https://github.com/
|
|
68
|
+
[releases page](https://github.com/ethereum/ssz-specs/releases), both built from
|
|
69
69
|
the tagged commit.
|
|
70
70
|
|
|
71
71
|
```bash
|
|
@@ -74,8 +74,8 @@ pip install eth-ssz-specs
|
|
|
74
74
|
|
|
75
75
|
```bash
|
|
76
76
|
TAG=v0.1.0
|
|
77
|
-
curl -sSLO "https://github.com/
|
|
78
|
-
curl -sSLO "https://github.com/
|
|
77
|
+
curl -sSLO "https://github.com/ethereum/ssz-specs/releases/download/$TAG/ssz-test-vectors-$TAG.tar.gz"
|
|
78
|
+
curl -sSLO "https://github.com/ethereum/ssz-specs/releases/download/$TAG/ssz-test-vectors-$TAG.tar.gz.sha256"
|
|
79
79
|
sha256sum --check "ssz-test-vectors-$TAG.tar.gz.sha256"
|
|
80
80
|
tar -xzf "ssz-test-vectors-$TAG.tar.gz" # extracts fixtures/
|
|
81
81
|
```
|
|
@@ -24,7 +24,7 @@ implementations can run to ensure compliance with the specifications.
|
|
|
24
24
|
## Releases
|
|
25
25
|
|
|
26
26
|
Each release ships the `eth-ssz-specs` package on PyPI and the reference tests on the
|
|
27
|
-
[releases page](https://github.com/
|
|
27
|
+
[releases page](https://github.com/ethereum/ssz-specs/releases), both built from
|
|
28
28
|
the tagged commit.
|
|
29
29
|
|
|
30
30
|
```bash
|
|
@@ -33,8 +33,8 @@ pip install eth-ssz-specs
|
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
35
|
TAG=v0.1.0
|
|
36
|
-
curl -sSLO "https://github.com/
|
|
37
|
-
curl -sSLO "https://github.com/
|
|
36
|
+
curl -sSLO "https://github.com/ethereum/ssz-specs/releases/download/$TAG/ssz-test-vectors-$TAG.tar.gz"
|
|
37
|
+
curl -sSLO "https://github.com/ethereum/ssz-specs/releases/download/$TAG/ssz-test-vectors-$TAG.tar.gz.sha256"
|
|
38
38
|
sha256sum --check "ssz-test-vectors-$TAG.tar.gz.sha256"
|
|
39
39
|
tar -xzf "ssz-test-vectors-$TAG.tar.gz" # extracts fixtures/
|
|
40
40
|
```
|
|
@@ -24,7 +24,7 @@ from ssz.exceptions import (
|
|
|
24
24
|
SSZUnionOptionsError,
|
|
25
25
|
SSZValueError,
|
|
26
26
|
)
|
|
27
|
-
from ssz.merkleization import ZERO_ROOT, Chunk, Root
|
|
27
|
+
from ssz.merkleization import ZERO_ROOT, Chunk, Root, hash_tree_root
|
|
28
28
|
from ssz.proofs import (
|
|
29
29
|
ACTIVE_FIELDS_KEY,
|
|
30
30
|
LENGTH_KEY,
|
|
@@ -110,6 +110,7 @@ __all__ = [
|
|
|
110
110
|
"gindex_length",
|
|
111
111
|
"gindex_parent",
|
|
112
112
|
"gindex_sibling",
|
|
113
|
+
"hash_tree_root",
|
|
113
114
|
"item_length",
|
|
114
115
|
"verify_merkle_multiproof",
|
|
115
116
|
"verify_merkle_proof",
|
|
@@ -72,7 +72,7 @@ class BitVector(SSZCollection[Boolean]):
|
|
|
72
72
|
"""
|
|
73
73
|
super().__pydantic_init_subclass__(**kwargs)
|
|
74
74
|
|
|
75
|
-
if
|
|
75
|
+
if cls.LENGTH is None:
|
|
76
76
|
return
|
|
77
77
|
|
|
78
78
|
length = cls.LENGTH
|
|
@@ -100,7 +100,7 @@ class BitVector(SSZCollection[Boolean]):
|
|
|
100
100
|
def _coerce_and_validate(cls, bits_input: Any) -> list[Boolean]:
|
|
101
101
|
"""Enforce the exact bit count and coerce inputs into booleans."""
|
|
102
102
|
# Subclasses must declare LENGTH before any instances can be validated.
|
|
103
|
-
if
|
|
103
|
+
if cls.LENGTH is None:
|
|
104
104
|
raise SSZDefinitionError(cls.__name__, "LENGTH")
|
|
105
105
|
|
|
106
106
|
# Materialize generic iterables into a tuple so the length check works.
|
|
@@ -264,14 +264,14 @@ class _SSZBitList(SSZCollection[Boolean]):
|
|
|
264
264
|
|
|
265
265
|
def append(self, value: Boolean) -> None:
|
|
266
266
|
"""Add one bit at the end, validating it and the resulting length."""
|
|
267
|
-
self.
|
|
267
|
+
self._begin_mutation()
|
|
268
268
|
element = self._validate_element(value)
|
|
269
269
|
self._validate_length(len(self.data) + 1)
|
|
270
270
|
cast("list[Boolean]", self.data).append(element)
|
|
271
271
|
|
|
272
272
|
def pop(self) -> Boolean:
|
|
273
273
|
"""Remove and return the last bit, validating the resulting length."""
|
|
274
|
-
self.
|
|
274
|
+
self._begin_mutation()
|
|
275
275
|
self._validate_length(len(self.data) - 1)
|
|
276
276
|
return cast("list[Boolean]", self.data).pop()
|
|
277
277
|
|
|
@@ -495,7 +495,7 @@ class BitList(_SSZBitList):
|
|
|
495
495
|
def _coerce_and_validate(cls, bits_input: Any) -> list[Boolean]:
|
|
496
496
|
"""Enforce the maximum bit count and coerce inputs into booleans."""
|
|
497
497
|
# Subclasses must declare LIMIT before any instances can be validated.
|
|
498
|
-
if
|
|
498
|
+
if cls.LIMIT is None:
|
|
499
499
|
raise SSZDefinitionError(cls.__name__, "LIMIT")
|
|
500
500
|
|
|
501
501
|
# Accept the natural input shapes:
|
|
@@ -18,7 +18,7 @@ class Boolean(int, SSZType):
|
|
|
18
18
|
- Inherits from int so true/false work natively in truthiness checks.
|
|
19
19
|
- Arithmetic (+ - * /) is disabled to prevent ambiguous operations.
|
|
20
20
|
- Bitwise ops (& | ^) reject operands of any other type.
|
|
21
|
-
- Equality rejects comparisons with anything but another boolean.
|
|
21
|
+
- Equality rejects comparisons with anything but another boolean, and hashing agrees.
|
|
22
22
|
|
|
23
23
|
Wire format:
|
|
24
24
|
|
|
@@ -211,7 +211,15 @@ class Boolean(int, SSZType):
|
|
|
211
211
|
return self.__xor__(other)
|
|
212
212
|
|
|
213
213
|
def __eq__(self, other: object) -> bool:
|
|
214
|
-
"""
|
|
214
|
+
"""
|
|
215
|
+
Strict equality — only another boolean compares; anything else raises.
|
|
216
|
+
|
|
217
|
+
Invariant: the bit is the whole of the value, so the hash below is the bit's hash.
|
|
218
|
+
A raw bool is still refused, which is the point of wrapping the bit at all.
|
|
219
|
+
|
|
220
|
+
Raises:
|
|
221
|
+
TypeError: If other is not a Boolean.
|
|
222
|
+
"""
|
|
215
223
|
if not isinstance(other, Boolean):
|
|
216
224
|
raise TypeError(
|
|
217
225
|
f"Unsupported operand type(s) for ==: "
|
|
@@ -225,6 +233,9 @@ class Boolean(int, SSZType):
|
|
|
225
233
|
|
|
226
234
|
Defined explicitly because the parent class's not-equal would otherwise
|
|
227
235
|
bypass the strict equality above.
|
|
236
|
+
|
|
237
|
+
Raises:
|
|
238
|
+
TypeError: If other is not a Boolean.
|
|
228
239
|
"""
|
|
229
240
|
if not isinstance(other, Boolean):
|
|
230
241
|
raise TypeError(
|
|
@@ -241,9 +252,12 @@ class Boolean(int, SSZType):
|
|
|
241
252
|
"""Return the user-facing form: True or False."""
|
|
242
253
|
return str(bool(self))
|
|
243
254
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
255
|
+
# A boolean hashes as the bit it holds, because equality compares the bit alone.
|
|
256
|
+
# Defining equality would otherwise clear the inherited hash, so it is spelled out.
|
|
257
|
+
#
|
|
258
|
+
# A raw bool now shares a bucket, so a membership test reaches the refusal instead of
|
|
259
|
+
# answering False from an empty one.
|
|
260
|
+
__hash__ = int.__hash__
|
|
247
261
|
|
|
248
262
|
|
|
249
263
|
Bit: TypeAlias = Boolean
|
|
@@ -13,7 +13,7 @@ Both flavors serialize as the raw bytes themselves — no length prefix, no deli
|
|
|
13
13
|
|
|
14
14
|
from collections.abc import Iterable, Sequence
|
|
15
15
|
from enum import Enum
|
|
16
|
-
from typing import IO, Any, ClassVar, Self, override
|
|
16
|
+
from typing import IO, TYPE_CHECKING, Any, ClassVar, Self, override
|
|
17
17
|
|
|
18
18
|
from pydantic import Field, field_serializer, field_validator
|
|
19
19
|
from pydantic.annotated_handlers import GetCoreSchemaHandler
|
|
@@ -49,7 +49,7 @@ class ByteVector(bytes, SSZType):
|
|
|
49
49
|
|
|
50
50
|
- Inherits from bytes so the instance is usable wherever a bytes value is expected.
|
|
51
51
|
- Subclasses pin the byte count by setting the class-level length.
|
|
52
|
-
- Equality
|
|
52
|
+
- Equality relates a type only to its ancestors and descendants; hashing agrees.
|
|
53
53
|
|
|
54
54
|
The spec reads a fixed byte array as a vector of single bytes, so its default is every
|
|
55
55
|
byte zero.
|
|
@@ -110,7 +110,7 @@ class ByteVector(bytes, SSZType):
|
|
|
110
110
|
SSZValueError: If the coerced byte count differs from the declared length.
|
|
111
111
|
TypeError: If a value is passed that no coercion accepts.
|
|
112
112
|
"""
|
|
113
|
-
if
|
|
113
|
+
if cls.LENGTH is None:
|
|
114
114
|
raise SSZDefinitionError(cls.__name__, "LENGTH")
|
|
115
115
|
|
|
116
116
|
# An omitted value is the default, which is every byte zero.
|
|
@@ -130,6 +130,25 @@ class ByteVector(bytes, SSZType):
|
|
|
130
130
|
raise SSZLengthError(cls.__name__, cls.LENGTH, len(coerced_bytes), unit="bytes")
|
|
131
131
|
return super().__new__(cls, coerced_bytes)
|
|
132
132
|
|
|
133
|
+
@classmethod
|
|
134
|
+
def _trusted(cls, data: bytes) -> Self:
|
|
135
|
+
"""
|
|
136
|
+
Wrap bytes of an already-established length, skipping every check.
|
|
137
|
+
|
|
138
|
+
# Safety
|
|
139
|
+
|
|
140
|
+
The byte count must follow from how the value was built, never from a caller.
|
|
141
|
+
A wrong width yields an instance.
|
|
142
|
+
It merkleizes to a wrong root rather than failing.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
data: Exactly the declared number of bytes.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
An instance wrapping those bytes, unvalidated.
|
|
149
|
+
"""
|
|
150
|
+
return bytes.__new__(cls, data)
|
|
151
|
+
|
|
133
152
|
@classmethod
|
|
134
153
|
def zero(cls) -> Self:
|
|
135
154
|
"""Return a new instance filled with zero bytes, which is also the default."""
|
|
@@ -176,8 +195,8 @@ class ByteVector(bytes, SSZType):
|
|
|
176
195
|
serialized_bytes = stream.read(scope)
|
|
177
196
|
if len(serialized_bytes) != scope:
|
|
178
197
|
raise SSZScopeError(cls.__name__, scope, len(serialized_bytes))
|
|
179
|
-
#
|
|
180
|
-
return
|
|
198
|
+
# The read delivered exactly the checked count, which the guard above pinned.
|
|
199
|
+
return cls._trusted(serialized_bytes)
|
|
181
200
|
|
|
182
201
|
@override
|
|
183
202
|
def encode_bytes(self) -> bytes:
|
|
@@ -246,8 +265,34 @@ class ByteVector(bytes, SSZType):
|
|
|
246
265
|
return f"{type_name}({self.hex()})"
|
|
247
266
|
|
|
248
267
|
def __eq__(self, other: object) -> bool:
|
|
249
|
-
"""
|
|
250
|
-
|
|
268
|
+
r"""
|
|
269
|
+
Equality between two byte arrays whose types are related by inheritance.
|
|
270
|
+
|
|
271
|
+
One of the two types must derive from the other, and nothing but a byte array may
|
|
272
|
+
meet one at all. A root is declared a kind of chunk so it is usable where a chunk
|
|
273
|
+
is expected, and this comparison honours that:
|
|
274
|
+
|
|
275
|
+
x = b"\x11" * 32
|
|
276
|
+
|
|
277
|
+
Root(x) == Chunk(x) -> True a root is a kind of chunk
|
|
278
|
+
Bytes32(x) == Root(x) -> refused siblings, neither derives from the other
|
|
279
|
+
Bytes32(x) == x -> refused raw bytes are not a byte array at all
|
|
280
|
+
|
|
281
|
+
Invariant: whenever this returns True, __hash__ agrees. Python requires that of
|
|
282
|
+
every type, because dict and set find a value by its hash before they compare
|
|
283
|
+
it. That requirement is what fixes the hash below: Root(x) and Chunk(x) are one
|
|
284
|
+
value here, so no hash that named the concrete type could keep step.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
other: The value to compare against.
|
|
288
|
+
|
|
289
|
+
Raises:
|
|
290
|
+
TypeError: If other is not a byte array, or is one whose type is a sibling
|
|
291
|
+
of this one rather than an ancestor or a descendant.
|
|
292
|
+
"""
|
|
293
|
+
if not isinstance(other, ByteVector) or not (
|
|
294
|
+
isinstance(other, type(self)) or isinstance(self, type(other))
|
|
295
|
+
):
|
|
251
296
|
raise TypeError(
|
|
252
297
|
f"Unsupported operand type(s) for ==: "
|
|
253
298
|
f"'{type(self).__name__}' and '{type(other).__name__}'"
|
|
@@ -256,21 +301,31 @@ class ByteVector(bytes, SSZType):
|
|
|
256
301
|
|
|
257
302
|
def __ne__(self, other: object) -> bool:
|
|
258
303
|
"""
|
|
259
|
-
|
|
304
|
+
Inequality under the same relation as equality; anything else raises.
|
|
260
305
|
|
|
261
306
|
Defined explicitly because the parent bytes class has its own not-equal
|
|
262
|
-
that would otherwise bypass the
|
|
307
|
+
that would otherwise bypass the type rule above.
|
|
308
|
+
|
|
309
|
+
Raises:
|
|
310
|
+
TypeError: If other is not a byte array, or is one whose type is a sibling
|
|
311
|
+
of this one rather than an ancestor or a descendant.
|
|
263
312
|
"""
|
|
264
|
-
if not isinstance(other, ByteVector)
|
|
313
|
+
if not isinstance(other, ByteVector) or not (
|
|
314
|
+
isinstance(other, type(self)) or isinstance(self, type(other))
|
|
315
|
+
):
|
|
265
316
|
raise TypeError(
|
|
266
317
|
f"Unsupported operand type(s) for !=: "
|
|
267
318
|
f"'{type(self).__name__}' and '{type(other).__name__}'"
|
|
268
319
|
)
|
|
269
320
|
return bytes.__ne__(self, other)
|
|
270
321
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
322
|
+
# A byte array hashes as the bytes it holds, because equality calls a root and a chunk
|
|
323
|
+
# of the same bytes one value, which leaves the concrete type no room in the hash.
|
|
324
|
+
#
|
|
325
|
+
# Two types the comparison refuses to relate now share a bucket when their bytes agree,
|
|
326
|
+
# so a lookup reaches the refusal instead of missing in silence. When the bytes differ
|
|
327
|
+
# so do the hashes, and absent is the right answer whatever the types were.
|
|
328
|
+
__hash__ = bytes.__hash__
|
|
274
329
|
|
|
275
330
|
|
|
276
331
|
class ByteList(SSZCollection[int]):
|
|
@@ -279,7 +334,7 @@ class ByteList(SSZCollection[int]):
|
|
|
279
334
|
|
|
280
335
|
- Subclasses pin the maximum byte count by setting the class-level limit.
|
|
281
336
|
- Serialization writes the raw bytes; the length is recovered from the wrapping context.
|
|
282
|
-
- Equality
|
|
337
|
+
- Equality relates a type only to its ancestors and descendants; hashing agrees.
|
|
283
338
|
|
|
284
339
|
For example, a 4-byte payload under a limit of 10:
|
|
285
340
|
|
|
@@ -299,7 +354,7 @@ class ByteList(SSZCollection[int]):
|
|
|
299
354
|
def _validate_byte_list_data(cls, value: Any) -> bytes:
|
|
300
355
|
"""Enforce the maximum byte count and coerce inputs into a plain bytes object."""
|
|
301
356
|
# Subclasses must declare LIMIT before any instances can be validated.
|
|
302
|
-
if
|
|
357
|
+
if cls.LIMIT is None:
|
|
303
358
|
raise SSZDefinitionError(cls.__name__, "LIMIT")
|
|
304
359
|
|
|
305
360
|
# Coerce the input first, then enforce the upper bound.
|
|
@@ -313,27 +368,46 @@ class ByteList(SSZCollection[int]):
|
|
|
313
368
|
"""Serialize the raw bytes to a 0x-prefixed hex string for JSON output."""
|
|
314
369
|
return "0x" + value.hex()
|
|
315
370
|
|
|
371
|
+
def _store(self, working: bytearray) -> None:
|
|
372
|
+
"""
|
|
373
|
+
Store mutated bytes, checking the byte count and nothing else.
|
|
374
|
+
|
|
375
|
+
Assigning to the field would re-coerce a payload that is already bytes.
|
|
376
|
+
|
|
377
|
+
Raises:
|
|
378
|
+
SSZLimitError: When the mutation leaves more bytes than the limit allows.
|
|
379
|
+
"""
|
|
380
|
+
self._validate_length(len(working))
|
|
381
|
+
payload = bytes(working)
|
|
382
|
+
# A type checker reads this field through an inherited sequence declaration, so the
|
|
383
|
+
# assignment spelling is kept for it and the dictionary write for the runtime.
|
|
384
|
+
if not TYPE_CHECKING:
|
|
385
|
+
self.__dict__["data"] = payload
|
|
386
|
+
self.__pydantic_fields_set__.add("data")
|
|
387
|
+
else: # pragma: no cover
|
|
388
|
+
self.data = payload
|
|
389
|
+
|
|
316
390
|
@override
|
|
317
391
|
def __setitem__(self, index: int | slice, value: int | Sequence[int]) -> None:
|
|
318
392
|
"""Replace the byte(s) at ``index``, revalidating the stored payload."""
|
|
319
|
-
self.
|
|
393
|
+
self._begin_mutation()
|
|
320
394
|
working = bytearray(self.data)
|
|
321
395
|
working[index] = value # ty: ignore[invalid-assignment]
|
|
322
|
-
self.
|
|
396
|
+
self._store(working)
|
|
323
397
|
|
|
324
398
|
def append(self, value: int) -> None:
|
|
325
399
|
"""Add one byte at the end, revalidating the stored payload."""
|
|
326
|
-
self.
|
|
400
|
+
self._begin_mutation()
|
|
327
401
|
working = bytearray(self.data)
|
|
328
402
|
working.append(value)
|
|
329
|
-
self.
|
|
403
|
+
self._store(working)
|
|
330
404
|
|
|
331
405
|
def pop(self) -> int:
|
|
332
406
|
"""Remove and return the last byte, revalidating the stored payload."""
|
|
333
|
-
self.
|
|
407
|
+
self._begin_mutation()
|
|
334
408
|
working = bytearray(self.data)
|
|
335
409
|
last = working.pop()
|
|
336
|
-
self.
|
|
410
|
+
self._store(working)
|
|
337
411
|
return last
|
|
338
412
|
|
|
339
413
|
@classmethod
|
|
@@ -418,8 +492,25 @@ class ByteList(SSZCollection[int]):
|
|
|
418
492
|
return f"{type_name}({self.data.hex()})"
|
|
419
493
|
|
|
420
494
|
def __eq__(self, other: object) -> bool:
|
|
421
|
-
"""
|
|
422
|
-
|
|
495
|
+
"""
|
|
496
|
+
Equality between two byte lists whose types are related by inheritance.
|
|
497
|
+
|
|
498
|
+
One of the two types must derive from the other, and nothing but a byte list may
|
|
499
|
+
meet one at all. Two unrelated limits are refused even when the payloads match,
|
|
500
|
+
because a payload under one limit is not the same value as under another.
|
|
501
|
+
|
|
502
|
+
Invariant: whenever this returns True, the hash agrees.
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
other: The value to compare against.
|
|
506
|
+
|
|
507
|
+
Raises:
|
|
508
|
+
TypeError: If other is not a byte list, or is one whose type is a sibling
|
|
509
|
+
of this one rather than an ancestor or a descendant.
|
|
510
|
+
"""
|
|
511
|
+
if not isinstance(other, ByteList) or not (
|
|
512
|
+
isinstance(other, type(self)) or isinstance(self, type(other))
|
|
513
|
+
):
|
|
423
514
|
raise TypeError(
|
|
424
515
|
f"Unsupported operand type(s) for ==: "
|
|
425
516
|
f"'{type(self).__name__}' and '{type(other).__name__}'"
|
|
@@ -428,11 +519,17 @@ class ByteList(SSZCollection[int]):
|
|
|
428
519
|
|
|
429
520
|
def __ne__(self, other: object) -> bool:
|
|
430
521
|
"""
|
|
431
|
-
|
|
522
|
+
Inequality under the same relation as equality; anything else raises.
|
|
432
523
|
|
|
433
|
-
Mirrors the
|
|
524
|
+
Mirrors the equality contract — both operators apply one type rule.
|
|
525
|
+
|
|
526
|
+
Raises:
|
|
527
|
+
TypeError: If other is not a byte list, or is one whose type is a sibling
|
|
528
|
+
of this one rather than an ancestor or a descendant.
|
|
434
529
|
"""
|
|
435
|
-
if not isinstance(other, ByteList)
|
|
530
|
+
if not isinstance(other, ByteList) or not (
|
|
531
|
+
isinstance(other, type(self)) or isinstance(self, type(other))
|
|
532
|
+
):
|
|
436
533
|
raise TypeError(
|
|
437
534
|
f"Unsupported operand type(s) for !=: "
|
|
438
535
|
f"'{type(self).__name__}' and '{type(other).__name__}'"
|
|
@@ -440,8 +537,8 @@ class ByteList(SSZCollection[int]):
|
|
|
440
537
|
return self.data != other.data
|
|
441
538
|
|
|
442
539
|
def __hash__(self) -> int:
|
|
443
|
-
"""Return
|
|
444
|
-
return hash(
|
|
540
|
+
"""Return the hash of the payload alone — the one thing equality above compares."""
|
|
541
|
+
return hash(self.data)
|
|
445
542
|
|
|
446
543
|
def hex(self) -> str:
|
|
447
544
|
"""Return the hexadecimal string representation of the underlying bytes."""
|
|
@@ -109,19 +109,25 @@ def _coerce_elements(element_type: type[SSZType], elements: Sequence[Any]) -> li
|
|
|
109
109
|
- Ancestor-class values go through the element type's constructor.
|
|
110
110
|
- A coercion failure re-raises with the high-level expectation in the message.
|
|
111
111
|
- The chained cause preserves the underlying coercion detail.
|
|
112
|
+
|
|
113
|
+
An element of exactly the declared class is settled first, before the ancestor test.
|
|
114
|
+
A class is a subclass of itself, so the test that order skips is one that could only
|
|
115
|
+
have passed. What changes is the cost of the common case: an identity check on two type
|
|
116
|
+
objects, rather than a walk of an abstract base class's registry.
|
|
112
117
|
"""
|
|
113
118
|
coerced: list[SSZType] = []
|
|
114
119
|
for element in elements:
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if type(element) is element_type:
|
|
120
|
+
element_class = type(element)
|
|
121
|
+
if element_class is element_type:
|
|
118
122
|
coerced.append(element)
|
|
119
123
|
continue
|
|
124
|
+
if not issubclass(element_type, element_class):
|
|
125
|
+
raise SSZTypeMismatch(element_type.__name__, element_class)
|
|
120
126
|
try:
|
|
121
127
|
coerced.append(cast(Any, element_type)(element))
|
|
122
128
|
except (SSZTypeError, SSZValueError, TypeError, ValueError) as exception:
|
|
123
129
|
raise SSZTypeMismatch(
|
|
124
|
-
element_type.__name__,
|
|
130
|
+
element_type.__name__, element_class, detail=str(exception)
|
|
125
131
|
) from exception
|
|
126
132
|
return coerced
|
|
127
133
|
|
|
@@ -343,18 +349,28 @@ class Vector[T: SSZType](_SSZSequence[T]):
|
|
|
343
349
|
"""
|
|
344
350
|
Give the elements their default, which is the element default at every position.
|
|
345
351
|
|
|
346
|
-
|
|
347
|
-
|
|
352
|
+
One instance is shared across every position when the element type cannot be
|
|
353
|
+
mutated, and built per position otherwise, since two positions holding one mutable
|
|
354
|
+
element would alias.
|
|
348
355
|
"""
|
|
349
356
|
super().__pydantic_init_subclass__(**kwargs)
|
|
350
357
|
|
|
351
358
|
# A shape that declared neither keeps its inherited default, and fails its own
|
|
352
359
|
# declaration check instead.
|
|
353
|
-
if not
|
|
360
|
+
if not hasattr(cls, "ELEMENT_TYPE") or cls.LENGTH is None:
|
|
354
361
|
return
|
|
355
362
|
|
|
356
363
|
element_type, length = cls.ELEMENT_TYPE, cls.LENGTH
|
|
357
|
-
|
|
364
|
+
# A uint, a boolean and a fixed byte array each subclass an immutable builtin, so
|
|
365
|
+
# no position can alter what another holds. The bitfield default already shares one
|
|
366
|
+
# boolean on that argument, and it holds for every such element type.
|
|
367
|
+
if issubclass(element_type, (int, bytes)):
|
|
368
|
+
shared = element_type()
|
|
369
|
+
cls.model_fields["data"].default_factory = lambda: [shared] * length
|
|
370
|
+
else:
|
|
371
|
+
cls.model_fields["data"].default_factory = lambda: [
|
|
372
|
+
element_type() for _ in range(length)
|
|
373
|
+
]
|
|
358
374
|
cls.model_rebuild(force=True)
|
|
359
375
|
|
|
360
376
|
@field_validator("data", mode="before")
|
|
@@ -374,7 +390,7 @@ class Vector[T: SSZType](_SSZSequence[T]):
|
|
|
374
390
|
The chained cause preserves the underlying coercion detail.
|
|
375
391
|
"""
|
|
376
392
|
# Subclasses must declare both annotations before any instance can validate.
|
|
377
|
-
if not hasattr(cls, "ELEMENT_TYPE") or
|
|
393
|
+
if not hasattr(cls, "ELEMENT_TYPE") or cls.LENGTH is None:
|
|
378
394
|
raise SSZDefinitionError(cls.__name__, "ELEMENT_TYPE and LENGTH")
|
|
379
395
|
|
|
380
396
|
# Reject strings and non-iterables, then materialize into a sequence.
|
|
@@ -501,14 +517,14 @@ class _SSZList[T: SSZType](_SSZSequence[T]):
|
|
|
501
517
|
|
|
502
518
|
def append(self, value: T) -> None:
|
|
503
519
|
"""Add one element at the end, validating it and the resulting length."""
|
|
504
|
-
self.
|
|
520
|
+
self._begin_mutation()
|
|
505
521
|
element = self._validate_element(value)
|
|
506
522
|
self._validate_length(len(self.data) + 1)
|
|
507
523
|
cast("list[T]", self.data).append(element)
|
|
508
524
|
|
|
509
525
|
def pop(self) -> T:
|
|
510
526
|
"""Remove and return the last element, validating the resulting length."""
|
|
511
|
-
self.
|
|
527
|
+
self._begin_mutation()
|
|
512
528
|
self._validate_length(len(self.data) - 1)
|
|
513
529
|
return cast("list[T]", self.data).pop()
|
|
514
530
|
|
|
@@ -653,7 +669,14 @@ class List[T: SSZType](_SSZList[T]):
|
|
|
653
669
|
- Variable-size elements use an offset table that also reveals the count.
|
|
654
670
|
|
|
655
671
|
The hash tree root mixes in the element count alongside the chunked data.
|
|
656
|
-
|
|
672
|
+
|
|
673
|
+
A declared capacity reaches the root only through the width of the tree it bounds.
|
|
674
|
+
|
|
675
|
+
- That width is the next power of two at or above the capacity counted in chunks.
|
|
676
|
+
- Two capacities that round to the same width bound the same tree.
|
|
677
|
+
- The same contents then root identically under both.
|
|
678
|
+
- Eight-byte elements pack four to a chunk, so a capacity of three roots as four does.
|
|
679
|
+
- So do twelve and sixteen, whose three chunks and four fill one four-leaf tree.
|
|
657
680
|
|
|
658
681
|
Subclasses declare LIMIT directly in the class body.
|
|
659
682
|
The element type is inferred from the generic parameter.
|
|
@@ -692,7 +715,7 @@ class List[T: SSZType](_SSZList[T]):
|
|
|
692
715
|
The chained cause preserves the underlying coercion detail.
|
|
693
716
|
"""
|
|
694
717
|
# Subclasses must declare both annotations before any instance can validate.
|
|
695
|
-
if not hasattr(cls, "ELEMENT_TYPE") or
|
|
718
|
+
if not hasattr(cls, "ELEMENT_TYPE") or cls.LIMIT is None:
|
|
696
719
|
raise SSZDefinitionError(cls.__name__, "ELEMENT_TYPE and LIMIT")
|
|
697
720
|
|
|
698
721
|
# Reject strings and non-iterables, then materialize into a sequence.
|
|
@@ -11,6 +11,7 @@ Both encode identically: fixed-size fields inline, variable-size fields behind o
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
import io
|
|
14
|
+
from functools import cache
|
|
14
15
|
from itertools import pairwise
|
|
15
16
|
from typing import IO, Any, ClassVar, Final, Self, override
|
|
16
17
|
|
|
@@ -85,10 +86,7 @@ def active_fields(width: int, gaps: tuple[int, ...] = ()) -> tuple[int, ...]:
|
|
|
85
86
|
# Narrow before anything reads the value.
|
|
86
87
|
#
|
|
87
88
|
# A width may be written with a uint.
|
|
88
|
-
#
|
|
89
|
-
# The comparison below would therefore raise rather than answer.
|
|
90
|
-
# A uint left among the positions would also miss the plain position it names.
|
|
91
|
-
# The two hash apart.
|
|
89
|
+
# Narrowing keeps the positions below plain, as well as the messages naming them.
|
|
92
90
|
width = int(width)
|
|
93
91
|
if width < 1:
|
|
94
92
|
raise SSZValueError(f"a layout holds at least one position, got a width of {width}")
|
|
@@ -136,10 +134,7 @@ class _SSZContainer(SSZModel):
|
|
|
136
134
|
|
|
137
135
|
def __hash__(self) -> int:
|
|
138
136
|
"""Hash by Merkle tree root — equal containers hash equally."""
|
|
139
|
-
|
|
140
|
-
from ssz.merkleization import hash_tree_root
|
|
141
|
-
|
|
142
|
-
return hash(hash_tree_root(self))
|
|
137
|
+
return hash(self.hash_tree_root())
|
|
143
138
|
|
|
144
139
|
@model_validator(mode="wrap")
|
|
145
140
|
@classmethod
|
|
@@ -220,8 +215,7 @@ class _SSZContainer(SSZModel):
|
|
|
220
215
|
bytes_read = 0
|
|
221
216
|
|
|
222
217
|
# Phase 1: each slot is either the field itself or an offset to its tail payload.
|
|
223
|
-
for name,
|
|
224
|
-
field_type: type[SSZType] = field_definition.annotation
|
|
218
|
+
for name, field_type in _field_types(cls):
|
|
225
219
|
if field_type.is_fixed_size():
|
|
226
220
|
width = field_type.get_byte_length()
|
|
227
221
|
fields[name] = field_type.deserialize(stream, width)
|
|
@@ -394,3 +388,18 @@ class ProgressiveContainer(_SSZContainer):
|
|
|
394
388
|
f"the layout sets {active_count} positions, "
|
|
395
389
|
f"and the struct declares {len(cls.model_fields)}",
|
|
396
390
|
)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
type _FieldType = tuple[str, type[SSZType]]
|
|
394
|
+
"""One field: its name and the type it declares."""
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
@cache
|
|
398
|
+
def _field_types(cls: type[_SSZContainer]) -> tuple[_FieldType, ...]:
|
|
399
|
+
"""
|
|
400
|
+
Each field's declared type, in the declaration order the wire format follows.
|
|
401
|
+
|
|
402
|
+
Only what the declaration settles is kept. A width is asked of the type each time,
|
|
403
|
+
because the attributes a type derives one from stay writable for as long as the type does.
|
|
404
|
+
"""
|
|
405
|
+
return tuple((name, field.annotation) for name, field in cls.model_fields.items())
|
|
@@ -43,10 +43,10 @@ class SSZTypeMismatch(SSZTypeError): # noqa: N818
|
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
class SSZDefinitionError(SSZTypeError):
|
|
46
|
-
"""An SSZ subclass
|
|
46
|
+
"""An SSZ subclass declares its attributes in a way its base cannot operate on."""
|
|
47
47
|
|
|
48
48
|
def __init__(self, type_name: str, requirement: str) -> None:
|
|
49
|
-
"""Record the type and
|
|
49
|
+
"""Record the type and what its declaration failed to satisfy."""
|
|
50
50
|
self.type_name = type_name
|
|
51
51
|
self.requirement = requirement
|
|
52
52
|
super().__init__(f"{type_name} must define {requirement}")
|