reduced crypto dependency in wallet to coincurve
This commit is contained in:
parent
fb57cfa5d8
commit
1eaa195363
12 changed files with 251 additions and 145 deletions
|
@ -17,7 +17,6 @@ from binascii import hexlify, unhexlify
|
|||
from traceback import format_exc
|
||||
from functools import wraps, partial
|
||||
|
||||
import ecdsa
|
||||
import base58
|
||||
from aiohttp import web
|
||||
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.constants import TXO_TYPES, CLAIM_TYPE_NAMES
|
||||
from lbry.wallet.bip32 import PrivateKey
|
||||
|
||||
from lbry import utils
|
||||
from lbry.conf import Config, Setting, NOT_SET
|
||||
|
@ -3041,7 +3041,7 @@ class Daemon(metaclass=JSONRPCServerType):
|
|||
'channel_id': channel.claim_id,
|
||||
'holding_address': address,
|
||||
'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=(',', ':')))
|
||||
|
||||
|
@ -3064,15 +3064,14 @@ class Daemon(metaclass=JSONRPCServerType):
|
|||
|
||||
decoded = base58.b58decode(channel_data)
|
||||
data = json.loads(decoded)
|
||||
channel_private_key = ecdsa.SigningKey.from_pem(
|
||||
data['signing_private_key'], hashfunc=hashlib.sha256
|
||||
channel_private_key = PrivateKey.from_pem(
|
||||
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
|
||||
holding_address = data['holding_address']
|
||||
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:
|
||||
holding_address = channels[0].get_address(self.ledger)
|
||||
|
|
|
@ -10,7 +10,7 @@ from lbry.schema.claim import Claim
|
|||
from lbry.schema.support import Support
|
||||
from lbry.torrent.torrent_manager import TorrentSource
|
||||
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.stream.managed_stream import ManagedStream
|
||||
|
||||
|
@ -138,7 +138,7 @@ class JSONResponseEncoder(JSONEncoder):
|
|||
return self.encode_claim(obj)
|
||||
if isinstance(obj, Support):
|
||||
return obj.to_dict()
|
||||
if isinstance(obj, PubKey):
|
||||
if isinstance(obj, PublicKey):
|
||||
return obj.extended_key_string()
|
||||
if isinstance(obj, datetime):
|
||||
return obj.strftime("%Y%m%dT%H:%M:%S")
|
||||
|
|
|
@ -2,6 +2,9 @@ import logging
|
|||
from typing import List
|
||||
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.message import DecodeError
|
||||
from hachoir.core.log import log as hachoir_log
|
||||
|
@ -346,7 +349,7 @@ class Channel(BaseClaim):
|
|||
|
||||
@property
|
||||
def public_key(self) -> str:
|
||||
return hexlify(self.message.public_key).decode()
|
||||
return hexlify(self.public_key_bytes).decode()
|
||||
|
||||
@public_key.setter
|
||||
def public_key(self, sd_public_key: str):
|
||||
|
@ -354,7 +357,11 @@ class Channel(BaseClaim):
|
|||
|
||||
@property
|
||||
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
|
||||
def public_key_bytes(self, public_key: bytes):
|
||||
|
|
|
@ -9,11 +9,10 @@ from hashlib import sha256
|
|||
from string import hexdigits
|
||||
from typing import Type, Dict, Tuple, Optional, Any, List
|
||||
|
||||
import ecdsa
|
||||
from lbry.error import InvalidPasswordError
|
||||
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 .constants import COIN, TXO_TYPES
|
||||
from .transaction import Transaction, Input, Output
|
||||
|
@ -36,44 +35,39 @@ def validate_claim_id(claim_id):
|
|||
|
||||
class DeterministicChannelKeyManager:
|
||||
|
||||
def __init__(self, account):
|
||||
def __init__(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.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):
|
||||
if self.private_key is None:
|
||||
return
|
||||
next_key = self.private_key.child(self.last_known)
|
||||
signing_key = ecdsa.SigningKey.from_secret_exponent(
|
||||
next_key.secret_exponent(), ecdsa.SECP256k1
|
||||
)
|
||||
public_key_bytes = signing_key.get_verifying_key().to_der()
|
||||
next_private_key = self.private_key.child(self.last_known)
|
||||
public_key = next_private_key.public_key
|
||||
public_key_bytes = public_key.pubkey_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
|
||||
|
||||
async def ensure_cache_primed(self):
|
||||
if self.private_key is not None:
|
||||
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
|
||||
while True:
|
||||
next_key = self.private_key.child(self.last_known)
|
||||
signing_key = ecdsa.SigningKey.from_secret_exponent(
|
||||
next_key.secret_exponent(), ecdsa.SECP256k1
|
||||
)
|
||||
public_key_bytes = signing_key.get_verifying_key().to_der()
|
||||
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
|
||||
next_private_key = self.private_key.child(self.last_known)
|
||||
public_key = next_private_key.public_key
|
||||
self.cache[public_key.address] = next_private_key
|
||||
if not await db.is_channel_key_used(self.account, public_key):
|
||||
return next_private_key
|
||||
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)
|
||||
|
||||
|
||||
|
@ -122,7 +116,7 @@ class AddressManager:
|
|||
def get_private_key(self, index: int) -> PrivateKey:
|
||||
raise NotImplementedError
|
||||
|
||||
def get_public_key(self, index: int) -> PubKey:
|
||||
def get_public_key(self, index: int) -> PublicKey:
|
||||
raise NotImplementedError
|
||||
|
||||
async def get_max_gap(self):
|
||||
|
@ -162,8 +156,8 @@ class HierarchicalDeterministic(AddressManager):
|
|||
@classmethod
|
||||
def from_dict(cls, account: 'Account', d: dict) -> Tuple[AddressManager, AddressManager]:
|
||||
return (
|
||||
cls(account, 0, **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.RECEIVE, **d.get('receiving', {'gap': 20, 'maximum_uses_per_address': 1})),
|
||||
cls(account, KeyPath.CHANGE, **d.get('change', {'gap': 6, 'maximum_uses_per_address': 1}))
|
||||
)
|
||||
|
||||
def merge(self, d: dict):
|
||||
|
@ -176,7 +170,7 @@ class HierarchicalDeterministic(AddressManager):
|
|||
def get_private_key(self, index: int) -> PrivateKey:
|
||||
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)
|
||||
|
||||
async def get_max_gap(self) -> int:
|
||||
|
@ -236,7 +230,7 @@ class SingleKey(AddressManager):
|
|||
@classmethod
|
||||
def from_dict(cls, account: 'Account', d: dict) \
|
||||
-> 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
|
||||
|
||||
def to_dict_instance(self):
|
||||
|
@ -245,7 +239,7 @@ class SingleKey(AddressManager):
|
|||
def get_private_key(self, index: int) -> PrivateKey:
|
||||
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
|
||||
|
||||
async def get_max_gap(self) -> int:
|
||||
|
@ -267,9 +261,6 @@ class SingleKey(AddressManager):
|
|||
|
||||
class Account:
|
||||
|
||||
mnemonic_class = Mnemonic
|
||||
private_key_class = PrivateKey
|
||||
public_key_class = PubKey
|
||||
address_generators: Dict[str, Type[AddressManager]] = {
|
||||
SingleKey.name: SingleKey,
|
||||
HierarchicalDeterministic.name: HierarchicalDeterministic,
|
||||
|
@ -277,7 +268,7 @@ class Account:
|
|||
|
||||
def __init__(self, ledger: 'Ledger', wallet: 'Wallet', name: str,
|
||||
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:
|
||||
self.ledger = ledger
|
||||
self.wallet = wallet
|
||||
|
@ -288,8 +279,8 @@ class Account:
|
|||
self.private_key_string = private_key_string
|
||||
self.init_vectors: Dict[str, bytes] = {}
|
||||
self.encrypted = encrypted
|
||||
self.private_key = private_key
|
||||
self.public_key = public_key
|
||||
self.private_key: Optional[PrivateKey] = private_key
|
||||
self.public_key: PublicKey = public_key
|
||||
generator_name = address_generator.get('name', HierarchicalDeterministic.name)
|
||||
self.address_generator = self.address_generators[generator_name]
|
||||
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):
|
||||
return cls.from_dict(ledger, wallet, {
|
||||
'name': name,
|
||||
'seed': cls.mnemonic_class().make_seed(),
|
||||
'seed': Mnemonic().make_seed(),
|
||||
'address_generator': address_generator or {}
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def get_private_key_from_seed(cls, ledger: 'Ledger', seed: str, password: str):
|
||||
return cls.private_key_class.from_seed(
|
||||
ledger, cls.mnemonic_class.mnemonic_to_seed(seed, password or 'lbryum')
|
||||
return PrivateKey.from_seed(
|
||||
ledger, Mnemonic.mnemonic_to_seed(seed, password or 'lbryum')
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def keys_from_dict(cls, ledger: 'Ledger', d: dict) \
|
||||
-> Tuple[str, Optional[PrivateKey], PubKey]:
|
||||
-> Tuple[str, Optional[PrivateKey], PublicKey]:
|
||||
seed = d.get('seed', '')
|
||||
private_key_string = d.get('private_key', '')
|
||||
private_key = None
|
||||
|
@ -493,7 +484,7 @@ class Account:
|
|||
assert not self.encrypted, "Cannot get private key on encrypted wallet account."
|
||||
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)
|
||||
|
||||
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):
|
||||
return await self.deterministic_channel_keys.generate_next_key()
|
||||
|
||||
def add_channel_private_key(self, private_key):
|
||||
public_key_bytes = private_key.get_verifying_key().to_der()
|
||||
channel_pubkey_hash = self.ledger.public_key_to_address(public_key_bytes)
|
||||
self.channel_keys[channel_pubkey_hash] = private_key.to_pem().decode()
|
||||
def add_channel_private_key(self, private_key: PrivateKey):
|
||||
self.channel_keys[private_key.address] = 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)
|
||||
private_key_pem = self.channel_keys.get(channel_pubkey_hash)
|
||||
if private_key_pem:
|
||||
return await asyncio.get_event_loop().run_in_executor(
|
||||
None, ecdsa.SigningKey.from_pem, private_key_pem, sha256
|
||||
)
|
||||
return PrivateKey.from_pem(self.ledger, private_key_pem)
|
||||
return self.deterministic_channel_keys.get_private_key_from_pubkey_hash(channel_pubkey_hash)
|
||||
|
||||
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:
|
||||
return
|
||||
channel_keys = {}
|
||||
for private_key_pem in self.channel_keys.values():
|
||||
if not isinstance(private_key_pem, str):
|
||||
continue
|
||||
if "-----BEGIN EC PRIVATE KEY-----" not in private_key_pem:
|
||||
if not private_key_pem.startswith("-----BEGIN"):
|
||||
continue
|
||||
public_key_der = await asyncio.get_event_loop().run_in_executor(None, to_der, private_key_pem)
|
||||
channel_keys[self.ledger.public_key_to_address(public_key_der)] = private_key_pem
|
||||
private_key = PrivateKey.from_pem(self.ledger, private_key_pem)
|
||||
channel_keys[private_key.address] = private_key_pem
|
||||
if self.channel_keys != channel_keys:
|
||||
self.channel_keys = channel_keys
|
||||
self.wallet.save()
|
||||
|
|
|
@ -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.base58 import Base58
|
||||
from .util import cachedproperty
|
||||
|
||||
|
||||
class KeyPath:
|
||||
RECEIVE = 0
|
||||
CHANGE = 1
|
||||
CHANNEL = 2
|
||||
|
||||
|
||||
class DerivationError(Exception):
|
||||
""" Raised when an invalid derivation occurs. """
|
||||
|
||||
|
@ -71,26 +82,26 @@ class _KeyBase:
|
|||
return Base58.encode_check(self.extended_key())
|
||||
|
||||
|
||||
class PubKey(_KeyBase):
|
||||
class PublicKey(_KeyBase):
|
||||
""" A BIP32 public key. """
|
||||
|
||||
def __init__(self, ledger, pubkey, chain_code, n, depth, parent=None):
|
||||
super().__init__(ledger, chain_code, n, depth, parent)
|
||||
if isinstance(pubkey, PublicKey):
|
||||
if isinstance(pubkey, cPublicKey):
|
||||
self.verifying_key = pubkey
|
||||
else:
|
||||
self.verifying_key = self._verifying_key_from_pubkey(pubkey)
|
||||
|
||||
@classmethod
|
||||
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)):
|
||||
raise TypeError('pubkey must be raw bytes')
|
||||
if len(pubkey) != 33:
|
||||
raise ValueError('pubkey must be 33 bytes')
|
||||
if pubkey[0] not in (2, 3):
|
||||
raise ValueError('invalid pubkey prefix byte')
|
||||
return PublicKey(pubkey)
|
||||
return cPublicKey(pubkey)
|
||||
|
||||
@cachedproperty
|
||||
def pubkey_bytes(self):
|
||||
|
@ -105,7 +116,7 @@ class PubKey(_KeyBase):
|
|||
def ec_point(self):
|
||||
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. """
|
||||
if not 0 <= n < (1 << 31):
|
||||
raise ValueError('invalid BIP32 public key child number')
|
||||
|
@ -113,7 +124,7 @@ class PubKey(_KeyBase):
|
|||
msg = self.pubkey_bytes + n.to_bytes(4, 'big')
|
||||
L_b, R_b = self._hmac_sha512(msg) # pylint: disable=invalid-name
|
||||
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):
|
||||
""" 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):
|
||||
super().__init__(ledger, chain_code, n, depth, parent)
|
||||
if isinstance(privkey, _PrivateKey):
|
||||
if isinstance(privkey, cPrivateKey):
|
||||
self.signing_key = privkey
|
||||
else:
|
||||
self.signing_key = self._signing_key_from_privkey(privkey)
|
||||
|
@ -146,7 +157,7 @@ class PrivateKey(_KeyBase):
|
|||
@classmethod
|
||||
def _signing_key_from_privkey(cls, private_key):
|
||||
""" 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
|
||||
def _private_key_secret_exponent(cls, private_key):
|
||||
|
@ -158,24 +169,36 @@ class PrivateKey(_KeyBase):
|
|||
return int.from_bytes(private_key, 'big')
|
||||
|
||||
@classmethod
|
||||
def from_seed(cls, ledger, seed):
|
||||
def from_seed(cls, ledger, seed) -> 'PrivateKey':
|
||||
# This hard-coded message string seems to be coin-independent...
|
||||
hmac = hmac_sha512(b'Bitcoin seed', seed)
|
||||
privkey, chain_code = hmac[:32], hmac[32:]
|
||||
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
|
||||
def private_key_bytes(self):
|
||||
""" Return the serialized private key (no leading zero byte). """
|
||||
return self.signing_key.secret
|
||||
|
||||
@cachedproperty
|
||||
def public_key(self):
|
||||
def public_key(self) -> PublicKey:
|
||||
""" Return the corresponding extended public key. """
|
||||
verifying_key = self.signing_key.public_key
|
||||
parent_pubkey = self.parent.public_key if self.parent else None
|
||||
return PubKey(self.ledger, verifying_key, self.chain_code, self.n, self.depth,
|
||||
parent_pubkey)
|
||||
return PublicKey(
|
||||
self.ledger, verifying_key, self.chain_code,
|
||||
self.n, self.depth, parent_pubkey
|
||||
)
|
||||
|
||||
def ec_point(self):
|
||||
return self.public_key.ec_point()
|
||||
|
@ -188,11 +211,12 @@ class PrivateKey(_KeyBase):
|
|||
""" Return the private key encoded in Wallet Import Format. """
|
||||
return self.ledger.private_key_to_wif(self.private_key_bytes)
|
||||
|
||||
@property
|
||||
def address(self):
|
||||
""" The public key as a P2PKH 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."""
|
||||
if not 0 <= n < (1 << 32):
|
||||
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. """
|
||||
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):
|
||||
"""Return the key's identifier as 20 bytes."""
|
||||
return self.public_key.identifier()
|
||||
|
@ -222,9 +268,12 @@ class PrivateKey(_KeyBase):
|
|||
b'\0' + self.private_key_bytes
|
||||
)
|
||||
|
||||
def to_pem(self):
|
||||
return self.signing_key.to_pem()
|
||||
|
||||
|
||||
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)):
|
||||
raise TypeError('extended key must be raw bytes')
|
||||
if len(ekey) != 78:
|
||||
|
@ -236,7 +285,7 @@ def _from_extended_key(ledger, ekey):
|
|||
|
||||
if ekey[:4] == ledger.extended_public_key_prefix:
|
||||
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:
|
||||
if ekey[45] != 0:
|
||||
raise ValueError('invalid extended private key prefix byte')
|
||||
|
@ -254,6 +303,6 @@ def from_extended_key_string(ledger, ekey_str):
|
|||
xpub6BsnM1W2Y7qLMiuhi7f7dbAwQZ5Cz5gYJCRzTNainXzQXYjFwtuQXHd
|
||||
3qfi3t3KJtHxshXezfjft93w4UE7BGMtKwhqEHae3ZA7d823DVrL
|
||||
|
||||
return a PubKey or PrivateKey.
|
||||
return a PublicKey or PrivateKey.
|
||||
"""
|
||||
return _from_extended_key(ledger, Base58.decode_check(ekey_str))
|
||||
|
|
|
@ -10,11 +10,10 @@ from contextvars import ContextVar
|
|||
from typing import Tuple, List, Union, Callable, Any, Awaitable, Iterable, Dict, Optional
|
||||
from datetime import date
|
||||
|
||||
import ecdsa
|
||||
from prometheus_client import Gauge, Counter, Histogram
|
||||
from lbry.utils import LockWithMetrics
|
||||
|
||||
from .bip32 import PubKey
|
||||
from .bip32 import PublicKey, PrivateKey
|
||||
from .transaction import Transaction, Output, OutputScript, TXRefImmutable, Input
|
||||
from .constants import TXO_TYPES, CLAIM_TYPES
|
||||
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)")
|
||||
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_my_input = constraints.get('include_is_my_input', 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)
|
||||
if 'pubkey' in cols:
|
||||
for address in addresses:
|
||||
address['pubkey'] = PubKey(
|
||||
address['pubkey'] = PublicKey(
|
||||
self.ledger, address.pop('pubkey'), address.pop('chain_code'),
|
||||
address.pop('n'), address.pop('depth')
|
||||
)
|
||||
|
@ -1243,11 +1244,15 @@ class Database(SQLiteMixin):
|
|||
async def set_address_history(self, address, history):
|
||||
await self._set_address_history(address, history)
|
||||
|
||||
async def is_channel_key_used(self, wallet, key: ecdsa.SigningKey):
|
||||
channels = await self.get_txos(wallet, txo_type=TXO_TYPES['channel'])
|
||||
other_key_string = key.to_string()
|
||||
async def is_channel_key_used(self, account, key: PublicKey):
|
||||
channels = await self.get_txos(
|
||||
accounts=[account], txo_type=TXO_TYPES['channel'],
|
||||
no_tx=True, no_channel_info=True
|
||||
)
|
||||
other_key_bytes = key.pubkey_bytes
|
||||
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 False
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ from .transaction import Transaction, Output
|
|||
from .header import Headers, UnvalidatedHeaders
|
||||
from .checkpoints import HASHES
|
||||
from .constants import TXO_TYPES, CLAIM_TYPES, COIN, NULL_HASH32
|
||||
from .bip32 import PubKey, PrivateKey
|
||||
from .bip32 import PublicKey, PrivateKey
|
||||
from .coinselection import CoinSelector
|
||||
|
||||
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 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)
|
||||
if match:
|
||||
_, address_info = match
|
||||
|
|
|
@ -507,7 +507,7 @@ class BlockProcessor:
|
|||
channel_pub_key_bytes = channel_meta.channel.public_key_bytes
|
||||
if channel_pub_key_bytes:
|
||||
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:
|
||||
self.pending_channel_counts[signing_channel_hash] += 1
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import struct
|
||||
import hashlib
|
||||
import logging
|
||||
import typing
|
||||
from binascii import hexlify, unhexlify
|
||||
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.primitives.serialization import load_der_public_key
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
|
@ -27,6 +27,7 @@ from .constants import COIN, NULL_HASH32
|
|||
from .bcd_data_stream import BCDataStream
|
||||
from .hash import TXRef, TXRefImmutable
|
||||
from .util import ReadOnlyList
|
||||
from .bip32 import PrivateKey, PublicKey
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
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,
|
||||
sent_supports: Optional[int] = None, sent_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:
|
||||
super().__init__(tx_ref, position)
|
||||
self.amount = amount
|
||||
|
@ -234,7 +236,7 @@ class Output(InputOutput):
|
|||
self.sent_tips = sent_tips
|
||||
self.received_tips = received_tips
|
||||
self.channel = channel
|
||||
self.private_key = private_key
|
||||
self.private_key: PrivateKey = private_key
|
||||
self.purchase: 'Output' = None # txo containing purchase metadata
|
||||
self.purchased_claim: 'Output' = None # resolved claim pointed to by purchase
|
||||
self.purchase_receipt: 'Output' = None # txo representing purchase receipt for this claim
|
||||
|
@ -424,25 +426,24 @@ class Output(InputOutput):
|
|||
]
|
||||
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
|
||||
def is_signature_valid(encoded_signature, signature_digest, public_key_bytes):
|
||||
try:
|
||||
public_key = load_der_public_key(public_key_bytes, default_backend())
|
||||
public_key.verify(encoded_signature, signature_digest, ec.ECDSA(Prehashed(hashes.SHA256())))
|
||||
return True
|
||||
except (ValueError, InvalidSignature):
|
||||
pass
|
||||
return False
|
||||
def is_signature_valid(signature, digest, public_key_bytes):
|
||||
signature = cdata_to_der(deserialize_compact(signature))
|
||||
public_key = cPublicKey(public_key_bytes)
|
||||
is_valid = public_key.verify(signature, digest, None)
|
||||
if not is_valid: # try old way
|
||||
# ytsync signed claims don't seem to validate with coincurve
|
||||
try:
|
||||
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):
|
||||
return self.is_signature_valid(
|
||||
self.get_encoded_signature(),
|
||||
self.signable.signature,
|
||||
self.get_signature_digest(ledger),
|
||||
channel.claim.channel.public_key_bytes
|
||||
)
|
||||
|
@ -455,27 +456,27 @@ class Output(InputOutput):
|
|||
self.signable.signing_channel_hash,
|
||||
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()
|
||||
|
||||
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]
|
||||
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()
|
||||
|
||||
def clear_signature(self):
|
||||
self.channel = None
|
||||
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.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()
|
||||
return self.private_key
|
||||
|
||||
def is_channel_private_key(self, private_key: ecdsa.SigningKey):
|
||||
return self.claim.channel.public_key_bytes == private_key.get_verifying_key().to_der()
|
||||
def is_channel_private_key(self, private_key: PrivateKey):
|
||||
return self.claim.channel.public_key_bytes == private_key.public_key.pubkey_bytes
|
||||
|
||||
@classmethod
|
||||
def pay_claim_name_pubkey_hash(
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
from binascii import unhexlify
|
||||
|
||||
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.transaction import Transaction
|
||||
|
||||
|
@ -63,6 +64,32 @@ class AccountManagement(CommandTestCase):
|
|||
accounts = await self.daemon.jsonrpc_account_list(account_id, include_claims=True)
|
||||
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):
|
||||
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"):
|
||||
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):
|
||||
seed = self.account.seed
|
||||
keys = self.account.deterministic_channel_keys
|
||||
|
@ -188,33 +260,33 @@ class AccountManagement(CommandTestCase):
|
|||
self.assertTrue(channel1b.has_private_key)
|
||||
self.assertEqual(
|
||||
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.assertEqual(
|
||||
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
|
||||
current_known = keys.last_known
|
||||
next_key = await keys.generate_next_key()
|
||||
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
|
||||
next_key = await keys.generate_next_key()
|
||||
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
|
||||
channel3a = await self.channel_create('@foo3')
|
||||
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']
|
||||
self.assertTrue(channel3b.has_private_key)
|
||||
self.assertEqual(
|
||||
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
|
||||
|
|
|
@ -19,12 +19,6 @@ from lbry.crypto.hash import sha256
|
|||
|
||||
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):
|
||||
pieces = [
|
||||
|
@ -33,7 +27,7 @@ def verify(channel, data, signature, channel_hash=None):
|
|||
data
|
||||
]
|
||||
return Output.is_signature_valid(
|
||||
get_encoded_signature(signature['signature']),
|
||||
unhexlify(signature['signature']),
|
||||
sha256(b''.join(pieces)),
|
||||
channel.claim.channel.public_key_bytes
|
||||
)
|
||||
|
@ -1123,17 +1117,17 @@ class ChannelCommands(CommandTestCase):
|
|||
tx = await self.channel_update(claim_id, bid='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
|
||||
with self.assertRaisesRegex(
|
||||
InsufficientFundsError, "Not enough funds to cover this transaction."):
|
||||
await self.channel_create('@foo2', '9.0')
|
||||
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
|
||||
tx = await self.channel_create('@foo3', '5.981266')
|
||||
tx = await self.channel_create('@foo3', '5.981322')
|
||||
await self.assertBalance(self.account, '0.0')
|
||||
self.assertEqual(len(tx['outputs']), 1) # no change
|
||||
self.assertItemCount(await self.daemon.jsonrpc_channel_list(), 2)
|
||||
|
@ -1249,7 +1243,7 @@ class ChannelCommands(CommandTestCase):
|
|||
await daemon2.jsonrpc_channel_import(exported_data)
|
||||
channels = (await daemon2.jsonrpc_channel_list())['items']
|
||||
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
|
||||
with self.assertRaisesRegex(AssertionError, 'Cannot find private key for signing output.'):
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
from binascii import unhexlify
|
||||
|
||||
import ecdsa
|
||||
|
||||
from lbry.testcase import AsyncioTestCase
|
||||
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 import Ledger, Database, Headers, Transaction, Input, Output
|
||||
from lbry.schema.claim import Claim
|
||||
|
@ -27,10 +25,10 @@ def get_tx():
|
|||
|
||||
async def get_channel(claim_name='@foo'):
|
||||
seed = Mnemonic.mnemonic_to_seed(Mnemonic().make_seed(), '')
|
||||
bip32_key = PrivateKey.from_seed(Ledger, seed)
|
||||
signing_key = ecdsa.SigningKey.from_secret_exponent(bip32_key.secret_exponent(), ecdsa.SECP256k1)
|
||||
key = PrivateKey.from_seed(Ledger, seed)
|
||||
channel_key = key.child(KeyPath.CHANNEL).child(0)
|
||||
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])
|
||||
return channel_txo
|
||||
|
||||
|
@ -160,13 +158,10 @@ class TestValidateSignContent(AsyncioTestCase):
|
|||
some_content = "MEANINGLESS CONTENT AEE3353320".encode()
|
||||
timestamp_str = "1630564175"
|
||||
channel = await get_channel()
|
||||
stream = get_stream()
|
||||
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]
|
||||
self.assertTrue(Output.is_signature_valid(
|
||||
encoded_signature,
|
||||
unhexlify(signature.encode()),
|
||||
sha256(b''.join(pieces)),
|
||||
channel.claim.channel.public_key_bytes
|
||||
))
|
||||
|
|
Loading…
Reference in a new issue