Skip to content

Commit 2c29f48

Browse files
Modify discrete log to support sync and async (#405)
* Modify discrete log to support sync and async * Address PR feedback * update version
1 parent 6d70f66 commit 2c29f48

File tree

7 files changed

+207
-114
lines changed

7 files changed

+207
-114
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "electionguard"
3-
version = "1.2.2"
3+
version = "1.2.3"
44
description = "ElectionGuard: Support for e2e verified elections."
55
license = "MIT"
66
authors = ["Microsoft <electionguard@microsoft.com>"]

src/electionguard/decrypt_with_shares.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
DecryptionShare,
77
get_shares_for_selection,
88
)
9-
from .dlog import discrete_log
9+
from .discrete_log import DiscreteLog
1010
from .group import ElementModP, ElementModQ, mult_p, div_p
1111
from .tally import (
1212
CiphertextTally,
@@ -165,7 +165,7 @@ def decrypt_selection_with_decryption_shares(
165165

166166
# Calculate 𝑀=𝐵⁄(∏𝑀𝑖) mod 𝑝.
167167
decrypted_value = div_p(selection.ciphertext.data, all_shares_product_M)
168-
d_log = discrete_log(decrypted_value)
168+
d_log = DiscreteLog().discrete_log(decrypted_value)
169169
return PlaintextTallySelection(
170170
selection.object_id,
171171
d_log,

src/electionguard/discrete_log.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# pylint: disable=global-statement
2+
# support for computing discrete logs, with a cache so they're never recomputed
3+
4+
import asyncio
5+
from typing import Dict, Tuple
6+
7+
from electionguard.singleton import Singleton
8+
9+
from .group import G, ElementModP, ONE_MOD_P, mult_p, int_to_p_unchecked
10+
11+
DLOG_CACHE = Dict[ElementModP, int]
12+
DLOG_MAX = 100_000_000
13+
"""The max number to calculate. This value is used to stop a race condition."""
14+
15+
16+
def discrete_log(element: ElementModP, cache: DLOG_CACHE) -> Tuple[int, DLOG_CACHE]:
17+
"""
18+
Computes the discrete log (base g, mod p) of the given element,
19+
with internal caching of results. Should run efficiently when called
20+
multiple times when the exponent is at most in the single-digit millions.
21+
Performance will degrade if it's much larger.
22+
23+
For the best possible performance,
24+
pre-compute the discrete log of a number you expect to have the biggest
25+
exponent you'll ever see. After that, the cache will be fully loaded,
26+
and every call will be nothing more than a dictionary lookup.
27+
"""
28+
29+
if element in cache:
30+
return (cache[element], cache)
31+
32+
cache = compute_discrete_log_cache(element, cache)
33+
return (cache[element], cache)
34+
35+
36+
async def discrete_log_async(
37+
element: ElementModP,
38+
cache: DLOG_CACHE,
39+
mutex: asyncio.Lock = asyncio.Lock(),
40+
) -> Tuple[int, DLOG_CACHE]:
41+
"""
42+
Computes the discrete log (base g, mod p) of the given element,
43+
with internal caching of results. Should run efficiently when called
44+
multiple times when the exponent is at most in the single-digit millions.
45+
Performance will degrade if it's much larger.
46+
47+
Note: *this function is thread-safe*. For the best possible performance,
48+
pre-compute the discrete log of a number you expect to have the biggest
49+
exponent you'll ever see. After that, the cache will be fully loaded,
50+
and every call will be nothing more than a dictionary lookup.
51+
"""
52+
if element in cache:
53+
return (cache[element], cache)
54+
55+
async with mutex:
56+
if element in cache:
57+
return (cache[element], cache)
58+
59+
cache = compute_discrete_log_cache(element, cache)
60+
return (cache[element], cache)
61+
62+
63+
def compute_discrete_log_cache(
64+
element: ElementModP, cache: DLOG_CACHE
65+
) -> Dict[ElementModP, int]:
66+
"""
67+
Compute a discrete log cache up to the specified element.
68+
"""
69+
if not cache:
70+
cache = {ONE_MOD_P: 0}
71+
max_element = list(cache)[-1]
72+
exponent = cache[max_element]
73+
74+
g = int_to_p_unchecked(G)
75+
while element != max_element:
76+
exponent = exponent + 1
77+
if exponent > DLOG_MAX:
78+
raise ValueError("size is larger than max.")
79+
max_element = mult_p(g, max_element)
80+
cache[max_element] = exponent
81+
print(f"max: {max_element}, exp: {exponent}")
82+
return cache
83+
84+
85+
class DiscreteLog(Singleton):
86+
"""
87+
A class instance of the discrete log that includes a cache.
88+
"""
89+
90+
_cache: DLOG_CACHE = {ONE_MOD_P: 0}
91+
_mutex = asyncio.Lock()
92+
93+
def discrete_log(self, element: ElementModP) -> int:
94+
(result, cache) = discrete_log(element, self._cache)
95+
self._cache = cache
96+
return result
97+
98+
async def discrete_log_async(self, element: ElementModP) -> int:
99+
(result, cache) = await discrete_log_async(element, self._cache, self._mutex)
100+
self._cache = cache
101+
return result

src/electionguard/dlog.py

Lines changed: 0 additions & 53 deletions
This file was deleted.

src/electionguard/elgamal.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import Iterable, NamedTuple, Optional
22

3-
from .dlog import discrete_log
3+
from .discrete_log import DiscreteLog
44
from .group import (
55
ElementModQ,
66
ElementModP,
@@ -48,7 +48,7 @@ def decrypt_known_product(self, product: ElementModP) -> int:
4848
:param product: The known product (blinding factor).
4949
:return: An exponentially encoded plaintext message.
5050
"""
51-
return discrete_log(mult_p(self.data, mult_inv_p(product)))
51+
return DiscreteLog().discrete_log(mult_p(self.data, mult_inv_p(product)))
5252

5353
def decrypt(self, secret_key: ElementModQ) -> int:
5454
"""
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import unittest
2+
3+
from hypothesis import given
4+
from hypothesis.strategies import integers
5+
6+
from electionguard.discrete_log import (
7+
discrete_log,
8+
discrete_log_async,
9+
DiscreteLog,
10+
)
11+
from electionguard.group import (
12+
ElementModP,
13+
ONE_MOD_P,
14+
mult_p,
15+
G,
16+
g_pow_p,
17+
int_to_q,
18+
int_to_p_unchecked,
19+
int_to_q_unchecked,
20+
P,
21+
)
22+
from electionguard.utils import get_optional
23+
24+
25+
def _discrete_log_uncached(e: ElementModP) -> int:
26+
"""
27+
A simpler implementation of discrete_log, only meant for comparison testing of the caching version.
28+
"""
29+
count = 0
30+
g_inv = int_to_p_unchecked(pow(G, -1, P))
31+
while e != ONE_MOD_P:
32+
e = mult_p(e, g_inv)
33+
count = count + 1
34+
35+
return count
36+
37+
38+
class TestDiscreteLogFunctions(unittest.TestCase):
39+
"""Discrete log tests"""
40+
41+
@given(integers(0, 100))
42+
def test_uncached(self, exp: int):
43+
plaintext = get_optional(int_to_q(exp))
44+
exp_plaintext = g_pow_p(plaintext)
45+
plaintext_again = _discrete_log_uncached(exp_plaintext)
46+
47+
self.assertEqual(exp, plaintext_again)
48+
49+
@given(integers(0, 1000))
50+
def test_cached(self, exp: int):
51+
cache = {ONE_MOD_P: 0}
52+
plaintext = get_optional(int_to_q(exp))
53+
exp_plaintext = g_pow_p(plaintext)
54+
(plaintext_again, returned_cache) = discrete_log(exp_plaintext, cache)
55+
56+
self.assertEqual(exp, plaintext_again)
57+
self.assertEqual(len(cache), len(returned_cache))
58+
59+
def test_cached_one(self):
60+
cache = {ONE_MOD_P: 0}
61+
plaintext = int_to_q_unchecked(1)
62+
ciphertext = g_pow_p(plaintext)
63+
(plaintext_again, returned_cache) = discrete_log(ciphertext, cache)
64+
65+
self.assertEqual(1, plaintext_again)
66+
self.assertEqual(len(cache), len(returned_cache))
67+
68+
async def test_cached_one_async(self):
69+
cache = {ONE_MOD_P: 0}
70+
plaintext = int_to_q_unchecked(1)
71+
ciphertext = g_pow_p(plaintext)
72+
(plaintext_again, returned_cache) = await discrete_log_async(ciphertext, cache)
73+
74+
self.assertEqual(1, plaintext_again)
75+
self.assertEqual(len(cache), len(returned_cache))
76+
77+
78+
class TestDiscreteLogClass(unittest.TestCase):
79+
"""Discrete log tests"""
80+
81+
@given(integers(0, 1000))
82+
def test_cached(self, exp: int):
83+
plaintext = get_optional(int_to_q(exp))
84+
exp_plaintext = g_pow_p(plaintext)
85+
plaintext_again = DiscreteLog().discrete_log(exp_plaintext)
86+
87+
self.assertEqual(exp, plaintext_again)
88+
89+
def test_cached_one(self):
90+
plaintext = int_to_q_unchecked(1)
91+
ciphertext = g_pow_p(plaintext)
92+
plaintext_again = DiscreteLog().discrete_log(ciphertext)
93+
94+
self.assertEqual(1, plaintext_again)
95+
96+
async def test_cached_one_async(self):
97+
plaintext = int_to_q_unchecked(1)
98+
ciphertext = g_pow_p(plaintext)
99+
plaintext_again = await DiscreteLog().discrete_log_async(ciphertext)
100+
101+
self.assertEqual(1, plaintext_again)

tests/property/test_dlog.py

Lines changed: 0 additions & 56 deletions
This file was deleted.

0 commit comments

Comments
 (0)