Skip to content

Commit 10c1ef5

Browse files
authored
xmss: more ssz containers (#209)
* xmss: more ssz containers * more ssz
1 parent f839a7c commit 10c1ef5

8 files changed

Lines changed: 112 additions & 57 deletions

File tree

src/lean_spec/subspecs/xmss/containers.py

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from ...types import StrictBaseModel, Uint64
88
from ...types.byte_arrays import BaseBytes
99
from ...types.collections import SSZList, SSZVector
10+
from ...types.container import Container
1011
from ..koalabear import P_BYTES, Fp
1112
from .constants import PRF_KEY_LENGTH, PROD_CONFIG
1213

@@ -90,14 +91,19 @@ class Parameter(SSZVector):
9091
LENGTH = PROD_CONFIG.PARAMETER_LEN
9192

9293

93-
Randomness = List[Fp]
94-
"""
95-
A type alias for the randomness `rho` (ρ) used during signing.
94+
class Randomness(SSZVector):
95+
"""
96+
The randomness `rho` (ρ) used during signing.
9697
97-
This value provides a variable input to the message hash, allowing the signer to
98-
repeatedly try hashing until a valid "codeword" is found. It must be included in
99-
the final signature for the verifier to reproduce the same hash.
100-
"""
98+
This value provides a variable input to the message hash, allowing the signer to
99+
repeatedly try hashing until a valid "codeword" is found. It must be included in
100+
the final signature for the verifier to reproduce the same hash.
101+
102+
SSZ notation: `Vector[Fp, RAND_LEN_FE]`
103+
"""
104+
105+
ELEMENT_TYPE = Fp
106+
LENGTH = PROD_CONFIG.RAND_LEN_FE
101107

102108

103109
def _serialize_digests(digests: HashDigestList) -> bytes:
@@ -155,7 +161,7 @@ class HashTreeOpening(StrictBaseModel):
155161
"""SSZ-compliant list of sibling hashes, from bottom to top."""
156162

157163

158-
class HashTreeLayer(StrictBaseModel):
164+
class HashTreeLayer(Container):
159165
"""
160166
Represents a single horizontal "slice" of the sparse Merkle tree.
161167
@@ -169,6 +175,33 @@ class HashTreeLayer(StrictBaseModel):
169175
"""SSZ-compliant list of hash digests stored for this layer."""
170176

171177

178+
LAYERS_LIMIT = PROD_CONFIG.LOG_LIFETIME + 1
179+
"""
180+
The maximum number of layers in a subtree.
181+
182+
This is `LOG_LIFETIME + 1` to accommodate all layers from 0 (leaves) to LOG_LIFETIME (root),
183+
inclusive. For PROD_CONFIG with LOG_LIFETIME=32, this allows up to 33 layers.
184+
"""
185+
186+
187+
class HashTreeLayers(SSZList):
188+
"""
189+
Variable-length list of Merkle tree layers.
190+
191+
In SSZ notation: `List[HashTreeLayer, LAYERS_LIMIT]`
192+
193+
This type represents the layers of a subtree, from the lowest layer up to the root.
194+
195+
The number of layers varies based on the subtree structure:
196+
- Bottom trees: `LOG_LIFETIME/2` layers
197+
- Top trees: `LOG_LIFETIME/2` layers
198+
- Maximum: `LOG_LIFETIME + 1` layers
199+
"""
200+
201+
ELEMENT_TYPE = HashTreeLayer
202+
LIMIT = LAYERS_LIMIT
203+
204+
172205
class PublicKey(StrictBaseModel):
173206
"""
174207
The public-facing component of a key pair.
@@ -298,7 +331,7 @@ def __bytes__(self) -> bytes:
298331
"""
299332
return (
300333
_serialize_digests(self.path.siblings)
301-
+ Fp.serialize_list(self.rho)
334+
+ Fp.serialize_list(cast(List[Fp], list(self.rho.data)))
302335
+ _serialize_digests(self.hashes)
303336
)
304337

@@ -399,7 +432,7 @@ def from_bytes(cls, data: bytes, config: XmssConfig) -> Signature:
399432

400433
return cls(
401434
path=HashTreeOpening(siblings=siblings),
402-
rho=rho,
435+
rho=Randomness(data=rho),
403436
hashes=hashes,
404437
)
405438

src/lean_spec/subspecs/xmss/merkle_tree.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
HashDigestList,
4747
HashDigestVector,
4848
HashTreeLayer,
49+
HashTreeLayers,
4950
HashTreeOpening,
5051
Parameter,
5152
)
@@ -166,7 +167,9 @@ def build(
166167

167168
# Return the completed tree containing all computed layers.
168169
# A full tree is represented as a HashSubTree with lowest_layer=0
169-
return HashSubTree(depth=Uint64(depth), lowest_layer=Uint64(0), layers=layers)
170+
return HashSubTree(
171+
depth=Uint64(depth), lowest_layer=Uint64(0), layers=HashTreeLayers(data=layers)
172+
)
170173

171174
def root(self, tree: HashSubTree) -> HashDigestVector:
172175
"""
@@ -176,7 +179,7 @@ def root(self, tree: HashSubTree) -> HashDigestVector:
176179
and serves as the primary component of the master public key.
177180
"""
178181
# The root is the single node in the final layer.
179-
return cast(HashDigestVector, tree.layers[-1].nodes[0])
182+
return cast(HashDigestVector, cast(HashTreeLayer, tree.layers.data[-1]).nodes[0])
180183

181184
def path(self, tree: HashSubTree, position: Uint64) -> HashTreeOpening:
182185
"""
@@ -204,10 +207,11 @@ def path(self, tree: HashSubTree, position: Uint64) -> HashTreeOpening:
204207
raise ValueError("Cannot generate path for empty tree.")
205208

206209
# Check that the position is within the tree's range.
207-
if position < tree.layers[0].start_index:
210+
first_layer = cast(HashTreeLayer, tree.layers.data[0])
211+
if position < first_layer.start_index:
208212
raise ValueError("Position (before start) is invalid.")
209213

210-
if position >= tree.layers[0].start_index + Uint64(len(tree.layers[0].nodes)):
214+
if position >= first_layer.start_index + Uint64(len(first_layer.nodes)):
211215
raise ValueError("Position (after end) is invalid.")
212216

213217
co_path: List[List[Fp]] = []
@@ -218,7 +222,7 @@ def path(self, tree: HashSubTree, position: Uint64) -> HashTreeOpening:
218222
# Determine the sibling's position by flipping the last bit (XOR with 1).
219223
sibling_position = current_position ^ Uint64(1)
220224
# Find the sibling's index within our sparsely stored `nodes` vector.
221-
layer = tree.layers[level]
225+
layer = cast(HashTreeLayer, tree.layers.data[level])
222226
sibling_index_in_vec = sibling_position - layer.start_index
223227
# Add the sibling's hash to the co-path.
224228
sibling_node = cast(HashDigestVector, layer.nodes.data[int(sibling_index_in_vec)])

src/lean_spec/subspecs/xmss/message_hash.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ def apply(
191191

192192
# The input is: rho || P || epoch || message || iteration.
193193
combined_input = (
194-
rho
194+
cast(List[Fp], list(rho.data))
195195
+ cast(List[Fp], list(parameter.data))
196196
+ epoch_fe
197197
+ message_fe

src/lean_spec/subspecs/xmss/prf.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
TEST_CONFIG,
2525
XmssConfig,
2626
)
27-
from .containers import PRFKey
27+
from .containers import PRFKey, Randomness
2828

2929
PRF_DOMAIN_SEP: bytes = bytes(
3030
[
@@ -172,7 +172,7 @@ def apply(self, key: PRFKey, epoch: Uint64, chain_index: Uint64) -> List[Fp]:
172172

173173
def get_randomness(
174174
self, key: PRFKey, epoch: Uint64, message: bytes, counter: Uint64
175-
) -> List[Fp]:
175+
) -> Randomness:
176176
"""
177177
Derives pseudorandom field elements for use in deterministic signing.
178178
@@ -200,7 +200,7 @@ def get_randomness(
200200
counter: The attempt number (used when retrying encoding).
201201
202202
Returns:
203-
A list of field elements to use as randomness for encoding (i.e., `rho`).
203+
Randomness for encoding (i.e., `rho`).
204204
"""
205205
config = self.config
206206

@@ -224,16 +224,18 @@ def get_randomness(
224224
num_bytes_to_read = PRF_BYTES_PER_FE * config.RAND_LEN_FE
225225
prf_output_bytes = hashlib.shake_128(input_data).digest(num_bytes_to_read)
226226

227-
# Convert to field elements
228-
return [
229-
Fp(
230-
value=int.from_bytes(
231-
prf_output_bytes[i * PRF_BYTES_PER_FE : (i + 1) * PRF_BYTES_PER_FE],
232-
"big",
227+
# Convert to field elements and wrap in Randomness
228+
return Randomness(
229+
data=[
230+
Fp(
231+
value=int.from_bytes(
232+
prf_output_bytes[i * PRF_BYTES_PER_FE : (i + 1) * PRF_BYTES_PER_FE],
233+
"big",
234+
)
233235
)
234-
)
235-
for i in range(config.RAND_LEN_FE)
236-
]
236+
for i in range(config.RAND_LEN_FE)
237+
]
238+
)
237239

238240

239241
PROD_PRF = Prf(config=PROD_CONFIG)

src/lean_spec/subspecs/xmss/rand.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def domain(self) -> List[Fp]:
4040

4141
def rho(self) -> Randomness:
4242
"""Generates randomness `rho` for message encoding."""
43-
return self.field_elements(self.config.RAND_LEN_FE)
43+
return Randomness(data=self.field_elements(self.config.RAND_LEN_FE))
4444

4545

4646
PROD_RAND = Rand(config=PROD_CONFIG)

src/lean_spec/subspecs/xmss/subtree.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
HashDigestList,
1717
HashDigestVector,
1818
HashTreeLayer,
19+
HashTreeLayers,
1920
HashTreeOpening,
2021
Parameter,
2122
)
@@ -72,10 +73,12 @@ class HashSubTree(StrictBaseModel):
7273
layers 16 through 32 (the root).
7374
"""
7475

75-
layers: List[HashTreeLayer]
76+
layers: HashTreeLayers
7677
"""
7778
The layers of this subtree, from `lowest_layer` to the root.
7879
80+
SSZ notation: `List[HashTreeLayer, LAYERS_LIMIT]`
81+
7982
- `layers[0]` corresponds to layer `lowest_layer` in the full tree
8083
- `layers[-1]` corresponds to the highest layer in this subtree
8184
- For bottom trees: the last layer contains a single root
@@ -179,7 +182,11 @@ def new(
179182
layers.append(current_layer)
180183

181184
# Return the completed subtree.
182-
return cls(depth=Uint64(depth), lowest_layer=Uint64(lowest_layer), layers=layers)
185+
return cls(
186+
depth=Uint64(depth),
187+
lowest_layer=Uint64(lowest_layer),
188+
layers=HashTreeLayers(data=layers),
189+
)
183190

184191
@classmethod
185192
def new_top_tree(
@@ -326,7 +333,7 @@ def new_bottom_tree(
326333
# the bottom_tree_index (if it's the left child of its parent in the top tree)
327334
# or bottom_tree_index (if it's the right child). Since we're at layer depth/2,
328335
# the position is simply bottom_tree_index.
329-
middle_layer = full_tree.layers[depth // 2]
336+
middle_layer = cast(HashTreeLayer, full_tree.layers.data[depth // 2])
330337

331338
# The root is at position (start_index >> (depth // 2)) = bottom_tree_index
332339
# within the middle layer. We need to find it in the stored nodes.
@@ -336,7 +343,7 @@ def new_bottom_tree(
336343
root = list(root_data)
337344

338345
# Truncate layers to keep only 0 through depth/2 - 1.
339-
truncated_layers = full_tree.layers[: (depth // 2)]
346+
truncated_layers = list(full_tree.layers.data[: (depth // 2)])
340347

341348
# Add a final layer containing just the root.
342349
root_vector = HashDigestVector(data=root)
@@ -345,7 +352,11 @@ def new_bottom_tree(
345352
)
346353
truncated_layers.append(root_layer)
347354

348-
return cls(depth=Uint64(depth), lowest_layer=Uint64(0), layers=truncated_layers)
355+
return cls(
356+
depth=Uint64(depth),
357+
lowest_layer=Uint64(0),
358+
layers=HashTreeLayers(data=truncated_layers),
359+
)
349360

350361
def root(self) -> HashDigestVector:
351362
"""
@@ -363,7 +374,7 @@ def root(self) -> HashDigestVector:
363374
if len(self.layers) == 0:
364375
raise ValueError("Cannot get root of empty subtree.")
365376

366-
highest_layer = self.layers[-1]
377+
highest_layer = cast(HashTreeLayer, self.layers.data[-1])
367378
if len(highest_layer.nodes.data) == 0:
368379
raise ValueError("Highest layer of subtree is empty.")
369380

@@ -394,7 +405,7 @@ def path(self, position: Uint64) -> HashTreeOpening:
394405
if len(self.layers) == 0:
395406
raise ValueError("Cannot generate path for empty subtree.")
396407

397-
lowest_layer = self.layers[0]
408+
lowest_layer = cast(HashTreeLayer, self.layers.data[0])
398409
if position < lowest_layer.start_index:
399410
raise ValueError("Position is before the subtree's start index.")
400411

@@ -407,8 +418,9 @@ def path(self, position: Uint64) -> HashTreeOpening:
407418

408419
# Iterate through layers from lowest to highest, EXCLUDING the final root layer.
409420
# The root layer doesn't contribute a sibling to the authentication path.
410-
# self.layers[:-1] gives all layers except the last (root) layer.
411-
for layer in self.layers[:-1]:
421+
# self.layers.data[:-1] gives all layers except the last (root) layer.
422+
for layer_raw in self.layers.data[:-1]:
423+
layer = cast(HashTreeLayer, layer_raw)
412424
# Determine the sibling's position by flipping the last bit.
413425
sibling_position = current_position ^ Uint64(1)
414426
sibling_index = sibling_position - layer.start_index
@@ -498,7 +510,7 @@ def combined_path(
498510
# Verify that the provided bottom_tree actually corresponds to this position.
499511
# The bottom tree's lowest layer starts at bottom_tree_index * leafs_per_bottom_tree.
500512
expected_start = bottom_tree_index * Uint64(leafs_per_bottom_tree)
501-
actual_start = bottom_tree.layers[0].start_index
513+
actual_start = cast(HashTreeLayer, bottom_tree.layers.data[0]).start_index
502514

503515
if actual_start != expected_start:
504516
raise ValueError(

0 commit comments

Comments
 (0)