Skip to content

Commit f839a7c

Browse files
authored
xmss: make Parameter ssz compatible (leanEthereum#208)
1 parent f59672e commit f839a7c

7 files changed

Lines changed: 55 additions & 33 deletions

File tree

src/lean_spec/subspecs/xmss/containers.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,18 @@ class HashDigestList(SSZList):
7777
LIMIT = NODE_LIST_LIMIT
7878

7979

80-
Parameter = List[Fp]
81-
"""
82-
A type alias for the public parameter `P`.
80+
class Parameter(SSZVector):
81+
"""
82+
The public parameter P.
83+
84+
This is a unique, randomly generated value associated with a single key pair. It
85+
is mixed into every hash computation to "personalize" the hash function, preventing
86+
certain cross-key attacks. It is public knowledge.
87+
"""
88+
89+
ELEMENT_TYPE = Fp
90+
LENGTH = PROD_CONFIG.PARAMETER_LEN
8391

84-
This is a unique, randomly generated value associated with a single key pair. It
85-
is mixed into every hash computation to "personalize" the hash function, preventing
86-
certain cross-key attacks. It is public knowledge.
87-
"""
8892

8993
Randomness = List[Fp]
9094
"""
@@ -199,7 +203,7 @@ def __bytes__(self) -> bytes:
199203
True
200204
"""
201205
return Fp.serialize_list(cast(List[Fp], list(self.root.data))) + Fp.serialize_list(
202-
self.parameter
206+
cast(List[Fp], list(self.parameter.data))
203207
)
204208

205209
def to_bytes(self, config: XmssConfig) -> bytes:
@@ -266,7 +270,7 @@ def from_bytes(cls, data: bytes, config: XmssConfig) -> PublicKey:
266270
root = Fp.deserialize_list(data[:root_len], config.HASH_LEN_FE)
267271
parameter = Fp.deserialize_list(data[root_len:], config.PARAMETER_LEN)
268272

269-
return cls(root=HashDigestVector(data=root), parameter=parameter)
273+
return cls(root=HashDigestVector(data=root), parameter=Parameter(data=parameter))
270274

271275

272276
class Signature(StrictBaseModel):

src/lean_spec/subspecs/xmss/message_hash.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030
from __future__ import annotations
3131

32-
from typing import List
32+
from typing import List, cast
3333

3434
from pydantic import model_validator
3535

@@ -190,7 +190,13 @@ def apply(
190190
iteration_separator = [Fp(value=i)]
191191

192192
# The input is: rho || P || epoch || message || iteration.
193-
combined_input = rho + parameter + epoch_fe + message_fe + iteration_separator
193+
combined_input = (
194+
rho
195+
+ cast(List[Fp], list(parameter.data))
196+
+ epoch_fe
197+
+ message_fe
198+
+ iteration_separator
199+
)
194200

195201
# Hash the combined input using Poseidon2 compression mode.
196202
iteration_output = self.poseidon.compress(

src/lean_spec/subspecs/xmss/rand.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def field_elements(self, length: int) -> List[Fp]:
3232

3333
def parameter(self) -> Parameter:
3434
"""Generates a random public parameter."""
35-
return self.field_elements(self.config.PARAMETER_LEN)
35+
return Parameter(data=self.field_elements(self.config.PARAMETER_LEN))
3636

3737
def domain(self) -> List[Fp]:
3838
"""Generates a random hash digest."""

src/lean_spec/subspecs/xmss/tweak_hash.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from __future__ import annotations
2828

2929
from itertools import chain
30-
from typing import List, Union
30+
from typing import List, Union, cast
3131

3232
from pydantic import Field, model_validator
3333

@@ -189,14 +189,19 @@ def apply(
189189
# Case 1: Hashing a single digest (used in hash chains).
190190
#
191191
# We use the efficient width-16 compression mode.
192-
input_vec = parameter + encoded_tweak + message_parts[0]
192+
input_vec = cast(List[Fp], list(parameter.data)) + encoded_tweak + message_parts[0]
193193
return self.poseidon.compress(input_vec, 16, config.HASH_LEN_FE)
194194

195195
elif len(message_parts) == 2:
196196
# Case 2: Hashing two digests (used for Merkle tree nodes).
197197
#
198198
# We use the slightly larger width-24 compression mode.
199-
input_vec = parameter + encoded_tweak + message_parts[0] + message_parts[1]
199+
input_vec = (
200+
cast(List[Fp], list(parameter.data))
201+
+ encoded_tweak
202+
+ message_parts[0]
203+
+ message_parts[1]
204+
)
200205
return self.poseidon.compress(input_vec, 24, config.HASH_LEN_FE)
201206

202207
else:
@@ -205,7 +210,7 @@ def apply(
205210
# We use the robust sponge mode.
206211
# First, flatten the list of message parts into a single vector.
207212
flattened_message = list(chain.from_iterable(message_parts))
208-
input_vec = parameter + encoded_tweak + flattened_message
213+
input_vec = cast(List[Fp], list(parameter.data)) + encoded_tweak + flattened_message
209214

210215
# Create a domain separator for the sponge mode based on the input dimensions.
211216
#

src/lean_spec/subspecs/xmss/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from ...types.uint import Uint64
66
from ..koalabear import Fp, P
77
from .constants import XmssConfig
8-
from .containers import HashDigestList, HashDigestVector, HashTreeLayer, PRFKey
8+
from .containers import HashDigestList, HashDigestVector, HashTreeLayer, Parameter, PRFKey
99
from .rand import Rand
1010

1111
if TYPE_CHECKING:
@@ -168,7 +168,7 @@ def bottom_tree_from_prf_key(
168168
config: XmssConfig,
169169
prf_key: PRFKey,
170170
bottom_tree_index: Uint64,
171-
parameter: List[Fp],
171+
parameter: Parameter,
172172
) -> "HashSubTree":
173173
"""
174174
Generates a single bottom tree on-demand from the PRF key.

tests/lean_spec/subspecs/xmss/test_containers.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
HashDigestList,
1111
HashDigestVector,
1212
HashTreeOpening,
13+
Parameter,
1314
PublicKey,
1415
Signature,
1516
)
@@ -21,7 +22,7 @@ class TestPublicKey:
2122
def test_bytes_protocol(self) -> None:
2223
"""Test that PublicKey implements Python's bytes protocol."""
2324
root = HashDigestVector(data=[Fp(value=i) for i in range(TEST_CONFIG.HASH_LEN_FE)])
24-
parameter = [Fp(value=i + 100) for i in range(TEST_CONFIG.PARAMETER_LEN)]
25+
parameter = Parameter(data=[Fp(value=i + 100) for i in range(TEST_CONFIG.PARAMETER_LEN)])
2526
pk = PublicKey(root=root, parameter=parameter)
2627

2728
# Test __bytes__()
@@ -32,7 +33,7 @@ def test_bytes_protocol(self) -> None:
3233
def test_to_bytes_with_validation(self) -> None:
3334
"""Test that to_bytes validates field lengths."""
3435
root = HashDigestVector(data=[Fp(value=i) for i in range(TEST_CONFIG.HASH_LEN_FE)])
35-
parameter = [Fp(value=i) for i in range(TEST_CONFIG.PARAMETER_LEN)]
36+
parameter = Parameter(data=[Fp(value=i) for i in range(TEST_CONFIG.PARAMETER_LEN)])
3637
pk = PublicKey(root=root, parameter=parameter)
3738

3839
# Valid serialization
@@ -43,15 +44,14 @@ def test_to_bytes_with_validation(self) -> None:
4344
with pytest.raises(ValueError, match="requires exactly"):
4445
HashDigestVector(data=[Fp(value=0)] * 5)
4546

46-
# Invalid parameter length
47-
invalid_pk = PublicKey(root=root, parameter=[Fp(value=0)] * 3)
48-
with pytest.raises(ValueError, match="Invalid parameter length"):
49-
invalid_pk.to_bytes(TEST_CONFIG)
47+
# Invalid parameter length - Parameter validates length at construction
48+
with pytest.raises(ValueError, match="requires exactly"):
49+
Parameter(data=[Fp(value=0)] * 3)
5050

5151
def test_roundtrip_test_config(self) -> None:
5252
"""Test serialization round-trip with TEST_CONFIG."""
5353
root = HashDigestVector(data=[Fp(value=i * 10) for i in range(TEST_CONFIG.HASH_LEN_FE)])
54-
parameter = [Fp(value=i * 20) for i in range(TEST_CONFIG.PARAMETER_LEN)]
54+
parameter = Parameter(data=[Fp(value=i * 20) for i in range(TEST_CONFIG.PARAMETER_LEN)])
5555
original = PublicKey(root=root, parameter=parameter)
5656

5757
# Serialize and deserialize
@@ -65,7 +65,7 @@ def test_roundtrip_test_config(self) -> None:
6565
def test_roundtrip_prod_config(self) -> None:
6666
"""Test serialization round-trip with PROD_CONFIG."""
6767
root = HashDigestVector(data=[Fp(value=i) for i in range(PROD_CONFIG.HASH_LEN_FE)])
68-
parameter = [Fp(value=i + 1000) for i in range(PROD_CONFIG.PARAMETER_LEN)]
68+
parameter = Parameter(data=[Fp(value=i + 1000) for i in range(PROD_CONFIG.PARAMETER_LEN)])
6969
original = PublicKey(root=root, parameter=parameter)
7070

7171
data = original.to_bytes(PROD_CONFIG)
@@ -86,7 +86,7 @@ def test_from_bytes_invalid_length(self) -> None:
8686
def test_serialization_format(self) -> None:
8787
"""Test that serialization follows the documented format: root || parameter."""
8888
root = HashDigestVector(data=[Fp(value=i) for i in range(TEST_CONFIG.HASH_LEN_FE)])
89-
parameter = [Fp(value=i + 100) for i in range(TEST_CONFIG.PARAMETER_LEN)]
89+
parameter = Parameter(data=[Fp(value=i + 100) for i in range(TEST_CONFIG.PARAMETER_LEN)])
9090
pk = PublicKey(root=root, parameter=parameter)
9191

9292
data = bytes(pk)
@@ -95,7 +95,7 @@ def test_serialization_format(self) -> None:
9595
from typing import List, cast
9696

9797
root_data = Fp.serialize_list(cast(List[Fp], list(root.data)))
98-
parameter_data = Fp.serialize_list(parameter)
98+
parameter_data = Fp.serialize_list(cast(List[Fp], list(parameter.data)))
9999

100100
assert data == root_data + parameter_data
101101
assert data[: len(root_data)] == root_data
@@ -255,7 +255,7 @@ class TestSerializationProperties:
255255
def test_public_key_deterministic(self) -> None:
256256
"""Test that serialization is deterministic."""
257257
root = HashDigestVector(data=[Fp(value=i) for i in range(TEST_CONFIG.HASH_LEN_FE)])
258-
parameter = [Fp(value=i) for i in range(TEST_CONFIG.PARAMETER_LEN)]
258+
parameter = Parameter(data=[Fp(value=i) for i in range(TEST_CONFIG.PARAMETER_LEN)])
259259
pk = PublicKey(root=root, parameter=parameter)
260260

261261
# Serialize multiple times
@@ -291,7 +291,7 @@ def test_different_values_produce_different_bytes(self) -> None:
291291
"""Test that different values produce different serializations."""
292292
root1 = HashDigestVector(data=[Fp(value=i) for i in range(TEST_CONFIG.HASH_LEN_FE)])
293293
root2 = HashDigestVector(data=[Fp(value=i + 1) for i in range(TEST_CONFIG.HASH_LEN_FE)])
294-
parameter = [Fp(value=0)] * TEST_CONFIG.PARAMETER_LEN
294+
parameter = Parameter(data=[Fp(value=0)] * TEST_CONFIG.PARAMETER_LEN)
295295

296296
pk1 = PublicKey(root=root1, parameter=parameter)
297297
pk2 = PublicKey(root=root2, parameter=parameter)

tests/lean_spec/subspecs/xmss/test_utils.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from lean_spec.subspecs.koalabear.field import Fp, P
99
from lean_spec.subspecs.xmss.constants import TEST_CONFIG
10+
from lean_spec.subspecs.xmss.containers import Parameter
1011
from lean_spec.subspecs.xmss.merkle_tree import TEST_MERKLE_TREE
1112
from lean_spec.subspecs.xmss.prf import TEST_PRF
1213
from lean_spec.subspecs.xmss.tweak_hash import TEST_TWEAK_HASHER
@@ -116,7 +117,9 @@ def test_bottom_tree_from_prf_key() -> None:
116117
prf_key = TEST_PRF.key_gen()
117118

118119
# Generate a random parameter
119-
parameter = [Fp(value=secrets.randbelow(P)) for _ in range(config.PARAMETER_LEN)]
120+
parameter = Parameter(
121+
data=[Fp(value=secrets.randbelow(P)) for _ in range(config.PARAMETER_LEN)]
122+
)
120123

121124
# Generate bottom tree 0
122125
bottom_tree = bottom_tree_from_prf_key(
@@ -148,7 +151,9 @@ def test_bottom_tree_from_prf_key_deterministic() -> None:
148151
"""Tests that bottom_tree_from_prf_key is deterministic."""
149152
config = TEST_CONFIG
150153
prf_key = TEST_PRF.key_gen()
151-
parameter = [Fp(value=secrets.randbelow(P)) for _ in range(config.PARAMETER_LEN)]
154+
parameter = Parameter(
155+
data=[Fp(value=secrets.randbelow(P)) for _ in range(config.PARAMETER_LEN)]
156+
)
152157

153158
# Generate the same bottom tree twice
154159
tree1 = bottom_tree_from_prf_key(
@@ -179,7 +184,9 @@ def test_bottom_tree_from_prf_key_different_indices() -> None:
179184
"""Tests that different bottom tree indices produce different trees."""
180185
config = TEST_CONFIG
181186
prf_key = TEST_PRF.key_gen()
182-
parameter = [Fp(value=secrets.randbelow(P)) for _ in range(config.PARAMETER_LEN)]
187+
parameter = Parameter(
188+
data=[Fp(value=secrets.randbelow(P)) for _ in range(config.PARAMETER_LEN)]
189+
)
183190

184191
# Generate two different bottom trees
185192
tree0 = bottom_tree_from_prf_key(

0 commit comments

Comments
 (0)