reduced crypto dependency in wallet to coincurve

This commit is contained in:
Lex Berezhny 2021-12-19 16:07:01 -05:00
parent fb57cfa5d8
commit 1eaa195363
12 changed files with 251 additions and 145 deletions

View file

@ -17,7 +17,6 @@ from binascii import hexlify, unhexlify
from traceback import format_exc from traceback import format_exc
from functools import wraps, partial from functools import wraps, partial
import ecdsa
import base58 import base58
from aiohttp import web from aiohttp import web
from prometheus_client import generate_latest as prom_generate_latest, Gauge, Histogram, Counter from prometheus_client import generate_latest as prom_generate_latest, Gauge, Histogram, Counter
@ -29,6 +28,7 @@ from lbry.wallet import (
) )
from lbry.wallet.dewies import dewies_to_lbc, lbc_to_dewies, dict_values_to_lbc from lbry.wallet.dewies import dewies_to_lbc, lbc_to_dewies, dict_values_to_lbc
from lbry.wallet.constants import TXO_TYPES, CLAIM_TYPE_NAMES from lbry.wallet.constants import TXO_TYPES, CLAIM_TYPE_NAMES
from lbry.wallet.bip32 import PrivateKey
from lbry import utils from lbry import utils
from lbry.conf import Config, Setting, NOT_SET from lbry.conf import Config, Setting, NOT_SET
@ -3041,7 +3041,7 @@ class Daemon(metaclass=JSONRPCServerType):
'channel_id': channel.claim_id, 'channel_id': channel.claim_id,
'holding_address': address, 'holding_address': address,
'holding_public_key': public_key.extended_key_string(), 'holding_public_key': public_key.extended_key_string(),
'signing_private_key': channel.private_key.to_pem().decode() 'signing_private_key': channel.private_key.signing_key.to_pem().decode()
} }
return base58.b58encode(json.dumps(export, separators=(',', ':'))) return base58.b58encode(json.dumps(export, separators=(',', ':')))
@ -3064,15 +3064,14 @@ class Daemon(metaclass=JSONRPCServerType):
decoded = base58.b58decode(channel_data) decoded = base58.b58decode(channel_data)
data = json.loads(decoded) data = json.loads(decoded)
channel_private_key = ecdsa.SigningKey.from_pem( channel_private_key = PrivateKey.from_pem(
data['signing_private_key'], hashfunc=hashlib.sha256 self.ledger, data['signing_private_key']
) )
public_key_der = channel_private_key.get_verifying_key().to_der()
# check that the holding_address hasn't changed since the export was made # check that the holding_address hasn't changed since the export was made
holding_address = data['holding_address'] holding_address = data['holding_address']
channels, _, _, _ = await self.ledger.claim_search( channels, _, _, _ = await self.ledger.claim_search(
wallet.accounts, public_key_id=self.ledger.public_key_to_address(public_key_der) wallet.accounts, public_key_id=channel_private_key.address
) )
if channels and channels[0].get_address(self.ledger) != holding_address: if channels and channels[0].get_address(self.ledger) != holding_address:
holding_address = channels[0].get_address(self.ledger) holding_address = channels[0].get_address(self.ledger)

View file

@ -10,7 +10,7 @@ from lbry.schema.claim import Claim
from lbry.schema.support import Support from lbry.schema.support import Support
from lbry.torrent.torrent_manager import TorrentSource from lbry.torrent.torrent_manager import TorrentSource
from lbry.wallet import Wallet, Ledger, Account, Transaction, Output from lbry.wallet import Wallet, Ledger, Account, Transaction, Output
from lbry.wallet.bip32 import PubKey from lbry.wallet.bip32 import PublicKey
from lbry.wallet.dewies import dewies_to_lbc from lbry.wallet.dewies import dewies_to_lbc
from lbry.stream.managed_stream import ManagedStream from lbry.stream.managed_stream import ManagedStream
@ -138,7 +138,7 @@ class JSONResponseEncoder(JSONEncoder):
return self.encode_claim(obj) return self.encode_claim(obj)
if isinstance(obj, Support): if isinstance(obj, Support):
return obj.to_dict() return obj.to_dict()
if isinstance(obj, PubKey): if isinstance(obj, PublicKey):
return obj.extended_key_string() return obj.extended_key_string()
if isinstance(obj, datetime): if isinstance(obj, datetime):
return obj.strftime("%Y%m%dT%H:%M:%S") return obj.strftime("%Y%m%dT%H:%M:%S")

View file

@ -2,6 +2,9 @@ import logging
from typing import List from typing import List
from binascii import hexlify, unhexlify from binascii import hexlify, unhexlify
from asn1crypto.keys import PublicKeyInfo
from coincurve import PublicKey as cPublicKey
from google.protobuf.json_format import MessageToDict from google.protobuf.json_format import MessageToDict
from google.protobuf.message import DecodeError from google.protobuf.message import DecodeError
from hachoir.core.log import log as hachoir_log from hachoir.core.log import log as hachoir_log
@ -346,7 +349,7 @@ class Channel(BaseClaim):
@property @property
def public_key(self) -> str: def public_key(self) -> str:
return hexlify(self.message.public_key).decode() return hexlify(self.public_key_bytes).decode()
@public_key.setter @public_key.setter
def public_key(self, sd_public_key: str): def public_key(self, sd_public_key: str):
@ -354,7 +357,11 @@ class Channel(BaseClaim):
@property @property
def public_key_bytes(self) -> bytes: def public_key_bytes(self) -> bytes:
return self.message.public_key if len(self.message.public_key) == 33:
return self.message.public_key
public_key_info = PublicKeyInfo.load(self.message.public_key)
public_key = cPublicKey(public_key_info.native['public_key'])
return public_key.format(compressed=True)
@public_key_bytes.setter @public_key_bytes.setter
def public_key_bytes(self, public_key: bytes): def public_key_bytes(self, public_key: bytes):

View file

@ -9,11 +9,10 @@ from hashlib import sha256
from string import hexdigits from string import hexdigits
from typing import Type, Dict, Tuple, Optional, Any, List from typing import Type, Dict, Tuple, Optional, Any, List
import ecdsa
from lbry.error import InvalidPasswordError from lbry.error import InvalidPasswordError
from lbry.crypto.crypt import aes_encrypt, aes_decrypt from lbry.crypto.crypt import aes_encrypt, aes_decrypt
from .bip32 import PrivateKey, PubKey, from_extended_key_string from .bip32 import PrivateKey, PublicKey, KeyPath, from_extended_key_string
from .mnemonic import Mnemonic from .mnemonic import Mnemonic
from .constants import COIN, TXO_TYPES from .constants import COIN, TXO_TYPES
from .transaction import Transaction, Input, Output from .transaction import Transaction, Input, Output
@ -36,44 +35,39 @@ def validate_claim_id(claim_id):
class DeterministicChannelKeyManager: class DeterministicChannelKeyManager:
def __init__(self, account): def __init__(self, account: 'Account'):
self.account = account self.account = account
self.public_key = account.public_key.child(2)
self.private_key = account.private_key.child(2) if account.private_key else None
self.last_known = 0 self.last_known = 0
self.cache = {} self.cache = {}
self.private_key: Optional[PrivateKey] = None
if account.private_key is not None:
self.private_key = account.private_key.child(KeyPath.CHANNEL)
def maybe_generate_deterministic_key_for_channel(self, txo): def maybe_generate_deterministic_key_for_channel(self, txo):
if self.private_key is None: if self.private_key is None:
return return
next_key = self.private_key.child(self.last_known) next_private_key = self.private_key.child(self.last_known)
signing_key = ecdsa.SigningKey.from_secret_exponent( public_key = next_private_key.public_key
next_key.secret_exponent(), ecdsa.SECP256k1 public_key_bytes = public_key.pubkey_bytes
)
public_key_bytes = signing_key.get_verifying_key().to_der()
if txo.claim.channel.public_key_bytes == public_key_bytes: if txo.claim.channel.public_key_bytes == public_key_bytes:
self.cache[self.account.ledger.public_key_to_address(public_key_bytes)] = signing_key self.cache[public_key.address] = next_private_key
self.last_known += 1 self.last_known += 1
async def ensure_cache_primed(self): async def ensure_cache_primed(self):
if self.private_key is not None: if self.private_key is not None:
await self.generate_next_key() await self.generate_next_key()
async def generate_next_key(self) -> ecdsa.SigningKey: async def generate_next_key(self) -> PrivateKey:
db = self.account.ledger.db db = self.account.ledger.db
while True: while True:
next_key = self.private_key.child(self.last_known) next_private_key = self.private_key.child(self.last_known)
signing_key = ecdsa.SigningKey.from_secret_exponent( public_key = next_private_key.public_key
next_key.secret_exponent(), ecdsa.SECP256k1 self.cache[public_key.address] = next_private_key
) if not await db.is_channel_key_used(self.account, public_key):
public_key_bytes = signing_key.get_verifying_key().to_der() return next_private_key
key_address = self.account.ledger.public_key_to_address(public_key_bytes)
self.cache[key_address] = signing_key
if not await db.is_channel_key_used(self.account.wallet, signing_key):
return signing_key
self.last_known += 1 self.last_known += 1
def get_private_key_from_pubkey_hash(self, pubkey_hash) -> ecdsa.SigningKey: def get_private_key_from_pubkey_hash(self, pubkey_hash) -> PrivateKey:
return self.cache.get(pubkey_hash) return self.cache.get(pubkey_hash)
@ -122,7 +116,7 @@ class AddressManager:
def get_private_key(self, index: int) -> PrivateKey: def get_private_key(self, index: int) -> PrivateKey:
raise NotImplementedError raise NotImplementedError
def get_public_key(self, index: int) -> PubKey: def get_public_key(self, index: int) -> PublicKey:
raise NotImplementedError raise NotImplementedError
async def get_max_gap(self): async def get_max_gap(self):
@ -162,8 +156,8 @@ class HierarchicalDeterministic(AddressManager):
@classmethod @classmethod
def from_dict(cls, account: 'Account', d: dict) -> Tuple[AddressManager, AddressManager]: def from_dict(cls, account: 'Account', d: dict) -> Tuple[AddressManager, AddressManager]:
return ( return (
cls(account, 0, **d.get('receiving', {'gap': 20, 'maximum_uses_per_address': 1})), cls(account, KeyPath.RECEIVE, **d.get('receiving', {'gap': 20, 'maximum_uses_per_address': 1})),
cls(account, 1, **d.get('change', {'gap': 6, 'maximum_uses_per_address': 1})) cls(account, KeyPath.CHANGE, **d.get('change', {'gap': 6, 'maximum_uses_per_address': 1}))
) )
def merge(self, d: dict): def merge(self, d: dict):
@ -176,7 +170,7 @@ class HierarchicalDeterministic(AddressManager):
def get_private_key(self, index: int) -> PrivateKey: def get_private_key(self, index: int) -> PrivateKey:
return self.account.private_key.child(self.chain_number).child(index) return self.account.private_key.child(self.chain_number).child(index)
def get_public_key(self, index: int) -> PubKey: def get_public_key(self, index: int) -> PublicKey:
return self.account.public_key.child(self.chain_number).child(index) return self.account.public_key.child(self.chain_number).child(index)
async def get_max_gap(self) -> int: async def get_max_gap(self) -> int:
@ -236,7 +230,7 @@ class SingleKey(AddressManager):
@classmethod @classmethod
def from_dict(cls, account: 'Account', d: dict) \ def from_dict(cls, account: 'Account', d: dict) \
-> Tuple[AddressManager, AddressManager]: -> Tuple[AddressManager, AddressManager]:
same_address_manager = cls(account, account.public_key, 0) same_address_manager = cls(account, account.public_key, KeyPath.RECEIVE)
return same_address_manager, same_address_manager return same_address_manager, same_address_manager
def to_dict_instance(self): def to_dict_instance(self):
@ -245,7 +239,7 @@ class SingleKey(AddressManager):
def get_private_key(self, index: int) -> PrivateKey: def get_private_key(self, index: int) -> PrivateKey:
return self.account.private_key return self.account.private_key
def get_public_key(self, index: int) -> PubKey: def get_public_key(self, index: int) -> PublicKey:
return self.account.public_key return self.account.public_key
async def get_max_gap(self) -> int: async def get_max_gap(self) -> int:
@ -267,9 +261,6 @@ class SingleKey(AddressManager):
class Account: class Account:
mnemonic_class = Mnemonic
private_key_class = PrivateKey
public_key_class = PubKey
address_generators: Dict[str, Type[AddressManager]] = { address_generators: Dict[str, Type[AddressManager]] = {
SingleKey.name: SingleKey, SingleKey.name: SingleKey,
HierarchicalDeterministic.name: HierarchicalDeterministic, HierarchicalDeterministic.name: HierarchicalDeterministic,
@ -277,7 +268,7 @@ class Account:
def __init__(self, ledger: 'Ledger', wallet: 'Wallet', name: str, def __init__(self, ledger: 'Ledger', wallet: 'Wallet', name: str,
seed: str, private_key_string: str, encrypted: bool, seed: str, private_key_string: str, encrypted: bool,
private_key: Optional[PrivateKey], public_key: PubKey, private_key: Optional[PrivateKey], public_key: PublicKey,
address_generator: dict, modified_on: float, channel_keys: dict) -> None: address_generator: dict, modified_on: float, channel_keys: dict) -> None:
self.ledger = ledger self.ledger = ledger
self.wallet = wallet self.wallet = wallet
@ -288,8 +279,8 @@ class Account:
self.private_key_string = private_key_string self.private_key_string = private_key_string
self.init_vectors: Dict[str, bytes] = {} self.init_vectors: Dict[str, bytes] = {}
self.encrypted = encrypted self.encrypted = encrypted
self.private_key = private_key self.private_key: Optional[PrivateKey] = private_key
self.public_key = public_key self.public_key: PublicKey = public_key
generator_name = address_generator.get('name', HierarchicalDeterministic.name) generator_name = address_generator.get('name', HierarchicalDeterministic.name)
self.address_generator = self.address_generators[generator_name] self.address_generator = self.address_generators[generator_name]
self.receiving, self.change = self.address_generator.from_dict(self, address_generator) self.receiving, self.change = self.address_generator.from_dict(self, address_generator)
@ -310,19 +301,19 @@ class Account:
name: str = None, address_generator: dict = None): name: str = None, address_generator: dict = None):
return cls.from_dict(ledger, wallet, { return cls.from_dict(ledger, wallet, {
'name': name, 'name': name,
'seed': cls.mnemonic_class().make_seed(), 'seed': Mnemonic().make_seed(),
'address_generator': address_generator or {} 'address_generator': address_generator or {}
}) })
@classmethod @classmethod
def get_private_key_from_seed(cls, ledger: 'Ledger', seed: str, password: str): def get_private_key_from_seed(cls, ledger: 'Ledger', seed: str, password: str):
return cls.private_key_class.from_seed( return PrivateKey.from_seed(
ledger, cls.mnemonic_class.mnemonic_to_seed(seed, password or 'lbryum') ledger, Mnemonic.mnemonic_to_seed(seed, password or 'lbryum')
) )
@classmethod @classmethod
def keys_from_dict(cls, ledger: 'Ledger', d: dict) \ def keys_from_dict(cls, ledger: 'Ledger', d: dict) \
-> Tuple[str, Optional[PrivateKey], PubKey]: -> Tuple[str, Optional[PrivateKey], PublicKey]:
seed = d.get('seed', '') seed = d.get('seed', '')
private_key_string = d.get('private_key', '') private_key_string = d.get('private_key', '')
private_key = None private_key = None
@ -493,7 +484,7 @@ class Account:
assert not self.encrypted, "Cannot get private key on encrypted wallet account." assert not self.encrypted, "Cannot get private key on encrypted wallet account."
return self.address_managers[chain].get_private_key(index) return self.address_managers[chain].get_private_key(index)
def get_public_key(self, chain: int, index: int) -> PubKey: def get_public_key(self, chain: int, index: int) -> PublicKey:
return self.address_managers[chain].get_public_key(index) return self.address_managers[chain].get_public_key(index)
def get_balance(self, confirmations=0, include_claims=False, read_only=False, **constraints): def get_balance(self, confirmations=0, include_claims=False, read_only=False, **constraints):
@ -567,34 +558,27 @@ class Account:
async def generate_channel_private_key(self): async def generate_channel_private_key(self):
return await self.deterministic_channel_keys.generate_next_key() return await self.deterministic_channel_keys.generate_next_key()
def add_channel_private_key(self, private_key): def add_channel_private_key(self, private_key: PrivateKey):
public_key_bytes = private_key.get_verifying_key().to_der() self.channel_keys[private_key.address] = private_key.to_pem().decode()
channel_pubkey_hash = self.ledger.public_key_to_address(public_key_bytes)
self.channel_keys[channel_pubkey_hash] = private_key.to_pem().decode()
async def get_channel_private_key(self, public_key_bytes) -> ecdsa.SigningKey: async def get_channel_private_key(self, public_key_bytes) -> PrivateKey:
channel_pubkey_hash = self.ledger.public_key_to_address(public_key_bytes) channel_pubkey_hash = self.ledger.public_key_to_address(public_key_bytes)
private_key_pem = self.channel_keys.get(channel_pubkey_hash) private_key_pem = self.channel_keys.get(channel_pubkey_hash)
if private_key_pem: if private_key_pem:
return await asyncio.get_event_loop().run_in_executor( return PrivateKey.from_pem(self.ledger, private_key_pem)
None, ecdsa.SigningKey.from_pem, private_key_pem, sha256
)
return self.deterministic_channel_keys.get_private_key_from_pubkey_hash(channel_pubkey_hash) return self.deterministic_channel_keys.get_private_key_from_pubkey_hash(channel_pubkey_hash)
async def maybe_migrate_certificates(self): async def maybe_migrate_certificates(self):
def to_der(private_key_pem):
return ecdsa.SigningKey.from_pem(private_key_pem, hashfunc=sha256).get_verifying_key().to_der()
if not self.channel_keys: if not self.channel_keys:
return return
channel_keys = {} channel_keys = {}
for private_key_pem in self.channel_keys.values(): for private_key_pem in self.channel_keys.values():
if not isinstance(private_key_pem, str): if not isinstance(private_key_pem, str):
continue continue
if "-----BEGIN EC PRIVATE KEY-----" not in private_key_pem: if not private_key_pem.startswith("-----BEGIN"):
continue continue
public_key_der = await asyncio.get_event_loop().run_in_executor(None, to_der, private_key_pem) private_key = PrivateKey.from_pem(self.ledger, private_key_pem)
channel_keys[self.ledger.public_key_to_address(public_key_der)] = private_key_pem channel_keys[private_key.address] = private_key_pem
if self.channel_keys != channel_keys: if self.channel_keys != channel_keys:
self.channel_keys = channel_keys self.channel_keys = channel_keys
self.wallet.save() self.wallet.save()

View file

@ -1,10 +1,21 @@
from coincurve import PublicKey, PrivateKey as _PrivateKey from asn1crypto.keys import PrivateKeyInfo, ECPrivateKey
from coincurve import PublicKey as cPublicKey, PrivateKey as cPrivateKey
from coincurve.utils import (
pem_to_der, lib as libsecp256k1, ffi as libsecp256k1_ffi
)
from coincurve.ecdsa import CDATA_SIG_LENGTH
from lbry.crypto.hash import hmac_sha512, hash160, double_sha256 from lbry.crypto.hash import hmac_sha512, hash160, double_sha256
from lbry.crypto.base58 import Base58 from lbry.crypto.base58 import Base58
from .util import cachedproperty from .util import cachedproperty
class KeyPath:
RECEIVE = 0
CHANGE = 1
CHANNEL = 2
class DerivationError(Exception): class DerivationError(Exception):
""" Raised when an invalid derivation occurs. """ """ Raised when an invalid derivation occurs. """
@ -71,26 +82,26 @@ class _KeyBase:
return Base58.encode_check(self.extended_key()) return Base58.encode_check(self.extended_key())
class PubKey(_KeyBase): class PublicKey(_KeyBase):
""" A BIP32 public key. """ """ A BIP32 public key. """
def __init__(self, ledger, pubkey, chain_code, n, depth, parent=None): def __init__(self, ledger, pubkey, chain_code, n, depth, parent=None):
super().__init__(ledger, chain_code, n, depth, parent) super().__init__(ledger, chain_code, n, depth, parent)
if isinstance(pubkey, PublicKey): if isinstance(pubkey, cPublicKey):
self.verifying_key = pubkey self.verifying_key = pubkey
else: else:
self.verifying_key = self._verifying_key_from_pubkey(pubkey) self.verifying_key = self._verifying_key_from_pubkey(pubkey)
@classmethod @classmethod
def _verifying_key_from_pubkey(cls, pubkey): def _verifying_key_from_pubkey(cls, pubkey):
""" Converts a 33-byte compressed pubkey into an PublicKey object. """ """ Converts a 33-byte compressed pubkey into an coincurve.PublicKey object. """
if not isinstance(pubkey, (bytes, bytearray)): if not isinstance(pubkey, (bytes, bytearray)):
raise TypeError('pubkey must be raw bytes') raise TypeError('pubkey must be raw bytes')
if len(pubkey) != 33: if len(pubkey) != 33:
raise ValueError('pubkey must be 33 bytes') raise ValueError('pubkey must be 33 bytes')
if pubkey[0] not in (2, 3): if pubkey[0] not in (2, 3):
raise ValueError('invalid pubkey prefix byte') raise ValueError('invalid pubkey prefix byte')
return PublicKey(pubkey) return cPublicKey(pubkey)
@cachedproperty @cachedproperty
def pubkey_bytes(self): def pubkey_bytes(self):
@ -105,7 +116,7 @@ class PubKey(_KeyBase):
def ec_point(self): def ec_point(self):
return self.verifying_key.point() return self.verifying_key.point()
def child(self, n: int): def child(self, n: int) -> 'PublicKey':
""" Return the derived child extended pubkey at index N. """ """ Return the derived child extended pubkey at index N. """
if not 0 <= n < (1 << 31): if not 0 <= n < (1 << 31):
raise ValueError('invalid BIP32 public key child number') raise ValueError('invalid BIP32 public key child number')
@ -113,7 +124,7 @@ class PubKey(_KeyBase):
msg = self.pubkey_bytes + n.to_bytes(4, 'big') msg = self.pubkey_bytes + n.to_bytes(4, 'big')
L_b, R_b = self._hmac_sha512(msg) # pylint: disable=invalid-name L_b, R_b = self._hmac_sha512(msg) # pylint: disable=invalid-name
derived_key = self.verifying_key.add(L_b) derived_key = self.verifying_key.add(L_b)
return PubKey(self.ledger, derived_key, R_b, n, self.depth + 1, self) return PublicKey(self.ledger, derived_key, R_b, n, self.depth + 1, self)
def identifier(self): def identifier(self):
""" Return the key's identifier as 20 bytes. """ """ Return the key's identifier as 20 bytes. """
@ -138,7 +149,7 @@ class PrivateKey(_KeyBase):
def __init__(self, ledger, privkey, chain_code, n, depth, parent=None): def __init__(self, ledger, privkey, chain_code, n, depth, parent=None):
super().__init__(ledger, chain_code, n, depth, parent) super().__init__(ledger, chain_code, n, depth, parent)
if isinstance(privkey, _PrivateKey): if isinstance(privkey, cPrivateKey):
self.signing_key = privkey self.signing_key = privkey
else: else:
self.signing_key = self._signing_key_from_privkey(privkey) self.signing_key = self._signing_key_from_privkey(privkey)
@ -146,7 +157,7 @@ class PrivateKey(_KeyBase):
@classmethod @classmethod
def _signing_key_from_privkey(cls, private_key): def _signing_key_from_privkey(cls, private_key):
""" Converts a 32-byte private key into an coincurve.PrivateKey object. """ """ Converts a 32-byte private key into an coincurve.PrivateKey object. """
return _PrivateKey.from_int(PrivateKey._private_key_secret_exponent(private_key)) return cPrivateKey.from_int(PrivateKey._private_key_secret_exponent(private_key))
@classmethod @classmethod
def _private_key_secret_exponent(cls, private_key): def _private_key_secret_exponent(cls, private_key):
@ -158,24 +169,36 @@ class PrivateKey(_KeyBase):
return int.from_bytes(private_key, 'big') return int.from_bytes(private_key, 'big')
@classmethod @classmethod
def from_seed(cls, ledger, seed): def from_seed(cls, ledger, seed) -> 'PrivateKey':
# This hard-coded message string seems to be coin-independent... # This hard-coded message string seems to be coin-independent...
hmac = hmac_sha512(b'Bitcoin seed', seed) hmac = hmac_sha512(b'Bitcoin seed', seed)
privkey, chain_code = hmac[:32], hmac[32:] privkey, chain_code = hmac[:32], hmac[32:]
return cls(ledger, privkey, chain_code, 0, 0) return cls(ledger, privkey, chain_code, 0, 0)
@classmethod
def from_pem(cls, ledger, pem) -> 'PrivateKey':
der = pem_to_der(pem.encode())
try:
key_int = ECPrivateKey.load(der).native['private_key']
except ValueError:
key_int = PrivateKeyInfo.load(der).native['private_key']['private_key']
private_key = cPrivateKey.from_int(key_int)
return cls(ledger, private_key, bytes((0,)*32), 0, 0)
@cachedproperty @cachedproperty
def private_key_bytes(self): def private_key_bytes(self):
""" Return the serialized private key (no leading zero byte). """ """ Return the serialized private key (no leading zero byte). """
return self.signing_key.secret return self.signing_key.secret
@cachedproperty @cachedproperty
def public_key(self): def public_key(self) -> PublicKey:
""" Return the corresponding extended public key. """ """ Return the corresponding extended public key. """
verifying_key = self.signing_key.public_key verifying_key = self.signing_key.public_key
parent_pubkey = self.parent.public_key if self.parent else None parent_pubkey = self.parent.public_key if self.parent else None
return PubKey(self.ledger, verifying_key, self.chain_code, self.n, self.depth, return PublicKey(
parent_pubkey) self.ledger, verifying_key, self.chain_code,
self.n, self.depth, parent_pubkey
)
def ec_point(self): def ec_point(self):
return self.public_key.ec_point() return self.public_key.ec_point()
@ -188,11 +211,12 @@ class PrivateKey(_KeyBase):
""" Return the private key encoded in Wallet Import Format. """ """ Return the private key encoded in Wallet Import Format. """
return self.ledger.private_key_to_wif(self.private_key_bytes) return self.ledger.private_key_to_wif(self.private_key_bytes)
@property
def address(self): def address(self):
""" The public key as a P2PKH address. """ """ The public key as a P2PKH address. """
return self.public_key.address return self.public_key.address
def child(self, n): def child(self, n) -> 'PrivateKey':
""" Return the derived child extended private key at index N.""" """ Return the derived child extended private key at index N."""
if not 0 <= n < (1 << 32): if not 0 <= n < (1 << 32):
raise ValueError('invalid BIP32 private key child number') raise ValueError('invalid BIP32 private key child number')
@ -211,6 +235,28 @@ class PrivateKey(_KeyBase):
""" Produce a signature for piece of data by double hashing it and signing the hash. """ """ Produce a signature for piece of data by double hashing it and signing the hash. """
return self.signing_key.sign(data, hasher=double_sha256) return self.signing_key.sign(data, hasher=double_sha256)
def sign_compact(self, digest):
""" Produce a compact signature. """
key = self.signing_key
signature = libsecp256k1_ffi.new('secp256k1_ecdsa_signature *')
signed = libsecp256k1.secp256k1_ecdsa_sign(
key.context.ctx, signature, digest, key.secret,
libsecp256k1_ffi.NULL, libsecp256k1_ffi.NULL
)
if not signed:
raise ValueError('The private key was invalid.')
serialized = libsecp256k1_ffi.new('unsigned char[%d]' % CDATA_SIG_LENGTH)
compacted = libsecp256k1.secp256k1_ecdsa_signature_serialize_compact(
key.context.ctx, serialized, signature
)
if compacted != 1:
raise ValueError('The signature could not be compacted.')
return bytes(libsecp256k1_ffi.buffer(serialized, CDATA_SIG_LENGTH))
def identifier(self): def identifier(self):
"""Return the key's identifier as 20 bytes.""" """Return the key's identifier as 20 bytes."""
return self.public_key.identifier() return self.public_key.identifier()
@ -222,9 +268,12 @@ class PrivateKey(_KeyBase):
b'\0' + self.private_key_bytes b'\0' + self.private_key_bytes
) )
def to_pem(self):
return self.signing_key.to_pem()
def _from_extended_key(ledger, ekey): def _from_extended_key(ledger, ekey):
"""Return a PubKey or PrivateKey from an extended key raw bytes.""" """Return a PublicKey or PrivateKey from an extended key raw bytes."""
if not isinstance(ekey, (bytes, bytearray)): if not isinstance(ekey, (bytes, bytearray)):
raise TypeError('extended key must be raw bytes') raise TypeError('extended key must be raw bytes')
if len(ekey) != 78: if len(ekey) != 78:
@ -236,7 +285,7 @@ def _from_extended_key(ledger, ekey):
if ekey[:4] == ledger.extended_public_key_prefix: if ekey[:4] == ledger.extended_public_key_prefix:
pubkey = ekey[45:] pubkey = ekey[45:]
key = PubKey(ledger, pubkey, chain_code, n, depth) key = PublicKey(ledger, pubkey, chain_code, n, depth)
elif ekey[:4] == ledger.extended_private_key_prefix: elif ekey[:4] == ledger.extended_private_key_prefix:
if ekey[45] != 0: if ekey[45] != 0:
raise ValueError('invalid extended private key prefix byte') raise ValueError('invalid extended private key prefix byte')
@ -254,6 +303,6 @@ def from_extended_key_string(ledger, ekey_str):
xpub6BsnM1W2Y7qLMiuhi7f7dbAwQZ5Cz5gYJCRzTNainXzQXYjFwtuQXHd xpub6BsnM1W2Y7qLMiuhi7f7dbAwQZ5Cz5gYJCRzTNainXzQXYjFwtuQXHd
3qfi3t3KJtHxshXezfjft93w4UE7BGMtKwhqEHae3ZA7d823DVrL 3qfi3t3KJtHxshXezfjft93w4UE7BGMtKwhqEHae3ZA7d823DVrL
return a PubKey or PrivateKey. return a PublicKey or PrivateKey.
""" """
return _from_extended_key(ledger, Base58.decode_check(ekey_str)) return _from_extended_key(ledger, Base58.decode_check(ekey_str))

View file

@ -10,11 +10,10 @@ from contextvars import ContextVar
from typing import Tuple, List, Union, Callable, Any, Awaitable, Iterable, Dict, Optional from typing import Tuple, List, Union, Callable, Any, Awaitable, Iterable, Dict, Optional
from datetime import date from datetime import date
import ecdsa
from prometheus_client import Gauge, Counter, Histogram from prometheus_client import Gauge, Counter, Histogram
from lbry.utils import LockWithMetrics from lbry.utils import LockWithMetrics
from .bip32 import PubKey from .bip32 import PublicKey, PrivateKey
from .transaction import Transaction, Output, OutputScript, TXRefImmutable, Input from .transaction import Transaction, Output, OutputScript, TXRefImmutable, Input
from .constants import TXO_TYPES, CLAIM_TYPES from .constants import TXO_TYPES, CLAIM_TYPES
from .util import date_to_julian_day from .util import date_to_julian_day
@ -977,7 +976,9 @@ class Database(SQLiteMixin):
sql.append("LEFT JOIN txi ON (txi.position=0 AND txi.txid=txo.txid)") sql.append("LEFT JOIN txi ON (txi.position=0 AND txi.txid=txo.txid)")
return await self.db.execute_fetchall(*query(' '.join(sql), **constraints), read_only=read_only) return await self.db.execute_fetchall(*query(' '.join(sql), **constraints), read_only=read_only)
async def get_txos(self, wallet=None, no_tx=False, no_channel_info=False, read_only=False, **constraints): async def get_txos(
self, wallet=None, no_tx=False, no_channel_info=False, read_only=False, **constraints
) -> List[Output]:
include_is_spent = constraints.get('include_is_spent', False) include_is_spent = constraints.get('include_is_spent', False)
include_is_my_input = constraints.get('include_is_my_input', False) include_is_my_input = constraints.get('include_is_my_input', False)
include_is_my_output = constraints.pop('include_is_my_output', False) include_is_my_output = constraints.pop('include_is_my_output', False)
@ -1203,7 +1204,7 @@ class Database(SQLiteMixin):
addresses = await self.select_addresses(', '.join(cols), read_only=read_only, **constraints) addresses = await self.select_addresses(', '.join(cols), read_only=read_only, **constraints)
if 'pubkey' in cols: if 'pubkey' in cols:
for address in addresses: for address in addresses:
address['pubkey'] = PubKey( address['pubkey'] = PublicKey(
self.ledger, address.pop('pubkey'), address.pop('chain_code'), self.ledger, address.pop('pubkey'), address.pop('chain_code'),
address.pop('n'), address.pop('depth') address.pop('n'), address.pop('depth')
) )
@ -1243,11 +1244,15 @@ class Database(SQLiteMixin):
async def set_address_history(self, address, history): async def set_address_history(self, address, history):
await self._set_address_history(address, history) await self._set_address_history(address, history)
async def is_channel_key_used(self, wallet, key: ecdsa.SigningKey): async def is_channel_key_used(self, account, key: PublicKey):
channels = await self.get_txos(wallet, txo_type=TXO_TYPES['channel']) channels = await self.get_txos(
other_key_string = key.to_string() accounts=[account], txo_type=TXO_TYPES['channel'],
no_tx=True, no_channel_info=True
)
other_key_bytes = key.pubkey_bytes
for channel in channels: for channel in channels:
if channel.private_key is not None and channel.private_key.to_string() == other_key_string: claim = channel.can_decode_claim
if claim and claim.channel.public_key_bytes == other_key_bytes:
return True return True
return False return False

View file

@ -26,7 +26,7 @@ from .transaction import Transaction, Output
from .header import Headers, UnvalidatedHeaders from .header import Headers, UnvalidatedHeaders
from .checkpoints import HASHES from .checkpoints import HASHES
from .constants import TXO_TYPES, CLAIM_TYPES, COIN, NULL_HASH32 from .constants import TXO_TYPES, CLAIM_TYPES, COIN, NULL_HASH32
from .bip32 import PubKey, PrivateKey from .bip32 import PublicKey, PrivateKey
from .coinselection import CoinSelector from .coinselection import CoinSelector
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -226,7 +226,7 @@ class Ledger(metaclass=LedgerRegistry):
return account.get_private_key(address_info['chain'], address_info['pubkey'].n) return account.get_private_key(address_info['chain'], address_info['pubkey'].n)
return None return None
async def get_public_key_for_address(self, wallet, address) -> Optional[PubKey]: async def get_public_key_for_address(self, wallet, address) -> Optional[PublicKey]:
match = await self._get_account_and_address_info_for_address(wallet, address) match = await self._get_account_and_address_info_for_address(wallet, address)
if match: if match:
_, address_info = match _, address_info = match

View file

@ -507,7 +507,7 @@ class BlockProcessor:
channel_pub_key_bytes = channel_meta.channel.public_key_bytes channel_pub_key_bytes = channel_meta.channel.public_key_bytes
if channel_pub_key_bytes: if channel_pub_key_bytes:
channel_signature_is_valid = Output.is_signature_valid( channel_signature_is_valid = Output.is_signature_valid(
txo.get_encoded_signature(), txo.get_signature_digest(self.ledger), channel_pub_key_bytes txo.signable.signature, txo.get_signature_digest(self.ledger), channel_pub_key_bytes
) )
if channel_signature_is_valid: if channel_signature_is_valid:
self.pending_channel_counts[signing_channel_hash] += 1 self.pending_channel_counts[signing_channel_hash] += 1

View file

@ -1,11 +1,11 @@
import struct import struct
import hashlib
import logging import logging
import typing import typing
from binascii import hexlify, unhexlify from binascii import hexlify, unhexlify
from typing import List, Iterable, Optional, Tuple from typing import List, Iterable, Optional, Tuple
import ecdsa from coincurve import PublicKey as cPublicKey
from coincurve.ecdsa import deserialize_compact, cdata_to_der
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import load_der_public_key from cryptography.hazmat.primitives.serialization import load_der_public_key
from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import hashes
@ -27,6 +27,7 @@ from .constants import COIN, NULL_HASH32
from .bcd_data_stream import BCDataStream from .bcd_data_stream import BCDataStream
from .hash import TXRef, TXRefImmutable from .hash import TXRef, TXRefImmutable
from .util import ReadOnlyList from .util import ReadOnlyList
from .bip32 import PrivateKey, PublicKey
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from lbry.wallet.account import Account from lbry.wallet.account import Account
@ -221,7 +222,8 @@ class Output(InputOutput):
is_my_output: Optional[bool] = None, is_my_input: Optional[bool] = None, is_my_output: Optional[bool] = None, is_my_input: Optional[bool] = None,
sent_supports: Optional[int] = None, sent_tips: Optional[int] = None, sent_supports: Optional[int] = None, sent_tips: Optional[int] = None,
received_tips: Optional[int] = None, received_tips: Optional[int] = None,
channel: Optional['Output'] = None, private_key: Optional[str] = None channel: Optional['Output'] = None,
private_key: Optional[PrivateKey] = None
) -> None: ) -> None:
super().__init__(tx_ref, position) super().__init__(tx_ref, position)
self.amount = amount self.amount = amount
@ -234,7 +236,7 @@ class Output(InputOutput):
self.sent_tips = sent_tips self.sent_tips = sent_tips
self.received_tips = received_tips self.received_tips = received_tips
self.channel = channel self.channel = channel
self.private_key = private_key self.private_key: PrivateKey = private_key
self.purchase: 'Output' = None # txo containing purchase metadata self.purchase: 'Output' = None # txo containing purchase metadata
self.purchased_claim: 'Output' = None # resolved claim pointed to by purchase self.purchased_claim: 'Output' = None # resolved claim pointed to by purchase
self.purchase_receipt: 'Output' = None # txo representing purchase receipt for this claim self.purchase_receipt: 'Output' = None # txo representing purchase receipt for this claim
@ -424,25 +426,24 @@ class Output(InputOutput):
] ]
return sha256(b''.join(pieces)) return sha256(b''.join(pieces))
def get_encoded_signature(self):
signature = hexlify(self.signable.signature)
r = int(signature[:int(len(signature)/2)], 16)
s = int(signature[int(len(signature)/2):], 16)
return ecdsa.util.sigencode_der(r, s, len(signature)*4)
@staticmethod @staticmethod
def is_signature_valid(encoded_signature, signature_digest, public_key_bytes): def is_signature_valid(signature, digest, public_key_bytes):
try: signature = cdata_to_der(deserialize_compact(signature))
public_key = load_der_public_key(public_key_bytes, default_backend()) public_key = cPublicKey(public_key_bytes)
public_key.verify(encoded_signature, signature_digest, ec.ECDSA(Prehashed(hashes.SHA256()))) is_valid = public_key.verify(signature, digest, None)
return True if not is_valid: # try old way
except (ValueError, InvalidSignature): # ytsync signed claims don't seem to validate with coincurve
pass try:
return False pk = ec.EllipticCurvePublicKey.from_encoded_point(ec.SECP256K1(), public_key_bytes)
pk.verify(signature, digest, ec.ECDSA(Prehashed(hashes.SHA256())))
return True
except (ValueError, InvalidSignature):
pass
return is_valid
def is_signed_by(self, channel: 'Output', ledger=None): def is_signed_by(self, channel: 'Output', ledger=None):
return self.is_signature_valid( return self.is_signature_valid(
self.get_encoded_signature(), self.signable.signature,
self.get_signature_digest(ledger), self.get_signature_digest(ledger),
channel.claim.channel.public_key_bytes channel.claim.channel.public_key_bytes
) )
@ -455,27 +456,27 @@ class Output(InputOutput):
self.signable.signing_channel_hash, self.signable.signing_channel_hash,
self.signable.to_message_bytes() self.signable.to_message_bytes()
])) ]))
self.signable.signature = channel.private_key.sign_digest_deterministic(digest, hashfunc=hashlib.sha256) self.signable.signature = channel.private_key.sign_compact(digest)
self.script.generate() self.script.generate()
def sign_data(self, data:bytes, timestamp:str) -> str: def sign_data(self, data: bytes, timestamp: str) -> str:
pieces = [timestamp.encode(), self.claim_hash, data] pieces = [timestamp.encode(), self.claim_hash, data]
digest = sha256(b''.join(pieces)) digest = sha256(b''.join(pieces))
signature = self.private_key.sign_digest_deterministic(digest, hashfunc=hashlib.sha256) signature = self.private_key.sign_compact(digest)
return hexlify(signature).decode() return hexlify(signature).decode()
def clear_signature(self): def clear_signature(self):
self.channel = None self.channel = None
self.signable.clear_signature() self.signable.clear_signature()
def set_channel_private_key(self, private_key: ecdsa.SigningKey): def set_channel_private_key(self, private_key: PrivateKey):
self.private_key = private_key self.private_key = private_key
self.claim.channel.public_key_bytes = self.private_key.get_verifying_key().to_der() self.claim.channel.public_key_bytes = private_key.public_key.pubkey_bytes
self.script.generate() self.script.generate()
return self.private_key return self.private_key
def is_channel_private_key(self, private_key: ecdsa.SigningKey): def is_channel_private_key(self, private_key: PrivateKey):
return self.claim.channel.public_key_bytes == private_key.get_verifying_key().to_der() return self.claim.channel.public_key_bytes == private_key.public_key.pubkey_bytes
@classmethod @classmethod
def pay_claim_name_pubkey_hash( def pay_claim_name_pubkey_hash(

View file

@ -1,7 +1,8 @@
from binascii import unhexlify from binascii import unhexlify
from lbry.testcase import CommandTestCase from lbry.testcase import CommandTestCase
from lbry.wallet.dewies import dewies_to_lbc from lbry.schema.claim import Claim
from lbry.wallet.dewies import dewies_to_lbc, lbc_to_dewies
from lbry.wallet.account import DeterministicChannelKeyManager from lbry.wallet.account import DeterministicChannelKeyManager
from lbry.wallet.transaction import Transaction from lbry.wallet.transaction import Transaction
@ -63,6 +64,32 @@ class AccountManagement(CommandTestCase):
accounts = await self.daemon.jsonrpc_account_list(account_id, include_claims=True) accounts = await self.daemon.jsonrpc_account_list(account_id, include_claims=True)
self.assertEqual(accounts['items'][0]['name'], 'recreated account') self.assertEqual(accounts['items'][0]['name'], 'recreated account')
async def test_wallet_migration(self):
old_id, new_id, valid_key = (
'mi9E8KqFfW5ngktU22pN2jpgsdf81ZbsGY',
'mqs77XbdnuxWN4cXrjKbSoGLkvAHa4f4B8',
'-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEIBZRTZ7tHnYCH3IE9mCo95'
'466L/ShYFhXGrjmSMFJw8eoAcGBSuBBAAK\noUQDQgAEmucoPz9nI+ChZrfhnh'
'0RZ/bcX0r2G0pYBmoNKovtKzXGa8y07D66MWsW\nqXptakqO/9KddIkBu5eJNS'
'UZzQCxPQ==\n-----END EC PRIVATE KEY-----\n'
)
# null certificates should get deleted
self.account.channel_keys = {
new_id: 'not valid key',
'foo': 'bar',
}
await self.account.maybe_migrate_certificates()
self.assertEqual(self.account.channel_keys, {})
self.account.channel_keys = {
new_id: 'not valid key',
'foo': 'bar',
'invalid address': valid_key,
}
await self.account.maybe_migrate_certificates()
self.assertEqual(self.account.channel_keys, {
new_id: valid_key
})
async def assertFindsClaims(self, claim_names, awaitable): async def assertFindsClaims(self, claim_names, awaitable):
self.assertEqual(claim_names, [txo.claim_name for txo in (await awaitable)['items']]) self.assertEqual(claim_names, [txo.claim_name for txo in (await awaitable)['items']])
@ -168,6 +195,51 @@ class AccountManagement(CommandTestCase):
with self.assertRaisesRegex(Exception, f"'{bad_address}' is not a valid address"): with self.assertRaisesRegex(Exception, f"'{bad_address}' is not a valid address"):
await self.daemon.jsonrpc_account_send('0.1', addresses=[bad_address]) await self.daemon.jsonrpc_account_send('0.1', addresses=[bad_address])
async def create_nondeterministic_channel(self, name, pubkey_bytes):
claim_address = await self.account.receiving.get_or_create_usable_address()
claim = Claim()
claim.channel.public_key_bytes = pubkey_bytes
tx = await Transaction.claim_create(
name, claim, lbc_to_dewies('1.0'),
claim_address, [self.account], self.account
)
await tx.sign([self.account])
async def command():
await self.daemon.broadcast_or_release(tx, False)
return tx
return await self.confirm_and_render(command(), True)
async def test_hybrid_channel_keys(self):
# non-deterministic channel
self.account.channel_keys = {
'mqs77XbdnuxWN4cXrjKbSoGLkvAHa4f4B8':
'-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEIBZRTZ7tHnYCH3IE9mCo95'
'466L/ShYFhXGrjmSMFJw8eoAcGBSuBBAAK\noUQDQgAEmucoPz9nI+ChZrfhnh'
'0RZ/bcX0r2G0pYBmoNKovtKzXGa8y07D66MWsW\nqXptakqO/9KddIkBu5eJNS'
'UZzQCxPQ==\n-----END EC PRIVATE KEY-----\n'
}
channel1 = await self.create_nondeterministic_channel('@foo1', unhexlify(
'3056301006072a8648ce3d020106052b8104000a034200049ae7283f3f6723e0a1'
'66b7e19e1d1167f6dc5f4af61b4a58066a0d2a8bed2b35c66bccb4ec3eba316b16'
'a97a6d6a4a8effd29d748901bb9789352519cd00b13d'
))
# deterministic channel
channel2 = await self.channel_create('@foo2')
stream1 = await self.stream_create('stream-in-channel1', '0.01', channel_id=self.get_claim_id(channel1))
stream2 = await self.stream_create('stream-in-channel2', '0.01', channel_id=self.get_claim_id(channel2))
resolved_stream1 = await self.resolve('@foo1/stream-in-channel1')
self.assertEqual('stream-in-channel1', resolved_stream1['name'])
self.assertTrue(resolved_stream1['is_channel_signature_valid'])
resolved_stream2 = await self.resolve('@foo2/stream-in-channel2')
self.assertEqual('stream-in-channel2', resolved_stream2['name'])
self.assertTrue(resolved_stream2['is_channel_signature_valid'])
async def test_deterministic_channel_keys(self): async def test_deterministic_channel_keys(self):
seed = self.account.seed seed = self.account.seed
keys = self.account.deterministic_channel_keys keys = self.account.deterministic_channel_keys
@ -188,33 +260,33 @@ class AccountManagement(CommandTestCase):
self.assertTrue(channel1b.has_private_key) self.assertTrue(channel1b.has_private_key)
self.assertEqual( self.assertEqual(
channel1a['outputs'][0]['value']['public_key_id'], channel1a['outputs'][0]['value']['public_key_id'],
self.ledger.public_key_to_address(channel1b.private_key.verifying_key.to_der()) channel1b.private_key.address
) )
self.assertTrue(channel2b.has_private_key) self.assertTrue(channel2b.has_private_key)
self.assertEqual( self.assertEqual(
channel2a['outputs'][0]['value']['public_key_id'], channel2a['outputs'][0]['value']['public_key_id'],
self.ledger.public_key_to_address(channel2b.private_key.verifying_key.to_der()) channel2b.private_key.address
) )
# repeatedly calling next channel key returns the same key when not used # repeatedly calling next channel key returns the same key when not used
current_known = keys.last_known current_known = keys.last_known
next_key = await keys.generate_next_key() next_key = await keys.generate_next_key()
self.assertEqual(current_known, keys.last_known) self.assertEqual(current_known, keys.last_known)
self.assertEqual(next_key.to_string(), (await keys.generate_next_key()).to_string()) self.assertEqual(next_key.address, (await keys.generate_next_key()).address)
# again, should be idempotent # again, should be idempotent
next_key = await keys.generate_next_key() next_key = await keys.generate_next_key()
self.assertEqual(current_known, keys.last_known) self.assertEqual(current_known, keys.last_known)
self.assertEqual(next_key.to_string(), (await keys.generate_next_key()).to_string()) self.assertEqual(next_key.address, (await keys.generate_next_key()).address)
# create third channel while both daemons running, second daemon should pick it up # create third channel while both daemons running, second daemon should pick it up
channel3a = await self.channel_create('@foo3') channel3a = await self.channel_create('@foo3')
self.assertEqual(current_known+1, keys.last_known) self.assertEqual(current_known+1, keys.last_known)
self.assertNotEqual(next_key.to_string(), (await keys.generate_next_key()).to_string()) self.assertNotEqual(next_key.address, (await keys.generate_next_key()).address)
channel3b, = (await self.daemon2.jsonrpc_channel_list(name='@foo3'))['items'] channel3b, = (await self.daemon2.jsonrpc_channel_list(name='@foo3'))['items']
self.assertTrue(channel3b.has_private_key) self.assertTrue(channel3b.has_private_key)
self.assertEqual( self.assertEqual(
channel3a['outputs'][0]['value']['public_key_id'], channel3a['outputs'][0]['value']['public_key_id'],
self.ledger.public_key_to_address(channel3b.private_key.verifying_key.to_der()) channel3b.private_key.address
) )
# channel key cache re-populated after simulated restart # channel key cache re-populated after simulated restart

View file

@ -19,12 +19,6 @@ from lbry.crypto.hash import sha256
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def get_encoded_signature(signature):
signature = signature.encode() if isinstance(signature, str) else signature
r = int(signature[:int(len(signature) / 2)], 16)
s = int(signature[int(len(signature) / 2):], 16)
return ecdsa.util.sigencode_der(r, s, len(signature) * 4)
def verify(channel, data, signature, channel_hash=None): def verify(channel, data, signature, channel_hash=None):
pieces = [ pieces = [
@ -33,7 +27,7 @@ def verify(channel, data, signature, channel_hash=None):
data data
] ]
return Output.is_signature_valid( return Output.is_signature_valid(
get_encoded_signature(signature['signature']), unhexlify(signature['signature']),
sha256(b''.join(pieces)), sha256(b''.join(pieces)),
channel.claim.channel.public_key_bytes channel.claim.channel.public_key_bytes
) )
@ -1123,17 +1117,17 @@ class ChannelCommands(CommandTestCase):
tx = await self.channel_update(claim_id, bid='4.0') tx = await self.channel_update(claim_id, bid='4.0')
self.assertEqual(tx['outputs'][0]['amount'], '4.0') self.assertEqual(tx['outputs'][0]['amount'], '4.0')
await self.assertBalance(self.account, '5.991447') await self.assertBalance(self.account, '5.991503')
# not enough funds # not enough funds
with self.assertRaisesRegex( with self.assertRaisesRegex(
InsufficientFundsError, "Not enough funds to cover this transaction."): InsufficientFundsError, "Not enough funds to cover this transaction."):
await self.channel_create('@foo2', '9.0') await self.channel_create('@foo2', '9.0')
self.assertItemCount(await self.daemon.jsonrpc_channel_list(), 1) self.assertItemCount(await self.daemon.jsonrpc_channel_list(), 1)
await self.assertBalance(self.account, '5.991447') await self.assertBalance(self.account, '5.991503')
# spend exactly amount available, no change # spend exactly amount available, no change
tx = await self.channel_create('@foo3', '5.981266') tx = await self.channel_create('@foo3', '5.981322')
await self.assertBalance(self.account, '0.0') await self.assertBalance(self.account, '0.0')
self.assertEqual(len(tx['outputs']), 1) # no change self.assertEqual(len(tx['outputs']), 1) # no change
self.assertItemCount(await self.daemon.jsonrpc_channel_list(), 2) self.assertItemCount(await self.daemon.jsonrpc_channel_list(), 2)
@ -1249,7 +1243,7 @@ class ChannelCommands(CommandTestCase):
await daemon2.jsonrpc_channel_import(exported_data) await daemon2.jsonrpc_channel_import(exported_data)
channels = (await daemon2.jsonrpc_channel_list())['items'] channels = (await daemon2.jsonrpc_channel_list())['items']
self.assertEqual(1, len(channels)) self.assertEqual(1, len(channels))
self.assertEqual(channel_private_key.to_string(), channels[0].private_key.to_string()) self.assertEqual(channel_private_key.private_key_bytes, channels[0].private_key.private_key_bytes)
# second wallet can't update until channel is sent to it # second wallet can't update until channel is sent to it
with self.assertRaisesRegex(AssertionError, 'Cannot find private key for signing output.'): with self.assertRaisesRegex(AssertionError, 'Cannot find private key for signing output.'):

View file

@ -1,10 +1,8 @@
from binascii import unhexlify from binascii import unhexlify
import ecdsa
from lbry.testcase import AsyncioTestCase from lbry.testcase import AsyncioTestCase
from lbry.wallet.constants import CENT, NULL_HASH32 from lbry.wallet.constants import CENT, NULL_HASH32
from lbry.wallet.bip32 import PrivateKey from lbry.wallet.bip32 import PrivateKey, KeyPath
from lbry.wallet.mnemonic import Mnemonic from lbry.wallet.mnemonic import Mnemonic
from lbry.wallet import Ledger, Database, Headers, Transaction, Input, Output from lbry.wallet import Ledger, Database, Headers, Transaction, Input, Output
from lbry.schema.claim import Claim from lbry.schema.claim import Claim
@ -27,10 +25,10 @@ def get_tx():
async def get_channel(claim_name='@foo'): async def get_channel(claim_name='@foo'):
seed = Mnemonic.mnemonic_to_seed(Mnemonic().make_seed(), '') seed = Mnemonic.mnemonic_to_seed(Mnemonic().make_seed(), '')
bip32_key = PrivateKey.from_seed(Ledger, seed) key = PrivateKey.from_seed(Ledger, seed)
signing_key = ecdsa.SigningKey.from_secret_exponent(bip32_key.secret_exponent(), ecdsa.SECP256k1) channel_key = key.child(KeyPath.CHANNEL).child(0)
channel_txo = Output.pay_claim_name_pubkey_hash(CENT, claim_name, Claim(), b'abc') channel_txo = Output.pay_claim_name_pubkey_hash(CENT, claim_name, Claim(), b'abc')
channel_txo.set_channel_private_key(signing_key) channel_txo.set_channel_private_key(channel_key)
get_tx().add_outputs([channel_txo]) get_tx().add_outputs([channel_txo])
return channel_txo return channel_txo
@ -160,13 +158,10 @@ class TestValidateSignContent(AsyncioTestCase):
some_content = "MEANINGLESS CONTENT AEE3353320".encode() some_content = "MEANINGLESS CONTENT AEE3353320".encode()
timestamp_str = "1630564175" timestamp_str = "1630564175"
channel = await get_channel() channel = await get_channel()
stream = get_stream()
signature = channel.sign_data(some_content, timestamp_str) signature = channel.sign_data(some_content, timestamp_str)
stream.signable.signature = unhexlify(signature.encode())
encoded_signature = stream.get_encoded_signature()
pieces = [timestamp_str.encode(), channel.claim_hash, some_content] pieces = [timestamp_str.encode(), channel.claim_hash, some_content]
self.assertTrue(Output.is_signature_valid( self.assertTrue(Output.is_signature_valid(
encoded_signature, unhexlify(signature.encode()),
sha256(b''.join(pieces)), sha256(b''.join(pieces)),
channel.claim.channel.public_key_bytes channel.claim.channel.public_key_bytes
)) ))