wip: initial import of twisted based wallet

This commit is contained in:
Lex Berezhny 2018-03-25 22:59:57 -04:00 committed by Jack Robison
parent 79d1da5ff8
commit ca8b2dd83e
No known key found for this signature in database
GPG key ID: DF25C68FE0239BB2
29 changed files with 14642 additions and 305 deletions

View file

@ -1,177 +0,0 @@
import json
import logging
import socket
from twisted.internet import defer, error
from twisted.protocols.basic import LineOnlyReceiver
from errors import RemoteServiceException, ProtocolException, ServiceException
log = logging.getLogger(__name__)
class StratumClientProtocol(LineOnlyReceiver):
delimiter = '\n'
def __init__(self):
self._connected = defer.Deferred()
def _get_id(self):
self.request_id += 1
return self.request_id
def _get_ip(self):
return self.transport.getPeer().host
def get_session(self):
return self.session
def connectionMade(self):
try:
self.transport.setTcpNoDelay(True)
self.transport.setTcpKeepAlive(True)
if hasattr(socket, "TCP_KEEPIDLE"):
self.transport.socket.setsockopt(socket.SOL_TCP, socket.TCP_KEEPIDLE,
120) # Seconds before sending keepalive probes
else:
log.debug("TCP_KEEPIDLE not available")
if hasattr(socket, "TCP_KEEPINTVL"):
self.transport.socket.setsockopt(socket.SOL_TCP, socket.TCP_KEEPINTVL,
1) # Interval in seconds between keepalive probes
else:
log.debug("TCP_KEEPINTVL not available")
if hasattr(socket, "TCP_KEEPCNT"):
self.transport.socket.setsockopt(socket.SOL_TCP, socket.TCP_KEEPCNT,
5) # Failed keepalive probles before declaring other end dead
else:
log.debug("TCP_KEEPCNT not available")
except Exception as err:
# Supported only by the socket transport,
# but there's really no better place in code to trigger this.
log.warning("Error setting up socket: %s", err)
self.request_id = 0
self.lookup_table = {}
self._connected.callback(True)
# Initiate connection session
self.session = {}
log.debug("Connected %s" % self.transport.getPeer().host)
def transport_write(self, data):
'''Overwrite this if transport needs some extra care about data written
to the socket, like adding message format in websocket.'''
try:
self.transport.write(data)
except AttributeError:
# Transport is disconnected
log.warning("transport is disconnected")
def writeJsonRequest(self, method, params, is_notification=False):
request_id = None if is_notification else self._get_id()
serialized = json.dumps({'id': request_id, 'method': method, 'params': params})
self.transport_write("%s\n" % serialized)
return request_id
def writeJsonResponse(self, data, message_id):
serialized = json.dumps({'id': message_id, 'result': data, 'error': None})
self.transport_write("%s\n" % serialized)
def writeJsonError(self, code, message, traceback, message_id):
serialized = json.dumps(
{'id': message_id, 'result': None, 'error': (code, message, traceback)}
)
self.transport_write("%s\n" % serialized)
def writeGeneralError(self, message, code=-1):
log.error(message)
return self.writeJsonError(code, message, None, None)
def process_response(self, data, message_id):
self.writeJsonResponse(data.result, message_id)
def process_failure(self, failure, message_id):
if not isinstance(failure.value, ServiceException):
# All handled exceptions should inherit from ServiceException class.
# Throwing other exception class means that it is unhandled error
# and we should log it.
log.exception(failure)
code = getattr(failure.value, 'code', -1)
if message_id != None:
tb = failure.getBriefTraceback()
self.writeJsonError(code, failure.getErrorMessage(), tb, message_id)
def dataReceived(self, data):
'''Original code from Twisted, hacked for request_counter proxying.
request_counter is hack for HTTP transport, didn't found cleaner solution how
to indicate end of request processing in asynchronous manner.
TODO: This would deserve some unit test to be sure that future twisted versions
will work nicely with this.'''
lines = (self._buffer + data).split(self.delimiter)
self._buffer = lines.pop(-1)
for line in lines:
if self.transport.disconnecting:
return
if len(line) > self.MAX_LENGTH:
return self.lineLengthExceeded(line)
else:
try:
self.lineReceived(line)
except Exception as exc:
# log.exception("Processing of message failed")
log.warning("Failed message: %s from %s" % (str(exc), self._get_ip()))
return error.ConnectionLost('Processing of message failed')
if len(self._buffer) > self.MAX_LENGTH:
return self.lineLengthExceeded(self._buffer)
def lineReceived(self, line):
try:
message = json.loads(line)
except (ValueError, TypeError):
# self.writeGeneralError("Cannot decode message '%s'" % line)
raise ProtocolException("Cannot decode message '%s'" % line.strip())
msg_id = message.get('id', 0)
msg_result = message.get('result')
msg_error = message.get('error')
if msg_id:
# It's a RPC response
# Perform lookup to the table of waiting requests.
try:
meta = self.lookup_table[msg_id]
del self.lookup_table[msg_id]
except KeyError:
# When deferred object for given message ID isn't found, it's an error
raise ProtocolException(
"Lookup for deferred object for message ID '%s' failed." % msg_id)
# If there's an error, handle it as errback
# If both result and error are null, handle it as a success with blank result
if msg_error != None:
meta['defer'].errback(
RemoteServiceException(msg_error[0], msg_error[1], msg_error[2])
)
else:
meta['defer'].callback(msg_result)
else:
raise ProtocolException("Cannot handle message '%s'" % line)
def rpc(self, method, params, is_notification=False):
'''
This method performs remote RPC call.
If method should expect an response, it store
request ID to lookup table and wait for corresponding
response message.
'''
request_id = self.writeJsonRequest(method, params, is_notification)
if is_notification:
return
d = defer.Deferred()
self.lookup_table[request_id] = {'defer': d, 'method': method, 'params': params}
return d

View file

@ -1,18 +0,0 @@
class TransportException(Exception):
pass
class ServiceException(Exception):
code = -2
class RemoteServiceException(Exception):
pass
class ProtocolException(Exception):
pass
class MethodNotFoundException(ServiceException):
code = -3

View file

@ -1,110 +0,0 @@
import logging
from twisted.internet import defer
from twisted.internet.protocol import ClientFactory
from client import StratumClientProtocol
from errors import TransportException
log = logging.getLogger()
class StratumClient(ClientFactory):
protocol = StratumClientProtocol
def __init__(self, connected_d=None):
self.client = None
self.connected_d = connected_d or defer.Deferred()
def buildProtocol(self, addr):
client = self.protocol()
client.factory = self
self.client = client
self.client._connected.addCallback(lambda _: self.connected_d.callback(self))
return client
def _rpc(self, method, params, *args, **kwargs):
if not self.client:
raise TransportException("Not connected")
return self.client.rpc(method, params, *args, **kwargs)
def blockchain_claimtrie_getvaluesforuris(self, block_hash, *uris):
return self._rpc('blockchain.claimtrie.getvaluesforuris',
[block_hash] + list(uris))
def blockchain_claimtrie_getvaluesforuri(self, block_hash, uri):
return self._rpc('blockchain.claimtrie.getvaluesforuri', [block_hash, uri])
def blockchain_claimtrie_getclaimssignedbynthtoname(self, name, n):
return self._rpc('blockchain.claimtrie.getclaimssignedbynthtoname', [name, n])
def blockchain_claimtrie_getclaimssignedbyid(self, certificate_id):
return self._rpc('blockchain.claimtrie.getclaimssignedbyid', [certificate_id])
def blockchain_claimtrie_getclaimssignedby(self, name):
return self._rpc('blockchain.claimtrie.getclaimssignedby', [name])
def blockchain_claimtrie_getnthclaimforname(self, name, n):
return self._rpc('blockchain.claimtrie.getnthclaimforname', [name, n])
def blockchain_claimtrie_getclaimsbyids(self, *claim_ids):
return self._rpc('blockchain.claimtrie.getclaimsbyids', list(claim_ids))
def blockchain_claimtrie_getclaimbyid(self, claim_id):
return self._rpc('blockchain.claimtrie.getclaimbyid', [claim_id])
def blockchain_claimtrie_get(self):
return self._rpc('blockchain.claimtrie.get', [])
def blockchain_block_get_block(self, block_hash):
return self._rpc('blockchain.block.get_block', [block_hash])
def blockchain_claimtrie_getclaimsforname(self, name):
return self._rpc('blockchain.claimtrie.getclaimsforname', [name])
def blockchain_claimtrie_getclaimsintx(self, txid):
return self._rpc('blockchain.claimtrie.getclaimsintx', [txid])
def blockchain_claimtrie_getvalue(self, name, block_hash=None):
return self._rpc('blockchain.claimtrie.getvalue', [name, block_hash])
def blockchain_relayfee(self):
return self._rpc('blockchain.relayfee', [])
def blockchain_estimatefee(self):
return self._rpc('blockchain.estimatefee', [])
def blockchain_transaction_get(self, txid):
return self._rpc('blockchain.transaction.get', [txid])
def blockchain_transaction_get_merkle(self, tx_hash, height, cache_only=False):
return self._rpc('blockchain.transaction.get_merkle', [tx_hash, height, cache_only])
def blockchain_transaction_broadcast(self, raw_transaction):
return self._rpc('blockchain.transaction.broadcast', [raw_transaction])
def blockchain_block_get_chunk(self, index, cache_only=False):
return self._rpc('blockchain.block.get_chunk', [index, cache_only])
def blockchain_block_get_header(self, height, cache_only=False):
return self._rpc('blockchain.block.get_header', [height, cache_only])
def blockchain_utxo_get_address(self, txid, pos):
return self._rpc('blockchain.utxo.get_address', [txid, pos])
def blockchain_address_listunspent(self, address):
return self._rpc('blockchain.address.listunspent', [address])
def blockchain_address_get_proof(self, address):
return self._rpc('blockchain.address.get_proof', [address])
def blockchain_address_get_balance(self, address):
return self._rpc('blockchain.address.get_balance', [address])
def blockchain_address_get_mempool(self, address):
return self._rpc('blockchain.address.get_mempool', [address])
def blockchain_address_get_history(self, address):
return self._rpc('blockchain.address.get_history', [address])
def blockchain_block_get_server_height(self):
return self._rpc('blockchain.block.get_server_height', [])

View file

83
lbrynet/wallet/account.py Normal file
View file

@ -0,0 +1,83 @@
import logging
from lbryschema.address import public_key_to_address
from .lbrycrd import deserialize_xkey
from .lbrycrd import CKD_pub
log = logging.getLogger(__name__)
def get_key_chain_from_xpub(xpub):
_, _, _, chain, key = deserialize_xkey(xpub)
return key, chain
def derive_key(parent_key, chain, sequence):
return CKD_pub(parent_key, chain, sequence)[0]
class AddressSequence:
def __init__(self, derived_keys, gap, age_checker, pub_key, chain_key):
self.gap = gap
self.is_old = age_checker
self.pub_key = pub_key
self.chain_key = chain_key
self.derived_keys = derived_keys
self.addresses = [
public_key_to_address(key.decode('hex'))
for key in derived_keys
]
def generate_next_address(self):
new_key, _ = derive_key(self.pub_key, self.chain_key, len(self.derived_keys))
address = public_key_to_address(new_key)
self.derived_keys.append(new_key.encode('hex'))
self.addresses.append(address)
return address
def has_gap(self):
if len(self.addresses) < self.gap:
return False
for address in self.addresses[-self.gap:]:
if self.is_old(address):
return False
return True
def ensure_enough_addresses(self):
starting_length = len(self.addresses)
while not self.has_gap():
self.generate_next_address()
return self.addresses[starting_length:]
class Account:
def __init__(self, data, receiving_gap, change_gap, age_checker):
self.xpub = data['xpub']
master_key, master_chain = get_key_chain_from_xpub(data['xpub'])
self.receiving = AddressSequence(
data.get('receiving', []), receiving_gap, age_checker,
*derive_key(master_key, master_chain, 0)
)
self.change = AddressSequence(
data.get('change', []), change_gap, age_checker,
*derive_key(master_key, master_chain, 1)
)
self.is_old = age_checker
def as_dict(self):
return {
'receiving': self.receiving.derived_keys,
'change': self.change.derived_keys,
'xpub': self.xpub
}
def ensure_enough_addresses(self):
return self.receiving.ensure_enough_addresses() + \
self.change.ensure_enough_addresses()
@property
def sequences(self):
return self.receiving, self.change

View file

@ -0,0 +1,133 @@
import struct
class SerializationError(Exception):
""" Thrown when there's a problem deserializing or serializing """
class BCDataStream(object):
def __init__(self):
self.input = None
self.read_cursor = 0
def clear(self):
self.input = None
self.read_cursor = 0
def write(self, bytes): # Initialize with string of bytes
if self.input is None:
self.input = bytes
else:
self.input += bytes
def read_string(self):
# Strings are encoded depending on length:
# 0 to 252 : 1-byte-length followed by bytes (if any)
# 253 to 65,535 : byte'253' 2-byte-length followed by bytes
# 65,536 to 4,294,967,295 : byte '254' 4-byte-length followed by bytes
# ... and the Bitcoin client is coded to understand:
# greater than 4,294,967,295 : byte '255' 8-byte-length followed by bytes of string
# ... but I don't think it actually handles any strings that big.
if self.input is None:
raise SerializationError("call write(bytes) before trying to deserialize")
try:
length = self.read_compact_size()
except IndexError:
raise SerializationError("attempt to read past end of buffer")
return self.read_bytes(length)
def write_string(self, string):
# Length-encoded as with read-string
self.write_compact_size(len(string))
self.write(string)
def read_bytes(self, length):
try:
result = self.input[self.read_cursor:self.read_cursor + length]
self.read_cursor += length
return result
except IndexError:
raise SerializationError("attempt to read past end of buffer")
return ''
def read_boolean(self):
return self.read_bytes(1)[0] != chr(0)
def read_int16(self):
return self._read_num('<h')
def read_uint16(self):
return self._read_num('<H')
def read_int32(self):
return self._read_num('<i')
def read_uint32(self):
return self._read_num('<I')
def read_int64(self):
return self._read_num('<q')
def read_uint64(self):
return self._read_num('<Q')
def write_boolean(self, val):
return self.write(chr(1) if val else chr(0))
def write_int16(self, val):
return self._write_num('<h', val)
def write_uint16(self, val):
return self._write_num('<H', val)
def write_int32(self, val):
return self._write_num('<i', val)
def write_uint32(self, val):
return self._write_num('<I', val)
def write_int64(self, val):
return self._write_num('<q', val)
def write_uint64(self, val):
return self._write_num('<Q', val)
def read_compact_size(self):
size = ord(self.input[self.read_cursor])
self.read_cursor += 1
if size == 253:
size = self._read_num('<H')
elif size == 254:
size = self._read_num('<I')
elif size == 255:
size = self._read_num('<Q')
return size
def write_compact_size(self, size):
if size < 0:
raise SerializationError("attempt to write size < 0")
elif size < 253:
self.write(chr(size))
elif size < 2 ** 16:
self.write('\xfd')
self._write_num('<H', size)
elif size < 2 ** 32:
self.write('\xfe')
self._write_num('<I', size)
elif size < 2 ** 64:
self.write('\xff')
self._write_num('<Q', size)
def _read_num(self, format):
(i,) = struct.unpack_from(format, self.input, self.read_cursor)
self.read_cursor += struct.calcsize(format)
return i
def _write_num(self, format, num):
s = struct.pack(format, num)
self.write(s)

View file

@ -0,0 +1,243 @@
import os
import logging
from twisted.internet import threads, defer
from lbryum.util import hex_to_int, int_to_hex, rev_hex
from lbryum.hashing import hash_encode, Hash, PoWHash
from .stream import StreamController
from .constants import blockchain_params, HEADER_SIZE
log = logging.getLogger(__name__)
class BlockchainHeaders:
def __init__(self, path, chain='lbrycrd_main'):
self.path = path
self.chain = chain
self.max_target = blockchain_params[chain]['max_target']
self.target_timespan = blockchain_params[chain]['target_timespan']
self.genesis_bits = blockchain_params[chain]['genesis_bits']
self._on_change_controller = StreamController()
self.on_changed = self._on_change_controller.stream
self._size = None
self._write_lock = defer.DeferredLock()
if not os.path.exists(path):
with open(path, 'wb'):
pass
def sync_read_length(self):
return os.path.getsize(self.path) / HEADER_SIZE
def __len__(self):
if self._size is None:
self._size = self.sync_read_length()
return self._size
def sync_read_header(self, height):
if 0 <= height < len(self):
with open(self.path, 'rb') as f:
f.seek(height * HEADER_SIZE)
return f.read(HEADER_SIZE)
def __getitem__(self, height):
assert not isinstance(height, slice),\
"Slicing of header chain has not been implemented yet."
header = self.sync_read_header(height)
return self._deserialize(height, header)
@defer.inlineCallbacks
def connect(self, start, headers):
yield self._write_lock.acquire()
try:
yield threads.deferToThread(self._sync_connect, start, headers)
finally:
self._write_lock.release()
def _sync_connect(self, start, headers):
previous_header = None
for header in self._iterate_headers(start, headers):
height = header['block_height']
if previous_header is None and height > 0:
previous_header = self[height-1]
self._verify_header(height, header, previous_header)
previous_header = header
with open(self.path, 'r+b') as f:
f.seek(start * HEADER_SIZE)
f.write(headers)
f.truncate()
_old_size = self._size
self._size = self.sync_read_length()
change = self._size - _old_size
log.info('saved {} header blocks'.format(change))
self._on_change_controller.add(change)
def _iterate_headers(self, height, headers):
assert len(headers) % HEADER_SIZE == 0
for idx in range(len(headers) / HEADER_SIZE):
start, end = idx * HEADER_SIZE, (idx + 1) * HEADER_SIZE
header = headers[start:end]
yield self._deserialize(height+idx, header)
def _verify_header(self, height, header, previous_header):
previous_hash = self._hash_header(previous_header)
assert previous_hash == header['prev_block_hash'], \
"prev hash mismatch: {} vs {}".format(previous_hash, header['prev_block_hash'])
bits, target = self._calculate_lbry_next_work_required(height, previous_header, header)
assert bits == header['bits'], \
"bits mismatch: {} vs {} (hash: {})".format(
bits, header['bits'], self._hash_header(header))
_pow_hash = self._pow_hash_header(header)
assert int('0x' + _pow_hash, 16) <= target, \
"insufficient proof of work: {} vs target {}".format(
int('0x' + _pow_hash, 16), target)
@staticmethod
def _serialize(header):
return ''.join([
int_to_hex(header['version'], 4),
rev_hex(header['prev_block_hash']),
rev_hex(header['merkle_root']),
rev_hex(header['claim_trie_root']),
int_to_hex(int(header['timestamp']), 4),
int_to_hex(int(header['bits']), 4),
int_to_hex(int(header['nonce']), 4)
])
@staticmethod
def _deserialize(height, header):
return {
'version': hex_to_int(header[0:4]),
'prev_block_hash': hash_encode(header[4:36]),
'merkle_root': hash_encode(header[36:68]),
'claim_trie_root': hash_encode(header[68:100]),
'timestamp': hex_to_int(header[100:104]),
'bits': hex_to_int(header[104:108]),
'nonce': hex_to_int(header[108:112]),
'block_height': height
}
def _hash_header(self, header):
if header is None:
return '0' * 64
return hash_encode(Hash(self._serialize(header).decode('hex')))
def _pow_hash_header(self, header):
if header is None:
return '0' * 64
return hash_encode(PoWHash(self._serialize(header).decode('hex')))
def _calculate_lbry_next_work_required(self, height, first, last):
""" See: lbrycrd/src/lbry.cpp """
if height == 0:
return self.genesis_bits, self.max_target
# bits to target
if self.chain != 'lbrycrd_regtest':
bits = last['bits']
bitsN = (bits >> 24) & 0xff
assert 0x03 <= bitsN <= 0x1f, \
"First part of bits should be in [0x03, 0x1d], but it was {}".format(hex(bitsN))
bitsBase = bits & 0xffffff
assert 0x8000 <= bitsBase <= 0x7fffff, \
"Second part of bits should be in [0x8000, 0x7fffff] but it was {}".format(bitsBase)
# new target
retargetTimespan = self.target_timespan
nActualTimespan = last['timestamp'] - first['timestamp']
nModulatedTimespan = retargetTimespan + (nActualTimespan - retargetTimespan) // 8
nMinTimespan = retargetTimespan - (retargetTimespan // 8)
nMaxTimespan = retargetTimespan + (retargetTimespan // 2)
# Limit adjustment step
if nModulatedTimespan < nMinTimespan:
nModulatedTimespan = nMinTimespan
elif nModulatedTimespan > nMaxTimespan:
nModulatedTimespan = nMaxTimespan
# Retarget
bnPowLimit = _ArithUint256(self.max_target)
bnNew = _ArithUint256.SetCompact(last['bits'])
bnNew *= nModulatedTimespan
bnNew //= nModulatedTimespan
if bnNew > bnPowLimit:
bnNew = bnPowLimit
return bnNew.GetCompact(), bnNew._value
class _ArithUint256:
""" See: lbrycrd/src/arith_uint256.cpp """
def __init__(self, value):
self._value = value
def __str__(self):
return hex(self._value)
@staticmethod
def fromCompact(nCompact):
"""Convert a compact representation into its value"""
nSize = nCompact >> 24
# the lower 23 bits
nWord = nCompact & 0x007fffff
if nSize <= 3:
return nWord >> 8 * (3 - nSize)
else:
return nWord << 8 * (nSize - 3)
@classmethod
def SetCompact(cls, nCompact):
return cls(cls.fromCompact(nCompact))
def bits(self):
"""Returns the position of the highest bit set plus one."""
bn = bin(self._value)[2:]
for i, d in enumerate(bn):
if d:
return (len(bn) - i) + 1
return 0
def GetLow64(self):
return self._value & 0xffffffffffffffff
def GetCompact(self):
"""Convert a value into its compact representation"""
nSize = (self.bits() + 7) // 8
nCompact = 0
if nSize <= 3:
nCompact = self.GetLow64() << 8 * (3 - nSize)
else:
bn = _ArithUint256(self._value >> 8 * (nSize - 3))
nCompact = bn.GetLow64()
# The 0x00800000 bit denotes the sign.
# Thus, if it is already set, divide the mantissa by 256 and increase the exponent.
if nCompact & 0x00800000:
nCompact >>= 8
nSize += 1
assert (nCompact & ~0x007fffff) == 0
assert nSize < 256
nCompact |= nSize << 24
return nCompact
def __mul__(self, x):
# Take the mod because we are limited to an unsigned 256 bit number
return _ArithUint256((self._value * x) % 2 ** 256)
def __ifloordiv__(self, x):
self._value = (self._value // x)
return self
def __gt__(self, x):
return self._value > x

View file

@ -0,0 +1,313 @@
import struct
import logging
from collections import defaultdict, namedtuple
from math import floor, log10
from .hashing import sha256
from .constants import COIN, TYPE_ADDRESS
from .transaction import Transaction
from .errors import NotEnoughFunds
log = logging.getLogger()
class PRNG(object):
"""
A simple deterministic PRNG. Used to deterministically shuffle a
set of coins - the same set of coins should produce the same output.
Although choosing UTXOs "randomly" we want it to be deterministic,
so if sending twice from the same UTXO set we choose the same UTXOs
to spend. This prevents attacks on users by malicious or stale
servers.
"""
def __init__(self, seed):
self.sha = sha256(seed)
self.pool = bytearray()
def get_bytes(self, n):
while len(self.pool) < n:
self.pool.extend(self.sha)
self.sha = sha256(self.sha)
result, self.pool = self.pool[:n], self.pool[n:]
return result
def random(self):
# Returns random double in [0, 1)
four = self.get_bytes(4)
return struct.unpack("I", four)[0] / 4294967296.0
def randint(self, start, end):
# Returns random integer in [start, end)
return start + int(self.random() * (end - start))
def choice(self, seq):
return seq[int(self.random() * len(seq))]
def shuffle(self, x):
for i in reversed(xrange(1, len(x))):
# pick an element in x[:i+1] with which to exchange x[i]
j = int(self.random() * (i + 1))
x[i], x[j] = x[j], x[i]
Bucket = namedtuple('Bucket', ['desc', 'size', 'value', 'coins'])
def strip_unneeded(bkts, sufficient_funds):
'''Remove buckets that are unnecessary in achieving the spend amount'''
bkts = sorted(bkts, key=lambda bkt: bkt.value)
for i in range(len(bkts)):
if not sufficient_funds(bkts[i + 1:]):
return bkts[i:]
# Shouldn't get here
return bkts
class CoinChooserBase:
def keys(self, coins):
raise NotImplementedError
def bucketize_coins(self, coins):
keys = self.keys(coins)
buckets = defaultdict(list)
for key, coin in zip(keys, coins):
buckets[key].append(coin)
def make_Bucket(desc, coins):
size = sum(Transaction.estimated_input_size(coin)
for coin in coins)
value = sum(coin['value'] for coin in coins)
return Bucket(desc, size, value, coins)
return map(make_Bucket, buckets.keys(), buckets.values())
def penalty_func(self, tx):
def penalty(candidate):
return 0
return penalty
def change_amounts(self, tx, count, fee_estimator, dust_threshold):
# Break change up if bigger than max_change
output_amounts = [o[2] for o in tx.outputs()]
# Don't split change of less than 0.02 BTC
max_change = max(max(output_amounts) * 1.25, 0.02 * COIN)
# Use N change outputs
for n in range(1, count + 1):
# How much is left if we add this many change outputs?
change_amount = max(0, tx.get_fee() - fee_estimator(n))
if change_amount // n <= max_change:
break
# Get a handle on the precision of the output amounts; round our
# change to look similar
def trailing_zeroes(val):
s = str(val)
return len(s) - len(s.rstrip('0'))
zeroes = map(trailing_zeroes, output_amounts)
min_zeroes = min(zeroes)
max_zeroes = max(zeroes)
zeroes = range(max(0, min_zeroes - 1), (max_zeroes + 1) + 1)
# Calculate change; randomize it a bit if using more than 1 output
remaining = change_amount
amounts = []
while n > 1:
average = remaining // n
amount = self.p.randint(int(average * 0.7), int(average * 1.3))
precision = min(self.p.choice(zeroes), int(floor(log10(amount))))
amount = int(round(amount, -precision))
amounts.append(amount)
remaining -= amount
n -= 1
# Last change output. Round down to maximum precision but lose
# no more than 100 satoshis to fees (2dp)
N = pow(10, min(2, zeroes[0]))
amount = (remaining // N) * N
amounts.append(amount)
assert sum(amounts) <= change_amount
return amounts
def change_outputs(self, tx, change_addrs, fee_estimator, dust_threshold):
amounts = self.change_amounts(tx, len(change_addrs), fee_estimator,
dust_threshold)
assert min(amounts) >= 0
assert len(change_addrs) >= len(amounts)
# If change is above dust threshold after accounting for the
# size of the change output, add it to the transaction.
dust = sum(amount for amount in amounts if amount < dust_threshold)
amounts = [amount for amount in amounts if amount >= dust_threshold]
change = [(TYPE_ADDRESS, addr, amount)
for addr, amount in zip(change_addrs, amounts)]
log.debug('change: %s', change)
if dust:
log.debug('not keeping dust %s', dust)
return change
def make_tx(self, coins, outputs, change_addrs, fee_estimator,
dust_threshold, abandon_txid=None):
'''Select unspent coins to spend to pay outputs. If the change is
greater than dust_threshold (after adding the change output to
the transaction) it is kept, otherwise none is sent and it is
added to the transaction fee.'''
# Deterministic randomness from coins
utxos = [c['prevout_hash'] + str(c['prevout_n']) for c in coins]
self.p = PRNG(''.join(sorted(utxos)))
# Copy the ouputs so when adding change we don't modify "outputs"
tx = Transaction.from_io([], outputs[:])
# Size of the transaction with no inputs and no change
base_size = tx.estimated_size()
spent_amount = tx.output_value()
claim_coin = None
if abandon_txid is not None:
claim_coins = [coin for coin in coins if coin['is_claim']]
assert len(claim_coins) >= 1
claim_coin = claim_coins[0]
spent_amount -= claim_coin['value']
coins = [coin for coin in coins if not coin['is_claim']]
def sufficient_funds(buckets):
'''Given a list of buckets, return True if it has enough
value to pay for the transaction'''
total_input = sum(bucket.value for bucket in buckets)
total_size = sum(bucket.size for bucket in buckets) + base_size
return total_input >= spent_amount + fee_estimator(total_size)
# Collect the coins into buckets, choose a subset of the buckets
buckets = self.bucketize_coins(coins)
buckets = self.choose_buckets(buckets, sufficient_funds,
self.penalty_func(tx))
if claim_coin is not None:
tx.add_inputs([claim_coin])
tx.add_inputs([coin for b in buckets for coin in b.coins])
tx_size = base_size + sum(bucket.size for bucket in buckets)
# This takes a count of change outputs and returns a tx fee;
# each pay-to-bitcoin-address output serializes as 34 bytes
def fee(count):
return fee_estimator(tx_size + count * 34)
change = self.change_outputs(tx, change_addrs, fee, dust_threshold)
tx.add_outputs(change)
log.debug("using %i inputs", len(tx.inputs()))
log.info("using buckets: %s", [bucket.desc for bucket in buckets])
return tx
class CoinChooserOldestFirst(CoinChooserBase):
'''Maximize transaction priority. Select the oldest unspent
transaction outputs in your wallet, that are sufficient to cover
the spent amount. Then, remove any unneeded inputs, starting with
the smallest in value.
'''
def keys(self, coins):
return [coin['prevout_hash'] + ':' + str(coin['prevout_n'])
for coin in coins]
def choose_buckets(self, buckets, sufficient_funds, penalty_func):
'''Spend the oldest buckets first.'''
# Unconfirmed coins are young, not old
def adj_height(height):
return 99999999 if height == 0 else height
buckets.sort(key=lambda b: max(adj_height(coin['height'])
for coin in b.coins))
selected = []
for bucket in buckets:
selected.append(bucket)
if sufficient_funds(selected):
return strip_unneeded(selected, sufficient_funds)
raise NotEnoughFunds()
class CoinChooserRandom(CoinChooserBase):
def keys(self, coins):
return [coin['prevout_hash'] + ':' + str(coin['prevout_n'])
for coin in coins]
def bucket_candidates(self, buckets, sufficient_funds):
'''Returns a list of bucket sets.'''
candidates = set()
# Add all singletons
for n, bucket in enumerate(buckets):
if sufficient_funds([bucket]):
candidates.add((n,))
# And now some random ones
attempts = min(100, (len(buckets) - 1) * 10 + 1)
permutation = range(len(buckets))
for i in range(attempts):
# Get a random permutation of the buckets, and
# incrementally combine buckets until sufficient
self.p.shuffle(permutation)
bkts = []
for count, index in enumerate(permutation):
bkts.append(buckets[index])
if sufficient_funds(bkts):
candidates.add(tuple(sorted(permutation[:count + 1])))
break
else:
raise NotEnoughFunds()
candidates = [[buckets[n] for n in c] for c in candidates]
return [strip_unneeded(c, sufficient_funds) for c in candidates]
def choose_buckets(self, buckets, sufficient_funds, penalty_func):
candidates = self.bucket_candidates(buckets, sufficient_funds)
penalties = [penalty_func(cand) for cand in candidates]
winner = candidates[penalties.index(min(penalties))]
log.debug("Bucket sets: %i", len(buckets))
log.debug("Winning penalty: %s", min(penalties))
return winner
class CoinChooserPrivacy(CoinChooserRandom):
'''Attempts to better preserve user privacy. First, if any coin is
spent from a user address, all coins are. Compared to spending
from other addresses to make up an amount, this reduces
information leakage about sender holdings. It also helps to
reduce blockchain UTXO bloat, and reduce future privacy loss that
would come from reusing that address' remaining UTXOs. Second, it
penalizes change that is quite different to the sent amount.
Third, it penalizes change that is too big.'''
def keys(self, coins):
return [coin['address'] for coin in coins]
def penalty_func(self, tx):
min_change = min(o[2] for o in tx.outputs()) * 0.75
max_change = max(o[2] for o in tx.outputs()) * 1.33
spent_amount = sum(o[2] for o in tx.outputs())
def penalty(buckets):
badness = len(buckets) - 1
total_input = sum(bucket.value for bucket in buckets)
change = float(total_input - spent_amount)
# Penalize change not roughly in output range
if change < min_change:
badness += (min_change - change) / (min_change + 10000)
elif change > max_change:
badness += (change - max_change) / (max_change + 10000)
# Penalize large change; 5 BTC excess ~= using 1 more input
badness += change / (COIN * 5)
return badness
return penalty
COIN_CHOOSERS = {'Priority': CoinChooserOldestFirst,
'Privacy': CoinChooserPrivacy}

View file

@ -0,0 +1,76 @@
from lbrynet import __version__
LBRYUM_VERSION = __version__
PROTOCOL_VERSION = '0.10' # protocol version requested
NEW_SEED_VERSION = 11 # lbryum versions >= 2.0
OLD_SEED_VERSION = 4 # lbryum versions < 2.0
# The hash of the mnemonic seed must begin with this
SEED_PREFIX = '01' # Electrum standard wallet
SEED_PREFIX_2FA = '101' # extended seed for two-factor authentication
RECOMMENDED_FEE = 50000
COINBASE_MATURITY = 100
COIN = 100000000
# supported types of transaction outputs
TYPE_ADDRESS = 1
TYPE_PUBKEY = 2
TYPE_SCRIPT = 4
TYPE_CLAIM = 8
TYPE_SUPPORT = 16
TYPE_UPDATE = 32
# claim related constants
EXPIRATION_BLOCKS = 262974
RECOMMENDED_CLAIMTRIE_HASH_CONFIRMS = 1
NO_SIGNATURE = 'ff'
NULL_HASH = '0000000000000000000000000000000000000000000000000000000000000000'
HEADER_SIZE = 112
BLOCKS_PER_CHUNK = 96
CLAIM_ID_SIZE = 20
HEADERS_URL = "https://s3.amazonaws.com/lbry-blockchain-headers/blockchain_headers_latest"
DEFAULT_PORTS = {'t': '50001', 's': '50002', 'h': '8081', 'g': '8082'}
NODES_RETRY_INTERVAL = 60
SERVER_RETRY_INTERVAL = 10
MAX_BATCH_QUERY_SIZE = 500
proxy_modes = ['socks4', 'socks5', 'http']
# Main network and testnet3 definitions
# these values follow the parameters in lbrycrd/src/chainparams.cpp
blockchain_params = {
'lbrycrd_main': {
'pubkey_address': 0,
'script_address': 5,
'pubkey_address_prefix': 85,
'script_address_prefix': 122,
'genesis_hash': '9c89283ba0f3227f6c03b70216b9f665f0118d5e0fa729cedf4fb34d6a34f463',
'max_target': 0x0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,
'genesis_bits': 0x1f00ffff,
'target_timespan': 150
},
'lbrycrd_testnet': {
'pubkey_address': 0,
'script_address': 5,
'pubkey_address_prefix': 111,
'script_address_prefix': 196,
'genesis_hash': '9c89283ba0f3227f6c03b70216b9f665f0118d5e0fa729cedf4fb34d6a34f463',
'max_target': 0x0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,
'genesis_bits': 0x1f00ffff,
'target_timespan': 150
},
'lbrycrd_regtest': {
'pubkey_address': 0,
'script_address': 5,
'pubkey_address_prefix': 111,
'script_address_prefix': 196,
'genesis_hash': '6e3fcf1299d4ec5d79c3a4c91d624a4acf9e2e173d95a1a0504f677669687556',
'max_target': 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,
'genesis_bits': 0x207fffff,
'target_timespan': 1
}
}

View file

@ -0,0 +1,47 @@
import exceptions
import types
class EnumException(exceptions.Exception):
pass
class Enumeration(object):
"""
enum-like type
From the Python Cookbook, downloaded from http://code.activestate.com/recipes/67107/
"""
def __init__(self, name, enumList):
self.__doc__ = name
lookup = {}
reverseLookup = {}
i = 0
uniqueNames = []
uniqueValues = []
for x in enumList:
if isinstance(x, types.TupleType):
x, i = x
if not isinstance(x, types.StringType):
raise EnumException, "enum name is not a string: " + x
if not isinstance(i, types.IntType):
raise EnumException, "enum value is not an integer: " + i
if x in uniqueNames:
raise EnumException, "enum name is not unique: " + x
if i in uniqueValues:
raise EnumException, "enum value is not unique for " + x
uniqueNames.append(x)
uniqueValues.append(i)
lookup[x] = i
reverseLookup[i] = x
i = i + 1
self.lookup = lookup
self.reverseLookup = reverseLookup
def __getattr__(self, attr):
if attr not in self.lookup:
raise AttributeError(attr)
return self.lookup[attr]
def whatis(self, value):
return self.reverseLookup[value]

43
lbrynet/wallet/errors.py Normal file
View file

@ -0,0 +1,43 @@
class TransportException(Exception):
pass
class ServiceException(Exception):
code = -2
class RemoteServiceException(Exception):
pass
class ProtocolException(Exception):
pass
class MethodNotFoundException(ServiceException):
code = -3
class NotEnoughFunds(Exception):
pass
class InvalidPassword(Exception):
def __str__(self):
return "Incorrect password"
class Timeout(Exception):
pass
class InvalidProofError(Exception):
pass
class ChainValidationError(Exception):
pass
class InvalidClaimId(Exception):
pass

50
lbrynet/wallet/hashing.py Normal file
View file

@ -0,0 +1,50 @@
import hashlib
import hmac
def sha256(x):
return hashlib.sha256(x).digest()
def sha512(x):
return hashlib.sha512(x).digest()
def ripemd160(x):
h = hashlib.new('ripemd160')
h.update(x)
return h.digest()
def Hash(x):
if type(x) is unicode:
x = x.encode('utf-8')
return sha256(sha256(x))
def PoWHash(x):
if type(x) is unicode:
x = x.encode('utf-8')
r = sha512(Hash(x))
r1 = ripemd160(r[:len(r) / 2])
r2 = ripemd160(r[len(r) / 2:])
r3 = Hash(r1 + r2)
return r3
def hash_encode(x):
return x[::-1].encode('hex')
def hash_decode(x):
return x.decode('hex')[::-1]
def hmac_sha_512(x, y):
return hmac.new(x, y, hashlib.sha512).digest()
def hash_160(public_key):
md = hashlib.new('ripemd160')
md.update(sha256(public_key))
return md.digest()

633
lbrynet/wallet/lbrycrd.py Normal file
View file

@ -0,0 +1,633 @@
import base64
import hashlib
import hmac
import struct
import logging
import aes
import ecdsa
from ecdsa import numbertheory, util
from ecdsa.curves import SECP256k1
from ecdsa.ecdsa import curve_secp256k1, generator_secp256k1
from ecdsa.ellipticcurve import Point
from ecdsa.util import number_to_string, string_to_number
from lbryschema.address import public_key_to_address
from lbryschema.schema import B58_CHARS
from lbryschema.base import b58encode_with_checksum, b58decode_strip_checksum
from . import msqr
from .util import rev_hex, var_int, int_to_hex
from .hashing import Hash, sha256, hash_160
from .errors import InvalidPassword, InvalidClaimId
from .constants import CLAIM_ID_SIZE
log = logging.getLogger(__name__)
# AES encryption
EncodeAES = lambda secret, s: base64.b64encode(aes.encryptData(secret, s))
DecodeAES = lambda secret, e: aes.decryptData(secret, base64.b64decode(e))
# get the claim id hash from txid bytes and int n
def claim_id_hash(txid, n):
return hash_160(txid + struct.pack('>I', n))
# deocde a claim_id hex string
def decode_claim_id_hex(claim_id_hex):
claim_id = rev_hex(claim_id_hex).decode('hex')
if len(claim_id) != CLAIM_ID_SIZE:
raise InvalidClaimId()
return claim_id
# encode claim id bytes into hex string
def encode_claim_id_hex(claim_id):
return rev_hex(claim_id.encode('hex'))
def strip_PKCS7_padding(s):
"""return s stripped of PKCS7 padding"""
if len(s) % 16 or not s:
raise ValueError("String of len %d can't be PCKS7-padded" % len(s))
numpads = ord(s[-1])
if numpads > 16:
raise ValueError("String ending with %r can't be PCKS7-padded" % s[-1])
if s[-numpads:] != numpads * chr(numpads):
raise ValueError("Invalid PKCS7 padding")
return s[:-numpads]
# backport padding fix to AES module
aes.strip_PKCS7_padding = strip_PKCS7_padding
def aes_encrypt_with_iv(key, iv, data):
mode = aes.AESModeOfOperation.modeOfOperation["CBC"]
key = map(ord, key)
iv = map(ord, iv)
data = aes.append_PKCS7_padding(data)
keysize = len(key)
assert keysize in aes.AES.keySize.values(), 'invalid key size: %s' % keysize
moo = aes.AESModeOfOperation()
(mode, length, ciph) = moo.encrypt(data, mode, key, keysize, iv)
return ''.join(map(chr, ciph))
def aes_decrypt_with_iv(key, iv, data):
mode = aes.AESModeOfOperation.modeOfOperation["CBC"]
key = map(ord, key)
iv = map(ord, iv)
keysize = len(key)
assert keysize in aes.AES.keySize.values(), 'invalid key size: %s' % keysize
data = map(ord, data)
moo = aes.AESModeOfOperation()
decr = moo.decrypt(data, None, mode, key, keysize, iv)
decr = strip_PKCS7_padding(decr)
return decr
def pw_encode(s, password):
if password:
secret = Hash(password)
return EncodeAES(secret, s.encode("utf8"))
else:
return s
def pw_decode(s, password):
if password is not None:
secret = Hash(password)
try:
d = DecodeAES(secret, s).decode("utf8")
except Exception:
raise InvalidPassword()
return d
else:
return s
def op_push(i):
if i < 0x4c:
return int_to_hex(i)
elif i < 0xff:
return '4c' + int_to_hex(i)
elif i < 0xffff:
return '4d' + int_to_hex(i, 2)
else:
return '4e' + int_to_hex(i, 4)
# pywallet openssl private key implementation
def i2o_ECPublicKey(pubkey, compressed=False):
# public keys are 65 bytes long (520 bits)
# 0x04 + 32-byte X-coordinate + 32-byte Y-coordinate
# 0x00 = point at infinity, 0x02 and 0x03 = compressed, 0x04 = uncompressed
# compressed keys: <sign> <x> where <sign> is 0x02 if y is even and 0x03 if y is odd
if compressed:
if pubkey.point.y() & 1:
key = '03' + '%064x' % pubkey.point.x()
else:
key = '02' + '%064x' % pubkey.point.x()
else:
key = '04' + \
'%064x' % pubkey.point.x() + \
'%064x' % pubkey.point.y()
return key.decode('hex')
# end pywallet openssl private key implementation
# functions from pywallet
def PrivKeyToSecret(privkey):
return privkey[9:9 + 32]
def SecretToASecret(secret, compressed=False, addrtype=0):
vchIn = chr((addrtype + 128) & 255) + secret
if compressed:
vchIn += '\01'
return b58encode_with_checksum(vchIn)
def ASecretToSecret(key, addrtype=0):
vch = b58decode_strip_checksum(key)
if vch and vch[0] == chr((addrtype + 128) & 255):
return vch[1:]
elif is_minikey(key):
return minikey_to_private_key(key)
else:
return False
def regenerate_key(sec):
b = ASecretToSecret(sec)
if not b:
return False
b = b[0:32]
return EC_KEY(b)
def GetPubKey(pubkey, compressed=False):
return i2o_ECPublicKey(pubkey, compressed)
def GetSecret(pkey):
return ('%064x' % pkey.secret).decode('hex')
def is_compressed(sec):
b = ASecretToSecret(sec)
return len(b) == 33
def public_key_from_private_key(sec):
# rebuild public key from private key, compressed or uncompressed
pkey = regenerate_key(sec)
assert pkey
compressed = is_compressed(sec)
public_key = GetPubKey(pkey.pubkey, compressed)
return public_key.encode('hex')
def address_from_private_key(sec):
public_key = public_key_from_private_key(sec)
address = public_key_to_address(public_key.decode('hex'))
return address
def is_private_key(key):
try:
k = ASecretToSecret(key)
return k is not False
except:
return False
# end pywallet functions
def is_minikey(text):
# Minikeys are typically 22 or 30 characters, but this routine
# permits any length of 20 or more provided the minikey is valid.
# A valid minikey must begin with an 'S', be in base58, and when
# suffixed with '?' have its SHA256 hash begin with a zero byte.
# They are widely used in Casascius physical bitoins.
return (len(text) >= 20 and text[0] == 'S'
and all(c in B58_CHARS for c in text)
and ord(sha256(text + '?')[0]) == 0)
def minikey_to_private_key(text):
return sha256(text)
def msg_magic(message):
varint = var_int(len(message))
encoded_varint = "".join([chr(int(varint[i:i + 2], 16)) for i in xrange(0, len(varint), 2)])
return "\x18Bitcoin Signed Message:\n" + encoded_varint + message
def verify_message(address, signature, message):
try:
EC_KEY.verify_message(address, signature, message)
return True
except Exception as e:
return False
def encrypt_message(message, pubkey):
return EC_KEY.encrypt_message(message, pubkey.decode('hex'))
def chunks(l, n):
return [l[i:i + n] for i in xrange(0, len(l), n)]
def ECC_YfromX(x, curved=curve_secp256k1, odd=True):
_p = curved.p()
_a = curved.a()
_b = curved.b()
for offset in range(128):
Mx = x + offset
My2 = pow(Mx, 3, _p) + _a * pow(Mx, 2, _p) + _b % _p
My = pow(My2, (_p + 1) / 4, _p)
if curved.contains_point(Mx, My):
if odd == bool(My & 1):
return [My, offset]
return [_p - My, offset]
raise Exception('ECC_YfromX: No Y found')
def negative_point(P):
return Point(P.curve(), P.x(), -P.y(), P.order())
def point_to_ser(P, comp=True):
if comp:
return (('%02x' % (2 + (P.y() & 1))) + ('%064x' % P.x())).decode('hex')
return ('04' + ('%064x' % P.x()) + ('%064x' % P.y())).decode('hex')
def ser_to_point(Aser):
curve = curve_secp256k1
generator = generator_secp256k1
_r = generator.order()
assert Aser[0] in ['\x02', '\x03', '\x04']
if Aser[0] == '\x04':
return Point(curve, string_to_number(Aser[1:33]), string_to_number(Aser[33:]), _r)
Mx = string_to_number(Aser[1:])
return Point(curve, Mx, ECC_YfromX(Mx, curve, Aser[0] == '\x03')[0], _r)
class MyVerifyingKey(ecdsa.VerifyingKey):
@classmethod
def from_signature(cls, sig, recid, h, curve):
""" See http://www.secg.org/download/aid-780/sec1-v2.pdf, chapter 4.1.6 """
curveFp = curve.curve
G = curve.generator
order = G.order()
# extract r,s from signature
r, s = util.sigdecode_string(sig, order)
# 1.1
x = r + (recid / 2) * order
# 1.3
alpha = (x * x * x + curveFp.a() * x + curveFp.b()) % curveFp.p()
beta = msqr.modular_sqrt(alpha, curveFp.p())
y = beta if (beta - recid) % 2 == 0 else curveFp.p() - beta
# 1.4 the constructor checks that nR is at infinity
R = Point(curveFp, x, y, order)
# 1.5 compute e from message:
e = string_to_number(h)
minus_e = -e % order
# 1.6 compute Q = r^-1 (sR - eG)
inv_r = numbertheory.inverse_mod(r, order)
Q = inv_r * (s * R + minus_e * G)
return cls.from_public_point(Q, curve)
class MySigningKey(ecdsa.SigningKey):
"""Enforce low S values in signatures"""
def sign_number(self, number, entropy=None, k=None):
curve = SECP256k1
G = curve.generator
order = G.order()
r, s = ecdsa.SigningKey.sign_number(self, number, entropy, k)
if s > order / 2:
s = order - s
return r, s
class EC_KEY(object):
def __init__(self, k):
secret = string_to_number(k)
self.pubkey = ecdsa.ecdsa.Public_key(generator_secp256k1, generator_secp256k1 * secret)
self.privkey = ecdsa.ecdsa.Private_key(self.pubkey, secret)
self.secret = secret
def get_public_key(self, compressed=True):
return point_to_ser(self.pubkey.point, compressed).encode('hex')
def sign(self, msg_hash):
private_key = MySigningKey.from_secret_exponent(self.secret, curve=SECP256k1)
public_key = private_key.get_verifying_key()
signature = private_key.sign_digest_deterministic(msg_hash, hashfunc=hashlib.sha256,
sigencode=ecdsa.util.sigencode_string)
assert public_key.verify_digest(signature, msg_hash, sigdecode=ecdsa.util.sigdecode_string)
return signature
def sign_message(self, message, compressed, address):
signature = self.sign(Hash(msg_magic(message)))
for i in range(4):
sig = chr(27 + i + (4 if compressed else 0)) + signature
try:
self.verify_message(address, sig, message)
return sig
except Exception:
log.exception("error: cannot sign message")
continue
raise Exception("error: cannot sign message")
@classmethod
def verify_message(cls, address, sig, message):
if len(sig) != 65:
raise Exception("Wrong encoding")
nV = ord(sig[0])
if nV < 27 or nV >= 35:
raise Exception("Bad encoding")
if nV >= 31:
compressed = True
nV -= 4
else:
compressed = False
recid = nV - 27
h = Hash(msg_magic(message))
public_key = MyVerifyingKey.from_signature(sig[1:], recid, h, curve=SECP256k1)
# check public key
public_key.verify_digest(sig[1:], h, sigdecode=ecdsa.util.sigdecode_string)
pubkey = point_to_ser(public_key.pubkey.point, compressed)
# check that we get the original signing address
addr = public_key_to_address(pubkey)
if address != addr:
raise Exception("Bad signature")
# ECIES encryption/decryption methods; AES-128-CBC with PKCS7 is used as the cipher;
# hmac-sha256 is used as the mac
@classmethod
def encrypt_message(cls, message, pubkey):
pk = ser_to_point(pubkey)
if not ecdsa.ecdsa.point_is_valid(generator_secp256k1, pk.x(), pk.y()):
raise Exception('invalid pubkey')
ephemeral_exponent = number_to_string(ecdsa.util.randrange(pow(2, 256)),
generator_secp256k1.order())
ephemeral = EC_KEY(ephemeral_exponent)
ecdh_key = point_to_ser(pk * ephemeral.privkey.secret_multiplier)
key = hashlib.sha512(ecdh_key).digest()
iv, key_e, key_m = key[0:16], key[16:32], key[32:]
ciphertext = aes_encrypt_with_iv(key_e, iv, message)
ephemeral_pubkey = ephemeral.get_public_key(compressed=True).decode('hex')
encrypted = 'BIE1' + ephemeral_pubkey + ciphertext
mac = hmac.new(key_m, encrypted, hashlib.sha256).digest()
return base64.b64encode(encrypted + mac)
def decrypt_message(self, encrypted):
encrypted = base64.b64decode(encrypted)
if len(encrypted) < 85:
raise Exception('invalid ciphertext: length')
magic = encrypted[:4]
ephemeral_pubkey = encrypted[4:37]
ciphertext = encrypted[37:-32]
mac = encrypted[-32:]
if magic != 'BIE1':
raise Exception('invalid ciphertext: invalid magic bytes')
try:
ephemeral_pubkey = ser_to_point(ephemeral_pubkey)
except AssertionError, e:
raise Exception('invalid ciphertext: invalid ephemeral pubkey')
if not ecdsa.ecdsa.point_is_valid(generator_secp256k1, ephemeral_pubkey.x(),
ephemeral_pubkey.y()):
raise Exception('invalid ciphertext: invalid ephemeral pubkey')
ecdh_key = point_to_ser(ephemeral_pubkey * self.privkey.secret_multiplier)
key = hashlib.sha512(ecdh_key).digest()
iv, key_e, key_m = key[0:16], key[16:32], key[32:]
if mac != hmac.new(key_m, encrypted[:-32], hashlib.sha256).digest():
raise Exception('invalid ciphertext: invalid mac')
return aes_decrypt_with_iv(key_e, iv, ciphertext)
# BIP32
def random_seed(n):
return "%032x" % ecdsa.util.randrange(pow(2, n))
BIP32_PRIME = 0x80000000
def get_pubkeys_from_secret(secret):
# public key
private_key = ecdsa.SigningKey.from_string(secret, curve=SECP256k1)
public_key = private_key.get_verifying_key()
K = public_key.to_string()
K_compressed = GetPubKey(public_key.pubkey, True)
return K, K_compressed
# Child private key derivation function (from master private key)
# k = master private key (32 bytes)
# c = master chain code (extra entropy for key derivation) (32 bytes)
# n = the index of the key we want to derive. (only 32 bits will be used)
# If n is negative (i.e. the 32nd bit is set), the resulting private key's
# corresponding public key can NOT be determined without the master private key.
# However, if n is positive, the resulting private key's corresponding
# public key can be determined without the master private key.
def CKD_priv(k, c, n):
is_prime = n & BIP32_PRIME
return _CKD_priv(k, c, rev_hex(int_to_hex(n, 4)).decode('hex'), is_prime)
def _CKD_priv(k, c, s, is_prime):
order = generator_secp256k1.order()
keypair = EC_KEY(k)
cK = GetPubKey(keypair.pubkey, True)
data = chr(0) + k + s if is_prime else cK + s
I = hmac.new(c, data, hashlib.sha512).digest()
k_n = number_to_string((string_to_number(I[0:32]) + string_to_number(k)) % order, order)
c_n = I[32:]
return k_n, c_n
# Child public key derivation function (from public key only)
# K = master public key
# c = master chain code
# n = index of key we want to derive
# This function allows us to find the nth public key, as long as n is
# non-negative. If n is negative, we need the master private key to find it.
def CKD_pub(cK, c, n):
if n & BIP32_PRIME:
raise Exception("CKD pub error")
return _CKD_pub(cK, c, rev_hex(int_to_hex(n, 4)).decode('hex'))
# helper function, callable with arbitrary string
def _CKD_pub(cK, c, s):
order = generator_secp256k1.order()
I = hmac.new(c, cK + s, hashlib.sha512).digest()
curve = SECP256k1
pubkey_point = string_to_number(I[0:32]) * curve.generator + ser_to_point(cK)
public_key = ecdsa.VerifyingKey.from_public_point(pubkey_point, curve=SECP256k1)
c_n = I[32:]
cK_n = GetPubKey(public_key.pubkey, True)
return cK_n, c_n
BITCOIN_HEADER_PRIV = "0488ade4"
BITCOIN_HEADER_PUB = "0488b21e"
TESTNET_HEADER_PRIV = "04358394"
TESTNET_HEADER_PUB = "043587cf"
BITCOIN_HEADERS = (BITCOIN_HEADER_PUB, BITCOIN_HEADER_PRIV)
TESTNET_HEADERS = (TESTNET_HEADER_PUB, TESTNET_HEADER_PRIV)
def _get_headers(testnet):
"""Returns the correct headers for either testnet or bitcoin, in the form
of a 2-tuple, like (public, private)."""
if testnet:
return TESTNET_HEADERS
else:
return BITCOIN_HEADERS
def deserialize_xkey(xkey):
xkey = b58decode_strip_checksum(xkey)
assert len(xkey) == 78
xkey_header = xkey[0:4].encode('hex')
# Determine if the key is a bitcoin key or a testnet key.
if xkey_header in TESTNET_HEADERS:
head = TESTNET_HEADER_PRIV
elif xkey_header in BITCOIN_HEADERS:
head = BITCOIN_HEADER_PRIV
else:
raise Exception("Unknown xkey header: '%s'" % xkey_header)
depth = ord(xkey[4])
fingerprint = xkey[5:9]
child_number = xkey[9:13]
c = xkey[13:13 + 32]
if xkey[0:4].encode('hex') == head:
K_or_k = xkey[13 + 33:]
else:
K_or_k = xkey[13 + 32:]
return depth, fingerprint, child_number, c, K_or_k
def get_xkey_name(xkey, testnet=False):
depth, fingerprint, child_number, c, K = deserialize_xkey(xkey)
n = int(child_number.encode('hex'), 16)
if n & BIP32_PRIME:
child_id = "%d'" % (n - BIP32_PRIME)
else:
child_id = "%d" % n
if depth == 0:
return ''
elif depth == 1:
return child_id
else:
raise BaseException("xpub depth error")
def xpub_from_xprv(xprv, testnet=False):
depth, fingerprint, child_number, c, k = deserialize_xkey(xprv)
K, cK = get_pubkeys_from_secret(k)
header_pub, _ = _get_headers(testnet)
xpub = header_pub.decode('hex') + chr(depth) + fingerprint + child_number + c + cK
return b58encode_with_checksum(xpub)
def bip32_root(seed, testnet=False):
header_pub, header_priv = _get_headers(testnet)
I = hmac.new("Bitcoin seed", seed, hashlib.sha512).digest()
master_k = I[0:32]
master_c = I[32:]
K, cK = get_pubkeys_from_secret(master_k)
xprv = (header_priv + "00" + "00000000" + "00000000").decode("hex") + master_c + chr(
0) + master_k
xpub = (header_pub + "00" + "00000000" + "00000000").decode("hex") + master_c + cK
return b58encode_with_checksum(xprv), b58encode_with_checksum(xpub)
def xpub_from_pubkey(cK, testnet=False):
header_pub, header_priv = _get_headers(testnet)
assert cK[0] in ['\x02', '\x03']
master_c = chr(0) * 32
xpub = (header_pub + "00" + "00000000" + "00000000").decode("hex") + master_c + cK
return b58encode_with_checksum(xpub)
def bip32_private_derivation(xprv, branch, sequence, testnet=False):
assert sequence.startswith(branch)
if branch == sequence:
return xprv, xpub_from_xprv(xprv, testnet)
header_pub, header_priv = _get_headers(testnet)
depth, fingerprint, child_number, c, k = deserialize_xkey(xprv)
sequence = sequence[len(branch):]
for n in sequence.split('/'):
if n == '':
continue
i = int(n[:-1]) + BIP32_PRIME if n[-1] == "'" else int(n)
parent_k = k
k, c = CKD_priv(k, c, i)
depth += 1
_, parent_cK = get_pubkeys_from_secret(parent_k)
fingerprint = hash_160(parent_cK)[0:4]
child_number = ("%08X" % i).decode('hex')
K, cK = get_pubkeys_from_secret(k)
xprv = header_priv.decode('hex') + chr(depth) + fingerprint + child_number + c + chr(0) + k
xpub = header_pub.decode('hex') + chr(depth) + fingerprint + child_number + c + cK
return b58encode_with_checksum(xprv), b58encode_with_checksum(xpub)
def bip32_public_derivation(xpub, branch, sequence, testnet=False):
header_pub, _ = _get_headers(testnet)
depth, fingerprint, child_number, c, cK = deserialize_xkey(xpub)
assert sequence.startswith(branch)
sequence = sequence[len(branch):]
for n in sequence.split('/'):
if n == '':
continue
i = int(n)
parent_cK = cK
cK, c = CKD_pub(cK, c, i)
depth += 1
fingerprint = hash_160(parent_cK)[0:4]
child_number = ("%08X" % i).decode('hex')
xpub = header_pub.decode('hex') + chr(depth) + fingerprint + child_number + c + cK
return b58encode_with_checksum(xpub)
def bip32_private_key(sequence, k, chain):
for i in sequence:
k, chain = CKD_priv(k, chain, i)
return SecretToASecret(k, True)

88
lbrynet/wallet/manager.py Normal file
View file

@ -0,0 +1,88 @@
import os
import logging
from twisted.internet import defer
import lbryschema
from .protocol import Network
from .blockchain import BlockchainHeaders
from .wallet import Wallet
log = logging.getLogger(__name__)
def chunks(l, n):
for i in range(0, len(l), n):
yield l[i:i+n]
class WalletManager:
def __init__(self, storage, config):
self.storage = storage
self.config = config
lbryschema.BLOCKCHAIN_NAME = config['chain']
self.headers = BlockchainHeaders(self.headers_path, config['chain'])
self.wallet = Wallet(self.wallet_path)
self.network = Network(config)
self.network.on_header.listen(self.process_header)
self.network.on_transaction.listen(self.process_transaction)
self._downloading_headers = False
@property
def headers_path(self):
filename = 'blockchain_headers'
if self.config['chain'] != 'lbrycrd_main':
filename = '{}_headers'.format(self.config['chain'].split("_")[1])
return os.path.join(self.config['wallet_path'], filename)
@property
def wallet_path(self):
return os.path.join(self.config['wallet_path'], 'wallets', 'default_wallet')
@defer.inlineCallbacks
def start(self):
self.wallet.load()
self.network.start()
yield self.network.on_connected.first
yield self.download_headers()
yield self.network.headers_subscribe()
yield self.download_transactions()
def stop(self):
return self.network.stop()
@defer.inlineCallbacks
def download_headers(self):
self._downloading_headers = True
while True:
sought_height = len(self.headers)
headers = yield self.network.block_headers(sought_height)
log.info("received {} headers starting at {} height".format(headers['count'], sought_height))
if headers['count'] <= 0:
break
yield self.headers.connect(sought_height, headers['hex'].decode('hex'))
self._downloading_headers = False
@defer.inlineCallbacks
def process_header(self, header):
if self._downloading_headers:
return
if header['block_height'] == len(self.headers):
# New header from network directly connects after the last local header.
yield self.headers.connect(len(self.headers), header['hex'].decode('hex'))
elif header['block_height'] > len(self.headers):
# New header is several heights ahead of local, do download instead.
yield self.download_headers()
@defer.inlineCallbacks
def download_transactions(self):
for addresses in chunks(self.wallet.addresses, 500):
self.network.rpc([
('blockchain.address.subscribe', [address])
for address in addresses
])
def process_transaction(self, tx):
pass

157
lbrynet/wallet/mnemonic.py Normal file
View file

@ -0,0 +1,157 @@
import hashlib
import hmac
import math
import os
import pkgutil
import string
import unicodedata
import logging
import ecdsa
import pbkdf2
from . import constants
from .hashing import hmac_sha_512
log = logging.getLogger(__name__)
# http://www.asahi-net.or.jp/~ax2s-kmtn/ref/unicode/e_asia.html
CJK_INTERVALS = [
(0x4E00, 0x9FFF, 'CJK Unified Ideographs'),
(0x3400, 0x4DBF, 'CJK Unified Ideographs Extension A'),
(0x20000, 0x2A6DF, 'CJK Unified Ideographs Extension B'),
(0x2A700, 0x2B73F, 'CJK Unified Ideographs Extension C'),
(0x2B740, 0x2B81F, 'CJK Unified Ideographs Extension D'),
(0xF900, 0xFAFF, 'CJK Compatibility Ideographs'),
(0x2F800, 0x2FA1D, 'CJK Compatibility Ideographs Supplement'),
(0x3190, 0x319F, 'Kanbun'),
(0x2E80, 0x2EFF, 'CJK Radicals Supplement'),
(0x2F00, 0x2FDF, 'CJK Radicals'),
(0x31C0, 0x31EF, 'CJK Strokes'),
(0x2FF0, 0x2FFF, 'Ideographic Description Characters'),
(0xE0100, 0xE01EF, 'Variation Selectors Supplement'),
(0x3100, 0x312F, 'Bopomofo'),
(0x31A0, 0x31BF, 'Bopomofo Extended'),
(0xFF00, 0xFFEF, 'Halfwidth and Fullwidth Forms'),
(0x3040, 0x309F, 'Hiragana'),
(0x30A0, 0x30FF, 'Katakana'),
(0x31F0, 0x31FF, 'Katakana Phonetic Extensions'),
(0x1B000, 0x1B0FF, 'Kana Supplement'),
(0xAC00, 0xD7AF, 'Hangul Syllables'),
(0x1100, 0x11FF, 'Hangul Jamo'),
(0xA960, 0xA97F, 'Hangul Jamo Extended A'),
(0xD7B0, 0xD7FF, 'Hangul Jamo Extended B'),
(0x3130, 0x318F, 'Hangul Compatibility Jamo'),
(0xA4D0, 0xA4FF, 'Lisu'),
(0x16F00, 0x16F9F, 'Miao'),
(0xA000, 0xA48F, 'Yi Syllables'),
(0xA490, 0xA4CF, 'Yi Radicals'),
]
def is_CJK(c):
n = ord(c)
for imin, imax, name in CJK_INTERVALS:
if imin <= n <= imax:
return True
return False
def prepare_seed(seed):
# normalize
seed = unicodedata.normalize('NFKD', unicode(seed))
# lower
seed = seed.lower()
# remove accents
seed = u''.join([c for c in seed if not unicodedata.combining(c)])
# normalize whitespaces
seed = u' '.join(seed.split())
# remove whitespaces between CJK
seed = u''.join([seed[i] for i in range(len(seed)) if not (seed[i] in string.whitespace
and is_CJK(seed[i - 1])
and is_CJK(seed[i + 1]))])
return seed
filenames = {
'en': 'english.txt',
'es': 'spanish.txt',
'ja': 'japanese.txt',
'pt': 'portuguese.txt',
'zh': 'chinese_simplified.txt'
}
class Mnemonic:
# Seed derivation no longer follows BIP39
# Mnemonic phrase uses a hash based checksum, instead of a wordlist-dependent checksum
def __init__(self, lang=None):
lang = lang or "en"
filename = filenames.get(lang[0:2], 'english.txt')
s = pkgutil.get_data('lbrynet', os.path.join('wallet', 'wordlist', filename))
s = unicodedata.normalize('NFKD', s.decode('utf8'))
lines = s.split('\n')
self.wordlist = []
for line in lines:
line = line.split('#')[0]
line = line.strip(' \r')
assert ' ' not in line
if line:
self.wordlist.append(line)
log.info("wordlist has %d words", len(self.wordlist))
@classmethod
def mnemonic_to_seed(cls, mnemonic, passphrase):
PBKDF2_ROUNDS = 2048
mnemonic = prepare_seed(mnemonic)
return pbkdf2.PBKDF2(mnemonic, 'lbryum' + passphrase, iterations=PBKDF2_ROUNDS,
macmodule=hmac, digestmodule=hashlib.sha512).read(64)
def mnemonic_encode(self, i):
n = len(self.wordlist)
words = []
while i:
x = i % n
i = i / n
words.append(self.wordlist[x])
return ' '.join(words)
def mnemonic_decode(self, seed):
n = len(self.wordlist)
words = seed.split()
i = 0
while words:
w = words.pop()
k = self.wordlist.index(w)
i = i * n + k
return i
def check_seed(self, seed, custom_entropy):
assert is_new_seed(seed)
i = self.mnemonic_decode(seed)
return i % custom_entropy == 0
def make_seed(self, num_bits=128, prefix=constants.SEED_PREFIX, custom_entropy=1):
n = int(math.ceil(math.log(custom_entropy, 2)))
# bits of entropy used by the prefix
k = len(prefix) * 4
# we add at least 16 bits
n_added = max(16, k + num_bits - n)
log.info("make_seed %s adding %d bits", prefix, n_added)
my_entropy = ecdsa.util.randrange(pow(2, n_added))
nonce = 0
while True:
nonce += 1
i = custom_entropy * (my_entropy + nonce)
seed = self.mnemonic_encode(i)
assert i == self.mnemonic_decode(seed)
if is_new_seed(seed, prefix):
break
log.info('%d words', len(seed.split()))
return seed
def is_new_seed(x, prefix=constants.SEED_PREFIX):
x = prepare_seed(x)
s = hmac_sha_512("Seed version", x.encode('utf8')).encode('hex')
return s.startswith(prefix)

96
lbrynet/wallet/msqr.py Normal file
View file

@ -0,0 +1,96 @@
# from http://eli.thegreenplace.net/2009/03/07/computing-modular-square-roots-in-python/
def modular_sqrt(a, p):
""" Find a quadratic residue (mod p) of 'a'. p
must be an odd prime.
Solve the congruence of the form:
x^2 = a (mod p)
And returns x. Note that p - x is also a root.
0 is returned is no square root exists for
these a and p.
The Tonelli-Shanks algorithm is used (except
for some simple cases in which the solution
is known from an identity). This algorithm
runs in polynomial time (unless the
generalized Riemann hypothesis is false).
"""
# Simple cases
#
if legendre_symbol(a, p) != 1:
return 0
elif a == 0:
return 0
elif p == 2:
return p
elif p % 4 == 3:
return pow(a, (p + 1) / 4, p)
# Partition p-1 to s * 2^e for an odd s (i.e.
# reduce all the powers of 2 from p-1)
#
s = p - 1
e = 0
while s % 2 == 0:
s /= 2
e += 1
# Find some 'n' with a legendre symbol n|p = -1.
# Shouldn't take long.
#
n = 2
while legendre_symbol(n, p) != -1:
n += 1
# Here be dragons!
# Read the paper "Square roots from 1; 24, 51,
# 10 to Dan Shanks" by Ezra Brown for more
# information
#
# x is a guess of the square root that gets better
# with each iteration.
# b is the "fudge factor" - by how much we're off
# with the guess. The invariant x^2 = ab (mod p)
# is maintained throughout the loop.
# g is used for successive powers of n to update
# both a and b
# r is the exponent - decreases with each update
#
x = pow(a, (s + 1) / 2, p)
b = pow(a, s, p)
g = pow(n, s, p)
r = e
while True:
t = b
m = 0
for m in xrange(r):
if t == 1:
break
t = pow(t, 2, p)
if m == 0:
return x
gs = pow(g, 2 ** (r - m - 1), p)
g = (gs * gs) % p
x = (x * gs) % p
b = (b * g) % p
r = m
def legendre_symbol(a, p):
""" Compute the Legendre symbol a|p using
Euler's criterion. p is a prime, a is
relatively prime to p (if p divides
a, then a|p = 0)
Returns 1 if a has a square root modulo
p, -1 otherwise.
"""
ls = pow(a, (p - 1) / 2, p)
return -1 if ls == p - 1 else ls

76
lbrynet/wallet/opcodes.py Normal file
View file

@ -0,0 +1,76 @@
import struct
from .enumeration import Enumeration
opcodes = Enumeration("Opcodes", [
("OP_0", 0), ("OP_PUSHDATA1", 76), "OP_PUSHDATA2", "OP_PUSHDATA4", "OP_1NEGATE", "OP_RESERVED",
"OP_1", "OP_2", "OP_3", "OP_4", "OP_5", "OP_6", "OP_7",
"OP_8", "OP_9", "OP_10", "OP_11", "OP_12", "OP_13", "OP_14", "OP_15", "OP_16",
"OP_NOP", "OP_VER", "OP_IF", "OP_NOTIF", "OP_VERIF", "OP_VERNOTIF", "OP_ELSE", "OP_ENDIF",
"OP_VERIFY",
"OP_RETURN", "OP_TOALTSTACK", "OP_FROMALTSTACK", "OP_2DROP", "OP_2DUP", "OP_3DUP", "OP_2OVER",
"OP_2ROT", "OP_2SWAP",
"OP_IFDUP", "OP_DEPTH", "OP_DROP", "OP_DUP", "OP_NIP", "OP_OVER", "OP_PICK", "OP_ROLL",
"OP_ROT",
"OP_SWAP", "OP_TUCK", "OP_CAT", "OP_SUBSTR", "OP_LEFT", "OP_RIGHT", "OP_SIZE", "OP_INVERT",
"OP_AND",
"OP_OR", "OP_XOR", "OP_EQUAL", "OP_EQUALVERIFY", "OP_RESERVED1", "OP_RESERVED2", "OP_1ADD",
"OP_1SUB", "OP_2MUL",
"OP_2DIV", "OP_NEGATE", "OP_ABS", "OP_NOT", "OP_0NOTEQUAL", "OP_ADD", "OP_SUB", "OP_MUL",
"OP_DIV",
"OP_MOD", "OP_LSHIFT", "OP_RSHIFT", "OP_BOOLAND", "OP_BOOLOR",
"OP_NUMEQUAL", "OP_NUMEQUALVERIFY", "OP_NUMNOTEQUAL", "OP_LESSTHAN",
"OP_GREATERTHAN", "OP_LESSTHANOREQUAL", "OP_GREATERTHANOREQUAL", "OP_MIN", "OP_MAX",
"OP_WITHIN", "OP_RIPEMD160", "OP_SHA1", "OP_SHA256", "OP_HASH160",
"OP_HASH256", "OP_CODESEPARATOR", "OP_CHECKSIG", "OP_CHECKSIGVERIFY", "OP_CHECKMULTISIG",
"OP_CHECKMULTISIGVERIFY", "OP_NOP1", "OP_NOP2", "OP_NOP3", "OP_NOP4", "OP_NOP5",
"OP_CLAIM_NAME",
"OP_SUPPORT_CLAIM", "OP_UPDATE_CLAIM",
("OP_SINGLEBYTE_END", 0xF0),
("OP_DOUBLEBYTE_BEGIN", 0xF000),
"OP_PUBKEY", "OP_PUBKEYHASH",
("OP_INVALIDOPCODE", 0xFFFF),
])
def script_GetOp(bytes):
i = 0
while i < len(bytes):
vch = None
opcode = ord(bytes[i])
i += 1
if opcode >= opcodes.OP_SINGLEBYTE_END:
opcode <<= 8
opcode |= ord(bytes[i])
i += 1
if opcode <= opcodes.OP_PUSHDATA4:
nSize = opcode
if opcode == opcodes.OP_PUSHDATA1:
nSize = ord(bytes[i])
i += 1
elif opcode == opcodes.OP_PUSHDATA2:
(nSize,) = struct.unpack_from('<H', bytes, i)
i += 2
elif opcode == opcodes.OP_PUSHDATA4:
(nSize,) = struct.unpack_from('<I', bytes, i)
i += 4
vch = bytes[i:i + nSize]
i += nSize
yield (opcode, vch, i)
def script_GetOpName(opcode):
return (opcodes.whatis(opcode)).replace("OP_", "")
def match_decoded(decoded, to_match):
if len(decoded) != len(to_match):
return False
for i, d in enumerate(decoded):
if to_match[i] == opcodes.OP_PUSHDATA4 and opcodes.OP_PUSHDATA4 >= d[0] > 0:
# Opcodes below OP_PUSHDATA4 all just push data onto stack, # and are equivalent.
continue
if to_match[i] != decoded[i][0]:
return False
return True

282
lbrynet/wallet/protocol.py Normal file
View file

@ -0,0 +1,282 @@
import sys
import time
import json
import socket
import logging
from itertools import cycle
from twisted.internet import defer, reactor, protocol, threads
from twisted.application.internet import ClientService, CancelledError
from twisted.internet.endpoints import clientFromString
from twisted.protocols.basic import LineOnlyReceiver
from errors import RemoteServiceException, ProtocolException
from errors import TransportException
from .stream import StreamController
log = logging.getLogger()
class StratumClientProtocol(LineOnlyReceiver):
delimiter = '\n'
def __init__(self):
self.request_id = 0
self.lookup_table = {}
self.session = {}
self.on_disconnected_controller = StreamController()
self.on_disconnected = self.on_disconnected_controller.stream
def _get_id(self):
self.request_id += 1
return self.request_id
@property
def _ip(self):
return self.transport.getPeer().host
def get_session(self):
return self.session
def connectionMade(self):
try:
self.transport.setTcpNoDelay(True)
self.transport.setTcpKeepAlive(True)
self.transport.socket.setsockopt(
socket.SOL_TCP, socket.TCP_KEEPIDLE, 120
# Seconds before sending keepalive probes
)
self.transport.socket.setsockopt(
socket.SOL_TCP, socket.TCP_KEEPINTVL, 1
# Interval in seconds between keepalive probes
)
self.transport.socket.setsockopt(
socket.SOL_TCP, socket.TCP_KEEPCNT, 5
# Failed keepalive probles before declaring other end dead
)
except Exception as err:
# Supported only by the socket transport,
# but there's really no better place in code to trigger this.
log.warning("Error setting up socket: %s", err)
def connectionLost(self, reason=None):
self.on_disconnected_controller.add(True)
def lineReceived(self, line):
try:
message = json.loads(line)
except (ValueError, TypeError):
raise ProtocolException("Cannot decode message '%s'" % line.strip())
msg_id = message.get('id', 0)
msg_result = message.get('result')
msg_error = message.get('error')
msg_method = message.get('method')
msg_params = message.get('params')
if msg_id:
# It's a RPC response
# Perform lookup to the table of waiting requests.
try:
meta = self.lookup_table[msg_id]
del self.lookup_table[msg_id]
except KeyError:
# When deferred object for given message ID isn't found, it's an error
raise ProtocolException(
"Lookup for deferred object for message ID '%s' failed." % msg_id)
# If there's an error, handle it as errback
# If both result and error are null, handle it as a success with blank result
if msg_error != None:
meta['defer'].errback(
RemoteServiceException(msg_error[0], msg_error[1], msg_error[2])
)
else:
meta['defer'].callback(msg_result)
elif msg_method:
if msg_method == 'blockchain.headers.subscribe':
self.network._on_header_controller.add(msg_params[0])
elif msg_method == 'blockchain.address.subscribe':
self.network._on_address_controller.add(msg_params)
else:
log.warning("Cannot handle message '%s'" % line)
def write_request(self, method, params, is_notification=False):
request_id = None if is_notification else self._get_id()
serialized = json.dumps({'id': request_id, 'method': method, 'params': params})
self.sendLine(serialized)
return request_id
def rpc(self, method, params, is_notification=False):
request_id = self.write_request(method, params, is_notification)
if is_notification:
return
d = defer.Deferred()
self.lookup_table[request_id] = {
'method': method,
'params': params,
'defer': d,
}
return d
class StratumClientFactory(protocol.ClientFactory):
protocol = StratumClientProtocol
def __init__(self, network):
self.network = network
self.client = None
def buildProtocol(self, addr):
client = self.protocol()
client.factory = self
client.network = self.network
self.client = client
return client
class Network:
def __init__(self, config):
self.config = config
self.client = None
self.service = None
self.running = False
self._on_connected_controller = StreamController()
self.on_connected = self._on_connected_controller.stream
self._on_header_controller = StreamController()
self.on_header = self._on_header_controller.stream
self._on_transaction_controller = StreamController()
self.on_transaction = self._on_transaction_controller.stream
@defer.inlineCallbacks
def start(self):
for server in cycle(self.config.get('default_servers')):
endpoint = clientFromString(reactor, 'tcp:{}:{}'.format(*server))
self.service = ClientService(endpoint, StratumClientFactory(self))
self.service.startService()
try:
self.client = yield self.service.whenConnected(failAfterFailures=2)
self._on_connected_controller.add(True)
yield self.client.on_disconnected.first
except CancelledError:
return
except Exception as e:
pass
finally:
self.client = None
if not self.running:
return
def stop(self):
self.running = False
if self.service is not None:
self.service.stopService()
if self.is_connected:
return self.client.on_disconnected.first
else:
return defer.succeed(True)
@property
def is_connected(self):
return self.client is not None and self.client.connected
def rpc(self, method, params, *args, **kwargs):
if self.is_connected:
return self.client.rpc(method, params, *args, **kwargs)
else:
raise TransportException("Attempting to send rpc request when connection is not available.")
def claimtrie_getvaluesforuris(self, block_hash, *uris):
return self.rpc(
'blockchain.claimtrie.getvaluesforuris', [block_hash] + list(uris)
)
def claimtrie_getvaluesforuri(self, block_hash, uri):
return self.rpc('blockchain.claimtrie.getvaluesforuri', [block_hash, uri])
def claimtrie_getclaimssignedbynthtoname(self, name, n):
return self.rpc('blockchain.claimtrie.getclaimssignedbynthtoname', [name, n])
def claimtrie_getclaimssignedbyid(self, certificate_id):
return self.rpc('blockchain.claimtrie.getclaimssignedbyid', [certificate_id])
def claimtrie_getclaimssignedby(self, name):
return self.rpc('blockchain.claimtrie.getclaimssignedby', [name])
def claimtrie_getnthclaimforname(self, name, n):
return self.rpc('blockchain.claimtrie.getnthclaimforname', [name, n])
def claimtrie_getclaimsbyids(self, *claim_ids):
return self.rpc('blockchain.claimtrie.getclaimsbyids', list(claim_ids))
def claimtrie_getclaimbyid(self, claim_id):
return self.rpc('blockchain.claimtrie.getclaimbyid', [claim_id])
def claimtrie_get(self):
return self.rpc('blockchain.claimtrie.get', [])
def block_get_block(self, block_hash):
return self.rpc('blockchain.block.get_block', [block_hash])
def claimtrie_getclaimsforname(self, name):
return self.rpc('blockchain.claimtrie.getclaimsforname', [name])
def claimtrie_getclaimsintx(self, txid):
return self.rpc('blockchain.claimtrie.getclaimsintx', [txid])
def claimtrie_getvalue(self, name, block_hash=None):
return self.rpc('blockchain.claimtrie.getvalue', [name, block_hash])
def relayfee(self):
return self.rpc('blockchain.relayfee', [])
def estimatefee(self):
return self.rpc('blockchain.estimatefee', [])
def transaction_get(self, txid):
return self.rpc('blockchain.transaction.get', [txid])
def transaction_get_merkle(self, tx_hash, height, cache_only=False):
return self.rpc('blockchain.transaction.get_merkle', [tx_hash, height, cache_only])
def transaction_broadcast(self, raw_transaction):
return self.rpc('blockchain.transaction.broadcast', [raw_transaction])
def block_get_chunk(self, index, cache_only=False):
return self.rpc('blockchain.block.get_chunk', [index, cache_only])
def block_get_header(self, height, cache_only=False):
return self.rpc('blockchain.block.get_header', [height, cache_only])
def block_headers(self, height, count=10000):
return self.rpc('blockchain.block.headers', [height, count])
def utxo_get_address(self, txid, pos):
return self.rpc('blockchain.utxo.get_address', [txid, pos])
def address_listunspent(self, address):
return self.rpc('blockchain.address.listunspent', [address])
def address_get_proof(self, address):
return self.rpc('blockchain.address.get_proof', [address])
def address_get_balance(self, address):
return self.rpc('blockchain.address.get_balance', [address])
def address_get_mempool(self, address):
return self.rpc('blockchain.address.get_mempool', [address])
def address_get_history(self, address):
return self.rpc('blockchain.address.get_history', [address])
def address_subscribe(self, addresses):
if isinstance(addresses, str):
return self.rpc('blockchain.address.subscribe', [addresses])
else:
msgs = map(lambda addr: ('blockchain.address.subscribe', [addr]), addresses)
self.network.send(msgs, self.addr_subscription_response)
def headers_subscribe(self):
return self.rpc('blockchain.headers.subscribe', [], True)

31
lbrynet/wallet/store.py Normal file
View file

@ -0,0 +1,31 @@
import os
import json
class JSONStore(dict):
def __init__(self, config, name):
self.config = config
self.path = os.path.join(self.config.path, name)
self.load()
def load(self):
try:
with open(self.path, 'r') as f:
self.update(json.loads(f.read()))
except:
pass
def save(self):
with open(self.path, 'w') as f:
s = json.dumps(self, indent=4, sort_keys=True)
r = f.write(s)
def __setitem__(self, key, value):
dict.__setitem__(self, key, value)
self.save()
def pop(self, key):
if key in self.keys():
dict.pop(self, key)
self.save()

127
lbrynet/wallet/stream.py Normal file
View file

@ -0,0 +1,127 @@
from twisted.internet.defer import Deferred
from twisted.python.failure import Failure
class BroadcastSubscription:
def __init__(self, controller, on_data, on_error, on_done):
self._controller = controller
self._previous = self._next = None
self._on_data = on_data
self._on_error = on_error
self._on_done = on_done
self.is_paused = False
self.is_canceled = False
self.is_closed = False
def pause(self):
self.is_paused = True
def resume(self):
self.is_paused = False
def cancel(self):
self._controller._cancel(self)
self.is_canceled = True
@property
def can_fire(self):
return not any((self.is_paused, self.is_canceled, self.is_closed))
def _add(self, data):
if self.can_fire and self._on_data is not None:
self._on_data(data)
def _add_error(self, error, traceback):
if self.can_fire and self._on_error is not None:
self._on_error(error, traceback)
def _close(self):
if self.can_fire and self._on_done is not None:
self._on_done()
self.is_closed = True
class StreamController:
def __init__(self):
self.stream = Stream(self)
self._first_subscription = None
self._last_subscription = None
@property
def has_listener(self):
return self._first_subscription is not None
@property
def _iterate_subscriptions(self):
next = self._first_subscription
while next is not None:
subscription = next
next = next._next
yield subscription
def add(self, event):
for subscription in self._iterate_subscriptions:
subscription._add(event)
def add_error(self, error, traceback):
for subscription in self._iterate_subscriptions:
subscription._add_error(error, traceback)
def close(self):
for subscription in self._iterate_subscriptions:
subscription._close()
def _cancel(self, subscription):
previous = subscription._previous
next = subscription._next
if previous is None:
self._first_subscription = next
else:
previous._next = next
if next is None:
self._last_subscription = previous
else:
next._previous = previous
subscription._next = subscription._previous = subscription
def _listen(self, on_data, on_error, on_done):
subscription = BroadcastSubscription(self, on_data, on_error, on_done)
old_last = self._last_subscription
self._last_subscription = subscription
subscription._previous = old_last
subscription._next = None
if old_last is None:
self._first_subscription = subscription
else:
old_last._next = subscription
return subscription
class Stream:
def __init__(self, controller):
self._controller = controller
def listen(self, on_data, on_error=None, on_done=None):
return self._controller._listen(on_data, on_error, on_done)
@property
def first(self):
deferred = Deferred()
subscription = self.listen(
lambda value: self._cancel_and_callback(subscription, deferred, value),
lambda error, traceback: self._cancel_and_error(subscription, deferred, error, traceback)
)
return deferred
@staticmethod
def _cancel_and_callback(subscription, deferred, value):
subscription.cancel()
deferred.callback(value)
@staticmethod
def _cancel_and_error(subscription, deferred, error, traceback):
subscription.cancel()
deferred.errback(Failure(error, exc_tb=traceback))

View file

@ -0,0 +1,702 @@
import sys
import hashlib
import logging
import ecdsa
from ecdsa.curves import SECP256k1
from lbryschema.address import hash_160_bytes_to_address, public_key_to_address
from lbryschema.address import address_to_hash_160
from .constants import TYPE_SCRIPT, TYPE_PUBKEY, TYPE_UPDATE, TYPE_SUPPORT, TYPE_CLAIM
from .constants import TYPE_ADDRESS, NO_SIGNATURE
from .opcodes import opcodes, match_decoded, script_GetOp
from .bcd_data_stream import BCDataStream
from .hashing import Hash, hash_160, hash_encode
from .lbrycrd import op_push
from .lbrycrd import point_to_ser, MyVerifyingKey, MySigningKey
from .lbrycrd import regenerate_key, public_key_from_private_key
from .lbrycrd import encode_claim_id_hex, claim_id_hash
from .util import profiler, var_int, int_to_hex, parse_sig, rev_hex
log = logging.getLogger()
def parse_xpub(x_pubkey):
if x_pubkey[0:2] in ['02', '03', '04']:
pubkey = x_pubkey
elif x_pubkey[0:2] == 'ff':
from lbryum.bip32 import BIP32_Account
xpub, s = BIP32_Account.parse_xpubkey(x_pubkey)
pubkey = BIP32_Account.derive_pubkey_from_xpub(xpub, s[0], s[1])
elif x_pubkey[0:2] == 'fd':
addrtype = ord(x_pubkey[2:4].decode('hex'))
hash160 = x_pubkey[4:].decode('hex')
pubkey = None
address = hash_160_bytes_to_address(hash160, addrtype)
else:
raise BaseException("Cannnot parse pubkey")
if pubkey:
address = public_key_to_address(pubkey.decode('hex'))
return pubkey, address
def parse_scriptSig(d, bytes):
try:
decoded = [x for x in script_GetOp(bytes)]
except Exception:
# coinbase transactions raise an exception
log.error("cannot find address in input script: {}".format(bytes.encode('hex')))
return
# payto_pubkey
match = [opcodes.OP_PUSHDATA4]
if match_decoded(decoded, match):
sig = decoded[0][1].encode('hex')
d['address'] = "(pubkey)"
d['signatures'] = [sig]
d['num_sig'] = 1
d['x_pubkeys'] = ["(pubkey)"]
d['pubkeys'] = ["(pubkey)"]
return
# non-generated TxIn transactions push a signature
# (seventy-something bytes) and then their public key
# (65 bytes) onto the stack:
match = [opcodes.OP_PUSHDATA4, opcodes.OP_PUSHDATA4]
if match_decoded(decoded, match):
sig = decoded[0][1].encode('hex')
x_pubkey = decoded[1][1].encode('hex')
try:
signatures = parse_sig([sig])
pubkey, address = parse_xpub(x_pubkey)
except:
import traceback
traceback.print_exc(file=sys.stdout)
log.error("cannot find address in input script: {}".format(bytes.encode('hex')))
return
d['signatures'] = signatures
d['x_pubkeys'] = [x_pubkey]
d['num_sig'] = 1
d['pubkeys'] = [pubkey]
d['address'] = address
return
# p2sh transaction, m of n
match = [opcodes.OP_0] + [opcodes.OP_PUSHDATA4] * (len(decoded) - 1)
if not match_decoded(decoded, match):
log.error("cannot find address in input script: {}".format(bytes.encode('hex')))
return
x_sig = [x[1].encode('hex') for x in decoded[1:-1]]
dec2 = [x for x in script_GetOp(decoded[-1][1])]
m = dec2[0][0] - opcodes.OP_1 + 1
n = dec2[-2][0] - opcodes.OP_1 + 1
op_m = opcodes.OP_1 + m - 1
op_n = opcodes.OP_1 + n - 1
match_multisig = [op_m] + [opcodes.OP_PUSHDATA4] * n + [op_n, opcodes.OP_CHECKMULTISIG]
if not match_decoded(dec2, match_multisig):
log.error("cannot find address in input script: {}".format(bytes.encode('hex')))
return
x_pubkeys = map(lambda x: x[1].encode('hex'), dec2[1:-2])
pubkeys = [parse_xpub(x)[0] for x in x_pubkeys] # xpub, addr = parse_xpub()
redeemScript = Transaction.multisig_script(pubkeys, m)
# write result in d
d['num_sig'] = m
d['signatures'] = parse_sig(x_sig)
d['x_pubkeys'] = x_pubkeys
d['pubkeys'] = pubkeys
d['redeemScript'] = redeemScript
d['address'] = hash_160_bytes_to_address(hash_160(redeemScript.decode('hex')), 5)
class NameClaim(object):
def __init__(self, name, value):
self.name = name
self.value = value
class ClaimUpdate(object):
def __init__(self, name, claim_id, value):
self.name = name
self.claim_id = claim_id
self.value = value
class ClaimSupport(object):
def __init__(self, name, claim_id):
self.name = name
self.claim_id = claim_id
def decode_claim_script(decoded_script):
if len(decoded_script) <= 6:
return False
op = 0
claim_type = decoded_script[op][0]
if claim_type == opcodes.OP_UPDATE_CLAIM:
if len(decoded_script) <= 7:
return False
if claim_type not in [
opcodes.OP_CLAIM_NAME,
opcodes.OP_SUPPORT_CLAIM,
opcodes.OP_UPDATE_CLAIM
]:
return False
op += 1
value = None
claim_id = None
claim = None
if not 0 <= decoded_script[op][0] <= opcodes.OP_PUSHDATA4:
return False
name = decoded_script[op][1]
op += 1
if not 0 <= decoded_script[op][0] <= opcodes.OP_PUSHDATA4:
return False
if decoded_script[0][0] in [
opcodes.OP_SUPPORT_CLAIM,
opcodes.OP_UPDATE_CLAIM
]:
claim_id = decoded_script[op][1]
if len(claim_id) != 20:
return False
else:
value = decoded_script[op][1]
op += 1
if decoded_script[0][0] == opcodes.OP_UPDATE_CLAIM:
value = decoded_script[op][1]
op += 1
if decoded_script[op][0] != opcodes.OP_2DROP:
return False
op += 1
if decoded_script[op][0] != opcodes.OP_DROP and decoded_script[0][0] == opcodes.OP_CLAIM_NAME:
return False
elif decoded_script[op][0] != opcodes.OP_2DROP and decoded_script[0][0] == \
opcodes.OP_UPDATE_CLAIM:
return False
op += 1
if decoded_script[0][0] == opcodes.OP_CLAIM_NAME:
if name is None or value is None:
return False
claim = NameClaim(name, value)
elif decoded_script[0][0] == opcodes.OP_UPDATE_CLAIM:
if name is None or value is None or claim_id is None:
return False
claim = ClaimUpdate(name, claim_id, value)
elif decoded_script[0][0] == opcodes.OP_SUPPORT_CLAIM:
if name is None or claim_id is None:
return False
claim = ClaimSupport(name, claim_id)
return claim, decoded_script[op:]
def get_address_from_output_script(script_bytes):
output_type = 0
decoded = [x for x in script_GetOp(script_bytes)]
r = decode_claim_script(decoded)
claim_args = None
if r is not False:
claim_info, decoded = r
if isinstance(claim_info, NameClaim):
claim_args = (claim_info.name, claim_info.value)
output_type |= TYPE_CLAIM
elif isinstance(claim_info, ClaimSupport):
claim_args = (claim_info.name, claim_info.claim_id)
output_type |= TYPE_SUPPORT
elif isinstance(claim_info, ClaimUpdate):
claim_args = (claim_info.name, claim_info.claim_id, claim_info.value)
output_type |= TYPE_UPDATE
# The Genesis Block, self-payments, and pay-by-IP-address payments look like:
# 65 BYTES:... CHECKSIG
match_pubkey = [opcodes.OP_PUSHDATA4, opcodes.OP_CHECKSIG]
# Pay-by-Bitcoin-address TxOuts look like:
# DUP HASH160 20 BYTES:... EQUALVERIFY CHECKSIG
match_p2pkh = [opcodes.OP_DUP, opcodes.OP_HASH160, opcodes.OP_PUSHDATA4, opcodes.OP_EQUALVERIFY,
opcodes.OP_CHECKSIG]
# p2sh
match_p2sh = [opcodes.OP_HASH160, opcodes.OP_PUSHDATA4, opcodes.OP_EQUAL]
if match_decoded(decoded, match_pubkey):
output_val = decoded[0][1].encode('hex')
output_type |= TYPE_PUBKEY
elif match_decoded(decoded, match_p2pkh):
output_val = hash_160_bytes_to_address(decoded[2][1])
output_type |= TYPE_ADDRESS
elif match_decoded(decoded, match_p2sh):
output_val = hash_160_bytes_to_address(decoded[1][1], 5)
output_type |= TYPE_ADDRESS
else:
output_val = bytes
output_type |= TYPE_SCRIPT
if output_type & (TYPE_CLAIM | TYPE_SUPPORT | TYPE_UPDATE):
output_val = (claim_args, output_val)
return output_type, output_val
def parse_input(vds):
d = {}
prevout_hash = hash_encode(vds.read_bytes(32))
prevout_n = vds.read_uint32()
scriptSig = vds.read_bytes(vds.read_compact_size())
d['scriptSig'] = scriptSig.encode('hex')
sequence = vds.read_uint32()
if prevout_hash == '00' * 32:
d['is_coinbase'] = True
else:
d['is_coinbase'] = False
d['prevout_hash'] = prevout_hash
d['prevout_n'] = prevout_n
d['sequence'] = sequence
d['pubkeys'] = []
d['signatures'] = {}
d['address'] = None
if scriptSig:
parse_scriptSig(d, scriptSig)
return d
def parse_output(vds, i):
d = {}
d['value'] = vds.read_int64()
scriptPubKey = vds.read_bytes(vds.read_compact_size())
d['type'], d['address'] = get_address_from_output_script(scriptPubKey)
d['scriptPubKey'] = scriptPubKey.encode('hex')
d['prevout_n'] = i
return d
def deserialize(raw):
vds = BCDataStream()
vds.write(raw.decode('hex'))
d = {}
start = vds.read_cursor
d['version'] = vds.read_int32()
n_vin = vds.read_compact_size()
d['inputs'] = list(parse_input(vds) for i in xrange(n_vin))
n_vout = vds.read_compact_size()
d['outputs'] = list(parse_output(vds, i) for i in xrange(n_vout))
d['lockTime'] = vds.read_uint32()
return d
def push_script(x):
return op_push(len(x) / 2) + x
class Transaction(object):
def __str__(self):
if self.raw is None:
self.raw = self.serialize()
return self.raw
def __init__(self, raw):
if raw is None:
self.raw = None
elif type(raw) in [str, unicode]:
self.raw = raw.strip() if raw else None
elif type(raw) is dict:
self.raw = raw['hex']
else:
raise BaseException("cannot initialize transaction", raw)
self._inputs = None
self._outputs = None
def update(self, raw):
self.raw = raw
self._inputs = None
self.deserialize()
def inputs(self):
if self._inputs is None:
self.deserialize()
return self._inputs
def outputs(self):
if self._outputs is None:
self.deserialize()
return self._outputs
def update_signatures(self, raw):
"""Add new signatures to a transaction"""
d = deserialize(raw)
for i, txin in enumerate(self.inputs()):
sigs1 = txin.get('signatures')
sigs2 = d['inputs'][i].get('signatures')
for sig in sigs2:
if sig in sigs1:
continue
for_sig = Hash(self.tx_for_sig(i).decode('hex'))
# der to string
order = ecdsa.ecdsa.generator_secp256k1.order()
r, s = ecdsa.util.sigdecode_der(sig.decode('hex'), order)
sig_string = ecdsa.util.sigencode_string(r, s, order)
pubkeys = txin.get('pubkeys')
compressed = True
for recid in range(4):
public_key = MyVerifyingKey.from_signature(sig_string, recid, for_sig,
curve=SECP256k1)
pubkey = point_to_ser(public_key.pubkey.point, compressed).encode('hex')
if pubkey in pubkeys:
public_key.verify_digest(sig_string, for_sig,
sigdecode=ecdsa.util.sigdecode_string)
j = pubkeys.index(pubkey)
log.error("adding sig {} {} {} {}".format(i, j, pubkey, sig))
self._inputs[i]['signatures'][j] = sig
self._inputs[i]['x_pubkeys'][j] = pubkey
break
# redo raw
self.raw = self.serialize()
def deserialize(self):
if self.raw is None:
self.raw = self.serialize()
if self._inputs is not None:
return
d = deserialize(self.raw)
self._inputs = d['inputs']
self._outputs = [(x['type'], x['address'], x['value']) for x in d['outputs']]
self.locktime = d['lockTime']
return d
@classmethod
def from_io(cls, inputs, outputs, locktime=0):
self = cls(None)
self._inputs = inputs
self._outputs = outputs
self.locktime = locktime
return self
@classmethod
def multisig_script(cls, public_keys, m):
n = len(public_keys)
assert n <= 15
assert m <= n
op_m = format(opcodes.OP_1 + m - 1, 'x')
op_n = format(opcodes.OP_1 + n - 1, 'x')
keylist = [op_push(len(k) / 2) + k for k in public_keys]
return op_m + ''.join(keylist) + op_n + 'ae'
@classmethod
def pay_script(cls, output_type, addr):
script = ''
if output_type & TYPE_CLAIM:
claim, addr = addr
claim_name, claim_value = claim
script += 'b5' # op_claim_name
script += push_script(claim_name.encode('hex'))
script += push_script(claim_value.encode('hex'))
script += '6d75' # op_2drop, op_drop
elif output_type & TYPE_SUPPORT:
claim, addr = addr
claim_name, claim_id = claim
script += 'b6'
script += push_script(claim_name.encode('hex'))
script += push_script(claim_id.encode('hex'))
script += '6d75'
elif output_type & TYPE_UPDATE:
claim, addr = addr
claim_name, claim_id, claim_value = claim
script += 'b7'
script += push_script(claim_name.encode('hex'))
script += push_script(claim_id.encode('hex'))
script += push_script(claim_value.encode('hex'))
script += '6d6d'
if output_type & TYPE_SCRIPT:
script += addr.encode('hex')
elif output_type & TYPE_ADDRESS: # op_2drop, op_drop
addrtype, hash_160 = address_to_hash_160(addr)
if addrtype == 0:
script += '76a9' # op_dup, op_hash_160
script += push_script(hash_160.encode('hex'))
script += '88ac' # op_equalverify, op_checksig
elif addrtype == 5:
script += 'a9' # op_hash_160
script += push_script(hash_160.encode('hex'))
script += '87' # op_equal
else:
raise Exception("Unknown address type: %s" % addrtype)
else:
raise Exception("Unknown output type: %s" % output_type)
return script
@classmethod
def input_script(cls, txin, i, for_sig):
# for_sig:
# -1 : do not sign, estimate length
# i>=0 : serialized tx for signing input i
# None : add all known signatures
p2sh = txin.get('redeemScript') is not None
num_sig = txin['num_sig'] if p2sh else 1
address = txin['address']
x_signatures = txin['signatures']
signatures = filter(None, x_signatures)
is_complete = len(signatures) == num_sig
if for_sig in [-1, None]:
# if we have enough signatures, we use the actual pubkeys
# use extended pubkeys (with bip32 derivation)
if for_sig == -1:
# we assume that signature will be 0x48 bytes long
pubkeys = txin['pubkeys']
sig_list = ["00" * 0x48] * num_sig
elif is_complete:
pubkeys = txin['pubkeys']
sig_list = ((sig + '01') for sig in signatures)
else:
pubkeys = txin['x_pubkeys']
sig_list = ((sig + '01') if sig else NO_SIGNATURE for sig in x_signatures)
script = ''.join(push_script(x) for x in sig_list)
if not p2sh:
x_pubkey = pubkeys[0]
if x_pubkey is None:
addrtype, h160 = address_to_hash_160(txin['address'])
x_pubkey = 'fd' + (chr(addrtype) + h160).encode('hex')
script += push_script(x_pubkey)
else:
script = '00' + script # put op_0 in front of script
redeem_script = cls.multisig_script(pubkeys, num_sig)
script += push_script(redeem_script)
elif for_sig == i:
script_type = TYPE_ADDRESS
if 'is_claim' in txin and txin['is_claim']:
script_type |= TYPE_CLAIM
address = ((txin['claim_name'], txin['claim_value']), address)
elif 'is_support' in txin and txin['is_support']:
script_type |= TYPE_SUPPORT
address = ((txin['claim_name'], txin['claim_id']), address)
elif 'is_update' in txin and txin['is_update']:
script_type |= TYPE_UPDATE
address = ((txin['claim_name'], txin['claim_id'], txin['claim_value']), address)
script = txin['redeemScript'] if p2sh else cls.pay_script(script_type, address)
else:
script = ''
return script
@classmethod
def serialize_input(cls, txin, i, for_sig):
# Prev hash and index
s = txin['prevout_hash'].decode('hex')[::-1].encode('hex')
s += int_to_hex(txin['prevout_n'], 4)
# Script length, script, sequence
script = cls.input_script(txin, i, for_sig)
s += var_int(len(script) / 2)
s += script
s += "ffffffff"
return s
def BIP_LI01_sort(self):
# See https://github.com/kristovatlas/rfc/blob/master/bips/bip-li01.mediawiki
self._inputs.sort(key=lambda i: (i['prevout_hash'], i['prevout_n']))
self._outputs.sort(key=lambda o: (o[2], self.pay_script(o[0], o[1])))
def serialize(self, for_sig=None):
inputs = self.inputs()
outputs = self.outputs()
s = int_to_hex(1, 4) # version
s += var_int(len(inputs)) # number of inputs
for i, txin in enumerate(inputs):
s += self.serialize_input(txin, i, for_sig)
s += var_int(len(outputs)) # number of outputs
for output in outputs:
output_type, addr, amount = output
s += int_to_hex(amount, 8) # amount
script = self.pay_script(output_type, addr)
s += var_int(len(script) / 2) # script length
s += script # script
s += int_to_hex(0, 4) # lock time
if for_sig is not None and for_sig != -1:
s += int_to_hex(1, 4) # hash type
return s
def tx_for_sig(self, i):
return self.serialize(for_sig=i)
def hash(self):
return Hash(self.raw.decode('hex'))[::-1].encode('hex')
def get_claim_id(self, nout):
if nout < 0:
raise IndexError
if not self._outputs[nout][0] & TYPE_CLAIM:
raise ValueError
tx_hash = rev_hex(self.hash()).decode('hex')
return encode_claim_id_hex(claim_id_hash(tx_hash, nout))
def add_inputs(self, inputs):
self._inputs.extend(inputs)
self.raw = None
def add_outputs(self, outputs):
self._outputs.extend(outputs)
self.raw = None
def input_value(self):
return sum(x['value'] for x in self.inputs())
def output_value(self):
return sum(val for tp, addr, val in self.outputs())
def get_fee(self):
return self.input_value() - self.output_value()
def is_final(self):
return not any([x.get('sequence') < 0xffffffff - 1 for x in self.inputs()])
@classmethod
def fee_for_size(cls, relay_fee, fee_per_kb, size):
'''Given a fee per kB in satoshis, and a tx size in bytes,
returns the transaction fee.'''
fee = int(fee_per_kb * size / 1000.)
if fee < relay_fee:
fee = relay_fee
return fee
@profiler
def estimated_size(self):
'''Return an estimated tx size in bytes.'''
return len(self.serialize(-1)) / 2 # ASCII hex string
@classmethod
def estimated_input_size(cls, txin):
'''Return an estimated of serialized input size in bytes.'''
return len(cls.serialize_input(txin, -1, -1)) / 2
def estimated_fee(self, relay_fee, fee_per_kb):
'''Return an estimated fee given a fee per kB in satoshis.'''
return self.fee_for_size(relay_fee, fee_per_kb, self.estimated_size())
def signature_count(self):
r = 0
s = 0
for txin in self.inputs():
if txin.get('is_coinbase'):
continue
signatures = filter(None, txin.get('signatures', []))
s += len(signatures)
r += txin.get('num_sig', -1)
return s, r
def is_complete(self):
s, r = self.signature_count()
return r == s
def inputs_without_script(self):
out = set()
for i, txin in enumerate(self.inputs()):
if txin.get('scriptSig') == '':
out.add(i)
return out
def inputs_to_sign(self):
out = set()
for txin in self.inputs():
num_sig = txin.get('num_sig')
if num_sig is None:
continue
x_signatures = txin['signatures']
signatures = filter(None, x_signatures)
if len(signatures) == num_sig:
# input is complete
continue
for k, x_pubkey in enumerate(txin['x_pubkeys']):
if x_signatures[k] is not None:
# this pubkey already signed
continue
out.add(x_pubkey)
return out
def sign(self, keypairs):
for i, txin in enumerate(self.inputs()):
num = txin['num_sig']
for x_pubkey in txin['x_pubkeys']:
signatures = filter(None, txin['signatures'])
if len(signatures) == num:
# txin is complete
break
if x_pubkey in keypairs.keys():
log.debug("adding signature for %s", x_pubkey)
# add pubkey to txin
txin = self._inputs[i]
x_pubkeys = txin['x_pubkeys']
ii = x_pubkeys.index(x_pubkey)
sec = keypairs[x_pubkey]
pubkey = public_key_from_private_key(sec)
txin['x_pubkeys'][ii] = pubkey
txin['pubkeys'][ii] = pubkey
self._inputs[i] = txin
# add signature
for_sig = Hash(self.tx_for_sig(i).decode('hex'))
pkey = regenerate_key(sec)
secexp = pkey.secret
private_key = MySigningKey.from_secret_exponent(secexp, curve=SECP256k1)
public_key = private_key.get_verifying_key()
sig = private_key.sign_digest_deterministic(for_sig, hashfunc=hashlib.sha256,
sigencode=ecdsa.util.sigencode_der)
assert public_key.verify_digest(sig, for_sig,
sigdecode=ecdsa.util.sigdecode_der)
txin['signatures'][ii] = sig.encode('hex')
self._inputs[i] = txin
log.debug("is_complete: %s", self.is_complete())
self.raw = self.serialize()
def get_outputs(self):
"""convert pubkeys to addresses"""
o = []
for type, x, v in self.outputs():
if type & (TYPE_CLAIM | TYPE_UPDATE | TYPE_SUPPORT):
x = x[1]
if type & TYPE_ADDRESS:
addr = x
elif type & TYPE_PUBKEY:
addr = public_key_to_address(x.decode('hex'))
else:
addr = 'SCRIPT ' + x.encode('hex')
o.append((addr, v)) # consider using yield (addr, v)
return o
def get_output_addresses(self):
return [addr for addr, val in self.get_outputs()]
def has_address(self, addr):
return (addr in self.get_output_addresses()) or (
addr in (tx.get("address") for tx in self.inputs()))
def as_dict(self):
if self.raw is None:
self.raw = self.serialize()
self.deserialize()
out = {
'hex': self.raw,
'complete': self.is_complete()
}
return out
def requires_fee(self, wallet):
# see https://en.bitcoin.it/wiki/Transaction_fees
#
# size must be smaller than 1 kbyte for free tx
size = len(self.serialize(-1)) / 2
if size >= 10000:
return True
# all outputs must be 0.01 BTC or larger for free tx
for addr, value in self.get_outputs():
if value < 1000000:
return True
# priority must be large enough for free tx
threshold = 57600000
weight = 0
for txin in self.inputs():
age = wallet.get_confirmations(txin["prevout_hash"])[0]
weight += txin["value"] * age
priority = weight / size
log.error("{} {}".format(priority, threshold))
return priority < threshold

117
lbrynet/wallet/util.py Normal file
View file

@ -0,0 +1,117 @@
import logging
import os
import re
from decimal import Decimal
import json
from .constants import NO_SIGNATURE
log = logging.getLogger(__name__)
def normalize_version(v):
return [int(x) for x in re.sub(r'(\.0+)*$', '', v).split(".")]
def json_decode(x):
try:
return json.loads(x, parse_float=Decimal)
except:
return x
def user_dir():
if "HOME" in os.environ:
return os.path.join(os.environ["HOME"], ".lbryum")
elif "APPDATA" in os.environ:
return os.path.join(os.environ["APPDATA"], "LBRYum")
elif "LOCALAPPDATA" in os.environ:
return os.path.join(os.environ["LOCALAPPDATA"], "LBRYum")
elif 'ANDROID_DATA' in os.environ:
try:
import jnius
env = jnius.autoclass('android.os.Environment')
_dir = env.getExternalStorageDirectory().getPath()
return _dir + '/lbryum/'
except ImportError:
pass
return "/sdcard/lbryum/"
else:
# raise Exception("No home directory found in environment variables.")
return
def format_satoshis(x, is_diff=False, num_zeros=0, decimal_point=8, whitespaces=False):
from locale import localeconv
if x is None:
return 'unknown'
x = int(x) # Some callers pass Decimal
scale_factor = pow(10, decimal_point)
integer_part = "{:n}".format(int(abs(x) / scale_factor))
if x < 0:
integer_part = '-' + integer_part
elif is_diff:
integer_part = '+' + integer_part
dp = localeconv()['decimal_point']
fract_part = ("{:0" + str(decimal_point) + "}").format(abs(x) % scale_factor)
fract_part = fract_part.rstrip('0')
if len(fract_part) < num_zeros:
fract_part += "0" * (num_zeros - len(fract_part))
result = integer_part + dp + fract_part
if whitespaces:
result += " " * (decimal_point - len(fract_part))
result = " " * (15 - len(result)) + result
return result.decode('utf8')
def rev_hex(s):
return s.decode('hex')[::-1].encode('hex')
def int_to_hex(i, length=1):
s = hex(i)[2:].rstrip('L')
s = "0" * (2 * length - len(s)) + s
return rev_hex(s)
def hex_to_int(s):
return int('0x' + s[::-1].encode('hex'), 16)
def var_int(i):
# https://en.bitcoin.it/wiki/Protocol_specification#Variable_length_integer
if i < 0xfd:
return int_to_hex(i)
elif i <= 0xffff:
return "fd" + int_to_hex(i, 2)
elif i <= 0xffffffff:
return "fe" + int_to_hex(i, 4)
else:
return "ff" + int_to_hex(i, 8)
# This function comes from bitcointools, bct-LICENSE.txt.
def long_hex(bytes):
return bytes.encode('hex_codec')
# This function comes from bitcointools, bct-LICENSE.txt.
def short_hex(bytes):
t = bytes.encode('hex_codec')
if len(t) < 11:
return t
return t[0:4] + "..." + t[-4:]
def parse_sig(x_sig):
s = []
for sig in x_sig:
if sig[-2:] == '01':
s.append(sig[:-2])
else:
assert sig == NO_SIGNATURE
s.append(None)
return s
def is_extended_pubkey(x_pubkey):
return x_pubkey[0:2] in ['fe', 'ff']

1499
lbrynet/wallet/wallet.py Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff