forked from LBRYCommunity/lbry-sdk
wip: initial import of twisted based wallet
This commit is contained in:
parent
79d1da5ff8
commit
ca8b2dd83e
29 changed files with 14642 additions and 305 deletions
|
@ -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
|
|
|
@ -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
|
|
|
@ -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', [])
|
|
0
lbrynet/wallet/__init__.py
Normal file
0
lbrynet/wallet/__init__.py
Normal file
83
lbrynet/wallet/account.py
Normal file
83
lbrynet/wallet/account.py
Normal 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
|
133
lbrynet/wallet/bcd_data_stream.py
Normal file
133
lbrynet/wallet/bcd_data_stream.py
Normal 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)
|
||||||
|
|
||||||
|
|
243
lbrynet/wallet/blockchain.py
Normal file
243
lbrynet/wallet/blockchain.py
Normal 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
|
313
lbrynet/wallet/coinchooser.py
Normal file
313
lbrynet/wallet/coinchooser.py
Normal 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}
|
76
lbrynet/wallet/constants.py
Normal file
76
lbrynet/wallet/constants.py
Normal 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
|
||||||
|
}
|
||||||
|
}
|
47
lbrynet/wallet/enumeration.py
Normal file
47
lbrynet/wallet/enumeration.py
Normal 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
43
lbrynet/wallet/errors.py
Normal 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
50
lbrynet/wallet/hashing.py
Normal 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
633
lbrynet/wallet/lbrycrd.py
Normal 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
88
lbrynet/wallet/manager.py
Normal 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
157
lbrynet/wallet/mnemonic.py
Normal 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
96
lbrynet/wallet/msqr.py
Normal 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
76
lbrynet/wallet/opcodes.py
Normal 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
282
lbrynet/wallet/protocol.py
Normal 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
31
lbrynet/wallet/store.py
Normal 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
127
lbrynet/wallet/stream.py
Normal 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))
|
702
lbrynet/wallet/transaction.py
Normal file
702
lbrynet/wallet/transaction.py
Normal 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
117
lbrynet/wallet/util.py
Normal 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
1499
lbrynet/wallet/wallet.py
Normal file
File diff suppressed because it is too large
Load diff
2048
lbrynet/wallet/wordlist/chinese_simplified.txt
Normal file
2048
lbrynet/wallet/wordlist/chinese_simplified.txt
Normal file
File diff suppressed because it is too large
Load diff
2048
lbrynet/wallet/wordlist/english.txt
Normal file
2048
lbrynet/wallet/wordlist/english.txt
Normal file
File diff suppressed because it is too large
Load diff
2048
lbrynet/wallet/wordlist/japanese.txt
Normal file
2048
lbrynet/wallet/wordlist/japanese.txt
Normal file
File diff suppressed because it is too large
Load diff
1654
lbrynet/wallet/wordlist/portuguese.txt
Normal file
1654
lbrynet/wallet/wordlist/portuguese.txt
Normal file
File diff suppressed because it is too large
Load diff
2048
lbrynet/wallet/wordlist/spanish.txt
Normal file
2048
lbrynet/wallet/wordlist/spanish.txt
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue