Merge pull request #3450 from lbryio/deterministic_channel_keys

deterministic channel keys (requires wallet server re-sync)
This commit is contained in:
Lex Berezhny 2021-12-23 15:38:15 -05:00 committed by GitHub
commit 8076000c27
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 505 additions and 148 deletions

View file

@ -8,7 +8,6 @@ import time
import inspect import inspect
import typing import typing
import random import random
import hashlib
import tracemalloc import tracemalloc
from decimal import Decimal from decimal import Decimal
from urllib.parse import urlencode, quote from urllib.parse import urlencode, quote
@ -17,7 +16,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 +27,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
@ -2704,12 +2703,13 @@ class Daemon(metaclass=JSONRPCServerType):
name, claim, amount, claim_address, funding_accounts, funding_accounts[0] name, claim, amount, claim_address, funding_accounts, funding_accounts[0]
) )
txo = tx.outputs[0] txo = tx.outputs[0]
await txo.generate_channel_private_key() txo.set_channel_private_key(
await funding_accounts[0].generate_channel_private_key()
)
await tx.sign(funding_accounts) await tx.sign(funding_accounts)
if not preview: if not preview:
account.add_channel_private_key(txo.private_key)
wallet.save() wallet.save()
await self.broadcast_or_release(tx, blocking) await self.broadcast_or_release(tx, blocking)
self.component_manager.loop.create_task(self.storage.save_claims([self._old_get_temp_claim_info( self.component_manager.loop.create_task(self.storage.save_claims([self._old_get_temp_claim_info(
@ -2858,7 +2858,9 @@ class Daemon(metaclass=JSONRPCServerType):
new_txo = tx.outputs[0] new_txo = tx.outputs[0]
if new_signing_key: if new_signing_key:
await new_txo.generate_channel_private_key() new_txo.set_channel_private_key(
await funding_accounts[0].generate_channel_private_key()
)
else: else:
new_txo.private_key = old_txo.private_key new_txo.private_key = old_txo.private_key
@ -2867,7 +2869,6 @@ class Daemon(metaclass=JSONRPCServerType):
await tx.sign(funding_accounts) await tx.sign(funding_accounts)
if not preview: if not preview:
account.add_channel_private_key(new_txo.private_key)
wallet.save() wallet.save()
await self.broadcast_or_release(tx, blocking) await self.broadcast_or_release(tx, blocking)
self.component_manager.loop.create_task(self.storage.save_claims([self._old_get_temp_claim_info( self.component_manager.loop.create_task(self.storage.save_claims([self._old_get_temp_claim_info(
@ -3039,7 +3040,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=(',', ':')))
@ -3062,15 +3063,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

@ -17,8 +17,10 @@ from functools import partial
from lbry.wallet import WalletManager, Wallet, Ledger, Account, Transaction from lbry.wallet import WalletManager, Wallet, Ledger, Account, Transaction
from lbry.conf import Config from lbry.conf import Config
from lbry.wallet.util import satoshis_to_coins from lbry.wallet.util import satoshis_to_coins
from lbry.wallet.dewies import lbc_to_dewies
from lbry.wallet.orchstr8 import Conductor from lbry.wallet.orchstr8 import Conductor
from lbry.wallet.orchstr8.node import BlockchainNode, WalletNode, HubNode from lbry.wallet.orchstr8.node import BlockchainNode, WalletNode, HubNode
from lbry.schema.claim import Claim
from lbry.extras.daemon.daemon import Daemon, jsonrpc_dumps_pretty from lbry.extras.daemon.daemon import Daemon, jsonrpc_dumps_pretty
from lbry.extras.daemon.components import Component, WalletComponent from lbry.extras.daemon.components import Component, WalletComponent
@ -506,6 +508,19 @@ class CommandTestCase(IntegrationTestCase):
return self.sout(tx) return self.sout(tx)
return tx return tx
async def create_nondeterministic_channel(self, name, price, pubkey_bytes, daemon=None):
account = (daemon or self.daemon).wallet_manager.default_account
claim_address = await 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(price),
claim_address, [self.account], self.account
)
await tx.sign([self.account])
await (daemon or self.daemon).broadcast_or_release(tx, False)
return self.sout(tx)
def create_upload_file(self, data, prefix=None, suffix=None): def create_upload_file(self, data, prefix=None, suffix=None):
file_path = tempfile.mktemp(prefix=prefix or "tmp", suffix=suffix or "", dir=self.daemon.conf.upload_dir) file_path = tempfile.mktemp(prefix=prefix or "tmp", suffix=suffix or "", dir=self.daemon.conf.upload_dir)
with open(file_path, 'w+b') as file: with open(file_path, 'w+b') as file:

View file

@ -10,7 +10,7 @@ from .wallet import Wallet, WalletStorage, TimestampedPreferences, ENCRYPT_ON_DI
from .manager import WalletManager from .manager import WalletManager
from .network import Network from .network import Network
from .ledger import Ledger, RegTestLedger, TestNetLedger, BlockHeightEvent from .ledger import Ledger, RegTestLedger, TestNetLedger, BlockHeightEvent
from .account import Account, AddressManager, SingleKey, HierarchicalDeterministic from .account import Account, AddressManager, SingleKey, HierarchicalDeterministic, DeterministicChannelKeyManager
from .transaction import Transaction, Output, Input from .transaction import Transaction, Output, Input
from .script import OutputScript, InputScript from .script import OutputScript, InputScript
from .database import SQLiteMixin, Database from .database import SQLiteMixin, Database

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
@ -34,6 +33,44 @@ def validate_claim_id(claim_id):
raise Exception("Claim id is not hex encoded") raise Exception("Claim id is not hex encoded")
class DeterministicChannelKeyManager:
def __init__(self, account: 'Account'):
self.account = account
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_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[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) -> PrivateKey:
db = self.account.ledger.db
while True:
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) -> PrivateKey:
return self.cache.get(pubkey_hash)
class AddressManager: class AddressManager:
name: str name: str
@ -79,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):
@ -119,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):
@ -133,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:
@ -193,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):
@ -202,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:
@ -224,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,
@ -234,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
@ -245,13 +279,14 @@ 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)
self.address_managers = {am.chain_number: am for am in (self.receiving, self.change)} self.address_managers = {am.chain_number: am for am in (self.receiving, self.change)}
self.channel_keys = channel_keys self.channel_keys = channel_keys
self.deterministic_channel_keys = DeterministicChannelKeyManager(self)
ledger.add_account(self) ledger.add_account(self)
wallet.add_account(self) wallet.add_account(self)
@ -266,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
@ -449,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):
@ -520,33 +555,30 @@ class Account:
return tx return tx
def add_channel_private_key(self, private_key): async def generate_channel_private_key(self):
public_key_bytes = private_key.get_verifying_key().to_der() return await self.deterministic_channel_keys.generate_next_key()
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): 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) -> 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)
)
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. """
@ -126,6 +137,10 @@ class PubKey(_KeyBase):
self.pubkey_bytes self.pubkey_bytes
) )
def verify(self, signature, data):
""" Produce a signature for piece of data by double hashing it and signing the hash. """
return self.verifying_key.verify(signature, data, hasher=double_sha256)
class PrivateKey(_KeyBase): class PrivateKey(_KeyBase):
"""A BIP32 private key.""" """A BIP32 private key."""
@ -134,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)
@ -142,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):
@ -154,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()
@ -184,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')
@ -207,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()
@ -218,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:
@ -232,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')
@ -250,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

@ -9,10 +9,11 @@ from dataclasses import dataclass
from contextvars import ContextVar 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
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
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
@ -975,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)
@ -1201,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')
) )
@ -1241,6 +1244,18 @@ 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, 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:
claim = channel.can_decode_claim
if claim and claim.channel.public_key_bytes == other_key_bytes:
return True
return False
@staticmethod @staticmethod
def constrain_purchases(constraints): def constrain_purchases(constraints):
accounts = constraints.pop('accounts', None) accounts = constraints.pop('accounts', None)

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
@ -470,6 +470,7 @@ class Ledger(metaclass=LedgerRegistry):
for address_manager in account.address_managers.values(): for address_manager in account.address_managers.values():
await self.subscribe_addresses(address_manager, await address_manager.get_addresses()) await self.subscribe_addresses(address_manager, await address_manager.get_addresses())
await account.ensure_address_gap() await account.ensure_address_gap()
await account.deterministic_channel_keys.ensure_cache_primed()
async def unsubscribe_account(self, account: Account): async def unsubscribe_account(self, account: Account):
for address in await account.get_addresses(): for address in await account.get_addresses():
@ -550,6 +551,7 @@ class Ledger(metaclass=LedgerRegistry):
) )
remote_history_txids = {txid for txid, _ in remote_history} remote_history_txids = {txid for txid, _ in remote_history}
async for tx in self.request_synced_transactions(to_request, remote_history_txids, address): async for tx in self.request_synced_transactions(to_request, remote_history_txids, address):
self.maybe_has_channel_key(tx)
pending_synced_history[tx_indexes[tx.id]] = f"{tx.id}:{tx.height}:" pending_synced_history[tx_indexes[tx.id]] = f"{tx.id}:{tx.height}:"
if len(pending_synced_history) % 100 == 0: if len(pending_synced_history) % 100 == 0:
log.info("Syncing address %s: %d/%d", address, len(pending_synced_history), len(to_request)) log.info("Syncing address %s: %d/%d", address, len(pending_synced_history), len(to_request))
@ -617,6 +619,12 @@ class Ledger(metaclass=LedgerRegistry):
tx.is_verified = merkle_root == header['merkle_root'] tx.is_verified = merkle_root == header['merkle_root']
return tx return tx
def maybe_has_channel_key(self, tx):
for txo in tx._outputs:
if txo.can_decode_claim and txo.claim.is_channel:
for account in self.accounts:
account.deterministic_channel_keys.maybe_generate_deterministic_key_for_channel(txo)
async def request_transactions(self, to_request: Tuple[Tuple[str, int], ...], cached=False): async def request_transactions(self, to_request: Tuple[Tuple[str, int], ...], cached=False):
batches = [[]] batches = [[]]
remote_heights = {} remote_heights = {}

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,14 +1,11 @@
import struct import struct
import hashlib
import logging import logging
import typing import typing
import asyncio
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 cryptography.hazmat.backends import default_backend from coincurve.ecdsa import deserialize_compact, cdata_to_der
from cryptography.hazmat.primitives.serialization import load_der_public_key
from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import Prehashed from cryptography.hazmat.primitives.asymmetric.utils import Prehashed
@ -28,6 +25,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
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from lbry.wallet.account import Account from lbry.wallet.account import Account
@ -222,7 +220,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
@ -235,7 +234,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
@ -425,25 +424,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
) )
@ -456,29 +454,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()
async def generate_channel_private_key(self): def set_channel_private_key(self, private_key: PrivateKey):
self.private_key = await asyncio.get_event_loop().run_in_executor( self.private_key = private_key
None, ecdsa.SigningKey.generate, ecdsa.SECP256k1, None, hashlib.sha256 self.claim.channel.public_key_bytes = private_key.public_key.pubkey_bytes
)
self.claim.channel.public_key_bytes = self.private_key.get_verifying_key().to_der()
self.script.generate() self.script.generate()
return self.private_key return self.private_key
def is_channel_private_key(self, private_key): 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

@ -18,7 +18,7 @@ ignore=words,server,rpc,schema,winpaths.py,migrator,undecorated.py
max-parents=10 max-parents=10
max-args=10 max-args=10
max-line-length=120 max-line-length=120
good-names=T,t,n,i,j,k,x,y,s,f,d,h,c,e,op,db,tx,io,cachedproperty,log,id,r,iv,ts,l good-names=T,t,n,i,j,k,x,y,s,f,d,h,c,e,op,db,tx,io,cachedproperty,log,id,r,iv,ts,l,pk
valid-metaclass-classmethod-first-arg=mcs valid-metaclass-classmethod-first-arg=mcs
disable= disable=
c-extension-no-member, c-extension-no-member,

View file

@ -1,5 +1,8 @@
from binascii import unhexlify
from lbry.testcase import CommandTestCase from lbry.testcase import CommandTestCase
from lbry.wallet.dewies import dewies_to_lbc from lbry.wallet.dewies import dewies_to_lbc
from lbry.wallet.account import DeterministicChannelKeyManager
def extract(d, keys): def extract(d, keys):
@ -60,15 +63,30 @@ class AccountManagement(CommandTestCase):
self.assertEqual(accounts['items'][0]['name'], 'recreated account') self.assertEqual(accounts['items'][0]['name'], 'recreated account')
async def test_wallet_migration(self): 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 # null certificates should get deleted
await self.channel_create('@foo1') self.account.channel_keys = {
await self.channel_create('@foo2') new_id: 'not valid key',
await self.channel_create('@foo3') 'foo': 'bar',
keys = list(self.account.channel_keys.keys()) }
self.account.channel_keys[keys[0]] = None
self.account.channel_keys[keys[1]] = "some invalid junk"
await self.account.maybe_migrate_certificates() await self.account.maybe_migrate_certificates()
self.assertEqual(list(self.account.channel_keys.keys()), [keys[2]]) 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']])
@ -174,3 +192,100 @@ class AccountManagement(CommandTestCase):
bad_address = address[0:20] + '9999999' + address[27:] bad_address = address[0:20] + '9999999' + address[27:]
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 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', '1.0', unhexlify(
'3056301006072a8648ce3d020106052b8104000a034200049ae7283f3f6723e0a1'
'66b7e19e1d1167f6dc5f4af61b4a58066a0d2a8bed2b35c66bccb4ec3eba316b16'
'a97a6d6a4a8effd29d748901bb9789352519cd00b13d'
))
await self.confirm_tx(channel1['txid'])
# deterministic channel
channel2 = await self.channel_create('@foo2')
await self.stream_create('stream-in-channel1', '0.01', channel_id=self.get_claim_id(channel1))
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
# create two channels and make sure they have different keys
channel1a = await self.channel_create('@foo1')
channel2a = await self.channel_create('@foo2')
self.assertNotEqual(
channel1a['outputs'][0]['value']['public_key'],
channel2a['outputs'][0]['value']['public_key'],
)
# start another daemon from the same seed
self.daemon2 = await self.add_daemon(seed=seed)
channel2b, channel1b = (await self.daemon2.jsonrpc_channel_list())['items']
# both daemons end up with the same channel signing keys automagically
self.assertTrue(channel1b.has_private_key)
self.assertEqual(
channel1a['outputs'][0]['value']['public_key_id'],
channel1b.private_key.address
)
self.assertTrue(channel2b.has_private_key)
self.assertEqual(
channel2a['outputs'][0]['value']['public_key_id'],
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.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.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.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'],
channel3b.private_key.address
)
# channel key cache re-populated after simulated restart
# reset cache
self.account.deterministic_channel_keys = DeterministicChannelKeyManager(self.account)
channel3c, channel2c, channel1c = (await self.daemon.jsonrpc_channel_list())['items']
self.assertFalse(channel1c.has_private_key)
self.assertFalse(channel2c.has_private_key)
self.assertFalse(channel3c.has_private_key)
# repopulate cache
await self.account.deterministic_channel_keys.ensure_cache_primed()
self.assertEqual(self.account.deterministic_channel_keys.last_known, keys.last_known)
channel3c, channel2c, channel1c = (await self.daemon.jsonrpc_channel_list())['items']
self.assertTrue(channel1c.has_private_key)
self.assertTrue(channel2c.has_private_key)
self.assertTrue(channel3c.has_private_key)

View file

@ -1,5 +1,6 @@
import asyncio import asyncio
import json import json
from binascii import unhexlify
from lbry.wallet import ENCRYPT_ON_DISK from lbry.wallet import ENCRYPT_ON_DISK
from lbry.error import InvalidPasswordError from lbry.error import InvalidPasswordError
@ -282,8 +283,19 @@ class WalletEncryptionAndSynchronization(CommandTestCase):
) )
# Channel Certificate # Channel Certificate
channel = await daemon2.jsonrpc_channel_create('@foo', '0.1') # non-deterministic channel
await self.confirm_tx(channel.id, self.daemon2.ledger) self.daemon2.wallet_manager.default_account.channel_keys['mqs77XbdnuxWN4cXrjKbSoGLkvAHa4f4B8'] = (
'-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEIBZRTZ7tHnYCH3IE9mCo95'
'466L/ShYFhXGrjmSMFJw8eoAcGBSuBBAAK\noUQDQgAEmucoPz9nI+ChZrfhnh'
'0RZ/bcX0r2G0pYBmoNKovtKzXGa8y07D66MWsW\nqXptakqO/9KddIkBu5eJNS'
'UZzQCxPQ==\n-----END EC PRIVATE KEY-----\n'
)
channel = await self.create_nondeterministic_channel('@foo', '0.1', unhexlify(
'3056301006072a8648ce3d020106052b8104000a034200049ae7283f3f6723e0a1'
'66b7e19e1d1167f6dc5f4af61b4a58066a0d2a8bed2b35c66bccb4ec3eba316b16'
'a97a6d6a4a8effd29d748901bb9789352519cd00b13d'
), self.daemon2)
await self.confirm_tx(channel['txid'], self.daemon2.ledger)
# both daemons will have the channel but only one has the cert so far # both daemons will have the channel but only one has the cert so far
self.assertItemCount(await daemon.jsonrpc_channel_list(), 1) self.assertItemCount(await daemon.jsonrpc_channel_list(), 1)

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

@ -1721,7 +1721,7 @@ def generate_signed_legacy(address: bytes, output: Output):
claim.SerializeToString(), claim.SerializeToString(),
output.claim_hash[::-1] output.claim_hash[::-1]
])) ]))
signature = output.private_key.sign_digest_deterministic(digest, hashfunc=hashlib.sha256) signature = output.private_key.sign_compact(digest)
claim.publisherSignature.version = 1 claim.publisherSignature.version = 1
claim.publisherSignature.signatureType = 1 claim.publisherSignature.signatureType = 1
claim.publisherSignature.signature = signature claim.publisherSignature.signature = signature

View file

@ -32,7 +32,9 @@ class BasicTransactionTest(IntegrationTestCase):
channel_txo = Output.pay_claim_name_pubkey_hash( channel_txo = Output.pay_claim_name_pubkey_hash(
l2d('1.0'), '@bar', channel, self.account.ledger.address_to_hash160(address1) l2d('1.0'), '@bar', channel, self.account.ledger.address_to_hash160(address1)
) )
await channel_txo.generate_channel_private_key() channel_txo.set_channel_private_key(
await self.account.generate_channel_private_key()
)
channel_txo.script.generate() channel_txo.script.generate()
channel_tx = await Transaction.create([], [channel_txo], [self.account], self.account) channel_tx = await Transaction.create([], [channel_txo], [self.account], self.account)

View file

@ -138,9 +138,7 @@ class TestTypesV1Compatibility(TestCase):
channel = cert.channel channel = cert.channel
self.assertEqual( self.assertEqual(
channel.public_key, channel.public_key,
'3056301006072a8648ce3d020106052b8104000a034200043878b1edd4a1373149909ef03f4339f6da9c2b' '033878b1edd4a1373149909ef03f4339f6da9c2bd2214c040fd2e530463ffe6609'
'd2214c040fd2e530463ffe66098eca14fc70b50ff3aefd106049a815f595ed5a13eda7419ad78d9ed7ae47'
'3f17'
) )
def test_unsigned_with_fee(self): def test_unsigned_with_fee(self):

View file

@ -1,7 +1,11 @@
import asyncio import asyncio
from binascii import hexlify from binascii import hexlify
from lbry.testcase import AsyncioTestCase from lbry.testcase import AsyncioTestCase
from lbry.wallet import Wallet, Ledger, Database, Headers, Account, SingleKey, HierarchicalDeterministic from lbry.wallet import (
Wallet, Ledger, Database, Headers,
Account, SingleKey, HierarchicalDeterministic,
DeterministicChannelKeyManager
)
class TestAccount(AsyncioTestCase): class TestAccount(AsyncioTestCase):

View file

@ -1,7 +1,7 @@
from binascii import unhexlify, hexlify from binascii import unhexlify, hexlify
from lbry.testcase import AsyncioTestCase from lbry.testcase import AsyncioTestCase
from lbry.wallet.bip32 import PubKey, PrivateKey, from_extended_key_string from lbry.wallet.bip32 import PublicKey, PrivateKey, from_extended_key_string
from lbry.wallet import Ledger, Database, Headers from lbry.wallet import Ledger, Database, Headers
from tests.unit.wallet.key_fixtures import expected_ids, expected_privkeys, expected_hardened_privkeys from tests.unit.wallet.key_fixtures import expected_ids, expected_privkeys, expected_hardened_privkeys
@ -11,24 +11,24 @@ class BIP32Tests(AsyncioTestCase):
def test_pubkey_validation(self): def test_pubkey_validation(self):
with self.assertRaisesRegex(TypeError, 'chain code must be raw bytes'): with self.assertRaisesRegex(TypeError, 'chain code must be raw bytes'):
PubKey(None, None, 1, None, None, None) PublicKey(None, None, 1, None, None, None)
with self.assertRaisesRegex(ValueError, 'invalid chain code'): with self.assertRaisesRegex(ValueError, 'invalid chain code'):
PubKey(None, None, b'abcd', None, None, None) PublicKey(None, None, b'abcd', None, None, None)
with self.assertRaisesRegex(ValueError, 'invalid child number'): with self.assertRaisesRegex(ValueError, 'invalid child number'):
PubKey(None, None, b'abcd'*8, -1, None, None) PublicKey(None, None, b'abcd'*8, -1, None, None)
with self.assertRaisesRegex(ValueError, 'invalid depth'): with self.assertRaisesRegex(ValueError, 'invalid depth'):
PubKey(None, None, b'abcd'*8, 0, 256, None) PublicKey(None, None, b'abcd'*8, 0, 256, None)
with self.assertRaisesRegex(TypeError, 'pubkey must be raw bytes'): with self.assertRaisesRegex(TypeError, 'pubkey must be raw bytes'):
PubKey(None, None, b'abcd'*8, 0, 255, None) PublicKey(None, None, b'abcd'*8, 0, 255, None)
with self.assertRaisesRegex(ValueError, 'pubkey must be 33 bytes'): with self.assertRaisesRegex(ValueError, 'pubkey must be 33 bytes'):
PubKey(None, b'abcd', b'abcd'*8, 0, 255, None) PublicKey(None, b'abcd', b'abcd'*8, 0, 255, None)
with self.assertRaisesRegex(ValueError, 'invalid pubkey prefix byte'): with self.assertRaisesRegex(ValueError, 'invalid pubkey prefix byte'):
PubKey( PublicKey(
None, None,
unhexlify('33d1a3dc8155673bc1e2214fa493ccc82d57961b66054af9b6b653ac28eeef3ffe'), unhexlify('33d1a3dc8155673bc1e2214fa493ccc82d57961b66054af9b6b653ac28eeef3ffe'),
b'abcd'*8, 0, 255, None b'abcd'*8, 0, 255, None
) )
pubkey = PubKey( # success pubkey = PublicKey( # success
None, None,
unhexlify('03d1a3dc8155673bc1e2214fa493ccc82d57961b66054af9b6b653ac28eeef3ffe'), unhexlify('03d1a3dc8155673bc1e2214fa493ccc82d57961b66054af9b6b653ac28eeef3ffe'),
b'abcd'*8, 0, 1, None b'abcd'*8, 0, 1, None
@ -37,7 +37,7 @@ class BIP32Tests(AsyncioTestCase):
pubkey.child(-1) pubkey.child(-1)
for i in range(20): for i in range(20):
new_key = pubkey.child(i) new_key = pubkey.child(i)
self.assertIsInstance(new_key, PubKey) self.assertIsInstance(new_key, PublicKey)
self.assertEqual(hexlify(new_key.identifier()), expected_ids[i]) self.assertEqual(hexlify(new_key.identifier()), expected_ids[i])
async def test_private_key_validation(self): async def test_private_key_validation(self):
@ -60,7 +60,7 @@ class BIP32Tests(AsyncioTestCase):
self.assertEqual( self.assertEqual(
ec_point[1], 86198965946979720220333266272536217633917099472454294641561154971209433250106 ec_point[1], 86198965946979720220333266272536217633917099472454294641561154971209433250106
) )
self.assertEqual('bUDcmraBp2zCV3QWmVVeQaEgepbs1b2gC9', private_key.address()) self.assertEqual('bUDcmraBp2zCV3QWmVVeQaEgepbs1b2gC9', private_key.address)
with self.assertRaisesRegex(ValueError, 'invalid BIP32 private key child number'): with self.assertRaisesRegex(ValueError, 'invalid BIP32 private key child number'):
private_key.child(-1) private_key.child(-1)
self.assertIsInstance(private_key.child(PrivateKey.HARDENED), PrivateKey) self.assertIsInstance(private_key.child(PrivateKey.HARDENED), PrivateKey)
@ -100,5 +100,5 @@ class BIP32Tests(AsyncioTestCase):
ledger, ledger,
'xpub661MyMwAqRbcF84AR8yfHoMzf4S2ct6mPJtvBtvNeyN9hBHuZ6uGJszkTSn5fQUCdz3XU17eBzFeAUwV6f' 'xpub661MyMwAqRbcF84AR8yfHoMzf4S2ct6mPJtvBtvNeyN9hBHuZ6uGJszkTSn5fQUCdz3XU17eBzFeAUwV6f'
'iW44g14WF52fYC5J483wqQ5ZP', 'iW44g14WF52fYC5J483wqQ5ZP',
), PubKey ), PublicKey
) )

View file

@ -2,10 +2,13 @@ from binascii import unhexlify
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, KeyPath
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
from lbry.crypto.hash import sha256 from lbry.crypto.hash import sha256
def get_output(amount=CENT, pubkey_hash=NULL_HASH32): def get_output(amount=CENT, pubkey_hash=NULL_HASH32):
return Transaction() \ return Transaction() \
.add_outputs([Output.pay_pubkey_hash(amount, pubkey_hash)]) \ .add_outputs([Output.pay_pubkey_hash(amount, pubkey_hash)]) \
@ -21,8 +24,11 @@ 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(), '')
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 = Output.pay_claim_name_pubkey_hash(CENT, claim_name, Claim(), b'abc')
await channel_txo.generate_channel_private_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
@ -114,6 +120,109 @@ class TestValidatingOldSignatures(AsyncioTestCase):
self.assertTrue(stream.is_signed_by(channel, ledger)) self.assertTrue(stream.is_signed_by(channel, ledger))
def test_another_signed_claim_made_by_ytsync(self):
stream_tx = Transaction(unhexlify(
b'010000000185870fabdd6bd2d57749afebc0b239e8d0ebeb6f3647d6cfcabd5ea2200ac632010000006b4'
b'83045022100877c86de154e39f21959bc2157865071924adb7930a7a8910714f27398cd2689022074270f'
b'074ae260fff319d5e0c030691821bc75b82ff0179898ac3eaeda4123eb01210200328f7f001f22ea25d72'
b'ba37379e3065020c4d8371d9199dc4e3770084e26b9ffffffff0240420f0000000000fdcc05b527746865'
b'2d637269746963616c2d6e6565642d666f722d696e646570656e64656e742d6d656469614d85050191bba'
b'd064bdc455b9ebddeeb559686b13f027615384ec7c9d981c3c21a6e3d723a654e86bd707d21174c4f697f'
b'5080cf367a3b2dfc059e6cc14a962631df69b9886f4d8b97cb339b14633966fd5ac7d75edacdf30ac5010'
b'a90010a304af34d1c1467ebfc8785e2a49c7d5bec3cc6db94db858f1dcf95e4256564fba586d6e01f496d'
b'f2a34344e021d2725ffd12197468652d637269746963616c2d6e6565642d666f722e6d703418ee97eac10'
b'22209766964656f2f6d70343230ba13e6b667a9acef7e1b1caa88b9eb1d4680dea84b1d3e838266595805'
b'ab3343855c20af35012f942ce0d5111ce080331a1f436f7079726967687465642028636f6e74616374207'
b'075626c69736865722928e2e3c98d065a0908800f10b80818f314423954686520437269746963616c204e'
b'65656420666f7220496e646570656e64656e74204d65646961207c20476c656e6e20477265656e77616c6'
b'44af006496e636c7564657320616e20696e74726f64756374696f6e20627920546f6d20576f6f64732e20'
b'5265636f7264656420696e204c616b65204a61636b736f6e2c2054657861732c206f6e20446563656d626'
b'57220342c20323032312e0a0a526f6e205061756c27732074776f2063616d706169676e7320666f722070'
b'7265736964656e7420283230303820616e64203230313229207765726520776174657273686564206d6f6'
b'd656e747320666f72206c6962657274792d6d696e6465642070656f706c652061726f756e642074686520'
b'776f726c642e205468652022526f6e205061756c205265766f6c7574696f6e22e2809463656e746572656'
b'42061726f756e642068697320756e64696c75746564206d657373616765206f662070656163652c207072'
b'6f70657274792c20616e64206d61726b657473e280946368616e6765642074686520776179206d696c6c6'
b'96f6e732074686f756768742061626f75742074686520416d65726963616e20656d7069726520616e6420'
b'74686520416d65726963616e2066696e616e6369616c2073797374656d2e2044722e205061756c2773206'
b'66f637573206f6e2063656e7472616c2062616e6b696e6720616e6420666f726569676e20706f6c696379'
b'2063617567687420706f6c6974696369616e7320616e642070756e64697473206f66662067756172642c2'
b'0666f7263696e67207468656d20746f20736372616d626c6520666f72206578706c616e6174696f6e7320'
b'6f66206f7572204d6964646c65204561737420706f6c69637920616e6420536f766965742d7374796c652'
b'063656e7472616c20706c616e6e696e6720617420746865204665642e20506f6c697469637320696e2041'
b'6d657269636120686173206e6f74206265656e207468652073616d652073696e636520746865202247697'
b'56c69616e69206d6f6d656e742220616e642022456e6420746865204665642e222054686520526f6e2050'
b'61756c205265766f6c7574696f6e2077617320626f7468206120706f6c69746963616c20616e642063756'
b'c747572616c207068656e6f6d656e6f6e2e0a0a303a303020496e74726f64756374696f6e20627920546f'
b'6d20576f6f64730a343a323720476c656e6e20477265656e77616c640a2e2e2e0a68747470733a2f2f777'
b'7772e796f75747562652e636f6d2f77617463683f763d4e4b70706d52467673453052292a276874747073'
b'3a2f2f7468756d626e61696c732e6c6272792e636f6d2f4e4b70706d5246767345305a046e6577735a096'
b'3617468656472616c5a0f636f72706f72617465206d656469615a08637269746963616c5a0f676c656e6e'
b'20677265656e77616c645a0b696e646570656e64656e745a0a6a6f75726e616c69736d5a056d656469615'
b'a056d697365735a08706f6c69746963735a0a70726f706167616e64615a08726f6e207061756c5a057472'
b'757468620208016d7576a9140969964db5b5744e2d2d0de797f5904efc80d02188acc8814200000000001'
b'976a91439086597f9cfc066f4749b8bb245bf561714fda888ac00000000'
))
stream = stream_tx.outputs[0]
channel_tx = Transaction(unhexlify(
b'01000000011d47b91b409b317e427adb87ec4b0bfc9fad2abf6ec3296f41918e4b3cb9d4e7010000006a4'
b'7304402205e53ef7fc643ed00f0240dd1c3302b82141f481ed071cbcdd6b6ec6166ffd4e002203eb28ce6'
b'39f80253f66ff3bf45288a60133d7f5625217d1ecf3b57da440b559f012103b852d61074eb995b702a800'
b'f284e937ece4fea7f023beb70e6b0d1bff36d64b9ffffffff0240420f0000000000fdde01b506406d6973'
b'65734db801001299010a583056301006072a8648ce3d020106052b8104000a034200047ddb1d639d7bdd0'
b'953d9ab0bf9e971a632f85f9823c1d85780aa3e0a702b503c2962d00f67360e803514bf5864710925aacb'
b'effd9597532c7e60eb21b4e3fd03223d2a3b68747470733a2f2f7468756d626e61696c732e6c6272792e6'
b'36f6d2f62616e6e65722d55436d54362d43684b7061694956753266684549734e7451420a6d697365736d'
b'656469614ad401466561747572656420766964656f732066726f6d20746865204d6973657320496e73746'
b'9747574652e20546865204d6973657320496e737469747574652070726f6d6f7465732041757374726961'
b'6e2065636f6e6f6d6963732c2066726565646f6d2c20616e6420706561636520696e20746865206c69626'
b'572616c20696e74656c6c65637475616c20747261646974696f6e206f66204c756477696720766f6e204d'
b'69736573207468726f7567682072657365617263682c207075626c697368696e672c20616e64206564756'
b'36174696f6e2e52362a3468747470733a2f2f7468756d626e61696c732e6c6272792e636f6d2f55436d54'
b'362d43684b7061694956753266684549734e74516d7576a914cd77ded2400e6569f03a2580244bb395f95'
b'f91fc88ac344ab701000000001976a914cabdbfce726d2fda92ffe0041a4303f6c6c34cda88ac00000000'
))
channel = channel_tx.outputs[0]
ledger = Ledger({
'db': Database(':memory:'),
'headers': Headers(':memory:')
})
self.assertTrue(stream.is_signed_by(channel, ledger))
def test_claim_signed_using_ecdsa_validates_with_coincurve(self):
channel_tx = Transaction(unhexlify(
"0100000001b91d829283c0d80cb8113d5f36b6da3dfe9df3e783f158bfb3fd1b2b178d7fc9010000006b48"
"3045022100f4e2b4ee38388c3d3a62f4b12fdd413f6f140168e85884bbeb33a3f2d3159ef502201721200f"
"4a4f3b87484d4f47c9054e31cd3ba451dd3886a7f9f854893e7c8cf90121023f9e906e0c120f3bf74feb40"
"f01ddeafbeb1856d91938c3bef25bed06767247cffffffff0200e1f5050000000081b505406368616e4c5d"
"00125a0a583056301006072a8648ce3d020106052b8104000a03420004d7fa13fd8e57f3a0b878eaaf3d17"
"9144d25ddbe4a3e4440a661f51b4134c6a13c9c98678ff8411932e60fd97d7baf03ea67ebcc21097230cfb"
"2241348aadb55e6d7576a9149c6d700f89c77f0e8c650ba05656f8f2392782d388acf47c95350000000019"
"76a914d9502233e0e1fc76e13e36c546f704c3124d5eaa88ac00000000"
))
channel = channel_tx.outputs[0]
stream_tx = Transaction(unhexlify(
"010000000116a1d90763f2e3a2348c7fb438a23f232b15e3ffe3f058c3b2ab52c8bed8dcb5010000006b48"
"30450221008f38561b3a16944c63b4f4f1562f1efe1b2060f31d249e234003ee5e3461756f02205773c99e"
"83c968728e4f2433a13871c6ad23f6c10368ac52fa62a09f3f7ef5fd012102597f39845b98e2415b777aa0"
"3849d346d287af7970deb05f11214b3418ae9d82ffffffff0200e1f50500000000fd0c01b505636c61696d"
"4ce8012e6e40fa5fee1b915af3b55131dcbcebee34ab9148292b084ce3741f2e0db49783f3d854ac885f2b"
"6304a76ef7048046e338dd414ba4c64e8468651768ffaaf550c8560637ac8c477ea481ac2a9264097240f4"
"ab0a90010a8d010a3056bf5dbae43f77a63d075b0f2ae9c7c3e3098db93779c7f9840da0f4db9c2f8c8454"
"f4edd1373e2b64ee2e68350d916e120b746d706c69647879363171180322186170706c69636174696f6e2f"
"6f637465742d73747265616d3230f293f5acf4310562d4a41f6620167fe6d83761a98d36738908ce5c8776"
"1642710e55352a396276a42eda92ff5856f46f6d7576a91434bd3dc4c45cc0635eb2ad5da658727e5442ca"
"0f88ace82f902f000000001976a91427b27c89eaebf68d063c107241584c07e5a6ccc688ac00000000"
))
stream = stream_tx.outputs[0]
ledger = Ledger({'db': Database(':memory:'), 'headers': Headers(':memory:')})
self.assertTrue(stream.is_signed_by(channel, ledger))
class TestValidateSignContent(AsyncioTestCase): class TestValidateSignContent(AsyncioTestCase):
@ -121,13 +230,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
)) ))