Skip to content

Commit f65ccd9

Browse files
authored
xmss: make a few types Uint64 to prepare for ssz (leanEthereum#200)
* xmss: make a few types Uint64 to prepare for ssz * fix tests * small fixes
1 parent 7bdc968 commit f65ccd9

4 files changed

Lines changed: 48 additions & 47 deletions

File tree

src/lean_spec/subspecs/xmss/containers.py

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

77
from pydantic import Field
88

9-
from ...types import StrictBaseModel
9+
from ...types import StrictBaseModel, Uint64
1010
from ..koalabear import P_BYTES, Fp
1111
from .constants import PRF_KEY_LENGTH
1212

@@ -105,7 +105,7 @@ class HashTreeLayer(StrictBaseModel):
105105
for the active range of leaves, not the entire conceptual layer.
106106
"""
107107

108-
start_index: int
108+
start_index: Uint64
109109
"""The starting index of the first node in this layer."""
110110
nodes: List[HashDigest]
111111
"""A list of the actual hash digests stored for this layer."""
@@ -120,7 +120,7 @@ class HashTree(StrictBaseModel):
120120
long lifetimes, prefer `HashSubTree` with the top-bottom tree approach.
121121
"""
122122

123-
depth: int
123+
depth: Uint64
124124
"""The total depth of the tree (e.g., 32 for a 2^32 leaf space)."""
125125
layers: List[HashTreeLayer]
126126
"""

src/lean_spec/subspecs/xmss/merkle_tree.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def enforce_strict_types(self) -> "MerkleTree":
8181
)
8282
return self
8383

84-
def _get_padded_layer(self, nodes: List[HashDigest], start_index: int) -> HashTreeLayer:
84+
def _get_padded_layer(self, nodes: List[HashDigest], start_index: Uint64) -> HashTreeLayer:
8585
"""
8686
Pads a layer of nodes with random hashes to simplify tree construction.
8787
@@ -99,21 +99,21 @@ def _get_padded_layer(self, nodes: List[HashDigest], start_index: int) -> HashTr
9999
A new `HashTreeLayer` with the necessary padding applied.
100100
"""
101101
nodes_with_padding: List[HashDigest] = []
102-
end_index = start_index + len(nodes) - 1
102+
end_index = start_index + Uint64(len(nodes)) - Uint64(1)
103103

104104
# Prepend random padding if the layer starts at an odd index.
105-
if start_index % 2 == 1:
105+
if start_index % Uint64(2) == Uint64(1):
106106
nodes_with_padding.append(self.rand.domain())
107107

108108
# The actual start index of the padded layer is always the even
109109
# number at or immediately before the original start_index.
110-
actual_start_index = start_index - (start_index % 2)
110+
actual_start_index = start_index - (start_index % Uint64(2))
111111

112112
# Add the actual node content.
113113
nodes_with_padding.extend(nodes)
114114

115115
# Append random padding if the layer ends at an even index.
116-
if end_index % 2 == 0:
116+
if end_index % Uint64(2) == Uint64(0):
117117
nodes_with_padding.append(self.rand.domain())
118118

119119
return HashTreeLayer(start_index=actual_start_index, nodes=nodes_with_padding)
@@ -162,7 +162,7 @@ def build(
162162

163163
# Start with the leaf hashes and apply the initial padding.
164164
layers: List[HashTreeLayer] = []
165-
current_layer = self._get_padded_layer(leaf_hashes, int(start_index))
165+
current_layer = self._get_padded_layer(leaf_hashes, start_index)
166166
layers.append(current_layer)
167167

168168
# Iterate from the leaf layer (level 0) up to the root.
@@ -179,20 +179,20 @@ def build(
179179
)
180180
):
181181
# Calculate the position of the parent node in the next level up.
182-
parent_index = (current_layer.start_index // 2) + i
182+
parent_index = (current_layer.start_index // Uint64(2)) + Uint64(i)
183183
# Create the tweak for hashing these two children.
184184
tweak = TreeTweak(level=level + 1, index=parent_index)
185185
# Hash the left and right children to get their parent.
186186
parent_node = self.hasher.apply(parameter, tweak, list(children))
187187
parents.append(parent_node)
188188

189189
# Pad the new list of parents to prepare for the next iteration.
190-
new_start_index = current_layer.start_index // 2
190+
new_start_index = current_layer.start_index // Uint64(2)
191191
current_layer = self._get_padded_layer(parents, new_start_index)
192192
layers.append(current_layer)
193193

194194
# Return the completed tree containing all computed layers.
195-
return HashTree(depth=depth, layers=layers)
195+
return HashTree(depth=Uint64(depth), layers=layers)
196196

197197
def root(self, tree: HashTree) -> HashDigest:
198198
"""
@@ -230,26 +230,26 @@ def path(self, tree: HashTree, position: Uint64) -> HashTreeOpening:
230230
raise ValueError("Cannot generate path for empty tree.")
231231

232232
# Check that the position is within the tree's range.
233-
if int(position) < tree.layers[0].start_index:
233+
if position < tree.layers[0].start_index:
234234
raise ValueError("Position (before start) is invalid.")
235235

236-
if int(position) >= tree.layers[0].start_index + len(tree.layers[0].nodes):
236+
if position >= tree.layers[0].start_index + Uint64(len(tree.layers[0].nodes)):
237237
raise ValueError("Position (after end) is invalid.")
238238

239239
co_path: List[HashDigest] = []
240-
current_position = int(position)
240+
current_position = position
241241

242242
# Iterate from the leaf layer (level 0) up to the layer below the root.
243-
for level in range(tree.depth):
243+
for level in range(int(tree.depth)):
244244
# Determine the sibling's position by flipping the last bit (XOR with 1).
245-
sibling_position = current_position ^ 1
245+
sibling_position = current_position ^ Uint64(1)
246246
# Find the sibling's index within our sparsely stored `nodes` vector.
247247
layer = tree.layers[level]
248248
sibling_index_in_vec = sibling_position - layer.start_index
249249
# Add the sibling's hash to the co-path.
250-
co_path.append(layer.nodes[sibling_index_in_vec])
250+
co_path.append(layer.nodes[int(sibling_index_in_vec)])
251251
# Move up to the parent's position for the next iteration.
252-
current_position //= 2
252+
current_position = current_position // Uint64(2)
253253

254254
return HashTreeOpening(siblings=co_path)
255255

src/lean_spec/subspecs/xmss/subtree.py

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from .tweak_hash import TweakHasher
2020

2121

22-
def _get_padded_layer(rand: Rand, nodes: List[HashDigest], start_index: int) -> HashTreeLayer:
22+
def _get_padded_layer(rand: Rand, nodes: List[HashDigest], start_index: Uint64) -> HashTreeLayer:
2323
"""
2424
Pads a layer of nodes with random hashes to simplify tree construction.
2525
@@ -38,21 +38,21 @@ def _get_padded_layer(rand: Rand, nodes: List[HashDigest], start_index: int) ->
3838
A new `HashTreeLayer` with the necessary padding applied.
3939
"""
4040
nodes_with_padding: List[HashDigest] = []
41-
end_index = start_index + len(nodes) - 1
41+
end_index = start_index + Uint64(len(nodes)) - Uint64(1)
4242

4343
# Prepend random padding if the layer starts at an odd index.
44-
if start_index % 2 == 1:
44+
if start_index % Uint64(2) == Uint64(1):
4545
nodes_with_padding.append(rand.domain())
4646

4747
# The actual start index of the padded layer is always the even
4848
# number at or immediately before the original start_index.
49-
actual_start_index = start_index - (start_index % 2)
49+
actual_start_index = start_index - (start_index % Uint64(2))
5050

5151
# Add the actual node content.
5252
nodes_with_padding.extend(nodes)
5353

5454
# Append random padding if the layer ends at an even index.
55-
if end_index % 2 == 0:
55+
if end_index % Uint64(2) == Uint64(0):
5656
nodes_with_padding.append(rand.domain())
5757

5858
return HashTreeLayer(start_index=actual_start_index, nodes=nodes_with_padding)
@@ -84,15 +84,15 @@ class HashSubTree(StrictBaseModel):
8484
- Two consecutive bottom trees (sliding window)
8585
"""
8686

87-
depth: int
87+
depth: Uint64
8888
"""
8989
The total depth of the full tree (e.g., 32 for a 2^32 leaf space).
9090
9191
This represents the depth of the complete Merkle tree, not just this subtree.
9292
A subtree starting from layer `k` will have `depth - k` layers stored.
9393
"""
9494

95-
lowest_layer: int
95+
lowest_layer: Uint64
9696
"""
9797
The lowest layer included in this subtree.
9898
@@ -175,7 +175,7 @@ def new(
175175

176176
# Start with the lowest layer nodes and apply initial padding.
177177
layers: List[HashTreeLayer] = []
178-
current_layer = _get_padded_layer(rand, lowest_layer_nodes, start_index)
178+
current_layer = _get_padded_layer(rand, lowest_layer_nodes, Uint64(start_index))
179179
layers.append(current_layer)
180180

181181
# Build the tree layer by layer from lowest_layer up to the root.
@@ -192,20 +192,20 @@ def new(
192192
)
193193
):
194194
# Calculate the position of the parent node in the next level up.
195-
parent_index = (current_layer.start_index // 2) + i
195+
parent_index = (current_layer.start_index // Uint64(2)) + Uint64(i)
196196
# Create the tweak for hashing these two children.
197197
tweak = TreeTweak(level=level + 1, index=parent_index)
198198
# Hash the left and right children to get their parent.
199199
parent_node = hasher.apply(parameter, tweak, list(children))
200200
parents.append(parent_node)
201201

202202
# Pad the new list of parents to prepare for the next iteration.
203-
new_start_index = current_layer.start_index // 2
203+
new_start_index = current_layer.start_index // Uint64(2)
204204
current_layer = _get_padded_layer(rand, parents, new_start_index)
205205
layers.append(current_layer)
206206

207207
# Return the completed subtree.
208-
return cls(depth=depth, lowest_layer=lowest_layer, layers=layers)
208+
return cls(depth=Uint64(depth), lowest_layer=Uint64(lowest_layer), layers=layers)
209209

210210
@classmethod
211211
def new_top_tree(
@@ -353,16 +353,16 @@ def new_bottom_tree(
353353

354354
# The root is at position (start_index >> (depth // 2)) = bottom_tree_index
355355
# within the middle layer. We need to find it in the stored nodes.
356-
root_position_in_layer = bottom_tree_index - middle_layer.start_index
357-
root = middle_layer.nodes[root_position_in_layer]
356+
root_position_in_layer = Uint64(bottom_tree_index) - middle_layer.start_index
357+
root = middle_layer.nodes[int(root_position_in_layer)]
358358

359359
# Truncate layers to keep only 0 through depth/2 - 1.
360360
truncated_layers = full_tree.layers[: (depth // 2)]
361361

362362
# Add a final layer containing just the root.
363-
truncated_layers.append(HashTreeLayer(start_index=bottom_tree_index, nodes=[root]))
363+
truncated_layers.append(HashTreeLayer(start_index=Uint64(bottom_tree_index), nodes=[root]))
364364

365-
return cls(depth=depth, lowest_layer=0, layers=truncated_layers)
365+
return cls(depth=Uint64(depth), lowest_layer=Uint64(0), layers=truncated_layers)
366366

367367
def root(self) -> HashDigest:
368368
"""
@@ -412,35 +412,35 @@ def path(self, position: Uint64) -> HashTreeOpening:
412412
raise ValueError("Cannot generate path for empty subtree.")
413413

414414
lowest_layer = self.layers[0]
415-
if int(position) < lowest_layer.start_index:
415+
if position < lowest_layer.start_index:
416416
raise ValueError("Position is before the subtree's start index.")
417417

418-
if int(position) >= lowest_layer.start_index + len(lowest_layer.nodes):
418+
if position >= lowest_layer.start_index + Uint64(len(lowest_layer.nodes)):
419419
raise ValueError("Position is beyond the subtree's range.")
420420

421421
co_path: List[HashDigest] = []
422-
current_position = int(position)
422+
current_position = position
423423

424424
# Iterate through layers from lowest to highest, EXCLUDING the final root layer.
425425
# The root layer doesn't contribute a sibling to the authentication path.
426426
# self.layers[:-1] gives all layers except the last (root) layer.
427427
for layer in self.layers[:-1]:
428428
# Determine the sibling's position by flipping the last bit.
429-
sibling_position = current_position ^ 1
429+
sibling_position = current_position ^ Uint64(1)
430430
sibling_index = sibling_position - layer.start_index
431431

432432
# Ensure the sibling exists in this layer
433-
if sibling_index < 0 or sibling_index >= len(layer.nodes):
433+
if sibling_index < Uint64(0) or sibling_index >= Uint64(len(layer.nodes)):
434434
raise ValueError(
435435
f"Sibling index {sibling_index} out of bounds for layer "
436436
f"with {len(layer.nodes)} nodes"
437437
)
438438

439439
# Add the sibling's hash to the co-path.
440-
co_path.append(layer.nodes[sibling_index])
440+
co_path.append(layer.nodes[int(sibling_index)])
441441

442442
# Move to the parent's position for the next iteration.
443-
current_position //= 2
443+
current_position = current_position // Uint64(2)
444444

445445
return HashTreeOpening(siblings=co_path)
446446

@@ -496,23 +496,23 @@ def combined_path(
496496
depth = top_tree.depth
497497

498498
# Validate even depth (required for top-bottom split).
499-
if depth % 2 != 0:
499+
if depth % Uint64(2) != Uint64(0):
500500
raise ValueError(
501501
f"Top-bottom tree traversal requires even depth, got {depth}. "
502502
f"Cannot split tree into equal top and bottom halves."
503503
)
504504

505505
# Calculate parameters for bottom trees.
506-
leafs_per_bottom_tree = 1 << (depth // 2)
506+
leafs_per_bottom_tree = 1 << int(depth // Uint64(2))
507507

508508
# Determine which bottom tree this position belongs to.
509509
#
510510
# Bottom tree index = floor(position / sqrt(LIFETIME))
511-
bottom_tree_index = int(position) // leafs_per_bottom_tree
511+
bottom_tree_index = position // Uint64(leafs_per_bottom_tree)
512512

513513
# Verify that the provided bottom_tree actually corresponds to this position.
514514
# The bottom tree's lowest layer starts at bottom_tree_index * leafs_per_bottom_tree.
515-
expected_start = bottom_tree_index * leafs_per_bottom_tree
515+
expected_start = bottom_tree_index * Uint64(leafs_per_bottom_tree)
516516
actual_start = bottom_tree.layers[0].start_index
517517

518518
if actual_start != expected_start:

tests/lean_spec/subspecs/xmss/test_utils.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
expand_activation_time,
1616
int_to_base_p,
1717
)
18+
from lean_spec.types import Uint64
1819

1920

2021
@pytest.mark.parametrize(
@@ -129,8 +130,8 @@ def test_bottom_tree_from_prf_key() -> None:
129130
)
130131

131132
# Verify structure
132-
assert bottom_tree.depth == config.LOG_LIFETIME
133-
assert bottom_tree.lowest_layer == 0
133+
assert bottom_tree.depth == Uint64(config.LOG_LIFETIME)
134+
assert bottom_tree.lowest_layer == Uint64(0)
134135
assert len(bottom_tree.layers) > 0
135136

136137
# Verify the root layer has exactly one node

0 commit comments

Comments
 (0)