Skip to content
This repository was archived by the owner on Jan 13, 2023. It is now read-only.

Commit 59bde1b

Browse files
authored
Merge pull request #36 from iotaledger/release/1.1.2
1.1.2
2 parents 93f67cf + ffcbf18 commit 59bde1b

File tree

4 files changed

+126
-22
lines changed

4 files changed

+126
-22
lines changed

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
name = 'PyOTA',
2020
description = 'IOTA API library for Python',
2121
url = 'https://github.com/iotaledger/iota.lib.py',
22-
version = '1.1.1',
22+
version = '1.1.2',
2323

2424
packages = find_packages('src'),
2525
include_package_data = True,

src/iota/api.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -822,8 +822,8 @@ def send_transfer(
822822
minWeightMagnitude = min_weight_magnitude,
823823
)
824824

825-
def send_trytes(self, trytes, depth, min_weight_magnitude=18):
826-
# type: (Iterable[TransactionTrytes], int, int) -> dict
825+
def send_trytes(self, trytes, depth, min_weight_magnitude=None):
826+
# type: (Iterable[TransactionTrytes], int, Optional[int]) -> dict
827827
"""
828828
Attaches transaction trytes to the Tangle, then broadcasts and
829829
stores them.
@@ -851,6 +851,9 @@ def send_trytes(self, trytes, depth, min_weight_magnitude=18):
851851
References:
852852
- https://github.com/iotaledger/wiki/blob/master/api-proposal.md#sendtrytes
853853
"""
854+
if min_weight_magnitude is None:
855+
min_weight_magnitude = self.default_min_weight_magnitude
856+
854857
return extended.SendTrytesCommand(self.adapter)(
855858
trytes = trytes,
856859
depth = depth,

src/iota/crypto/addresses.py

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
import hashlib
66
from abc import ABCMeta, abstractmethod as abstract_method
7-
from typing import Dict, Generator, Iterable, List, MutableSequence, Optional
7+
from contextlib import contextmanager as context_manager
8+
from threading import Lock
9+
from typing import Dict, Generator, Iterable, List, MutableSequence, \
10+
Optional, Tuple
811

912
from iota import Address, TRITS_PER_TRYTE, TrytesCompatible
1013
from iota.crypto import Curl
@@ -23,6 +26,19 @@ class BaseAddressCache(with_metaclass(ABCMeta)):
2326
"""
2427
Base functionality for classes that cache generated addresses.
2528
"""
29+
LockType = Lock
30+
"""
31+
The type of locking mechanism used by :py:meth:`acquire_lock`.
32+
33+
Defaults to ``threading.Lock``, but you can change it if you want to
34+
use a different mechanism (e.g., multithreading or distributed).
35+
"""
36+
37+
def __init__(self):
38+
super(BaseAddressCache, self).__init__()
39+
40+
self._lock = self.LockType()
41+
2642
@abstract_method
2743
def get(self, seed, index):
2844
# type: (Seed, int) -> Optional[Address]
@@ -34,6 +50,18 @@ def get(self, seed, index):
3450
'Not implemented in {cls}.'.format(cls=type(self).__name__),
3551
)
3652

53+
@context_manager
54+
def acquire_lock(self):
55+
"""
56+
Acquires a lock on the cache instance, to prevent invalid cache
57+
misses when multiple threads access the cache concurrently.
58+
59+
Note: Acquire lock before checking the cache, and do not release it
60+
until after the cache hit/miss is resolved.
61+
"""
62+
with self._lock:
63+
yield
64+
3765
@abstract_method
3866
def set(self, seed, index, address):
3967
# type: (Seed, int, Address) -> None
@@ -45,6 +73,17 @@ def set(self, seed, index, address):
4573
'Not implemented in {cls}.'.format(cls=type(self).__name__),
4674
)
4775

76+
@staticmethod
77+
def _gen_cache_key(seed, index):
78+
# type: (Seed, int) -> binary_type
79+
"""
80+
Generates an obfuscated cache key so that we're not storing seeds
81+
in cleartext.
82+
"""
83+
h = hashlib.new('sha256')
84+
h.update(binary_type(seed) + b':' + binary_type(index))
85+
return h.digest()
86+
4887

4988
class MemoryAddressCache(BaseAddressCache):
5089
"""
@@ -63,17 +102,6 @@ def set(self, seed, index, address):
63102
# type: (Seed, int, Address) -> None
64103
self.cache[self._gen_cache_key(seed, index)] = address
65104

66-
@staticmethod
67-
def _gen_cache_key(seed, index):
68-
# type: (Seed, int) -> binary_type
69-
"""
70-
Generates an obfuscated cache key so that we're not storing seeds
71-
in cleartext.
72-
"""
73-
h = hashlib.new('sha256')
74-
h.update(binary_type(seed) + b':' + binary_type(index))
75-
return h.digest()
76-
77105

78106
class AddressGenerator(Iterable[Address]):
79107
"""
@@ -213,18 +241,19 @@ def create_iterator(self, start=0, step=1):
213241

214242
while True:
215243
if self.cache:
216-
address = self.cache.get(self.seed, key_iterator.current)
244+
with self.cache.acquire_lock():
245+
address = self.cache.get(self.seed, key_iterator.current)
217246

218-
if not address:
219-
address = self._generate_address(key_iterator)
220-
self.cache.set(self.seed, address.key_index, address)
247+
if not address:
248+
address = self._generate_address(key_iterator)
249+
self.cache.set(self.seed, address.key_index, address)
221250
else:
222251
address = self._generate_address(key_iterator)
223252

224253
yield address
225254

226255
@staticmethod
227-
def address_from_digest(digest_trits, key_index):
256+
def address_from_digest_trits(digest_trits, key_index):
228257
# type: (List[int], int) -> Address
229258
"""
230259
Generates an address from a private key digest.
@@ -247,13 +276,13 @@ def _generate_address(self, key_iterator):
247276
248277
Used in the event of a cache miss.
249278
"""
250-
return self.address_from_digest(*self._get_digest_params(key_iterator))
279+
return self.address_from_digest_trits(*self._get_digest_params(key_iterator))
251280

252281
@staticmethod
253282
def _get_digest_params(key_iterator):
254283
# type: (KeyIterator) -> Tuple[List[int], int]
255284
"""
256-
Extracts parameters for :py:meth:`address_from_digest`.
285+
Extracts parameters for :py:meth:`address_from_digest_trits`.
257286
258287
Split into a separate method so that it can be mocked during unit
259288
tests.

test/crypto/addresses_test.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from __future__ import absolute_import, division, print_function, \
33
unicode_literals
44

5+
from threading import Thread
6+
from time import sleep
57
from typing import List, Tuple
68
from unittest import TestCase
79

@@ -44,6 +46,26 @@ def setUp(self):
4446
b'CFANWBQFGMFKITZBJDSYLGXYUIQVCMXFWSWFRNHRV'
4547
)
4648

49+
# noinspection SpellCheckingInspection
50+
def test_address_from_digest(self):
51+
"""
52+
Generating an address from a private key digest.
53+
"""
54+
digest =\
55+
Hash(
56+
b'ABQXVJNER9MPMXMBPNMFBMDGTXRWSYHNZKGAGUOI'
57+
b'JKOJGZVGHCUXXGFZEMMGDSGWDCKJXO9ILLFAKGGZE'
58+
)
59+
60+
self.assertEqual(
61+
AddressGenerator.address_from_digest_trits(digest.as_trits(), 0),
62+
63+
Address(
64+
b'QLOEDSBXXOLLUJYLEGKEPYDRIJJTPIMEPKMFHUVJ'
65+
b'MPMLYYCLPQPANEVDSERQWPVNHCAXYRLAYMBHJLWWR'
66+
),
67+
)
68+
4769
def test_get_addresses_single(self):
4870
"""
4971
Generating a single address.
@@ -329,3 +351,53 @@ def test_cache_miss_seed(self):
329351
generator2 = AddressGenerator(Seed.random())
330352
generator2.get_addresses(42)
331353
self.assertEqual(mock_generate_address.call_count, 2)
354+
355+
def test_thread_safety(self):
356+
"""
357+
Address cache is thread-safe, eliminating invalid cache misses when
358+
multiple threads attempt to access the cache concurrently.
359+
"""
360+
AddressGenerator.cache = MemoryAddressCache()
361+
362+
seed = Seed.random()
363+
364+
generated = []
365+
366+
def get_address():
367+
generator = AddressGenerator(seed)
368+
generated.extend(generator.get_addresses(0))
369+
370+
# noinspection PyUnusedLocal
371+
def mock_generate_address(address_generator, key_iterator):
372+
# type: (AddressGenerator, KeyIterator) -> Address
373+
# Insert a teensy delay, to make it more likely that multiple
374+
# threads hit the cache concurrently.
375+
sleep(0.01)
376+
377+
# Note that in this test, the address generator always returns a
378+
# new instance.
379+
return Address(self.addy, key_index=key_iterator.current)
380+
381+
with patch(
382+
'iota.crypto.addresses.AddressGenerator._generate_address',
383+
mock_generate_address,
384+
):
385+
threads = [Thread(target=get_address) for _ in range(100)]
386+
387+
for t in threads:
388+
t.start()
389+
390+
for t in threads:
391+
t.join()
392+
393+
# Quick sanity check.
394+
self.assertEqual(len(generated), len(threads))
395+
396+
# If the cache is operating in a thread-safe manner, then it will
397+
# always return the exact same instance, given the same seed and
398+
# key index.
399+
expected = generated[0]
400+
for actual in generated[1:]:
401+
# Compare `id` values instead of using ``self.assertIs`` because
402+
# the failure message is a bit easier to understand.
403+
self.assertEqual(id(actual), id(expected))

0 commit comments

Comments
 (0)