You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
150 lines
5.6 KiB
150 lines
5.6 KiB
# Implementation of the Trusted Dealer Key Generation approach for FROST mentioned |
|
# in https://datatracker.ietf.org/doc/draft-irtf-cfrg-frost/15/ (Appendix D). |
|
# |
|
# It's worth noting that this isn't the only compatible method (with BIP FROST Signing), |
|
# there are alternative key generation methods available, such as BIP-FROST-DKG: |
|
# https://github.com/BlockstreamResearch/bip-frost-dkg |
|
|
|
# todo: use the `Scalar` type like BIP-DKG? |
|
#todo: this shows mypy error, but the file runs |
|
|
|
from typing import Tuple, List, NewType |
|
import unittest |
|
# todo: replace random module with secrets |
|
import random |
|
import secrets |
|
# for [1] import functions from reference |
|
# [2] specify path for bip340 when running reference.py |
|
# import sys, os |
|
# script_dir = os.path.dirname(os.path.abspath(__file__)) |
|
# parent_dir = os.path.abspath(os.path.join(script_dir, '..')) |
|
# sys.path.append(parent_dir) |
|
from jmfrost.secp256k1lab.secp256k1 import G, GE, Scalar |
|
|
|
curve_order = GE.ORDER |
|
# point on the secret polynomial, represents a signer's secret share |
|
PolyPoint = Tuple[int, int] |
|
# point on the secp256k1 curve, represents a signer's public share |
|
ECPoint = GE |
|
|
|
# |
|
# The following helper functions and types were copied from reference.py |
|
# |
|
PlainPk = NewType('PlainPk', bytes) |
|
|
|
def xbytes(P: GE) -> bytes: |
|
return P.to_bytes_xonly() |
|
|
|
def cbytes(P: GE) -> bytes: |
|
return P.to_bytes_compressed() |
|
|
|
def derive_interpolating_value_internal(L: List[int], x_i: int) -> Scalar: |
|
num, deno = 1, 1 |
|
for x_j in L: |
|
if x_j == x_i: |
|
continue |
|
num *= x_j |
|
deno *= (x_j - x_i) |
|
return Scalar.from_int_wrapping(num * pow(deno, curve_order - 2, curve_order)) |
|
# |
|
# End of helper functions and types copied from reference.py. |
|
# |
|
|
|
# evaluates poly using Horner's method, assuming coeff[0] corresponds |
|
# to the coefficient of highest degree term |
|
def polynomial_evaluate(coeffs: List[int], x: int) -> int: |
|
res = 0 |
|
for coeff in coeffs: |
|
res = res * x + coeff |
|
return res % curve_order |
|
|
|
|
|
def secret_share_combine(shares: List[PolyPoint]) -> Scalar: |
|
x_coords = [] |
|
for (x, y) in shares: |
|
x_coords.append(x) |
|
|
|
secret = Scalar(0) |
|
for (x, y) in shares: |
|
delta = y * derive_interpolating_value_internal(x_coords, x) |
|
secret += delta |
|
return secret |
|
|
|
# coeffs shouldn't include the const term (i.e. secret) |
|
def secret_share_shard(secret: int, coeffs: List[int], max_participants: int) -> List[PolyPoint]: |
|
coeffs = coeffs + [secret] |
|
|
|
secshares: List[PolyPoint] = [] |
|
for x_i in range(1, max_participants + 1): |
|
y_i = polynomial_evaluate(coeffs, x_i) |
|
secshare_i = (x_i, y_i) |
|
secshares.append(secshare_i) |
|
return secshares |
|
|
|
def trusted_dealer_keygen(secret_key: Scalar, max_participants: int, min_participants: int) -> Tuple[ECPoint, List[PolyPoint], List[ECPoint]]: |
|
assert secret_key != 0 |
|
assert (2 <= min_participants <= max_participants) |
|
# we don't force BIP340 compatibility of group pubkey in keygen |
|
P = secret_key * G |
|
assert not P.infinity |
|
|
|
coeffs = [] |
|
for i in range(min_participants - 1): |
|
coeffs.append(random.randint(1, curve_order - 1)) |
|
secshares = secret_share_shard(int(secret_key), coeffs, max_participants) |
|
pubshares = [] |
|
for secshare in secshares: |
|
X = secshare[1] * G |
|
assert not X.infinity |
|
pubshares.append(X) |
|
return (P, secshares, pubshares) |
|
|
|
# Test vector from RFC draft. |
|
# section F.5 of https://datatracker.ietf.org/doc/draft-irtf-cfrg-frost/15/ |
|
class Tests(unittest.TestCase): |
|
def setUp(self) -> None: |
|
self.max_participants = 3 |
|
self.min_participants = 2 |
|
self.poly = [ |
|
0xfbf85eadae3058ea14f19148bb72b45e4399c0b16028acaf0395c9b03c823579, |
|
0x0d004150d27c3bf2a42f312683d35fac7394b1e9e318249c1bfe7f0795a83114, |
|
] |
|
self.shares: List[PolyPoint] = [ |
|
(1, 0x08f89ffe80ac94dcb920c26f3f46140bfc7f95b493f8310f5fc1ea2b01f4254c), |
|
(2, 0x04f0feac2edcedc6ce1253b7fab8c86b856a797f44d83d82a385554e6e401984), |
|
(3, 0x00e95d59dd0d46b0e303e500b62b7ccb0e555d49f5b849f5e748c071da8c0dbc), |
|
] |
|
self.secret = 0x0d004150d27c3bf2a42f312683d35fac7394b1e9e318249c1bfe7f0795a83114 |
|
|
|
def test_polynomial_evaluate(self) -> None: |
|
coeffs = self.poly.copy() |
|
expected_secret = self.secret |
|
|
|
self.assertEqual(polynomial_evaluate(coeffs, 0), expected_secret) |
|
|
|
def test_secret_share_combine(self) -> None: |
|
shares: List[PolyPoint] = self.shares.copy() |
|
expected_secret = self.secret |
|
|
|
self.assertEqual(secret_share_combine([shares[0], shares[1]]), expected_secret) |
|
self.assertEqual(secret_share_combine([shares[1], shares[2]]), expected_secret) |
|
self.assertEqual(secret_share_combine([shares[0], shares[2]]), expected_secret) |
|
self.assertEqual(secret_share_combine(shares), expected_secret) |
|
|
|
def test_trusted_dealer_keygen(self) -> None: |
|
secret_key = Scalar.from_bytes_wrapping(secrets.token_bytes(32)) |
|
max_participants = 5 |
|
min_participants = 3 |
|
group_pk, secshares, pubshares = trusted_dealer_keygen(secret_key, max_participants, min_participants) |
|
|
|
# group_pk need not be xonly (i.e., have even y always) |
|
self.assertEqual(group_pk, secret_key * G) |
|
self.assertEqual(secret_share_combine(secshares), secret_key) |
|
self.assertEqual(len(secshares), max_participants) |
|
self.assertEqual(len(pubshares), max_participants) |
|
for i in range(len(pubshares)): |
|
with self.subTest(i=i): |
|
self.assertEqual(pubshares[i], secshares[i][1] * G) |
|
|
|
if __name__=='__main__': |
|
unittest.main()
|
|
|