start a new module for isolating resolve related code before we can refactor it

This commit is contained in:
Victor Shyba 2018-07-11 22:31:50 -03:00 committed by Jack Robison
parent 036663ae62
commit 9e89b3e40e
No known key found for this signature in database
GPG key ID: DF25C68FE0239BB2
3 changed files with 302 additions and 188 deletions

View file

@ -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))

View file

@ -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

197
lbrynet/wallet/resolve.py Normal file
View file

@ -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