From 9e89b3e40e2a916291e7899a2fba4bd5c9c7d383 Mon Sep 17 00:00:00 2001 From: Victor Shyba Date: Wed, 11 Jul 2018 22:31:50 -0300 Subject: [PATCH] start a new module for isolating resolve related code before we can refactor it --- lbrynet/wallet/claim_proofs.py | 98 ++++++++++++++++ lbrynet/wallet/ledger.py | 195 ++------------------------------ lbrynet/wallet/resolve.py | 197 +++++++++++++++++++++++++++++++++ 3 files changed, 302 insertions(+), 188 deletions(-) create mode 100644 lbrynet/wallet/claim_proofs.py create mode 100644 lbrynet/wallet/resolve.py diff --git a/lbrynet/wallet/claim_proofs.py b/lbrynet/wallet/claim_proofs.py new file mode 100644 index 000000000..2cb392cf8 --- /dev/null +++ b/lbrynet/wallet/claim_proofs.py @@ -0,0 +1,98 @@ +import binascii + +from lbryschema.hashing import sha256 + + +class InvalidProofError(Exception): pass + + +def height_to_vch(n): + r = [0 for i in range(8)] + r[4] = n >> 24 + r[5] = n >> 16 + r[6] = n >> 8 + r[7] = n % 256 + # need to reset each value mod 256 because for values like 67784 + # 67784 >> 8 = 264, which is obviously larger then the maximum + # value input into chr() + return ''.join([chr(x % 256) for x in r]) + + +def get_hash_for_outpoint(txhash, nOut, nHeightOfLastTakeover): + txhash_hash = Hash(txhash) + nOut_hash = Hash(str(nOut)) + height_of_last_takeover_hash = Hash(height_to_vch(nHeightOfLastTakeover)) + outPointHash = Hash(txhash_hash + nOut_hash + height_of_last_takeover_hash) + return outPointHash + + +# noinspection PyPep8 +def verify_proof(proof, rootHash, name): + previous_computed_hash = None + reverse_computed_name = '' + verified_value = False + for i, node in enumerate(proof['nodes'][::-1]): + found_child_in_chain = False + to_hash = '' + previous_child_character = None + for child in node['children']: + if child['character'] < 0 or child['character'] > 255: + raise InvalidProofError("child character not int between 0 and 255") + if previous_child_character: + if previous_child_character >= child['character']: + raise InvalidProofError("children not in increasing order") + previous_child_character = child['character'] + to_hash += chr(child['character']) + if 'nodeHash' in child: + if len(child['nodeHash']) != 64: + raise InvalidProofError("invalid child nodeHash") + to_hash += binascii.unhexlify(child['nodeHash'])[::-1] + else: + if previous_computed_hash is None: + raise InvalidProofError("previous computed hash is None") + if found_child_in_chain is True: + raise InvalidProofError("already found the next child in the chain") + found_child_in_chain = True + reverse_computed_name += chr(child['character']) + to_hash += previous_computed_hash + + if not found_child_in_chain: + if i != 0: + raise InvalidProofError("did not find the alleged child") + if i == 0 and 'txhash' in proof and 'nOut' in proof and 'last takeover height' in proof: + if len(proof['txhash']) != 64: + raise InvalidProofError("txhash was invalid: {}".format(proof['txhash'])) + if not isinstance(proof['nOut'], (long, int)): + raise InvalidProofError("nOut was invalid: {}".format(proof['nOut'])) + if not isinstance(proof['last takeover height'], (long, int)): + raise InvalidProofError( + 'last takeover height was invalid: {}'.format(proof['last takeover height'])) + to_hash += get_hash_for_outpoint( + binascii.unhexlify(proof['txhash'])[::-1], + proof['nOut'], + proof['last takeover height'] + ) + verified_value = True + elif 'valueHash' in node: + if len(node['valueHash']) != 64: + raise InvalidProofError("valueHash was invalid") + to_hash += binascii.unhexlify(node['valueHash'])[::-1] + + previous_computed_hash = Hash(to_hash) + + if previous_computed_hash != binascii.unhexlify(rootHash)[::-1]: + raise InvalidProofError("computed hash does not match roothash") + if 'txhash' in proof and 'nOut' in proof: + if not verified_value: + raise InvalidProofError("mismatch between proof claim and outcome") + if 'txhash' in proof and 'nOut' in proof: + if name != reverse_computed_name[::-1]: + raise InvalidProofError("name did not match proof") + if not name.startswith(reverse_computed_name[::-1]): + raise InvalidProofError("name fragment does not match proof") + return True + +def Hash(x): + if type(x) is unicode: + x = x.encode('utf-8') + return sha256(sha256(x)) diff --git a/lbrynet/wallet/ledger.py b/lbrynet/wallet/ledger.py index dc235d802..e60847763 100644 --- a/lbrynet/wallet/ledger.py +++ b/lbrynet/wallet/ledger.py @@ -1,15 +1,14 @@ import logging import struct -from ecdsa import BadSignatureError from six import int2byte from binascii import unhexlify from twisted.internet import defer -from lbrynet.core.Error import UnknownNameError, UnknownClaimID, UnknownURI, UnknownOutpoint -from lbryschema.address import is_address -from lbryschema.claim import ClaimDict +from lbrynet.core.Error import UnknownNameError, UnknownClaimID, UnknownURI +from .resolve import format_amount_value, _get_permanent_url, validate_claim_signature_and_get_channel_name +from .resolve import _verify_proof, _handle_claim_result from lbryschema.decode import smart_decode from lbryschema.error import URIParseError, DecodeError from lbryschema.uri import parse_lbry_uri @@ -20,7 +19,6 @@ from torba.util import int_to_hex, rev_hex, hash_encode from .account import Account from .network import Network from .database import WalletDatabase -from .claim_proofs import verify_proof, InvalidProofError from .transaction import Transaction @@ -184,9 +182,10 @@ class MainNetLedger(BaseLedger): height = certificate_response['height'] depth = self.headers.height - height certificate_result = _verify_proof(self, parsed_uri.name, - claim_trie_root, - certificate_response, - height, depth) + claim_trie_root, + certificate_response, + height, depth, + transaction_class=self.transaction_class) result['certificate'] = self.parse_and_validate_claim_result(certificate_result, raw=raw) elif certificate_resolution_type == "claim_id": @@ -433,183 +432,3 @@ class RegTestLedger(MainNetLedger): genesis_hash = '6e3fcf1299d4ec5d79c3a4c91d624a4acf9e2e173d95a1a0504f677669687556' genesis_bits = 0x207fffff target_timespan = 1 - -# Format amount to be decimal encoded string -# Format value to be hex encoded string -# TODO: refactor. Came from lbryum, there could be another part of torba doing it -def format_amount_value(obj): - COIN = 100000000 - if isinstance(obj, dict): - for k, v in obj.iteritems(): - if k == 'amount' or k == 'effective_amount': - if not isinstance(obj[k], float): - obj[k] = float(obj[k]) / float(COIN) - elif k == 'supports' and isinstance(v, list): - obj[k] = [{'txid': txid, 'nout': nout, 'amount': float(amount) / float(COIN)} - for (txid, nout, amount) in v] - elif isinstance(v, (list, dict)): - obj[k] = format_amount_value(v) - elif isinstance(obj, list): - obj = [format_amount_value(o) for o in obj] - return obj - -def _get_permanent_url(claim_result): - if claim_result.get('has_signature') and claim_result.get('channel_name'): - return "{0}#{1}/{2}".format( - claim_result['channel_name'], - claim_result['value']['publisherSignature']['certificateId'], - claim_result['name'] - ) - else: - return "{0}#{1}".format( - claim_result['name'], - claim_result['claim_id'] - ) -def _verify_proof(ledger, name, claim_trie_root, result, height, depth): - """ - Verify proof for name claim - """ - - def _build_response(name, value, claim_id, txid, n, amount, effective_amount, - claim_sequence, claim_address, supports): - r = { - 'name': name, - 'value': value.encode('hex'), - 'claim_id': claim_id, - 'txid': txid, - 'nout': n, - 'amount': amount, - 'effective_amount': effective_amount, - 'height': height, - 'depth': depth, - 'claim_sequence': claim_sequence, - 'address': claim_address, - 'supports': supports - } - return r - - def _parse_proof_result(name, result): - support_amount = sum([amt for (stxid, snout, amt) in result['supports']]) - supports = result['supports'] - if 'txhash' in result['proof'] and 'nOut' in result['proof']: - if 'transaction' in result: - tx = Transaction(raw=unhexlify(result['transaction'])) - nOut = result['proof']['nOut'] - if result['proof']['txhash'] == tx.hex_id: - if 0 <= nOut < len(tx.outputs): - claim_output = tx.outputs[nOut] - effective_amount = claim_output.amount + support_amount - claim_address = ledger.hash160_to_address(claim_output.script.values['pubkey_hash']) - claim_id = result['claim_id'] - claim_sequence = result['claim_sequence'] - claim_script = claim_output.script - decoded_name, decoded_value = claim_script.values['claim_name'], claim_script.values['claim'] - if decoded_name == name: - return _build_response(name, decoded_value, claim_id, - tx.hex_id, nOut, claim_output.amount, - effective_amount, claim_sequence, - claim_address, supports) - return {'error': 'name in proof did not match requested name'} - outputs = len(tx['outputs']) - return {'error': 'invalid nOut: %d (let(outputs): %d' % (nOut, outputs)} - return {'error': "computed txid did not match given transaction: %s vs %s" % - (tx.hex_id, result['proof']['txhash']) - } - return {'error': "didn't receive a transaction with the proof"} - return {'error': 'name is not claimed'} - - if 'proof' in result: - try: - verify_proof(result['proof'], claim_trie_root, name) - except InvalidProofError: - return {'error': "Proof was invalid"} - return _parse_proof_result(name, result) - else: - return {'error': "proof not in result"} - - -def validate_claim_signature_and_get_channel_name(claim, certificate_claim, - claim_address, decoded_certificate=None): - if not certificate_claim: - return False, None - certificate = decoded_certificate or smart_decode(certificate_claim['value']) - if not isinstance(certificate, ClaimDict): - raise TypeError("Certificate is not a ClaimDict: %s" % str(type(certificate))) - if _validate_signed_claim(claim, claim_address, certificate): - return True, certificate_claim['name'] - return False, None - - -def _validate_signed_claim(claim, claim_address, certificate): - if not claim.has_signature: - raise Exception("Claim is not signed") - if not is_address(claim_address): - raise Exception("Not given a valid claim address") - try: - if claim.validate_signature(claim_address, certificate.protobuf): - return True - except BadSignatureError: - # print_msg("Signature for %s is invalid" % claim_id) - return False - except Exception as err: - log.error("Signature for %s is invalid, reason: %s - %s", claim_address, - str(type(err)), err) - return False - return False - - -# TODO: The following came from code handling lbryum results. Now that it's all in one place a refactor should unify it. -def _decode_claim_result(claim): - if 'has_signature' in claim and claim['has_signature']: - if not claim['signature_is_valid']: - log.warning("lbry://%s#%s has an invalid signature", - claim['name'], claim['claim_id']) - try: - decoded = smart_decode(claim['value']) - claim_dict = decoded.claim_dict - claim['value'] = claim_dict - claim['hex'] = decoded.serialized.encode('hex') - except DecodeError: - claim['hex'] = claim['value'] - claim['value'] = None - claim['error'] = "Failed to decode value" - return claim - -def _handle_claim_result(results): - if not results: - #TODO: cannot determine what name we searched for here - # we should fix lbryum commands that return None - raise UnknownNameError("") - - if 'error' in results: - if results['error'] in ['name is not claimed', 'claim not found']: - if 'claim_id' in results: - raise UnknownClaimID(results['claim_id']) - elif 'name' in results: - raise UnknownNameError(results['name']) - elif 'uri' in results: - raise UnknownURI(results['uri']) - elif 'outpoint' in results: - raise UnknownOutpoint(results['outpoint']) - raise Exception(results['error']) - - # case where return value is {'certificate':{'txid', 'value',...},...} - if 'certificate' in results: - results['certificate'] = _decode_claim_result(results['certificate']) - - # case where return value is {'claim':{'txid','value',...},...} - if 'claim' in results: - results['claim'] = _decode_claim_result(results['claim']) - - # case where return value is {'txid','value',...} - # returned by queries that are not name resolve related - # (getclaimbyoutpoint, getclaimbyid, getclaimsfromtx) - elif 'value' in results: - results = _decode_claim_result(results) - - # case where there is no 'certificate', 'value', or 'claim' key - elif 'certificate' not in results: - msg = 'result in unexpected format:{}'.format(results) - assert False, msg - - return results diff --git a/lbrynet/wallet/resolve.py b/lbrynet/wallet/resolve.py new file mode 100644 index 000000000..847a2a944 --- /dev/null +++ b/lbrynet/wallet/resolve.py @@ -0,0 +1,197 @@ +import logging + +from ecdsa import BadSignatureError +from binascii import unhexlify + +from lbrynet.core.Error import UnknownNameError, UnknownClaimID, UnknownURI, UnknownOutpoint +from lbryschema.address import is_address +from lbryschema.claim import ClaimDict +from lbryschema.decode import smart_decode +from lbryschema.error import DecodeError + +from .claim_proofs import verify_proof, InvalidProofError +log = logging.getLogger(__name__) + + +# Format amount to be decimal encoded string +# Format value to be hex encoded string +# TODO: refactor. Came from lbryum, there could be another part of torba doing it +def format_amount_value(obj): + COIN = 100000000 + if isinstance(obj, dict): + for k, v in obj.iteritems(): + if k == 'amount' or k == 'effective_amount': + if not isinstance(obj[k], float): + obj[k] = float(obj[k]) / float(COIN) + elif k == 'supports' and isinstance(v, list): + obj[k] = [{'txid': txid, 'nout': nout, 'amount': float(amount) / float(COIN)} + for (txid, nout, amount) in v] + elif isinstance(v, (list, dict)): + obj[k] = format_amount_value(v) + elif isinstance(obj, list): + obj = [format_amount_value(o) for o in obj] + return obj + + +def _get_permanent_url(claim_result): + if claim_result.get('has_signature') and claim_result.get('channel_name'): + return "{0}#{1}/{2}".format( + claim_result['channel_name'], + claim_result['value']['publisherSignature']['certificateId'], + claim_result['name'] + ) + else: + return "{0}#{1}".format( + claim_result['name'], + claim_result['claim_id'] + ) + + +def _verify_proof(ledger, name, claim_trie_root, result, height, depth, transaction_class): + """ + Verify proof for name claim + """ + + def _build_response(name, value, claim_id, txid, n, amount, effective_amount, + claim_sequence, claim_address, supports): + r = { + 'name': name, + 'value': value.encode('hex'), + 'claim_id': claim_id, + 'txid': txid, + 'nout': n, + 'amount': amount, + 'effective_amount': effective_amount, + 'height': height, + 'depth': depth, + 'claim_sequence': claim_sequence, + 'address': claim_address, + 'supports': supports + } + return r + + def _parse_proof_result(name, result): + support_amount = sum([amt for (stxid, snout, amt) in result['supports']]) + supports = result['supports'] + if 'txhash' in result['proof'] and 'nOut' in result['proof']: + if 'transaction' in result: + tx = transaction_class(raw=unhexlify(result['transaction'])) + nOut = result['proof']['nOut'] + if result['proof']['txhash'] == tx.hex_id: + if 0 <= nOut < len(tx.outputs): + claim_output = tx.outputs[nOut] + effective_amount = claim_output.amount + support_amount + claim_address = ledger.hash160_to_address(claim_output.script.values['pubkey_hash']) + claim_id = result['claim_id'] + claim_sequence = result['claim_sequence'] + claim_script = claim_output.script + decoded_name, decoded_value = claim_script.values['claim_name'], claim_script.values['claim'] + if decoded_name == name: + return _build_response(name, decoded_value, claim_id, + tx.hex_id, nOut, claim_output.amount, + effective_amount, claim_sequence, + claim_address, supports) + return {'error': 'name in proof did not match requested name'} + outputs = len(tx['outputs']) + return {'error': 'invalid nOut: %d (let(outputs): %d' % (nOut, outputs)} + return {'error': "computed txid did not match given transaction: %s vs %s" % + (tx.hex_id, result['proof']['txhash']) + } + return {'error': "didn't receive a transaction with the proof"} + return {'error': 'name is not claimed'} + + if 'proof' in result: + try: + verify_proof(result['proof'], claim_trie_root, name) + except InvalidProofError: + return {'error': "Proof was invalid"} + return _parse_proof_result(name, result) + else: + return {'error': "proof not in result"} + + +def validate_claim_signature_and_get_channel_name(claim, certificate_claim, + claim_address, decoded_certificate=None): + if not certificate_claim: + return False, None + certificate = decoded_certificate or smart_decode(certificate_claim['value']) + if not isinstance(certificate, ClaimDict): + raise TypeError("Certificate is not a ClaimDict: %s" % str(type(certificate))) + if _validate_signed_claim(claim, claim_address, certificate): + return True, certificate_claim['name'] + return False, None + + +def _validate_signed_claim(claim, claim_address, certificate): + if not claim.has_signature: + raise Exception("Claim is not signed") + if not is_address(claim_address): + raise Exception("Not given a valid claim address") + try: + if claim.validate_signature(claim_address, certificate.protobuf): + return True + except BadSignatureError: + # print_msg("Signature for %s is invalid" % claim_id) + return False + except Exception as err: + log.error("Signature for %s is invalid, reason: %s - %s", claim_address, + str(type(err)), err) + return False + return False + + +# TODO: The following came from code handling lbryum results. Now that it's all in one place a refactor should unify it. +def _decode_claim_result(claim): + if 'has_signature' in claim and claim['has_signature']: + if not claim['signature_is_valid']: + log.warning("lbry://%s#%s has an invalid signature", + claim['name'], claim['claim_id']) + try: + decoded = smart_decode(claim['value']) + claim_dict = decoded.claim_dict + claim['value'] = claim_dict + claim['hex'] = decoded.serialized.encode('hex') + except DecodeError: + claim['hex'] = claim['value'] + claim['value'] = None + claim['error'] = "Failed to decode value" + return claim + +def _handle_claim_result(results): + if not results: + #TODO: cannot determine what name we searched for here + # we should fix lbryum commands that return None + raise UnknownNameError("") + + if 'error' in results: + if results['error'] in ['name is not claimed', 'claim not found']: + if 'claim_id' in results: + raise UnknownClaimID(results['claim_id']) + elif 'name' in results: + raise UnknownNameError(results['name']) + elif 'uri' in results: + raise UnknownURI(results['uri']) + elif 'outpoint' in results: + raise UnknownOutpoint(results['outpoint']) + raise Exception(results['error']) + + # case where return value is {'certificate':{'txid', 'value',...},...} + if 'certificate' in results: + results['certificate'] = _decode_claim_result(results['certificate']) + + # case where return value is {'claim':{'txid','value',...},...} + if 'claim' in results: + results['claim'] = _decode_claim_result(results['claim']) + + # case where return value is {'txid','value',...} + # returned by queries that are not name resolve related + # (getclaimbyoutpoint, getclaimbyid, getclaimsfromtx) + elif 'value' in results: + results = _decode_claim_result(results) + + # case where there is no 'certificate', 'value', or 'claim' key + elif 'certificate' not in results: + msg = 'result in unexpected format:{}'.format(results) + assert False, msg + + return results