start a new module for isolating resolve related code before we can refactor it
This commit is contained in:
parent
036663ae62
commit
9e89b3e40e
3 changed files with 302 additions and 188 deletions
98
lbrynet/wallet/claim_proofs.py
Normal file
98
lbrynet/wallet/claim_proofs.py
Normal 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))
|
|
@ -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
197
lbrynet/wallet/resolve.py
Normal 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
|
Loading…
Add table
Reference in a new issue