From 1ec8f0b0b4b32338dae33d14443f7dc6c7ab5ac2 Mon Sep 17 00:00:00 2001 From: Lex Berezhny Date: Mon, 18 Mar 2019 00:59:13 -0400 Subject: [PATCH] wip --- lbrynet/extras/wallet/manager.py | 15 +- lbrynet/extras/wallet/transaction.py | 27 +-- lbrynet/schema/claim.py | 227 +++++++++++++-------- lbrynet/schema/compat.py | 9 +- tests/unit/schema/test_claim_from_bytes.py | 3 + 5 files changed, 166 insertions(+), 115 deletions(-) diff --git a/lbrynet/extras/wallet/manager.py b/lbrynet/extras/wallet/manager.py index 577b39847..42b628706 100644 --- a/lbrynet/extras/wallet/manager.py +++ b/lbrynet/extras/wallet/manager.py @@ -6,7 +6,7 @@ from binascii import unhexlify from datetime import datetime from typing import Optional -from lbrynet.schema.constants import SECP256k1 +from lbrynet.schema.claim import Claim from torba.client.basemanager import BaseWalletManager from torba.rpc.jsonrpc import CodeMessageError @@ -394,15 +394,11 @@ class LbryWalletManager(BaseWalletManager): def get_utxos(account: BaseAccount): return account.get_utxos() - async def claim_name(self, account, name, amount, claim_dict, certificate=None, claim_address=None): - claim = ClaimDict.load_dict(claim_dict) + async def claim_name(self, account, name, amount, claim: Claim, certificate=None, claim_address=None): if not claim_address: claim_address = await account.receiving.get_or_create_usable_address() if certificate: - claim = claim.sign( - certificate.private_key, claim_address, certificate.claim_id, curve=SECP256k1, name=name, - force_detached=False # TODO: delete it and make True default everywhere when its out - ) + claim = claim.sign(certificate.claim_id, certificate.private_key) existing_claims = await account.get_claims( claim_name_type__any={'is_claim': 1, 'is_update': 1}, # exclude is_supports claim_name=name @@ -417,9 +413,12 @@ class LbryWalletManager(BaseWalletManager): ) else: raise NameError(f"More than one other claim exists with the name '{name}'.") + if certificate: + claim.sign(certificate.claim_id, certificate.private_key, tx.inputs[0].txo_ref.id.encode()) + tx._reset() await account.ledger.broadcast(tx) await self.old_db.save_claims([self._old_get_temp_claim_info( - tx, tx.outputs[0], claim_address, claim_dict, name, dewies_to_lbc(amount) + tx, tx.outputs[0], claim_address, claim, name, dewies_to_lbc(amount) )]) # TODO: release reserved tx outputs in case anything fails by this point return tx diff --git a/lbrynet/extras/wallet/transaction.py b/lbrynet/extras/wallet/transaction.py index d27c09fa9..a60619918 100644 --- a/lbrynet/extras/wallet/transaction.py +++ b/lbrynet/extras/wallet/transaction.py @@ -4,8 +4,7 @@ from typing import List, Iterable, Optional from torba.client.basetransaction import BaseTransaction, BaseInput, BaseOutput from torba.client.hash import hash160 -from lbrynet.schema.decode import smart_decode -from lbrynet.schema.claim import ClaimDict +from lbrynet.schema.claim import Claim from lbrynet.extras.wallet.account import Account from lbrynet.extras.wallet.script import InputScript, OutputScript @@ -19,12 +18,12 @@ class Output(BaseOutput): script: OutputScript script_class = OutputScript - __slots__ = '_claim_dict', 'channel', 'private_key' + __slots__ = '_claim', 'channel', 'private_key' def __init__(self, *args, channel: Optional['Output'] = None, private_key: Optional[str] = None, **kwargs) -> None: super().__init__(*args, **kwargs) - self._claim_dict = None + self._claim = None self.channel = channel self.private_key = private_key @@ -60,17 +59,13 @@ class Output(BaseOutput): raise ValueError('No claim_name associated.') @property - def claim(self) -> ClaimDict: + def claim(self) -> Claim: if self.is_claim: - return smart_decode(self.script.values['claim']) + if self._claim is None: + self._claim = Claim.from_bytes(self.script.values['claim']) + return self._claim raise ValueError('Only claim name and claim update have the claim payload.') - @property - def claim_dict(self) -> dict: - if self._claim_dict is None: - self._claim_dict = self.claim.claim_dict - return self._claim_dict - @property def permanent_url(self) -> str: if self.script.is_claim_involved: @@ -124,11 +119,11 @@ class Transaction(BaseTransaction): return cls.create([], [output], funding_accounts, change_account) @classmethod - def claim(cls, name: str, meta: ClaimDict, amount: int, holding_address: bytes, + def claim(cls, name: str, meta: Claim, amount: int, holding_address: bytes, funding_accounts: List[Account], change_account: Account): ledger = cls.ensure_all_have_same_ledger(funding_accounts, change_account) claim_output = Output.pay_claim_name_pubkey_hash( - amount, name, meta.serialized, ledger.address_to_hash160(holding_address) + amount, name, meta.to_bytes(), ledger.address_to_hash160(holding_address) ) return cls.create([], [claim_output], funding_accounts, change_account) @@ -142,12 +137,12 @@ class Transaction(BaseTransaction): return cls.create([], [claim_output], funding_accounts, change_account) @classmethod - def update(cls, previous_claim: Output, meta: ClaimDict, amount: int, holding_address: bytes, + def update(cls, previous_claim: Output, meta: Claim, amount: int, holding_address: bytes, funding_accounts: List[Account], change_account: Account): ledger = cls.ensure_all_have_same_ledger(funding_accounts, change_account) updated_claim = Output.pay_update_claim_pubkey_hash( amount, previous_claim.claim_name, previous_claim.claim_id, - meta.serialized, ledger.address_to_hash160(holding_address) + meta.to_bytes(), ledger.address_to_hash160(holding_address) ) return cls.create([Input.spend(previous_claim)], [updated_claim], funding_accounts, change_account) diff --git a/lbrynet/schema/claim.py b/lbrynet/schema/claim.py index 74c18fdb0..e2f8bdfb0 100644 --- a/lbrynet/schema/claim.py +++ b/lbrynet/schema/claim.py @@ -3,6 +3,15 @@ from collections import OrderedDict from typing import List, Tuple from decimal import Decimal from binascii import hexlify, unhexlify +from hashlib import sha256 + +import ecdsa +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.serialization import load_der_public_key +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric.utils import Prehashed +from ecdsa.util import sigencode_der from google.protobuf import json_format # pylint: disable=no-name-in-module from google.protobuf.message import DecodeError as DecodeError_pb # pylint: disable=no-name-in-module,import-error @@ -204,10 +213,14 @@ class ClaimDict(OrderedDict): class Claim: - __slots__ = '_claim', + __slots__ = '_claim', 'signature', 'certificate_id', 'signature_type', 'unsigned_payload' def __init__(self, claim_message=None): self._claim = claim_message or ClaimMessage() + self.signature = None + self.signature_type = 'SECP256k1' + self.certificate_id = None + self.unsigned_payload = None @property def is_undetermined(self): @@ -221,6 +234,40 @@ class Claim: def is_channel(self): return self._claim.WhichOneof('type') == 'channel' + @property + def is_signed(self): + return self.signature is not None + + def is_signed_by(self, channel: 'Channel', claim_address): + if self.unsigned_payload: + digest = sha256(b''.join([ + claim_address, self.unsigned_payload, self.certificate_id + ])).digest() + public_key = load_der_public_key(channel.public_key_bytes, default_backend()) + hash = hashes.SHA256() + signature = hexlify(self.signature) + r = int(signature[:int(len(signature)/2)], 16) + s = int(signature[int(len(signature)/2):], 16) + encoded_sig = sigencode_der(r, s, len(signature)*4) + public_key.verify(encoded_sig, digest, ec.ECDSA(Prehashed(hash))) + return True + else: + digest = sha256(b''.join([ + self.certificate_id.encode(), + first_input_txid_nout.encode(), + self.to_bytes() + ])).digest() + + def sign(self, certificate_id: str, private_key_text: str, first_input_txid_nout): + digest = sha256(b''.join([ + certificate_id.encode(), + first_input_txid_nout.encode(), + self.to_bytes() + ])).digest() + private_key = ecdsa.SigningKey.from_pem(private_key_text, hashfunc="sha256") + self.signature = private_key.sign_digest_deterministic(digest, hashfunc="sha256") + self.certificate_id = certificate_id + @property def stream_message(self): if self.is_undetermined: @@ -400,6 +447,95 @@ class Fee: self._fee.currency = FeeMessage.USD +class Channel: + + __slots__ = '_claim', '_channel' + + def __init__(self, claim: Claim = None): + self._claim = claim or Claim() + self._channel = self._claim.channel_message + + @property + def claim(self) -> Claim: + return self._claim + + @property + def tags(self) -> List: + return self._channel.tags + + @property + def public_key(self) -> str: + return hexlify(self._channel.public_key).decode() + + @public_key.setter + def public_key(self, sd_public_key: str): + self._channel.public_key = unhexlify(sd_public_key.encode()) + + @property + def public_key_bytes(self) -> bytes: + return self._channel.public_key + + @public_key_bytes.setter + def public_key_bytes(self, public_key: bytes): + self._channel.public_key = public_key + + @property + def language(self) -> str: + return self._channel.language + + @language.setter + def language(self, language: str): + self._channel.language = language + + @property + def title(self) -> str: + return self._channel.title + + @title.setter + def title(self, title: str): + self._channel.title = title + + @property + def description(self) -> str: + return self._channel.description + + @description.setter + def description(self, description: str): + self._channel.description = description + + @property + def contact_email(self) -> str: + return self._channel.contact_email + + @contact_email.setter + def contact_email(self, contact_email: str): + self._channel.contact_email = contact_email + + @property + def homepage_url(self) -> str: + return self._channel.homepage_url + + @homepage_url.setter + def homepage_url(self, homepage_url: str): + self._channel.homepage_url = homepage_url + + @property + def thumbnail_url(self) -> str: + return self._channel.thumbnail_url + + @thumbnail_url.setter + def thumbnail_url(self, thumbnail_url: str): + self._channel.thumbnail_url = thumbnail_url + + @property + def cover_url(self) -> str: + return self._channel.cover_url + + @cover_url.setter + def cover_url(self, cover_url: str): + self._channel.cover_url = cover_url + + class Stream: __slots__ = '_claim', '_stream' @@ -523,92 +659,3 @@ class Stream: @release_time.setter def release_time(self, release_time: int): self._stream.release_time = release_time - - -class Channel: - - __slots__ = '_claim', '_channel' - - def __init__(self, claim: Claim = None): - self._claim = claim or Claim() - self._channel = self._claim.channel_message - - @property - def claim(self) -> Claim: - return self._claim - - @property - def tags(self) -> List: - return self._channel.tags - - @property - def public_key(self) -> str: - return hexlify(self._channel.public_key).decode() - - @public_key.setter - def public_key(self, sd_public_key: str): - self._channel.public_key = unhexlify(sd_public_key.encode()) - - @property - def public_key_bytes(self) -> bytes: - return self._channel.public_key - - @public_key_bytes.setter - def public_key_bytes(self, public_key: bytes): - self._channel.public_key = public_key - - @property - def language(self) -> str: - return self._channel.language - - @language.setter - def language(self, language: str): - self._channel.language = language - - @property - def title(self) -> str: - return self._channel.title - - @title.setter - def title(self, title: str): - self._channel.title = title - - @property - def description(self) -> str: - return self._channel.description - - @description.setter - def description(self, description: str): - self._channel.description = description - - @property - def contact_email(self) -> str: - return self._channel.contact_email - - @contact_email.setter - def contact_email(self, contact_email: str): - self._channel.contact_email = contact_email - - @property - def homepage_url(self) -> str: - return self._channel.homepage_url - - @homepage_url.setter - def homepage_url(self, homepage_url: str): - self._channel.homepage_url = homepage_url - - @property - def thumbnail_url(self) -> str: - return self._channel.thumbnail_url - - @thumbnail_url.setter - def thumbnail_url(self, thumbnail_url: str): - self._channel.thumbnail_url = thumbnail_url - - @property - def cover_url(self) -> str: - return self._channel.cover_url - - @cover_url.setter - def cover_url(self, cover_url: str): - self._channel.cover_url = cover_url diff --git a/lbrynet/schema/compat.py b/lbrynet/schema/compat.py index 046eb0064..f0527c398 100644 --- a/lbrynet/schema/compat.py +++ b/lbrynet/schema/compat.py @@ -1,9 +1,9 @@ import json from decimal import Decimal -from lbrynet.schema.address import decode_address, encode_address from lbrynet.schema.types.v1.legacy_claim_pb2 import Claim as OldClaimMessage from lbrynet.schema.types.v1.metadata_pb2 import Metadata as MetadataMessage +from lbrynet.schema.types.v1.certificate_pb2 import KeyType from lbrynet.schema.types.v1.fee_pb2 import Fee as FeeMessage @@ -60,6 +60,13 @@ def from_types_v1(claim, payload: bytes): stream.fee.usd = Decimal(fee.amount) else: raise ValueError(f'Unsupported currency: {currency}') + if old.HasField('publisherSignature'): + sig = old.publisherSignature + claim.signature = sig.signature + claim.signature_type = KeyType.Name(sig.signatureType) + claim.certificate_id = sig.certificateId + old.ClearField("publisherSignature") + claim.unsigned_payload = old.SerializeToString() elif old.claimType == 2: channel = claim.channel channel.public_key_bytes = old.certificate.publicKey diff --git a/tests/unit/schema/test_claim_from_bytes.py b/tests/unit/schema/test_claim_from_bytes.py index 37aef4f8d..fec47d525 100644 --- a/tests/unit/schema/test_claim_from_bytes.py +++ b/tests/unit/schema/test_claim_from_bytes.py @@ -2,6 +2,7 @@ from unittest import TestCase from binascii import unhexlify from lbrynet.schema import Claim +from lbrynet.schema.base import b58decode class TestOldJSONSchemaCompatibility(TestCase): @@ -143,6 +144,8 @@ class TestTypesV1Compatibility(TestCase): '3f17' ) + self.assertTrue(stream.is_signed_by(channel, b58decode('bb4UAfujhmvTgyx7ufoEa4aevum6hKSW36'))) + def test_unsigned_with_fee(self): claim = Claim.from_bytes(unhexlify( b'080110011ad6010801127c080410011a08727067206d69646922046d6964692a08727067206d696469322'