Skip to content

Commit de0aadc

Browse files
authored
xmss: better hypercube documentation (leanEthereum#27)
* xmss: better hypercube documentation * cleanup
1 parent 1f1c562 commit de0aadc

1 file changed

Lines changed: 115 additions & 95 deletions

File tree

src/lean_spec/subspecs/xmss/hypercube.py

Lines changed: 115 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,28 @@
11
"""
2-
Implements the mathematical operations for hypercube layers.
3-
4-
This module provides the necessary functions to work with vertices in a
5-
v-dimensional hypercube with coordinates in the range [0, w-1]. A key concept
6-
is the partitioning of the hypercube's vertices into "layers". A vertex belongs
7-
to layer `d`, where `d` is its distance from the sink vertex `(w-1, ..., w-1)`.
8-
9-
The core functionalities are:
10-
1. **Precomputation and Caching**: Computes and caches the sizes
11-
of each layer for different hypercube configurations (`w` and `v`).
12-
2. **Mapping**: Provides bijective mappings between an integer index within a
13-
layer and the unique vertex (a list of coordinates) it represents.
2+
Implements the mathematical operations for hypercube-based encodings.
3+
4+
This module provides the core algorithms for working with vertices in a
5+
v-dimensional hypercube where coordinates are in the range [0, w-1]. This is a
6+
foundational component for the "Top of the Hypercube" signature schemes.
7+
8+
Core Concepts
9+
-------------
10+
1. **Hypercube (`[w]^v`)**: The set of all possible coordinate vectors for a
11+
signature. The dimension `v` corresponds to the number of hash chains (and
12+
thus signature size), and the base `w` corresponds to the length of each
13+
hash chain.
14+
15+
2. **Layers (`d`)**: The hypercube's vertices are partitioned into "layers" based
16+
on their **verification cost**. The layer `d` of a vertex is its distance
17+
from the sink vertex `(w-1, ..., w-1)`, calculated as:
18+
`d = (w-1)*v - sum(coordinates)`.
19+
A smaller `d` means a lower verification cost, making these "top layers"
20+
the most desirable for encoding messages into.
21+
22+
3. **Mapping Problem**: The central challenge is to deterministically and
23+
efficiently map a single integer (derived from a message hash) to a unique
24+
coordinate vector within a specific layer or a set of top layers. This
25+
module provides the building blocks for that mapping.
1426
1527
This logic is a direct translation of the algorithms described in the paper
1628
"At the top of the hypercube" (eprint 2025/889)
@@ -20,7 +32,9 @@
2032
from __future__ import annotations
2133

2234
import bisect
35+
import math
2336
from functools import lru_cache
37+
from itertools import accumulate
2438
from typing import List, Tuple
2539

2640
from pydantic import BaseModel, ConfigDict
@@ -31,112 +45,117 @@
3145

3246
class LayerInfo(BaseModel):
3347
"""
34-
Stores the precomputed sizes and cumulative sums for a
35-
hypercube's layers.
48+
A data structure to store precomputed sizes and cumulative sums for the
49+
layers of a single hypercube configuration (fixed `w` and `v`).
50+
51+
This object makes subsequent calculations, like finding the total size of a
52+
range of layers, highly efficient.
3653
"""
3754

3855
model_config = ConfigDict(frozen=True)
3956
sizes: List[int]
40-
"""The number of vertices in each layer `d`."""
57+
"""A list where `sizes[d]` is the number of vertices in layer `d`."""
4158
prefix_sums: List[int]
4259
"""
43-
The cumulative number of vertices up to and including layer `d`.
60+
A list where `prefix_sums[d]` is the cumulative number of vertices from
61+
layer 0 up to and including layer `d`.
4462
45-
`prefix_sums[d] = sizes[0] + ... + sizes[d]`.
63+
Mathematically: `prefix_sums[d] = sizes[0] + ... + sizes[d]`.
4664
"""
4765

4866
def sizes_sum_in_range(self, start: int, end: int) -> int:
49-
"""Calculates the sum of sizes in an inclusive range [start, end]."""
67+
"""
68+
Calculates the sum of `sizes` in an inclusive range [start, end].
69+
70+
This is an O(1) operation thanks to the precomputed `prefix_sums`.
71+
"""
72+
# If the range is invalid, the sum is zero.
5073
if start > end:
5174
return 0
75+
# If the range starts from the beginning, the sum is simply the
76+
# prefix sum at the end of the range.
5277
if start == 0:
5378
return self.prefix_sums[end]
79+
# Otherwise, the sum is the difference between the prefix sum at the
80+
# end and the prefix sum of the elements just before the start.
5481
else:
5582
return self.prefix_sums[end] - self.prefix_sums[start - 1]
5683

5784

58-
@lru_cache(maxsize=None)
59-
def prepare_layer_info(w: int) -> List[LayerInfo]:
85+
def _calculate_layer_size(w: int, v: int, d: int) -> int:
6086
"""
61-
Precomputes and caches the number of vertices in each layer of a hypercube.
87+
Calculates a hypercube layer's size using a direct combinatorial formula.
88+
89+
This function answers the question: "How many unique coordinate vectors
90+
(vertices) exist in a specific layer `d`?"
6291
63-
It calculates the size of every layer for hypercubes with a given
64-
base `w` (where coordinates are in `[0, w-1]`) for all dimensions
65-
`v` up to `MAX_DIMENSION`.
92+
The problem is mathematically equivalent to finding the number of integer
93+
solutions to the equation:
94+
x_1 + x_2 + ... + x_v = k
95+
subject to the constraint that each coordinate `x_i` is in the range
96+
`0 <= x_i <= w-1`. The required sum `k` is derived from the layer's
97+
distance `d`.
6698
67-
This precomputation is based on the recurrence relation from
68-
Lemma 8 of the paper "At the top of the hypercube" (eprint 2025/889).
99+
The solution uses two key combinatorial techniques:
100+
1. **Stars and Bars**: To find the number of solutions without the
101+
upper-bound constraint (`x_i <= w-1`).
102+
103+
2. **Inclusion-Exclusion Principle**: To correct the count by systematically
104+
adding and subtracting the solutions that violate the upper bound.
69105
70106
Args:
71-
w: The base of the hypercube.
107+
w: The hypercube base (coordinates are `0` to `w-1`).
108+
v: The hypercube dimension (number of coordinates).
109+
d: The target layer's distance from the sink vertex `(w-1, ..., w-1)`.
72110
73111
Returns:
74-
A list where the element at index `v` is a `LayerInfo` object
75-
containing the layer sizes for a `v`-dimensional hypercube.
112+
The total number of vertices in the specified layer.
76113
"""
77-
# Initialize a list to store the results for each dimension `v`.
114+
# A vertex is in layer `d` if its coordinates sum to `k = v * (w - 1) - d`.
78115
#
79-
# Index 0 is unused to allow for direct indexing, e.g., `all_info[v]`.
80-
all_info = [LayerInfo(sizes=[], prefix_sums=[])] * (MAX_DIMENSION + 1)
116+
# This `coord_sum` is the `k` in our combinatorial problem.
117+
coord_sum = v * (w - 1) - d
81118

82-
# BASE CASE
83-
#
84-
# For a 1-dimensional hypercube (v=1), which is just a line of `w` points
85-
# with coordinates [0], [1], ..., [w-1].
119+
# This is the compact implementation of the inclusion-exclusion principle.
86120
#
87-
# The distance `d` from the sink `[w-1]` is simply `(w-1) - coordinate`.
121+
# It directly calculates the sum: Σ (-1)^s * C(v,s) * C(k - s*w + v-1, v-1)
122+
return sum(
123+
((-1) ** s) * math.comb(v, s) * math.comb(coord_sum - s * w + v - 1, v - 1)
124+
for s in range(coord_sum // w + 1)
125+
)
126+
127+
128+
@lru_cache(maxsize=None)
129+
def prepare_layer_info(w: int) -> List[LayerInfo]:
130+
"""
131+
Precomputes and caches layer information using a direct combinatorial formula.
88132
89-
# Each of the `w` possible layers contains exactly one vertex.
90-
dim1_sizes = [1] * w
91-
# The prefix sums (cumulative sizes) are therefore just [1, 2, 3, ..., w].
92-
dim1_prefix_sums = list(range(1, w + 1))
93-
# Store the result for v=1, which will seed the inductive step.
94-
all_info[1] = LayerInfo(sizes=dim1_sizes, prefix_sums=dim1_prefix_sums)
133+
For each dimension `v` up to `MAX_DIMENSION`, this function calculates the
134+
size of every layer `d` directly, without relying on the results from
135+
smaller dimensions. While less computationally efficient than the recursive
136+
method, this implementation is more concise and mathematically direct.
137+
138+
Args:
139+
w: The base of the hypercube.
140+
141+
Returns:
142+
A list where `list[v]` is a `LayerInfo` object for a `v`-dim hypercube.
143+
"""
144+
all_info = [LayerInfo(sizes=[], prefix_sums=[])] * (MAX_DIMENSION + 1)
95145

96-
# Now, build the layer info for all higher dimensions up to the maximum.
97-
for v in range(2, MAX_DIMENSION + 1):
146+
for v in range(1, MAX_DIMENSION + 1):
98147
# The maximum possible distance `d` in a v-dimensional hypercube.
99148
max_d = (w - 1) * v
100-
# Retrieve the already-computed data for the previous dimension (v-1).
101-
prev_layer_info = all_info[v - 1]
102-
103-
# This list will store the computed size of each layer `d`
104-
# for dimension `v`.
105-
current_sizes: List[int] = []
106-
for d in range(max_d + 1):
107-
# Implements the recurrence l_d(v) = Σ l_{d-j}(v-1) from the paper.
108-
# `j` is one coordinate's distance contribution.
109-
110-
# Calculate the valid range [j_min, j_max] for `j`.
111-
j_min = max(0, d - (w - 1) * (v - 1))
112-
j_max = min(w - 1, d)
113-
114-
# Translate the sum over `j` to an index range `k`,
115-
# where k = d - j.
116-
#
117-
# This allows for an efficient lookup using prefix sums.
118-
k_min = d - j_max
119-
k_max = d - j_min
120-
121-
# Calculate the sum using the precomputed prefix sums
122-
# from the previous dimension's `LayerInfo`.
123-
layer_size = prev_layer_info.sizes_sum_in_range(k_min, k_max)
124-
current_sizes.append(layer_size)
125-
126-
# After computing all layer sizes for dimension `v`, we compute their
127-
# prefix sums.
128-
#
129-
# This is needed for the *next* iteration (for dimension v+1).
130-
current_prefix_sums: List[int] = []
131-
current_sum = 0
132-
for size in current_sizes:
133-
current_sum += size
134-
current_prefix_sums.append(current_sum)
149+
150+
# Directly compute the size of each layer using the helper function.
151+
sizes = [_calculate_layer_size(w, v, d) for d in range(max_d + 1)]
152+
153+
# Compute the cumulative sums from the list of sizes.
154+
prefix_sums = list(accumulate(sizes))
135155

136156
# Store the complete layer info for the current dimension `v`.
137-
all_info[v] = LayerInfo(sizes=current_sizes, prefix_sums=current_prefix_sums)
157+
all_info[v] = LayerInfo(sizes=sizes, prefix_sums=prefix_sums)
138158

139-
# Return the complete table of layer information for the given base `w`.
140159
return all_info
141160

142161

@@ -152,16 +171,18 @@ def hypercube_part_size(w: int, v: int, d: int) -> int:
152171

153172
def hypercube_find_layer(w: int, v: int, x: int) -> Tuple[int, int]:
154173
"""
155-
Given a global index `x`, finds the layer `d` it belongs to and its
156-
local index (`remainder`) within that layer.
174+
Given a global index `x`, finds its layer `d` and local offset `remainder`.
175+
176+
This function determines which "layer bucket" a global index falls into.
157177
158178
Args:
159179
w: The hypercube base.
160180
v: The hypercube dimension.
161-
x: The global index of a vertex (from 0 to w**v - 1).
181+
x: The global index of a vertex, from 0 to (w**v - 1).
162182
163183
Returns:
164-
A tuple `(d, remainder)`.
184+
A tuple `(d, remainder)`, where `d` is the layer and `remainder` is
185+
the local index (offset) of the vertex within that layer.
165186
"""
166187
prefix_sums = prepare_layer_info(w)[v].prefix_sums
167188
# Use binary search to efficiently find the correct layer.
@@ -187,17 +208,16 @@ def map_to_vertex(w: int, v: int, d: int, x: int) -> List[int]:
187208
"""
188209
Maps an integer index `x` to a unique vertex in a specific hypercube layer.
189210
190-
This function provides a bijective mapping from an integer `x`
191-
(derived from a hash) to a unique list of `v` coordinates,
192-
`[a_0, ..., a_{v-1}]`.
193-
194-
The algorithm works iteratively, determining one coordinate at a time.
211+
This function provides a bijective mapping from a location `(d, x)` to a
212+
unique coordinate vector `[a_0, ..., a_{v-1}]`. The algorithm works
213+
iteratively, determining one coordinate at a time by reducing the problem
214+
to a smaller subproblem in a hypercube of one less dimension.
195215
196216
Args:
197217
w: The hypercube base (coordinates are in `[0, w-1]`).
198218
v: The hypercube dimension (the number of coordinates).
199219
d: The target layer, defined by its distance from the sink vertex.
200-
x: The integer index within the layer `d`, must be `0 <= x < size(d)`.
220+
x: The integer index (offset) within layer `d`. Must be `0 <= x < size(d)`.
201221
202222
Returns:
203223
A list of `v` integers representing the coordinates of the vertex.
@@ -218,8 +238,7 @@ def map_to_vertex(w: int, v: int, d: int, x: int) -> List[int]:
218238
dim_remaining = v - i
219239
prev_dim_layer_info = layer_info_cache[dim_remaining]
220240

221-
# This loop finds which block of sub-hypercubes the index `x_curr`
222-
# falls into.
241+
# This loop finds which block of sub-hypercubes the index `x_curr` falls into.
223242
#
224243
# It skips over full blocks by subtracting their size
225244
# from `x_curr` until the correct one is found.
@@ -230,7 +249,8 @@ def map_to_vertex(w: int, v: int, d: int, x: int) -> List[int]:
230249
if x_curr >= count:
231250
x_curr -= count
232251
else:
233-
ji = j # Found the correct block.
252+
# Found the correct block.
253+
ji = j
234254
break
235255

236256
if ji == -1:

0 commit comments

Comments
 (0)