keystore: fix memory leak for LRU cache
Using `@functools.lru_cache` on an instance method behaves in interesting ways. The cache kept a ref around for `self`, so in effect we were never GC-ing keystore objects. Effectively there was a single global cache for derive_pubkey, with keys `(keystore, for_change, n)`. This PR now changes the caching to be per-keystore: each ks has a cache, keyed `(for_change, n)`. GC-ing individual keystores should now be possible, which should result in cleaning up just their own cache. This also enables the corresponding previously silence flake8-bugbear check for `@functools.lru_cache`. (note that the check can selectively be disabled by adding a comment on the relevant line: `# noqa: B019`)
This commit is contained in:
@@ -225,7 +225,7 @@ task:
|
|||||||
# - https://pycodestyle.pycqa.org/en/latest/intro.html#error-codes
|
# - https://pycodestyle.pycqa.org/en/latest/intro.html#error-codes
|
||||||
# - https://github.com/PyCQA/flake8-bugbear/tree/8c0e7eb04217494d48d0ab093bf5b31db0921989#list-of-warnings
|
# - https://github.com/PyCQA/flake8-bugbear/tree/8c0e7eb04217494d48d0ab093bf5b31db0921989#list-of-warnings
|
||||||
ELECTRUM_LINTERS: E9,E101,E129,E273,E274,E703,E71,E722,F5,F6,F7,F8,W191,W29,B,B909
|
ELECTRUM_LINTERS: E9,E101,E129,E273,E274,E703,E71,E722,F5,F6,F7,F8,W191,W29,B,B909
|
||||||
ELECTRUM_LINTERS_IGNORE: B007,B009,B010,B019,B036,B042,F541,F841
|
ELECTRUM_LINTERS_IGNORE: B007,B009,B010,B036,B042,F541,F841
|
||||||
- name: "linter: Flake8 Non-Mandatory"
|
- name: "linter: Flake8 Non-Mandatory"
|
||||||
env:
|
env:
|
||||||
ELECTRUM_LINTERS: E,F,W,C90,B
|
ELECTRUM_LINTERS: E,F,W,C90,B
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import hashlib
|
|||||||
import re
|
import re
|
||||||
import copy
|
import copy
|
||||||
from typing import Tuple, TYPE_CHECKING, Union, Sequence, Optional, Dict, List, NamedTuple, Any, Type
|
from typing import Tuple, TYPE_CHECKING, Union, Sequence, Optional, Dict, List, NamedTuple, Any, Type
|
||||||
from functools import lru_cache, wraps
|
from functools import wraps
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
import electrum_ecc as ecc
|
import electrum_ecc as ecc
|
||||||
@@ -52,6 +52,7 @@ from .util import (InvalidPassword, WalletFileException,
|
|||||||
from .mnemonic import Mnemonic, Wordlist, calc_seed_type, is_seed
|
from .mnemonic import Mnemonic, Wordlist, calc_seed_type, is_seed
|
||||||
from .plugin import run_hook
|
from .plugin import run_hook
|
||||||
from .logging import Logger
|
from .logging import Logger
|
||||||
|
from .lrucache import LRUCache
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .gui.common_qt.util import TaskThread
|
from .gui.common_qt.util import TaskThread
|
||||||
@@ -398,6 +399,9 @@ class Deterministic_KeyStore(Software_KeyStore):
|
|||||||
|
|
||||||
class MasterPublicKeyMixin(ABC):
|
class MasterPublicKeyMixin(ABC):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._pubkey_cache = LRUCache(maxsize=10**4) # type: LRUCache[Sequence[int], bytes] # path->pubkey
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_master_public_key(self) -> str:
|
def get_master_public_key(self) -> str:
|
||||||
pass
|
pass
|
||||||
@@ -434,8 +438,14 @@ class MasterPublicKeyMixin(ABC):
|
|||||||
def get_key_origin_info(self) -> Optional[KeyOriginInfo]:
|
def get_key_origin_info(self) -> Optional[KeyOriginInfo]:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def derive_pubkey(self, for_change: int, n: int) -> bytes:
|
def derive_pubkey(self, for_change: int, n: int) -> bytes:
|
||||||
|
key = (for_change, n)
|
||||||
|
if key not in self._pubkey_cache:
|
||||||
|
self._pubkey_cache[key] = self._derive_pubkey(*key)
|
||||||
|
return self._pubkey_cache[key]
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _derive_pubkey(self, for_change: int, n: int) -> bytes:
|
||||||
"""Returns pubkey at given path.
|
"""Returns pubkey at given path.
|
||||||
May raise CannotDerivePubkey.
|
May raise CannotDerivePubkey.
|
||||||
"""
|
"""
|
||||||
@@ -501,6 +511,7 @@ class MasterPublicKeyMixin(ABC):
|
|||||||
class Xpub(MasterPublicKeyMixin):
|
class Xpub(MasterPublicKeyMixin):
|
||||||
|
|
||||||
def __init__(self, *, derivation_prefix: str = None, root_fingerprint: str = None):
|
def __init__(self, *, derivation_prefix: str = None, root_fingerprint: str = None):
|
||||||
|
MasterPublicKeyMixin.__init__(self)
|
||||||
self.xpub = None
|
self.xpub = None
|
||||||
self.xpub_receive = None
|
self.xpub_receive = None
|
||||||
self.xpub_change = None
|
self.xpub_change = None
|
||||||
@@ -608,8 +619,7 @@ class Xpub(MasterPublicKeyMixin):
|
|||||||
self._derivation_prefix = derivation_prefix
|
self._derivation_prefix = derivation_prefix
|
||||||
self.is_requesting_to_be_rewritten_to_wallet_file = True
|
self.is_requesting_to_be_rewritten_to_wallet_file = True
|
||||||
|
|
||||||
@lru_cache(maxsize=None)
|
def _derive_pubkey(self, for_change: int, n: int) -> bytes:
|
||||||
def derive_pubkey(self, for_change: int, n: int) -> bytes:
|
|
||||||
for_change = int(for_change)
|
for_change = int(for_change)
|
||||||
if for_change not in (0, 1):
|
if for_change not in (0, 1):
|
||||||
raise CannotDerivePubkey("forbidden path")
|
raise CannotDerivePubkey("forbidden path")
|
||||||
@@ -624,7 +634,7 @@ class Xpub(MasterPublicKeyMixin):
|
|||||||
return self.get_pubkey_from_xpub(xpub, (n,))
|
return self.get_pubkey_from_xpub(xpub, (n,))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_pubkey_from_xpub(self, xpub: str, sequence) -> bytes:
|
def get_pubkey_from_xpub(cls, xpub: str, sequence) -> bytes:
|
||||||
node = BIP32Node.from_xkey(xpub).subkey_at_public_derivation(sequence)
|
node = BIP32Node.from_xkey(xpub).subkey_at_public_derivation(sequence)
|
||||||
return node.eckey.get_public_key_bytes(compressed=True)
|
return node.eckey.get_public_key_bytes(compressed=True)
|
||||||
|
|
||||||
@@ -729,6 +739,7 @@ class Old_KeyStore(MasterPublicKeyMixin, Deterministic_KeyStore):
|
|||||||
type = 'old'
|
type = 'old'
|
||||||
|
|
||||||
def __init__(self, d: dict):
|
def __init__(self, d: dict):
|
||||||
|
MasterPublicKeyMixin.__init__(self)
|
||||||
Deterministic_KeyStore.__init__(self, d)
|
Deterministic_KeyStore.__init__(self, d)
|
||||||
self.mpk = d.get('mpk') # type: Optional[str]
|
self.mpk = d.get('mpk') # type: Optional[str]
|
||||||
self._root_fingerprint = None
|
self._root_fingerprint = None
|
||||||
@@ -807,8 +818,7 @@ class Old_KeyStore(MasterPublicKeyMixin, Deterministic_KeyStore):
|
|||||||
public_key = master_public_key + z*ecc.GENERATOR
|
public_key = master_public_key + z*ecc.GENERATOR
|
||||||
return public_key.get_public_key_bytes(compressed=False)
|
return public_key.get_public_key_bytes(compressed=False)
|
||||||
|
|
||||||
@lru_cache(maxsize=None)
|
def _derive_pubkey(self, for_change, n) -> bytes:
|
||||||
def derive_pubkey(self, for_change, n) -> bytes:
|
|
||||||
for_change = int(for_change)
|
for_change = int(for_change)
|
||||||
if for_change not in (0, 1):
|
if for_change not in (0, 1):
|
||||||
raise CannotDerivePubkey("forbidden path")
|
raise CannotDerivePubkey("forbidden path")
|
||||||
|
|||||||
Reference in New Issue
Block a user