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…
Reference in a new issue