Skip to content

Commit 4e059b9

Browse files
authored
Merge pull request #240 from tomato42/less_locking
implement lock-less algorithm in PointJacobi
2 parents e3b0dcc + 2fcfca2 commit 4e059b9

File tree

3 files changed

+185
-132
lines changed

3 files changed

+185
-132
lines changed

src/ecdsa/ellipticcurve.py

Lines changed: 69 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@
4949

5050
from six import python_2_unicode_compatible
5151
from . import numbertheory
52-
from ._rwlock import RWLock
5352

5453

5554
@python_2_unicode_compatible
@@ -167,88 +166,64 @@ def __init__(self, curve, x, y, z, order=None, generator=False):
167166
cause to precompute multiplication table generation for it
168167
"""
169168
self.__curve = curve
170-
# since it's generally better (faster) to use scaled points vs unscaled
171-
# ones, use writer-biased RWLock for locking:
172-
self._update_lock = RWLock()
173169
if GMPY: # pragma: no branch
174-
self.__x = mpz(x)
175-
self.__y = mpz(y)
176-
self.__z = mpz(z)
170+
self.__coords = (mpz(x), mpz(y), mpz(z))
177171
self.__order = order and mpz(order)
178172
else: # pragma: no branch
179-
self.__x = x
180-
self.__y = y
181-
self.__z = z
173+
self.__coords = (x, y, z)
182174
self.__order = order
183175
self.__generator = generator
184176
self.__precompute = []
185177

186178
def _maybe_precompute(self):
187-
if self.__generator:
188-
# since we lack promotion of read-locks to write-locks, we do a
189-
# "acquire-read-lock, check, acquire-write-lock plus recheck" cycle
190-
try:
191-
self._update_lock.reader_acquire()
192-
if self.__precompute:
193-
return
194-
finally:
195-
self._update_lock.reader_release()
196-
197-
try:
198-
self._update_lock.writer_acquire()
199-
if self.__precompute:
200-
return
201-
order = self.__order
202-
assert order
203-
i = 1
204-
order *= 2
205-
doubler = PointJacobi(
206-
self.__curve, self.__x, self.__y, self.__z, order
207-
)
208-
order *= 2
209-
self.__precompute.append((doubler.x(), doubler.y()))
210-
211-
while i < order:
212-
i *= 2
213-
doubler = doubler.double().scale()
214-
self.__precompute.append((doubler.x(), doubler.y()))
215-
216-
finally:
217-
self._update_lock.writer_release()
179+
if not self.__generator or self.__precompute:
180+
return
181+
182+
# since this code will execute just once, and it's fully deterministic,
183+
# depend on atomicity of the last assignment to switch from empty
184+
# self.__precompute to filled one and just ignore the unlikely
185+
# situation when two threads execute it at the same time (as it won't
186+
# lead to inconsistent __precompute)
187+
order = self.__order
188+
assert order
189+
precompute = []
190+
i = 1
191+
order *= 2
192+
coord_x, coord_y, coord_z = self.__coords
193+
doubler = PointJacobi(self.__curve, coord_x, coord_y, coord_z, order)
194+
order *= 2
195+
precompute.append((doubler.x(), doubler.y()))
196+
197+
while i < order:
198+
i *= 2
199+
doubler = doubler.double().scale()
200+
precompute.append((doubler.x(), doubler.y()))
201+
202+
self.__precompute = precompute
218203

219204
def __getstate__(self):
220-
try:
221-
self._update_lock.reader_acquire()
222-
state = self.__dict__.copy()
223-
finally:
224-
self._update_lock.reader_release()
225-
del state["_update_lock"]
205+
# while this code can execute at the same time as _maybe_precompute()
206+
# is updating the __precompute or scale() is updating the __coords,
207+
# there is no requirement for consistency between __coords and
208+
# __precompute
209+
state = self.__dict__.copy()
226210
return state
227211

228212
def __setstate__(self, state):
229213
self.__dict__.update(state)
230-
self._update_lock = RWLock()
231214

232215
def __eq__(self, other):
233216
"""Compare for equality two points with each-other.
234217
235218
Note: only points that lay on the same curve can be equal.
236219
"""
237-
try:
238-
self._update_lock.reader_acquire()
239-
if other is INFINITY:
240-
return not self.__y or not self.__z
241-
x1, y1, z1 = self.__x, self.__y, self.__z
242-
finally:
243-
self._update_lock.reader_release()
220+
x1, y1, z1 = self.__coords
221+
if other is INFINITY:
222+
return not y1 or not z1
244223
if isinstance(other, Point):
245224
x2, y2, z2 = other.x(), other.y(), 1
246225
elif isinstance(other, PointJacobi):
247-
try:
248-
other._update_lock.reader_acquire()
249-
x2, y2, z2 = other.__x, other.__y, other.__z
250-
finally:
251-
other._update_lock.reader_release()
226+
x2, y2, z2 = other.__coords
252227
else:
253228
return NotImplemented
254229
if self.__curve != other.curve():
@@ -289,14 +264,9 @@ def x(self):
289264
call x() and y() on the returned instance. Or call `scale()`
290265
and then x() and y() on the returned instance.
291266
"""
292-
try:
293-
self._update_lock.reader_acquire()
294-
if self.__z == 1:
295-
return self.__x
296-
x = self.__x
297-
z = self.__z
298-
finally:
299-
self._update_lock.reader_release()
267+
x, _, z = self.__coords
268+
if z == 1:
269+
return x
300270
p = self.__curve.p()
301271
z = numbertheory.inverse_mod(z, p)
302272
return x * z ** 2 % p
@@ -310,14 +280,9 @@ def y(self):
310280
call x() and y() on the returned instance. Or call `scale()`
311281
and then x() and y() on the returned instance.
312282
"""
313-
try:
314-
self._update_lock.reader_acquire()
315-
if self.__z == 1:
316-
return self.__y
317-
y = self.__y
318-
z = self.__z
319-
finally:
320-
self._update_lock.reader_release()
283+
_, y, z = self.__coords
284+
if z == 1:
285+
return y
321286
p = self.__curve.p()
322287
z = numbertheory.inverse_mod(z, p)
323288
return y * z ** 3 % p
@@ -328,37 +293,28 @@ def scale(self):
328293
329294
Modifies point in place, returns self.
330295
"""
331-
try:
332-
self._update_lock.reader_acquire()
333-
if self.__z == 1:
334-
return self
335-
finally:
336-
self._update_lock.reader_release()
337-
338-
try:
339-
self._update_lock.writer_acquire()
340-
# scaling already scaled point is safe (as inverse of 1 is 1) and
341-
# quick so we don't need to optimise for the unlikely event when
342-
# two threads hit the lock at the same time
343-
p = self.__curve.p()
344-
z_inv = numbertheory.inverse_mod(self.__z, p)
345-
zz_inv = z_inv * z_inv % p
346-
self.__x = self.__x * zz_inv % p
347-
self.__y = self.__y * zz_inv * z_inv % p
348-
# we are setting the z last so that the check above will return
349-
# true only after all values were already updated
350-
self.__z = 1
351-
finally:
352-
self._update_lock.writer_release()
296+
x, y, z = self.__coords
297+
if z == 1:
298+
return self
299+
300+
# scaling is deterministic, so even if two threads execute the below
301+
# code at the same time, they will set __coords to the same value
302+
p = self.__curve.p()
303+
z_inv = numbertheory.inverse_mod(z, p)
304+
zz_inv = z_inv * z_inv % p
305+
x = x * zz_inv % p
306+
y = y * zz_inv * z_inv % p
307+
self.__coords = (x, y, 1)
353308
return self
354309

355310
def to_affine(self):
356311
"""Return point in affine form."""
357-
if not self.__y or not self.__z:
312+
_, y, z = self.__coords
313+
if not y or not z:
358314
return INFINITY
359315
self.scale()
360-
# after point is scaled, it's immutable, so no need to perform locking
361-
return Point(self.__curve, self.__x, self.__y, self.__order)
316+
x, y, z = self.__coords
317+
return Point(self.__curve, x, y, self.__order)
362318

363319
@staticmethod
364320
def from_affine(point, generator=False):
@@ -423,17 +379,13 @@ def _double(self, X1, Y1, Z1, p, a):
423379

424380
def double(self):
425381
"""Add a point to itself."""
426-
if not self.__y:
382+
X1, Y1, Z1 = self.__coords
383+
384+
if not Y1:
427385
return INFINITY
428386

429387
p, a = self.__curve.p(), self.__curve.a()
430388

431-
try:
432-
self._update_lock.reader_acquire()
433-
X1, Y1, Z1 = self.__x, self.__y, self.__z
434-
finally:
435-
self._update_lock.reader_release()
436-
437389
X3, Y3, Z3 = self._double(X1, Y1, Z1, p, a)
438390

439391
if not Y3 or not Z3:
@@ -546,16 +498,9 @@ def __add__(self, other):
546498
raise ValueError("The other point is on different curve")
547499

548500
p = self.__curve.p()
549-
try:
550-
self._update_lock.reader_acquire()
551-
X1, Y1, Z1 = self.__x, self.__y, self.__z
552-
finally:
553-
self._update_lock.reader_release()
554-
try:
555-
other._update_lock.reader_acquire()
556-
X2, Y2, Z2 = other.__x, other.__y, other.__z
557-
finally:
558-
other._update_lock.reader_release()
501+
X1, Y1, Z1 = self.__coords
502+
X2, Y2, Z2 = other.__coords
503+
559504
X3, Y3, Z3 = self._add(X1, Y1, Z1, X2, Y2, Z2, p)
560505

561506
if not Y3 or not Z3:
@@ -603,7 +548,7 @@ def _naf(mult):
603548

604549
def __mul__(self, other):
605550
"""Multiply point by an integer."""
606-
if not self.__y or not other:
551+
if not self.__coords[1] or not other:
607552
return INFINITY
608553
if other == 1:
609554
return self
@@ -615,8 +560,7 @@ def __mul__(self, other):
615560
return self._mul_precompute(other)
616561

617562
self = self.scale()
618-
# once scaled, point is immutable, not need to lock
619-
X2, Y2 = self.__x, self.__y
563+
X2, Y2, _ = self.__coords
620564
X3, Y3, Z3 = 0, 0, 1
621565
p, a = self.__curve.p(), self.__curve.a()
622566
_double = self._double
@@ -664,11 +608,10 @@ def mul_add(self, self_mul, other, other_mul):
664608

665609
# as we have 6 unique points to work with, we can't scale all of them,
666610
# but do scale the ones that are used most often
667-
# (post scale() points are immutable so no need for locking)
668611
self.scale()
669-
X1, Y1, Z1 = self.__x, self.__y, self.__z
612+
X1, Y1, Z1 = self.__coords
670613
other.scale()
671-
X2, Y2, Z2 = other.__x, other.__y, other.__z
614+
X2, Y2, Z2 = other.__coords
672615

673616
_double = self._double
674617
_add = self._add
@@ -736,13 +679,8 @@ def mul_add(self, self_mul, other, other_mul):
736679

737680
def __neg__(self):
738681
"""Return negated point."""
739-
try:
740-
self._update_lock.reader_acquire()
741-
return PointJacobi(
742-
self.__curve, self.__x, -self.__y, self.__z, self.__order
743-
)
744-
finally:
745-
self._update_lock.reader_release()
682+
x, y, z = self.__coords
683+
return PointJacobi(self.__curve, x, -y, z, self.__order)
746684

747685

748686
class Point(object):

0 commit comments

Comments
 (0)