From 719adf8b8685ded9d5d24f9b4293bf93622af7b5 Mon Sep 17 00:00:00 2001 From: Mostafa Date: Fri, 18 Jul 2025 19:02:38 +0800 Subject: [PATCH 1/2] feat: Shamir's secret sharing --- pactus/crypto/sss/__init__.py | 0 pactus/crypto/sss/sss.py | 114 ++++++++++++++++++++++++++++++++++ pactus/utils/utils.py | 26 -------- tests/test_crypto_sss.py | 29 +++++++++ tests/test_utils.py | 63 ------------------- 5 files changed, 143 insertions(+), 89 deletions(-) create mode 100644 pactus/crypto/sss/__init__.py create mode 100644 pactus/crypto/sss/sss.py create mode 100644 tests/test_crypto_sss.py delete mode 100644 tests/test_utils.py diff --git a/pactus/crypto/sss/__init__.py b/pactus/crypto/sss/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pactus/crypto/sss/sss.py b/pactus/crypto/sss/sss.py new file mode 100644 index 0000000..0c867b0 --- /dev/null +++ b/pactus/crypto/sss/sss.py @@ -0,0 +1,114 @@ +""" +The following Python implementation of Shamir's secret sharing is +released into the Public Domain under the terms of CC0 and OWFa: +https://creativecommons.org/publicdomain/zero/1.0/ +http://www.openwebfoundation.org/legal/the-owf-1-0-agreements/owfa-1-0 + +See the bottom few lines for usage. Tested on Python 2 and 3. +""" + +import functools +import random + +_RINT = functools.partial(random.SystemRandom().randint, 0) + + +def _eval_at(poly, x, prime): + """ + Evaluates polynomial (coefficient tuple) at x, used to generate a + shamir pool in make_random_shares below. + """ + accum = 0 + for coeff in reversed(poly): + accum *= x + accum += coeff + accum %= prime + + return accum + + +def _extended_gcd(a: int, b: int) -> int: + """ + Division in integers modulus p means finding the inverse of the + denominator modulo p and then multiplying the numerator by this + inverse (Note: inverse of A is B such that A*B % p == 1). This can + be computed via the extended Euclidean algorithm + http://en.wikipedia.org/wiki/Modular_multiplicative_inverse#Computation + """ + x = 0 + last_x = 1 + y = 1 + last_y = 0 + while b != 0: + quot = a // b + a, b = b, a % b + x, last_x = last_x - quot * x, x + y, last_y = last_y - quot * y, y + + return last_x, last_y + + +def _divmod(num: int, den: int, p: int) -> int: + """ + Compute num / den modulo prime p + + To explain this, the result will be such that: + den * _divmod(num, den, p) % p == num + """ + inv, _ = _extended_gcd(den, p) + + return num * inv + + +def _lagrange_interpolate(x: int, x_s: list[int], y_s: list[int], p: int) -> int: + """ + Find the y-value for the given x, given n (x, y) points; + k points will define a polynomial of up to kth order. + """ + k = len(x_s) + assert k == len(set(x_s)), "points must be distinct" + + def PI(vals): # upper-case PI -- product of inputs + accum = 1 + for v in vals: + accum *= v + return accum + + nums = [] # avoid inexact division + dens = [] + for i in range(k): + others = list(x_s) + cur = others.pop(i) + nums.append(PI(x - o for o in others)) + dens.append(PI(cur - o for o in others)) + + den = PI(dens) + num = sum([_divmod(nums[i] * den * y_s[i] % p, dens[i], p) for i in range(k)]) + + return (_divmod(num, den, p) + p) % p + + +def make_random_shares(secret: int, minimum: int, shares: int, prime: int) -> list[tuple[int, int]]: + """ + Generates a random shamir pool for a given secret, returns share points. + """ + if minimum > shares: + raise ValueError("Pool secret would be irrecoverable.") + poly = [secret] + [_RINT(prime - 1) for i in range(minimum - 1)] + points = [(i, _eval_at(poly, i, prime)) for i in range(1, shares + 1)] + + return points + + +def recover_secret(shares: list[tuple[int, int]], prime: int) -> int: + """ + Recover the secret from share points + (points (x,y) on the polynomial). + """ + if len(shares) < 2: + msg = "need at least two shares" + raise ValueError(msg) + + x_s, y_s = zip(*shares) + + return _lagrange_interpolate(0, x_s, y_s, prime) diff --git a/pactus/utils/utils.py b/pactus/utils/utils.py index 39e9ad9..69df1b2 100644 --- a/pactus/utils/utils.py +++ b/pactus/utils/utils.py @@ -17,29 +17,3 @@ def encode_from_base256_with_type(hrp: str, typ: str, data: bytes) -> str: converted = bech32m.convertbits(list(data), 8, 5, pad=True) converted = [typ, *converted] return bech32m.bech32_encode(hrp, converted, bech32m.Encoding.BECH32M) - - -def evaluate_polynomial(c: list[int], x: int, mod: int) -> int | None: - """ - Evaluate the polynomial f(x) = c[0] + c[1] * x + c[2] * x^2 + ... + c[n-1] * x^(n-1). - - Args: - c: List of polynomial coefficients (c[0] is the constant term) - x: The value at which to evaluate the polynomial - mod: The modulus to use for the evaluation - - Returns: - The computed value f(x) if success, None otherwise - - """ - if not c: - return None - - if len(c) == 1: - return c[0] - - y = c[-1] - for i in range(len(c) - 2, -1, -1): - y = (y * x + c[i]) % mod - - return y diff --git a/tests/test_crypto_sss.py b/tests/test_crypto_sss.py new file mode 100644 index 0000000..36c5fa4 --- /dev/null +++ b/tests/test_crypto_sss.py @@ -0,0 +1,29 @@ +import unittest +from pactus.crypto.sss import sss + + +class TestEvaluatePolynomial(unittest.TestCase): + def test_wikipedia_example(self): + # https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing + self.assertEqual(sss._eval_at([1234, 166, 94], 1, 2**127 - 1), 1494) + self.assertEqual(sss._eval_at([1234, 166, 94], 2, 2**127 - 1), 1942) + self.assertEqual(sss._eval_at([1234, 166, 94], 3, 2**127 - 1), 2578) + self.assertEqual(sss._eval_at([1234, 166, 94], 4, 2**127 - 1), 3402) + self.assertEqual(sss._eval_at([1234, 166, 94], 5, 2**127 - 1), 4414) + self.assertEqual(sss._eval_at([1234, 166, 94], 6, 2**127 - 1), 5614) + + +class TestRecover(unittest.TestCase): + def test_recover_secret_1(self): + shares = [(1, 1494), (2, 1942), (3, 2578)] + prime = 2**127 - 1 + self.assertEqual(sss.recover_secret(shares, prime), 1234) + + def test_recover_secret_2(self): + shares = [(1, 1494), (3, 2578), (6, 5614)] + prime = 2**127 - 1 + self.assertEqual(sss.recover_secret(shares, prime), 1234) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_utils.py b/tests/test_utils.py deleted file mode 100644 index 0a91a52..0000000 --- a/tests/test_utils.py +++ /dev/null @@ -1,63 +0,0 @@ -import unittest -from pactus.utils import utils - - -class TestEvaluatePolynomial(unittest.TestCase): - def test_empty_coefficients(self): - self.assertIsNone(utils.evaluate_polynomial([], 2, 7)) - - def test_single_coefficient(self): - self.assertEqual(utils.evaluate_polynomial([5], 10, 7), 5) - - def test_x_zero(self): - # f(0) = c[0] % mod - self.assertEqual(utils.evaluate_polynomial([3, 2, 1], 0, 5), 3 % 5) - - def test_x_one(self): - # f(1) = sum(c) % mod - self.assertEqual(utils.evaluate_polynomial([1, 2, 3], 1, 7), (1 + 2 + 3) % 7) - - def test_multiple_coefficients(self): - # f(2) = 1 + 2*2 + 3*2^2 = 1 + 4 + 12 = 17 % 5 = 2 - self.assertEqual(utils.evaluate_polynomial([1, 2, 3], 2, 5), 2) - - def test_negative_coefficients(self): - # f(2) = -1 + 2*2 + (-3)*2^2 = -1 + 4 - 12 = -9 % 7 = 5 - self.assertEqual(utils.evaluate_polynomial([-1, 2, -3], 2, 7), 5) - - def test_negative_x(self): - # f(-1) = 2 + 3*(-1) + 4*(-1)^2 = 2 - 3 + 4 = 3 % 6 = 3 - self.assertEqual(utils.evaluate_polynomial([2, 3, 4], -1, 6), 3) - - def test_large_modulus(self): - # f(3) = 2 + 4*3 + 5*9 = 2 + 12 + 45 = 59 % 1000 = 59 - self.assertEqual(utils.evaluate_polynomial([2, 4, 5], 3, 1000), 59) - - def test_modulus_one(self): - # Any value mod 1 is 0 - self.assertEqual(utils.evaluate_polynomial([1, 2, 3], 5, 1), 0) - - def test_wikipedia_example(self): - # https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing - self.assertEqual( - utils.evaluate_polynomial([1234, 166, 94], 1, 2**127 - 1), 1494 - ) - self.assertEqual( - utils.evaluate_polynomial([1234, 166, 94], 2, 2**127 - 1), 1942 - ) - self.assertEqual( - utils.evaluate_polynomial([1234, 166, 94], 3, 2**127 - 1), 2578 - ) - self.assertEqual( - utils.evaluate_polynomial([1234, 166, 94], 4, 2**127 - 1), 3402 - ) - self.assertEqual( - utils.evaluate_polynomial([1234, 166, 94], 5, 2**127 - 1), 4414 - ) - self.assertEqual( - utils.evaluate_polynomial([1234, 166, 94], 6, 2**127 - 1), 5614 - ) - - -if __name__ == "__main__": - unittest.main() From a270754729b8c721a6abb350a2bf65a7efd34ffa Mon Sep 17 00:00:00 2001 From: Mostafa Date: Tue, 22 Jul 2025 17:37:29 +0800 Subject: [PATCH 2/2] chore: fix linting issues --- pactus/crypto/sss/sss.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/pactus/crypto/sss/sss.py b/pactus/crypto/sss/sss.py index 0c867b0..8f66994 100644 --- a/pactus/crypto/sss/sss.py +++ b/pactus/crypto/sss/sss.py @@ -2,10 +2,11 @@ The following Python implementation of Shamir's secret sharing is released into the Public Domain under the terms of CC0 and OWFa: https://creativecommons.org/publicdomain/zero/1.0/ -http://www.openwebfoundation.org/legal/the-owf-1-0-agreements/owfa-1-0 +http://www.openwebfoundation.org/legal/the-owf-1-0-agreements/owfa-1-0. See the bottom few lines for usage. Tested on Python 2 and 3. """ +from __future__ import annotations import functools import random @@ -13,9 +14,9 @@ _RINT = functools.partial(random.SystemRandom().randint, 0) -def _eval_at(poly, x, prime): +def _eval_at(poly: list[int], x: int, prime: int) -> int: """ - Evaluates polynomial (coefficient tuple) at x, used to generate a + Evaluate polynomial (coefficient tuple) at x, used to generate a shamir pool in make_random_shares below. """ accum = 0 @@ -33,7 +34,7 @@ def _extended_gcd(a: int, b: int) -> int: denominator modulo p and then multiplying the numerator by this inverse (Note: inverse of A is B such that A*B % p == 1). This can be computed via the extended Euclidean algorithm - http://en.wikipedia.org/wiki/Modular_multiplicative_inverse#Computation + http://en.wikipedia.org/wiki/Modular_multiplicative_inverse#Computation. """ x = 0 last_x = 1 @@ -50,7 +51,7 @@ def _extended_gcd(a: int, b: int) -> int: def _divmod(num: int, den: int, p: int) -> int: """ - Compute num / den modulo prime p + Compute num / den modulo prime p. To explain this, the result will be such that: den * _divmod(num, den, p) % p == num @@ -66,9 +67,11 @@ def _lagrange_interpolate(x: int, x_s: list[int], y_s: list[int], p: int) -> int k points will define a polynomial of up to kth order. """ k = len(x_s) - assert k == len(set(x_s)), "points must be distinct" + if k != len(set(x_s)): + msg = "points must be distinct" + raise ValueError(msg) - def PI(vals): # upper-case PI -- product of inputs + def _pi(vals: list[int]) -> int: # upper-case PI -- product of inputs accum = 1 for v in vals: accum *= v @@ -79,25 +82,23 @@ def PI(vals): # upper-case PI -- product of inputs for i in range(k): others = list(x_s) cur = others.pop(i) - nums.append(PI(x - o for o in others)) - dens.append(PI(cur - o for o in others)) + nums.append(_pi(x - o for o in others)) + dens.append(_pi(cur - o for o in others)) - den = PI(dens) + den = _pi(dens) num = sum([_divmod(nums[i] * den * y_s[i] % p, dens[i], p) for i in range(k)]) return (_divmod(num, den, p) + p) % p def make_random_shares(secret: int, minimum: int, shares: int, prime: int) -> list[tuple[int, int]]: - """ - Generates a random shamir pool for a given secret, returns share points. - """ + """Generate a random shamir pool for a given secret, returns share points.""" if minimum > shares: - raise ValueError("Pool secret would be irrecoverable.") + msg = "Pool secret would be irrecoverable." + raise ValueError(msg) poly = [secret] + [_RINT(prime - 1) for i in range(minimum - 1)] - points = [(i, _eval_at(poly, i, prime)) for i in range(1, shares + 1)] + return [(i, _eval_at(poly, i, prime)) for i in range(1, shares + 1)] - return points def recover_secret(shares: list[tuple[int, int]], prime: int) -> int: