From 3f6226c9a3853a890cd862dc2dbd3a5703954af2 Mon Sep 17 00:00:00 2001 From: Thomas Coratger Date: Thu, 31 Jul 2025 19:10:49 +0200 Subject: [PATCH 1/3] subspec: implement Poseidon2 spec with tests --- src/lean_spec/subspecs/poseidon2/__init__.py | 17 +- .../subspecs/poseidon2/permutation.py | 325 ++++++++++++++++++ .../subspecs/poseidon2/test_permutation.py | 137 ++++++++ .../subspecs/poseidon2/test_poseidon2.py | 2 - 4 files changed, 475 insertions(+), 6 deletions(-) create mode 100644 src/lean_spec/subspecs/poseidon2/permutation.py create mode 100644 tests/lean_spec/subspecs/poseidon2/test_permutation.py delete mode 100644 tests/lean_spec/subspecs/poseidon2/test_poseidon2.py diff --git a/src/lean_spec/subspecs/poseidon2/__init__.py b/src/lean_spec/subspecs/poseidon2/__init__.py index 024deb0e..83e710fb 100644 --- a/src/lean_spec/subspecs/poseidon2/__init__.py +++ b/src/lean_spec/subspecs/poseidon2/__init__.py @@ -1,4 +1,13 @@ -""" -Specifications for the Poseidon2 cryptographic permutation for -zero-knowledge applications. -""" +"""Specification for the Poseidon2 permutation.""" + +from .permutation import ( + PARAMS_16, + PARAMS_24, + permute, +) + +__all__ = [ + "permute", + "PARAMS_16", + "PARAMS_24", +] diff --git a/src/lean_spec/subspecs/poseidon2/permutation.py b/src/lean_spec/subspecs/poseidon2/permutation.py new file mode 100644 index 00000000..c02072ca --- /dev/null +++ b/src/lean_spec/subspecs/poseidon2/permutation.py @@ -0,0 +1,325 @@ +""" +A minimal Python specification for the Poseidon2 permutation. + +The design is based on the paper "Poseidon2: A Faster Version of the Poseidon +Hash Function" (https://eprint.iacr.org/2023/323). +""" + +from itertools import chain +from typing import List, NamedTuple + +from ..koalabear.field import Fp + +# ================================================================= +# Poseidon2 Parameter Definitions +# ================================================================= + +S_BOX_DEGREE = 3 +""" +The S-box exponent `d`. + +For fields where `gcd(d, p-1) = 1`, `x -> x^d` is a permutation. + +For KoalaBear, `d=3` is chosen for its low degree. +""" + + +class Poseidon2Params(NamedTuple): + """ + Encapsulates all necessary parameters for a specific Poseidon2 instance. + + This structure holds the configuration for a given state width, including + the number of rounds and the constants for the internal linear layer. + + Attributes: + WIDTH (int): The size of the state (t). + + ROUNDS_F (int): The total number of "full" rounds, where the S-box is + applied to the entire state. + + ROUNDS_P (int): The number of "partial" rounds, where the S-box is + applied to only the first element of the state. + + INTERNAL_DIAG_VECTORS (List[Fp]): The diagonal vectors for the + efficient internal linear layer matrix (M_I). + """ + + WIDTH: int + ROUNDS_F: int + ROUNDS_P: int + INTERNAL_DIAG_VECTORS: List[Fp] + + +def _generate_round_constants(params: Poseidon2Params) -> List[Fp]: + """ + Generates a deterministic list of round constants for the permutation. + + Round constants are added in each round to break symmetries and prevent + attacks like slide or interpolation attacks. + + Args: + params: The object defining the permutation's configuration. + + Returns: + A list of Fp elements to be used as round constants. + """ + # The total number of constants needed for the entire permutation. + # + # This is the sum of constants for all full rounds and all partial rounds. + # - Full rounds require `WIDTH` constants each + # (one for each state element). + # - Partial rounds require 1 constant each + # (for the first state element). + total_constants = (params.ROUNDS_F * params.WIDTH) + params.ROUNDS_P + + # For the specification, we generate the constants as a deterministic d + # sequence of integers. + # + # This is sufficient to define the algorithm's mechanics. + # + # Real-world implementations would use constants generated from a secure, + # pseudo-random source. + return [Fp(value=i) for i in range(total_constants)] + + +# Parameters for WIDTH = 16 +PARAMS_16 = Poseidon2Params( + WIDTH=16, + ROUNDS_F=8, + ROUNDS_P=20, + INTERNAL_DIAG_VECTORS=[ + Fp(value=-2), + Fp(value=1), + Fp(value=2), + Fp(value=1) / Fp(value=2), + Fp(value=3), + Fp(value=4), + Fp(value=-1) / Fp(value=2), + Fp(value=-3), + Fp(value=-4), + Fp(value=1) / Fp(value=2**8), + Fp(value=1) / Fp(value=8), + Fp(value=1) / Fp(value=2**24), + Fp(value=-1) / Fp(value=2**8), + Fp(value=-1) / Fp(value=8), + Fp(value=-1) / Fp(value=16), + Fp(value=-1) / Fp(value=2**24), + ], +) + +# Parameters for WIDTH = 24 +PARAMS_24 = Poseidon2Params( + WIDTH=24, + ROUNDS_F=8, + ROUNDS_P=23, + INTERNAL_DIAG_VECTORS=[ + Fp(value=-2), + Fp(value=1), + Fp(value=2), + Fp(value=1) / Fp(value=2), + Fp(value=3), + Fp(value=4), + Fp(value=-1) / Fp(value=2), + Fp(value=-3), + Fp(value=-4), + Fp(value=1) / Fp(value=2**8), + Fp(value=1) / Fp(value=4), + Fp(value=1) / Fp(value=8), + Fp(value=1) / Fp(value=16), + Fp(value=1) / Fp(value=32), + Fp(value=1) / Fp(value=64), + Fp(value=1) / Fp(value=2**24), + Fp(value=-1) / Fp(value=2**8), + Fp(value=-1) / Fp(value=8), + Fp(value=-1) / Fp(value=16), + Fp(value=-1) / Fp(value=32), + Fp(value=-1) / Fp(value=64), + Fp(value=-1) / Fp(value=2**7), + Fp(value=-1) / Fp(value=2**9), + Fp(value=-1) / Fp(value=2**24), + ], +) + +# Base 4x4 matrix, used in the external linear layer. +M4_MATRIX = [ + [Fp(value=2), Fp(value=3), Fp(value=1), Fp(value=1)], + [Fp(value=1), Fp(value=2), Fp(value=3), Fp(value=1)], + [Fp(value=1), Fp(value=1), Fp(value=2), Fp(value=3)], + [Fp(value=3), Fp(value=1), Fp(value=1), Fp(value=2)], +] + +# ================================================================= +# Linear Layers +# ================================================================= + + +def _apply_m4(chunk: List[Fp]) -> List[Fp]: + """ + Applies the 4x4 M4 MDS matrix to a 4-element chunk of the state. + This is a helper function for the external linear layer. + + Args: + chunk: A list of 4 Fp elements. + + Returns: + The transformed 4-element chunk. + """ + # Initialize the result vector with zeros. + result = [Fp(value=0)] * 4 + # Perform standard matrix-vector multiplication. + for i in range(4): + for j in range(4): + result[i] += M4_MATRIX[i][j] * chunk[j] + return result + + +def external_linear_layer(state: List[Fp], width: int) -> List[Fp]: + """ + Applies the external linear layer (M_E). + + This layer provides strong diffusion across the entire state and is used + in the full rounds. For a state of size t=4k, it's constructed from the + base M4 matrix to form a larger circulant-like matrix, which is efficient + while ensuring that a change in any single element affects all other + elements after application. + + The process follows Appendix B of the paper. + + Args: + state: The current state vector. + width: The width `t` of the state. + + Returns: + The state vector after applying the external linear layer. + """ + # Apply the M4 matrix to each 4-element chunk of the state. + # + # This provides strong local diffusion within each block. + state_after_m4 = list( + chain.from_iterable( + _apply_m4(state[i : i + 4]) for i in range(0, width, 4) + ) + ) + + # Apply the outer circulant structure for global diffusion. + # + # We precompute the four sums of elements at the same offset in each chunk. + # For each k in 0..4: + # sums[k] = state[k] + state[4 + k] + state[8 + k] + ... up to width + sums = [ + sum((state_after_m4[j + k] for j in range(0, width, 4)), Fp(value=0)) + for k in range(4) + ] + + # Add the corresponding sum to each element of the state. + state_after_circulant = [ + s + sums[i % 4] for i, s in enumerate(state_after_m4) + ] + + return state_after_circulant + + +def internal_linear_layer( + state: List[Fp], params: Poseidon2Params +) -> List[Fp]: + """ + Applies the internal linear layer (M_I). + + This layer is used during partial rounds and is optimized for speed. Its + matrix is constructed as M_I = J + D, where J is the all-ones matrix and D + is a diagonal matrix. This structure allows the matrix-vector product to be + computed in O(t) time instead of O(t^2), as M_I * s = J*s + D*s. + The term J*s is a vector where each element is the sum of + all elements in s. + + Args: + state: The current state vector. + params: The Poseidon2Params object containing the diagonal vectors. + + Returns: + The state vector after applying the internal linear layer. + """ + # Calculate the sum of all elements in the state vector. + s_sum = sum(state, Fp(value=0)) + # For each element s_i, compute s_i' = d_i * s_i + sum(s). + # This is the efficient computation of (J + D)s. + new_state = [ + s * d + s_sum + for s, d in zip(state, params.INTERNAL_DIAG_VECTORS, strict=False) + ] + return new_state + + +# ================================================================= +# Core Permutation +# ================================================================= + + +def permute(state: List[Fp], params: Poseidon2Params) -> List[Fp]: + """ + Performs the full Poseidon2 permutation on the given state. + + The permutation follows the structure: + Initial Layer -> Full Rounds -> Partial Rounds -> Full Rounds + + Args: + state: A list of Fp elements representing the current state. + params: The object defining the permutation's configuration. + + Returns: + The new state after applying the permutation. + """ + # Ensure the input state has the correct dimensions. + if len(state) != params.WIDTH: + raise ValueError(f"Input state must have length {params.WIDTH}") + + # Generate the deterministic round constants for this parameter set. + round_constants = _generate_round_constants(params) + # The number of full rounds is split between the beginning and end. + half_rounds_f = params.ROUNDS_F // 2 + # Initialize index for accessing the flat list of round constants. + const_idx = 0 + + # 1. Initial Linear Layer + # + # Another linear layer is applied at the start to prevent certain algebraic + # attacks by ensuring the permutation begins with a diffusion layer. + state = external_linear_layer(list(state), params.WIDTH) + + # 2. First Half of Full Rounds (R_F / 2) + for _r in range(half_rounds_f): + # Add round constants to the entire state. + state = [ + s + round_constants[const_idx + i] for i, s in enumerate(state) + ] + const_idx += params.WIDTH + # Apply the S-box (x -> x^d) to the full state. + state = [s**S_BOX_DEGREE for s in state] + # Apply the external linear layer for diffusion. + state = external_linear_layer(state, params.WIDTH) + + # 3. Partial Rounds (R_P) + for _r in range(params.ROUNDS_P): + # Add a single round constant to the first state element. + state[0] += round_constants[const_idx] + const_idx += 1 + # Apply the S-box to the first state element only. + # + # This is the main optimization of the Hades design. + state[0] = state[0] ** S_BOX_DEGREE + # Apply the internal linear layer. + state = internal_linear_layer(state, params) + + # 4. Second Half of Full Rounds (R_F / 2) + for _r in range(half_rounds_f): + # Add round constants to the entire state. + state = [ + s + round_constants[const_idx + i] for i, s in enumerate(state) + ] + const_idx += params.WIDTH + # Apply the S-box to the full state. + state = [s**S_BOX_DEGREE for s in state] + # Apply the external linear layer for diffusion. + state = external_linear_layer(state, params.WIDTH) + + return state diff --git a/tests/lean_spec/subspecs/poseidon2/test_permutation.py b/tests/lean_spec/subspecs/poseidon2/test_permutation.py new file mode 100644 index 00000000..4f33b459 --- /dev/null +++ b/tests/lean_spec/subspecs/poseidon2/test_permutation.py @@ -0,0 +1,137 @@ +""" +Tests for the Poseidon2 permutation for widths 16 and 24. +""" + +from typing import List + +import pytest + +from lean_spec.subspecs.koalabear.field import Fp +from lean_spec.subspecs.poseidon2.permutation import ( + PARAMS_16, + PARAMS_24, + Poseidon2Params, + permute, +) + +# --- Test Vectors --- + +# Input vector for width 16 +INPUT_16 = [ + Fp(value=894848333), + Fp(value=1437655012), + Fp(value=1200606629), + Fp(value=1690012884), + Fp(value=71131202), + Fp(value=1749206695), + Fp(value=1717947831), + Fp(value=120589055), + Fp(value=19776022), + Fp(value=42382981), + Fp(value=1831865506), + Fp(value=724844064), + Fp(value=171220207), + Fp(value=1299207443), + Fp(value=227047920), + Fp(value=1783754913), +] +# Expected output for width 16. +EXPECTED_16 = [ + Fp(value=675842289), + Fp(value=66192714), + Fp(value=579861851), + Fp(value=1465025982), + Fp(value=810227449), + Fp(value=1161478289), + Fp(value=1411410716), + Fp(value=1917188212), + Fp(value=80707562), + Fp(value=1051450322), + Fp(value=1441355554), + Fp(value=1096596517), + Fp(value=1967136522), + Fp(value=1656393635), + Fp(value=1897269296), + Fp(value=218235760), +] + +# Input vector for width 24 +INPUT_24 = [ + Fp(value=886409618), + Fp(value=1327899896), + Fp(value=1902407911), + Fp(value=591953491), + Fp(value=648428576), + Fp(value=1844789031), + Fp(value=1198336108), + Fp(value=355597330), + Fp(value=1799586834), + Fp(value=59617783), + Fp(value=790334801), + Fp(value=1968791836), + Fp(value=559272107), + Fp(value=31054313), + Fp(value=1042221543), + Fp(value=474748436), + Fp(value=135686258), + Fp(value=263665994), + Fp(value=1962340735), + Fp(value=1741539604), + Fp(value=2026927696), + Fp(value=449439011), + Fp(value=1131357108), + Fp(value=50869465), +] +# Expected output for width 24. +EXPECTED_24 = [ + Fp(value=545335348), + Fp(value=483654611), + Fp(value=76149348), + Fp(value=1039423716), + Fp(value=273226798), + Fp(value=1112250891), + Fp(value=1803002062), + Fp(value=283727456), + Fp(value=1270538134), + Fp(value=740691354), + Fp(value=824972956), + Fp(value=1586235276), + Fp(value=1576922813), + Fp(value=300527652), + Fp(value=1319772393), + Fp(value=1464054027), + Fp(value=624250646), + Fp(value=2110444609), + Fp(value=213054218), + Fp(value=830776390), + Fp(value=257630621), + Fp(value=1575823798), + Fp(value=546963080), + Fp(value=850531490), +] + + +@pytest.mark.parametrize( + "params, input_state, expected_output", + [ + (PARAMS_16, INPUT_16, EXPECTED_16), + (PARAMS_24, INPUT_24, EXPECTED_24), + ], + ids=["width_16", "width_24"], +) +def test_permutation_vector( + params: Poseidon2Params, input_state: List[Fp], expected_output: List[Fp] +): + """ + Tests the Poseidon2 permutation against known answer vectors. + + This serves as a regression test to ensure the logic is consistent. + """ + # Run the permutation + output_state = permute(input_state, params) + + # Verify the output + assert len(output_state) == params.WIDTH + assert output_state == expected_output, ( + f"Permutation output for width {params.WIDTH} did not match." + ) diff --git a/tests/lean_spec/subspecs/poseidon2/test_poseidon2.py b/tests/lean_spec/subspecs/poseidon2/test_poseidon2.py deleted file mode 100644 index 86efcb00..00000000 --- a/tests/lean_spec/subspecs/poseidon2/test_poseidon2.py +++ /dev/null @@ -1,2 +0,0 @@ -def test_import_poseidon2(): - import lean_spec.subspecs.poseidon2 # noqa: F401 From 18d43d5a156d290082341633953b38ce904547b0 Mon Sep 17 00:00:00 2001 From: Thomas Coratger Date: Thu, 31 Jul 2025 21:43:19 +0200 Subject: [PATCH 2/3] fix using pydantic --- .../subspecs/poseidon2/permutation.py | 97 +++++++++++-------- .../subspecs/poseidon2/test_permutation.py | 4 +- 2 files changed, 61 insertions(+), 40 deletions(-) diff --git a/src/lean_spec/subspecs/poseidon2/permutation.py b/src/lean_spec/subspecs/poseidon2/permutation.py index c02072ca..7b437d0a 100644 --- a/src/lean_spec/subspecs/poseidon2/permutation.py +++ b/src/lean_spec/subspecs/poseidon2/permutation.py @@ -1,3 +1,5 @@ +# subspecs/poseidon2/permutation.py + """ A minimal Python specification for the Poseidon2 permutation. @@ -6,7 +8,9 @@ """ from itertools import chain -from typing import List, NamedTuple +from typing import List + +from pydantic import BaseModel, ConfigDict, Field, model_validator from ..koalabear.field import Fp @@ -24,30 +28,44 @@ """ -class Poseidon2Params(NamedTuple): +class Poseidon2Params(BaseModel): """ Encapsulates all necessary parameters for a specific Poseidon2 instance. This structure holds the configuration for a given state width, including the number of rounds and the constants for the internal linear layer. + """ - Attributes: - WIDTH (int): The size of the state (t). - - ROUNDS_F (int): The total number of "full" rounds, where the S-box is - applied to the entire state. - - ROUNDS_P (int): The number of "partial" rounds, where the S-box is - applied to only the first element of the state. + # Configuration to make the model immutable and prevent extra arguments. + model_config = ConfigDict( + frozen=True, + extra="forbid", + arbitrary_types_allowed=True, + ) - INTERNAL_DIAG_VECTORS (List[Fp]): The diagonal vectors for the - efficient internal linear layer matrix (M_I). - """ + width: int = Field(gt=0, description="The size of the state (t).") + rounds_f: int = Field(gt=0, description="Total number of 'full' rounds.") + rounds_p: int = Field( + ge=0, description="Total number of 'partial' rounds." + ) + internal_diag_vectors: List[Fp] = Field( + min_length=1, + description=( + "Diagonal vectors for the efficient " + "internal linear layer matrix (M_I)." + ), + ) - WIDTH: int - ROUNDS_F: int - ROUNDS_P: int - INTERNAL_DIAG_VECTORS: List[Fp] + @model_validator(mode="after") + def check_vector_length(self) -> "Poseidon2Params": + """Length of the diagonal vector should match the state width.""" + if len(self.internal_diag_vectors) != self.width: + raise ValueError( + f"Length of internal diagonal vector " + "({len(self.internal_diag_vectors)}) " + f"must be equal to width ({self.width})." + ) + return self def _generate_round_constants(params: Poseidon2Params) -> List[Fp]: @@ -66,11 +84,11 @@ def _generate_round_constants(params: Poseidon2Params) -> List[Fp]: # The total number of constants needed for the entire permutation. # # This is the sum of constants for all full rounds and all partial rounds. - # - Full rounds require `WIDTH` constants each + # - Full rounds require `width` constants each # (one for each state element). # - Partial rounds require 1 constant each # (for the first state element). - total_constants = (params.ROUNDS_F * params.WIDTH) + params.ROUNDS_P + total_constants = (params.rounds_f * params.width) + params.rounds_p # For the specification, we generate the constants as a deterministic d # sequence of integers. @@ -84,10 +102,10 @@ def _generate_round_constants(params: Poseidon2Params) -> List[Fp]: # Parameters for WIDTH = 16 PARAMS_16 = Poseidon2Params( - WIDTH=16, - ROUNDS_F=8, - ROUNDS_P=20, - INTERNAL_DIAG_VECTORS=[ + width=16, + rounds_f=8, + rounds_p=20, + internal_diag_vectors=[ Fp(value=-2), Fp(value=1), Fp(value=2), @@ -109,10 +127,10 @@ def _generate_round_constants(params: Poseidon2Params) -> List[Fp]: # Parameters for WIDTH = 24 PARAMS_24 = Poseidon2Params( - WIDTH=24, - ROUNDS_F=8, - ROUNDS_P=23, - INTERNAL_DIAG_VECTORS=[ + width=24, + rounds_f=8, + rounds_p=23, + internal_diag_vectors=[ Fp(value=-2), Fp(value=1), Fp(value=2), @@ -203,9 +221,12 @@ def external_linear_layer(state: List[Fp], width: int) -> List[Fp]: # Apply the outer circulant structure for global diffusion. # + # This is equivalent to multiplying by circ(2*I, I, ..., I) + # after the M4 stage. + # # We precompute the four sums of elements at the same offset in each chunk. # For each k in 0..4: - # sums[k] = state[k] + state[4 + k] + state[8 + k] + ... up to width + # sums[k] = state[k] + state[4 + k] + state[8 + k] + ... up to width sums = [ sum((state_after_m4[j + k] for j in range(0, width, 4)), Fp(value=0)) for k in range(4) @@ -245,7 +266,7 @@ def internal_linear_layer( # This is the efficient computation of (J + D)s. new_state = [ s * d + s_sum - for s, d in zip(state, params.INTERNAL_DIAG_VECTORS, strict=False) + for s, d in zip(state, params.internal_diag_vectors, strict=False) ] return new_state @@ -270,13 +291,13 @@ def permute(state: List[Fp], params: Poseidon2Params) -> List[Fp]: The new state after applying the permutation. """ # Ensure the input state has the correct dimensions. - if len(state) != params.WIDTH: - raise ValueError(f"Input state must have length {params.WIDTH}") + if len(state) != params.width: + raise ValueError(f"Input state must have length {params.width}") # Generate the deterministic round constants for this parameter set. round_constants = _generate_round_constants(params) # The number of full rounds is split between the beginning and end. - half_rounds_f = params.ROUNDS_F // 2 + half_rounds_f = params.rounds_f // 2 # Initialize index for accessing the flat list of round constants. const_idx = 0 @@ -284,7 +305,7 @@ def permute(state: List[Fp], params: Poseidon2Params) -> List[Fp]: # # Another linear layer is applied at the start to prevent certain algebraic # attacks by ensuring the permutation begins with a diffusion layer. - state = external_linear_layer(list(state), params.WIDTH) + state = external_linear_layer(list(state), params.width) # 2. First Half of Full Rounds (R_F / 2) for _r in range(half_rounds_f): @@ -292,14 +313,14 @@ def permute(state: List[Fp], params: Poseidon2Params) -> List[Fp]: state = [ s + round_constants[const_idx + i] for i, s in enumerate(state) ] - const_idx += params.WIDTH + const_idx += params.width # Apply the S-box (x -> x^d) to the full state. state = [s**S_BOX_DEGREE for s in state] # Apply the external linear layer for diffusion. - state = external_linear_layer(state, params.WIDTH) + state = external_linear_layer(state, params.width) # 3. Partial Rounds (R_P) - for _r in range(params.ROUNDS_P): + for _r in range(params.rounds_p): # Add a single round constant to the first state element. state[0] += round_constants[const_idx] const_idx += 1 @@ -316,10 +337,10 @@ def permute(state: List[Fp], params: Poseidon2Params) -> List[Fp]: state = [ s + round_constants[const_idx + i] for i, s in enumerate(state) ] - const_idx += params.WIDTH + const_idx += params.width # Apply the S-box to the full state. state = [s**S_BOX_DEGREE for s in state] # Apply the external linear layer for diffusion. - state = external_linear_layer(state, params.WIDTH) + state = external_linear_layer(state, params.width) return state diff --git a/tests/lean_spec/subspecs/poseidon2/test_permutation.py b/tests/lean_spec/subspecs/poseidon2/test_permutation.py index 4f33b459..4dc539a1 100644 --- a/tests/lean_spec/subspecs/poseidon2/test_permutation.py +++ b/tests/lean_spec/subspecs/poseidon2/test_permutation.py @@ -131,7 +131,7 @@ def test_permutation_vector( output_state = permute(input_state, params) # Verify the output - assert len(output_state) == params.WIDTH + assert len(output_state) == params.width assert output_state == expected_output, ( - f"Permutation output for width {params.WIDTH} did not match." + f"Permutation output for width {params.width} did not match." ) From acc46993ce159e80f44694c9de4ed4da447f6529 Mon Sep 17 00:00:00 2001 From: Thomas Coratger Date: Fri, 1 Aug 2025 17:32:59 +0200 Subject: [PATCH 3/3] fix comment --- .../subspecs/poseidon2/permutation.py | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/src/lean_spec/subspecs/poseidon2/permutation.py b/src/lean_spec/subspecs/poseidon2/permutation.py index 7b437d0a..188e28c2 100644 --- a/src/lean_spec/subspecs/poseidon2/permutation.py +++ b/src/lean_spec/subspecs/poseidon2/permutation.py @@ -68,35 +68,27 @@ def check_vector_length(self) -> "Poseidon2Params": return self -def _generate_round_constants(params: Poseidon2Params) -> List[Fp]: +def _generate_spec_test_round_constants(params: Poseidon2Params) -> List[Fp]: """ - Generates a deterministic list of round constants for the permutation. + Generates a deterministic list of round constants for testing the spec. - Round constants are added in each round to break symmetries and prevent - attacks like slide or interpolation attacks. + !!! WARNING !!! + This function produces a simple, predictable sequence of integers for the + sole purpose of testing the permutation's algebraic structure. Production + implementations MUST use constants generated from a secure, + unpredictable source. Args: params: The object defining the permutation's configuration. Returns: - A list of Fp elements to be used as round constants. + A list of Fp elements to be used as round constants for tests. """ # The total number of constants needed for the entire permutation. - # - # This is the sum of constants for all full rounds and all partial rounds. - # - Full rounds require `width` constants each - # (one for each state element). - # - Partial rounds require 1 constant each - # (for the first state element). total_constants = (params.rounds_f * params.width) + params.rounds_p - # For the specification, we generate the constants as a deterministic d - # sequence of integers. - # - # This is sufficient to define the algorithm's mechanics. - # - # Real-world implementations would use constants generated from a secure, - # pseudo-random source. + # For the specification, we generate the constants as a deterministic + # sequence of integers. This is sufficient to define the mechanics. return [Fp(value=i) for i in range(total_constants)] @@ -295,7 +287,7 @@ def permute(state: List[Fp], params: Poseidon2Params) -> List[Fp]: raise ValueError(f"Input state must have length {params.width}") # Generate the deterministic round constants for this parameter set. - round_constants = _generate_round_constants(params) + round_constants = _generate_spec_test_round_constants(params) # The number of full rounds is split between the beginning and end. half_rounds_f = params.rounds_f // 2 # Initialize index for accessing the flat list of round constants.