eddsa-threshold 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. eddsa_threshold/__init__.py +1 -0
  2. eddsa_threshold/eddsa/__init__.py +1 -0
  3. eddsa_threshold/eddsa/algorithms/__init__.py +1 -0
  4. eddsa_threshold/eddsa/algorithms/ed25519.py +84 -0
  5. eddsa_threshold/eddsa/algorithms/ed25519ctx.py +23 -0
  6. eddsa_threshold/eddsa/algorithms/ed25519ph.py +23 -0
  7. eddsa_threshold/eddsa/algorithms/ed448.py +84 -0
  8. eddsa_threshold/eddsa/algorithms/ed448ph.py +23 -0
  9. eddsa_threshold/eddsa/curves/__init__.py +1 -0
  10. eddsa_threshold/eddsa/curves/base/__init__.py +1 -0
  11. eddsa_threshold/eddsa/curves/base/edwards_curve.py +91 -0
  12. eddsa_threshold/eddsa/curves/base/encoding.py +54 -0
  13. eddsa_threshold/eddsa/curves/base/field_ops.py +48 -0
  14. eddsa_threshold/eddsa/curves/base/scalar_ops.py +34 -0
  15. eddsa_threshold/eddsa/curves/ed25519/__init__.py +1 -0
  16. eddsa_threshold/eddsa/curves/ed25519/constants.py +37 -0
  17. eddsa_threshold/eddsa/curves/ed25519/ed25519_curve.py +79 -0
  18. eddsa_threshold/eddsa/curves/ed25519/encoding.py +80 -0
  19. eddsa_threshold/eddsa/curves/ed25519/field_ops.py +12 -0
  20. eddsa_threshold/eddsa/curves/ed25519/scalar_ops.py +12 -0
  21. eddsa_threshold/eddsa/curves/ed448/__init__.py +1 -0
  22. eddsa_threshold/eddsa/curves/ed448/constants.py +37 -0
  23. eddsa_threshold/eddsa/curves/ed448/ed448_curve.py +76 -0
  24. eddsa_threshold/eddsa/curves/ed448/encoding.py +77 -0
  25. eddsa_threshold/eddsa/curves/ed448/field_ops.py +12 -0
  26. eddsa_threshold/eddsa/curves/ed448/scalar_ops.py +12 -0
  27. eddsa_threshold/eddsa/keys/__init__.py +1 -0
  28. eddsa_threshold/eddsa/keys/ed25519_keypair.py +46 -0
  29. eddsa_threshold/eddsa/keys/ed448_keypair.py +45 -0
  30. eddsa_threshold/eddsa/keys/keypair.py +50 -0
  31. eddsa_threshold/eddsa/util/__init__.py +1 -0
  32. eddsa_threshold/eddsa/util/dom.py +17 -0
  33. eddsa_threshold/eddsa/util/hash_bindings.py +12 -0
  34. eddsa_threshold/frost/__init__.py +1 -0
  35. eddsa_threshold/frost/coordinator.py +185 -0
  36. eddsa_threshold/frost/core/__init__.py +1 -0
  37. eddsa_threshold/frost/core/base/__init__.py +1 -0
  38. eddsa_threshold/frost/core/base/frost_hashing.py +27 -0
  39. eddsa_threshold/frost/core/ed25519/__init__.py +1 -0
  40. eddsa_threshold/frost/core/ed25519/frost_hashing.py +26 -0
  41. eddsa_threshold/frost/core/ed448/__init__.py +1 -0
  42. eddsa_threshold/frost/core/ed448/frost_hashing.py +27 -0
  43. eddsa_threshold/frost/core/frost_types.py +53 -0
  44. eddsa_threshold/frost/core/polynomial.py +39 -0
  45. eddsa_threshold/frost/core/secrets/__init__.py +1 -0
  46. eddsa_threshold/frost/core/secrets/secret_sharing.py +26 -0
  47. eddsa_threshold/frost/core/secrets/shamir_secret_sharing.py +41 -0
  48. eddsa_threshold/frost/core/util.py +109 -0
  49. eddsa_threshold/frost/participant.py +140 -0
  50. eddsa_threshold/frost/trusted_dealer.py +72 -0
  51. eddsa_threshold-0.2.0.dist-info/METADATA +39 -0
  52. eddsa_threshold-0.2.0.dist-info/RECORD +55 -0
  53. eddsa_threshold-0.2.0.dist-info/WHEEL +5 -0
  54. eddsa_threshold-0.2.0.dist-info/licenses/LICENSE +14 -0
  55. eddsa_threshold-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,185 @@
1
+ from typing import Final
2
+
3
+ from eddsa_threshold.eddsa.curves.base.edwards_curve import EdwardsCurve
4
+ from eddsa_threshold.frost.core.base.frost_hashing import FrostHashing
5
+ from eddsa_threshold.frost.core.frost_types import GroupInfo, NonceCommitment, ParticipantId, SecretValue, SessionId, SigningPackage, SigningSession, VSSCommitment
6
+ from eddsa_threshold.frost.core.util import check_participant_bounds, compute_binding_factors, compute_group_commitment, derive_group_info
7
+
8
+
9
+ class FrostCoordinator:
10
+ """
11
+ Coordinator-side implementation for a 2-round FROST signing flow.
12
+
13
+ Cryptographic operations are delegated through callback hooks so this class can stay curve/algorithm agnostic.
14
+ """
15
+
16
+ def __init__(self, threshold: int, participant_ids: list[ParticipantId], hashing: FrostHashing, curve: EdwardsCurve):
17
+ if threshold <= 0:
18
+ raise ValueError("threshold must be positive")
19
+
20
+ check_participant_bounds(threshold, participant_ids, curve.scalar_ops)
21
+
22
+ self.THRESHOLD: Final[int] = threshold
23
+ self.PARTICIPANT_IDS: Final[list[ParticipantId]] = participant_ids
24
+ self.MAX_PARTICIPANTS: Final[int] = len(participant_ids)
25
+
26
+ self._dealer_info_set: bool = False
27
+
28
+ self._HASHING: Final[FrostHashing] = hashing
29
+ self._CURVE: Final[EdwardsCurve] = curve
30
+
31
+ self._signing_sessions: dict[SessionId, SigningSession] = {}
32
+
33
+ def set_dealer_info(self, vss_commitment: list[VSSCommitment]) -> None:
34
+ """
35
+ Set the group info after receiving it from the trusted dealer.
36
+ """
37
+
38
+ if self._dealer_info_set:
39
+ raise ValueError(f"coordinator has already set their dealer info")
40
+
41
+ self._GROUP_INFO = derive_group_info(self.THRESHOLD, self.MAX_PARTICIPANTS, vss_commitment, self._CURVE)
42
+ self._dealer_info_set = True
43
+
44
+ def create_signing_session(self, message: bytes) -> SessionId:
45
+ """
46
+ Initializes a signing session for the given message.
47
+ """
48
+
49
+ if not self._dealer_info_set:
50
+ raise ValueError("dealer info has not been set yet")
51
+
52
+ # for now, session id is a hash of the message + randomness (allow for multiple signing sessions for the same message)
53
+ session_id = self._HASHING.h2(message) + self._CURVE.scalar_ops.random_scalar()
54
+
55
+ signing_session = SigningSession(session_id, message)
56
+ self._signing_sessions[session_id] = signing_session
57
+
58
+ return session_id
59
+
60
+ def register_participant_to_session(self, session_id: SessionId, participant_id: ParticipantId) -> None:
61
+ """
62
+ Registers a participant to a signing session.
63
+ """
64
+
65
+ if session_id not in self._signing_sessions:
66
+ raise ValueError("signing session not found")
67
+
68
+ signing_session = self._signing_sessions[session_id]
69
+
70
+ if signing_session.signing_in_progress:
71
+ raise ValueError("cannot register participant to session after signing has started")
72
+
73
+ if participant_id not in self.PARTICIPANT_IDS:
74
+ raise ValueError("participant id not recognized")
75
+
76
+ if participant_id in signing_session.participant_ids:
77
+ raise ValueError("participant already registered to session")
78
+
79
+ signing_session.participant_ids.append(participant_id)
80
+
81
+ def start_signing_session(self, session_id: SessionId) -> None:
82
+ """
83
+ Marks the signing session as started, preventing further participant registrations.
84
+ """
85
+
86
+ if session_id not in self._signing_sessions:
87
+ raise ValueError("signing session not found")
88
+
89
+ signing_session = self._signing_sessions[session_id]
90
+
91
+ if len(signing_session.participant_ids) < self.THRESHOLD:
92
+ raise ValueError("not enough participants registered to start signing session")
93
+
94
+ signing_session.signing_in_progress = True
95
+
96
+ def receive_commitment(self, session_id: SessionId, participant_id: ParticipantId, commitment: NonceCommitment) -> None:
97
+ """
98
+ Receives a nonce commitment from a participant and stores it in the signing session.
99
+ """
100
+
101
+ if session_id not in self._signing_sessions:
102
+ raise ValueError("signing session not found")
103
+
104
+ signing_session = self._signing_sessions[session_id]
105
+
106
+ if not signing_session.signing_in_progress:
107
+ raise ValueError("signing session has not started yet")
108
+
109
+ if participant_id not in signing_session.participant_ids:
110
+ raise ValueError("participant id not registered to this signing session")
111
+
112
+ if participant_id in signing_session.commitments:
113
+ raise ValueError("commitment already received from this participant")
114
+
115
+ signing_session.commitments[participant_id] = commitment
116
+
117
+ if len(signing_session.commitments) == len(signing_session.participant_ids):
118
+ signing_session.round_one_completed = True
119
+
120
+ def create_signing_package(self, session_id: SessionId) -> SigningPackage:
121
+ """
122
+ Creates the signing package to be sent to the participants after round one is complete.
123
+ """
124
+
125
+ if session_id not in self._signing_sessions:
126
+ raise ValueError("signing session not found")
127
+
128
+ signing_session = self._signing_sessions[session_id]
129
+
130
+ if not signing_session.round_one_completed:
131
+ raise ValueError("round one not completed yet")
132
+
133
+ return SigningPackage(session_id, signing_session.message, signing_session.participant_ids, signing_session.commitments)
134
+
135
+ def receive_signature_share(self, session_id: SessionId, participant_id: ParticipantId, signature_share: SecretValue) -> None:
136
+ """
137
+ Receives a signature share from a participant and stores it in the signing session.
138
+ """
139
+
140
+ if session_id not in self._signing_sessions:
141
+ raise ValueError("signing session not found")
142
+
143
+ signing_session = self._signing_sessions[session_id]
144
+
145
+ if not signing_session.round_one_completed:
146
+ raise ValueError("round one not completed yet")
147
+
148
+ if participant_id not in signing_session.participant_ids:
149
+ raise ValueError("participant id not registered to this signing session")
150
+
151
+ if participant_id in signing_session.signature_shares:
152
+ raise ValueError("signature share already received from this participant")
153
+
154
+ signing_session.signature_shares[participant_id] = signature_share
155
+
156
+ if len(signing_session.signature_shares) == len(signing_session.participant_ids):
157
+ signing_session.round_two_completed = True
158
+
159
+ def aggregate(self, session_id: SessionId) -> bytes:
160
+ """
161
+ Aggregates the signature shares into a final signature.
162
+ """
163
+
164
+ if session_id not in self._signing_sessions:
165
+ raise ValueError("signing session not found")
166
+
167
+ signing_session = self._signing_sessions[session_id]
168
+
169
+ if not signing_session.round_two_completed:
170
+ raise ValueError("round two not completed yet")
171
+
172
+ commitments = list(signing_session.commitments.values())
173
+ signature_shares = list(signing_session.signature_shares.values())
174
+
175
+ binding_factors = compute_binding_factors(self._GROUP_INFO.group_public_key, commitments, signing_session.message, self._HASHING, self._CURVE.encoding)
176
+
177
+ group_commitment = compute_group_commitment(commitments, binding_factors, self._CURVE)
178
+
179
+ z = 0
180
+ for z_i in signature_shares:
181
+ z = z + z_i
182
+
183
+ z = self._CURVE.scalar_ops.reduce(z)
184
+
185
+ return self._CURVE.encoding.encode_point(group_commitment) + self._CURVE.encoding.encode_scalar(z)
@@ -0,0 +1 @@
1
+ """FROST core primitives."""
@@ -0,0 +1 @@
1
+ """Base FROST interfaces."""
@@ -0,0 +1,27 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+
4
+ class FrostHashing(ABC):
5
+ """
6
+ FrostHashing is the base class that ensures that all hashing functions used in the FROST protocol are present.
7
+ """
8
+
9
+ @abstractmethod
10
+ def h1(self, m: bytes) -> int:
11
+ raise NotImplementedError()
12
+
13
+ @abstractmethod
14
+ def h2(self, m: bytes) -> int:
15
+ raise NotImplementedError()
16
+
17
+ @abstractmethod
18
+ def h3(self, m: bytes) -> int:
19
+ raise NotImplementedError()
20
+
21
+ @abstractmethod
22
+ def h4(self, m: bytes) -> bytes:
23
+ raise NotImplementedError()
24
+
25
+ @abstractmethod
26
+ def h5(self, m: bytes) -> bytes:
27
+ raise NotImplementedError()
@@ -0,0 +1 @@
1
+ """Ed25519-specific FROST helpers."""
@@ -0,0 +1,26 @@
1
+ from eddsa_threshold.eddsa.curves.ed25519.constants import L
2
+ from eddsa_threshold.eddsa.util.hash_bindings import sha512
3
+ from eddsa_threshold.frost.core.base.frost_hashing import FrostHashing
4
+
5
+
6
+ class Ed25519FrostHashing(FrostHashing):
7
+ """
8
+ Ed25519FrostHashing is the implementation of the FrostHashing interface for Ed25519.
9
+ """
10
+
11
+ _CONTEXT_STRING = b"FROST-ED25519-SHA512-v1"
12
+
13
+ def h1(self, m: bytes) -> int:
14
+ return int.from_bytes(sha512(self._CONTEXT_STRING + b"rho" + m), "little") % L
15
+
16
+ def h2(self, m: bytes) -> int:
17
+ return int.from_bytes(sha512(m), "little") % L
18
+
19
+ def h3(self, m: bytes) -> int:
20
+ return int.from_bytes(sha512(self._CONTEXT_STRING + b"nonce" + m), "little") % L
21
+
22
+ def h4(self, m: bytes) -> bytes:
23
+ return sha512(self._CONTEXT_STRING + b"msg" + m)
24
+
25
+ def h5(self, m: bytes) -> bytes:
26
+ return sha512(self._CONTEXT_STRING + b"com" + m)
@@ -0,0 +1 @@
1
+ """Ed448-specific FROST helpers."""
@@ -0,0 +1,27 @@
1
+ from eddsa_threshold.eddsa.curves.ed448.constants import L
2
+ from eddsa_threshold.eddsa.util.hash_bindings import shake256
3
+ from eddsa_threshold.frost.core.base.frost_hashing import FrostHashing
4
+
5
+
6
+ class Ed448FrostHashing(FrostHashing):
7
+ """
8
+ Ed448FrostHashing is the implementation of the FrostHashing interface for Ed448.
9
+ """
10
+
11
+ _CONTEXT_STRING = b"FROST-ED448-SHAKE256-v1"
12
+ _DIGEST_SIZE = 114
13
+
14
+ def h1(self, m: bytes) -> int:
15
+ return int.from_bytes(shake256(self._CONTEXT_STRING + b"rho" + m, self._DIGEST_SIZE), "little") % L
16
+
17
+ def h2(self, m: bytes) -> int:
18
+ return int.from_bytes(shake256(b"SigEd448" + b'\x00' + b'\x00' + m, self._DIGEST_SIZE), "little") % L
19
+
20
+ def h3(self, m: bytes) -> int:
21
+ return int.from_bytes(shake256(self._CONTEXT_STRING + b"nonce" + m, self._DIGEST_SIZE), "little") % L
22
+
23
+ def h4(self, m: bytes) -> bytes:
24
+ return shake256(self._CONTEXT_STRING + b"msg" + m, self._DIGEST_SIZE)
25
+
26
+ def h5(self, m: bytes) -> bytes:
27
+ return shake256(self._CONTEXT_STRING + b"com" + m, self._DIGEST_SIZE)
@@ -0,0 +1,53 @@
1
+ from dataclasses import dataclass, field
2
+
3
+ from typing import Tuple
4
+
5
+ ParticipantId = int
6
+ SessionId = int
7
+ SecretValue = int
8
+ VSSCommitment = Tuple
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class SecretShare:
13
+ index: ParticipantId
14
+ value: SecretValue
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class GroupInfo:
19
+ group_public_key: bytes
20
+ public_keys: dict[ParticipantId, bytes]
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class NonceCommitment:
25
+ participant_id: ParticipantId
26
+ hiding_nonce_commitment: Tuple[int, int]
27
+ binding_nonce_commitment: Tuple[int, int]
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class BindingFactor:
32
+ participant_id: ParticipantId
33
+ binding_factor: int
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class SigningPackage:
38
+ session_id: SessionId
39
+ message: bytes
40
+ participant_ids: list[ParticipantId]
41
+ commitments: dict[ParticipantId, NonceCommitment]
42
+
43
+
44
+ @dataclass
45
+ class SigningSession:
46
+ session_id: SessionId
47
+ message: bytes
48
+ participant_ids: list[ParticipantId] = field(default_factory=list)
49
+ commitments: dict[ParticipantId, NonceCommitment] = field(default_factory=dict)
50
+ signature_shares: dict[ParticipantId, SecretValue] = field(default_factory=dict)
51
+ signing_in_progress: bool = False
52
+ round_one_completed: bool = False
53
+ round_two_completed: bool = False
@@ -0,0 +1,39 @@
1
+ from eddsa_threshold.eddsa.curves.base.scalar_ops import ScalarOps
2
+
3
+
4
+ def evaluate_polynomial(coeffs: list[int], x: int, scalar_ops: ScalarOps) -> int:
5
+ """
6
+ Evaluates a polynomial at a given x using Horner's method.
7
+ """
8
+
9
+ # https://en.wikipedia.org/wiki/Horner's_method
10
+ result = 0
11
+ for coeff in reversed(coeffs):
12
+ result = result * x + coeff
13
+
14
+ return scalar_ops.reduce(result)
15
+
16
+
17
+ def derive_interpolating_value(L: list[int], x_i: int, x: int, scalar_ops: ScalarOps) -> int:
18
+ """
19
+ Compute Lagrange basis coefficient λ_i(x) for arbitrary interpolation point x.
20
+ """
21
+
22
+ if x_i not in L:
23
+ raise ValueError("x_i not in set")
24
+
25
+ if len(set(L)) != len(L):
26
+ raise ValueError("duplicate x values")
27
+
28
+ numerator = 1
29
+ denominator = 1
30
+
31
+ # Compute the Lagrange basis polynomial
32
+ # https://en.wikipedia.org/wiki/Lagrange_polynomial
33
+ for x_j in L:
34
+ if x_j == x_i:
35
+ continue
36
+ numerator = numerator * (x - x_j)
37
+ denominator = denominator * (x_i - x_j)
38
+
39
+ return scalar_ops.reduce(numerator * scalar_ops.inv(scalar_ops.reduce(denominator)))
@@ -0,0 +1 @@
1
+ """Secret sharing helpers."""
@@ -0,0 +1,26 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Final, Tuple
3
+
4
+ from eddsa_threshold.eddsa.curves.base.scalar_ops import ScalarOps
5
+ from eddsa_threshold.frost.core.frost_types import SecretShare
6
+
7
+
8
+ class SecretSharing(ABC):
9
+ def __init__(self, threshold: int, num_shares: int, scalar_ops: ScalarOps):
10
+ self.T: Final[int] = threshold
11
+ self.N: Final[int] = num_shares
12
+ self.scalar_ops: Final[ScalarOps] = scalar_ops
13
+
14
+ @abstractmethod
15
+ def split(self, secret: int) -> Tuple[list[SecretShare], list[int]]:
16
+ """
17
+ Split the secret into N shares with a threshold of T.
18
+ """
19
+ raise NotImplementedError
20
+
21
+ @abstractmethod
22
+ def reconstruct(self, shares: list[SecretShare]) -> int:
23
+ """
24
+ Reconstruct the secret from at least T shares.
25
+ """
26
+ raise NotImplementedError
@@ -0,0 +1,41 @@
1
+ from typing import Tuple
2
+
3
+ from eddsa_threshold.eddsa.curves.base.scalar_ops import ScalarOps
4
+ from eddsa_threshold.frost.core.polynomial import evaluate_polynomial, derive_interpolating_value
5
+ from eddsa_threshold.frost.core.secrets.secret_sharing import SecretSharing
6
+ from eddsa_threshold.frost.core.frost_types import SecretShare
7
+
8
+
9
+ class ShamirSecretSharing(SecretSharing):
10
+ def split(self, secret: int) -> Tuple[list[SecretShare], list[int]]:
11
+ """
12
+ Split the secret into N shares with a threshold of T.
13
+ Uses Shamir's Secret Sharing with random coefficients.
14
+ """
15
+
16
+ # Generate random coefficients for the polynomial f(x) = secret + a1*x + a2*x^2 + ... + a_{T-1}*x^{T-1}
17
+ # Uses .random_scalar(), NOT PRODUCTION SECURE, but fine for this project.
18
+ coeffs = [secret] + [self.scalar_ops.random_scalar() for _ in range(1, self.T)]
19
+
20
+ shares = []
21
+ for i in range(1, self.N + 1):
22
+ secret_key_share_i = evaluate_polynomial(coeffs, i, self.scalar_ops)
23
+ shares.append(SecretShare(i, secret_key_share_i))
24
+
25
+ return shares, coeffs
26
+
27
+ def reconstruct(self, shares: list[SecretShare]) -> int:
28
+ """
29
+ Reconstruct the secret from at least T shares using Lagrange interpolation.
30
+ """
31
+
32
+ if len(shares) < self.T:
33
+ raise ValueError("Not enough shares to reconstruct the secret")
34
+
35
+ secret = self.scalar_ops.identity
36
+ for share_i in shares:
37
+ # Add share_i contribution to the secret
38
+ lagrange_coeff = derive_interpolating_value([share_j.index for share_j in shares], share_i.index, 0, self.scalar_ops)
39
+ secret += share_i.value * lagrange_coeff
40
+
41
+ return self.scalar_ops.reduce(secret)
@@ -0,0 +1,109 @@
1
+ from typing import Tuple
2
+
3
+ from eddsa_threshold.eddsa.curves.base.edwards_curve import EdwardsCurve
4
+ from eddsa_threshold.eddsa.curves.base.encoding import Encoding
5
+ from eddsa_threshold.eddsa.curves.base.scalar_ops import ScalarOps
6
+ from eddsa_threshold.frost.core.base.frost_hashing import FrostHashing
7
+ from eddsa_threshold.frost.core.frost_types import BindingFactor, GroupInfo, NonceCommitment, ParticipantId, VSSCommitment
8
+
9
+
10
+ def encode_group_commitments(commitments: list[NonceCommitment], encoding: Encoding) -> bytes:
11
+ """
12
+ Encodes a list of NonceCommitments into bytes.
13
+ """
14
+
15
+ # Sort by participant id for deterministic encoding
16
+ group_commitments = sorted(commitments, key=lambda c: c.participant_id)
17
+
18
+ encoded_group_commitment = b""
19
+
20
+ for commitment in group_commitments:
21
+ encoded_commitment = encoding.encode_scalar(commitment.participant_id) + encoding.encode_point(
22
+ commitment.hiding_nonce_commitment) + encoding.encode_point(commitment.binding_nonce_commitment)
23
+ encoded_group_commitment = encoded_group_commitment + encoded_commitment
24
+
25
+ return encoded_group_commitment
26
+
27
+
28
+ def compute_binding_factors(group_public_key: bytes, commitments: list[NonceCommitment], message: bytes, hashing: FrostHashing, encoding: Encoding) -> list[BindingFactor]:
29
+ """
30
+ Computes the binding factors for a signing session given the group public key, the list of NonceCommitments, and the message.
31
+ """
32
+
33
+ message_hash = hashing.h4(message)
34
+ encoded_commitments_hash = hashing.h5(encode_group_commitments(commitments, encoding))
35
+
36
+ rho_prefix = group_public_key + message_hash + encoded_commitments_hash
37
+
38
+ binding_factors = []
39
+ for commitment in commitments:
40
+ rho_input = rho_prefix + encoding.encode_scalar(commitment.participant_id)
41
+ binding_factor = hashing.h1(rho_input)
42
+ binding_factors.append(BindingFactor(commitment.participant_id, binding_factor))
43
+ return binding_factors
44
+
45
+
46
+ def binding_factor_for_participant(participant_id: ParticipantId, binding_factors: list[BindingFactor]) -> int:
47
+ """
48
+ Retrieves the binding factor for a given participant id from a list of BindingFactors.
49
+ """
50
+
51
+ for factor in binding_factors:
52
+ if factor.participant_id == participant_id:
53
+ return factor.binding_factor
54
+
55
+ raise ValueError(f"binding factor not found for participant {participant_id}")
56
+
57
+
58
+ # EdwardsCurve for now, because this project only implements FROST for EdDSA, but this can be made more generic if needed.
59
+ def compute_group_commitment(commitments: list[NonceCommitment], binding_factors: list[BindingFactor], curve: EdwardsCurve) -> Tuple:
60
+ """
61
+ Computes the group commitment from a list of NonceCommitments and BindingFactors.
62
+ """
63
+
64
+ group_commitment = (0, 1, 1, 0) # Neutral element in extended coordinates
65
+
66
+ for commitment in commitments:
67
+ binding_factor = binding_factor_for_participant(commitment.participant_id, binding_factors)
68
+ binding_nonce = curve.scalar_mult(binding_factor, curve.affine_to_extended(commitment.binding_nonce_commitment))
69
+
70
+ group_commitment = curve.add(group_commitment, curve.affine_to_extended(commitment.hiding_nonce_commitment))
71
+ group_commitment = curve.add(group_commitment, binding_nonce)
72
+
73
+ return curve.extended_to_affine(group_commitment)
74
+
75
+
76
+ def check_participant_bounds(threshold: int, participant_ids: list[ParticipantId], scalar_ops: ScalarOps) -> None:
77
+ """
78
+ Checks that participant ids are within valid bounds
79
+ """
80
+
81
+ if threshold <= 0:
82
+ raise ValueError("threshold must be positive")
83
+ if len(participant_ids) < threshold:
84
+ raise ValueError("number of participants must be >= threshold")
85
+ if len(participant_ids) <= 0:
86
+ raise ValueError("number of participants must be positive")
87
+ if len(participant_ids) >= scalar_ops.order:
88
+ raise ValueError("number of participants must be less than the curve order")
89
+
90
+ unique_ids = set(participant_ids)
91
+ if len(unique_ids) != len(participant_ids):
92
+ raise ValueError("participant ids must be unique")
93
+
94
+
95
+ def derive_group_info(threshold: int, max_participants: int, vss_commitment: list[VSSCommitment], curve: EdwardsCurve) -> GroupInfo:
96
+ """
97
+ Derives the group public key and participant public keys from the VSS commitment.
98
+ """
99
+
100
+ group_public_key = curve.encode_extended_point(vss_commitment[0]) # the first commitment is the group public key
101
+
102
+ participant_public_keys: dict[ParticipantId, bytes] = {}
103
+
104
+ for i in range(1, max_participants + 1):
105
+ participant_i_pk = (0, 1, 1, 0)
106
+ for j in range(0, threshold):
107
+ participant_i_pk = curve.add(curve.scalar_mult(pow(i, j), vss_commitment[j]), participant_i_pk)
108
+ participant_public_keys[i] = curve.encode_extended_point(participant_i_pk)
109
+ return GroupInfo(group_public_key, participant_public_keys)