2018-05-25 08:03:25 +02:00
|
|
|
import os
|
2018-06-26 23:22:05 +02:00
|
|
|
import logging
|
2018-05-25 08:03:25 +02:00
|
|
|
from binascii import hexlify, unhexlify
|
2018-07-17 06:09:02 +02:00
|
|
|
from typing import Dict, Type, Iterable
|
2018-05-25 08:03:25 +02:00
|
|
|
from operator import itemgetter
|
2018-06-26 23:22:05 +02:00
|
|
|
from collections import namedtuple
|
2018-05-25 08:03:25 +02:00
|
|
|
|
2018-07-17 06:09:02 +02:00
|
|
|
from twisted.internet import defer
|
2018-05-25 08:03:25 +02:00
|
|
|
|
2018-06-11 15:33:32 +02:00
|
|
|
from torba import baseaccount
|
|
|
|
from torba import basedatabase
|
|
|
|
from torba import baseheader
|
|
|
|
from torba import basenetwork
|
|
|
|
from torba import basetransaction
|
2018-07-29 02:52:54 +02:00
|
|
|
from torba.coinselection import CoinSelector
|
|
|
|
from torba.constants import COIN, NULL_HASH32
|
|
|
|
from torba.stream import StreamController
|
|
|
|
from torba.hash import hash160, double_sha256, sha256, Base58
|
2018-05-25 08:03:25 +02:00
|
|
|
|
2018-06-26 23:22:05 +02:00
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
2018-07-29 02:52:54 +02:00
|
|
|
LedgerType = Type['BaseLedger']
|
|
|
|
|
2018-05-25 08:03:25 +02:00
|
|
|
|
2018-06-11 15:33:32 +02:00
|
|
|
class LedgerRegistry(type):
|
2018-07-29 02:52:54 +02:00
|
|
|
|
|
|
|
ledgers: Dict[str, LedgerType] = {}
|
2018-05-25 08:03:25 +02:00
|
|
|
|
2018-06-11 15:33:32 +02:00
|
|
|
def __new__(mcs, name, bases, attrs):
|
2018-07-29 02:52:54 +02:00
|
|
|
cls: LedgerType = super().__new__(mcs, name, bases, attrs)
|
2018-06-11 15:33:32 +02:00
|
|
|
if not (name == 'BaseLedger' and not bases):
|
|
|
|
ledger_id = cls.get_id()
|
|
|
|
assert ledger_id not in mcs.ledgers,\
|
|
|
|
'Ledger with id "{}" already registered.'.format(ledger_id)
|
|
|
|
mcs.ledgers[ledger_id] = cls
|
|
|
|
return cls
|
2018-05-25 08:03:25 +02:00
|
|
|
|
2018-06-11 15:33:32 +02:00
|
|
|
@classmethod
|
2018-07-29 02:52:54 +02:00
|
|
|
def get_ledger_class(mcs, ledger_id: str) -> LedgerType:
|
2018-06-11 15:33:32 +02:00
|
|
|
return mcs.ledgers[ledger_id]
|
2018-05-25 08:03:25 +02:00
|
|
|
|
|
|
|
|
2018-06-26 23:22:05 +02:00
|
|
|
class TransactionEvent(namedtuple('TransactionEvent', ('address', 'tx', 'height', 'is_verified'))):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2018-07-29 02:52:54 +02:00
|
|
|
class BaseLedger(metaclass=LedgerRegistry):
|
2018-05-25 08:03:25 +02:00
|
|
|
|
2018-07-29 02:52:54 +02:00
|
|
|
name: str
|
|
|
|
symbol: str
|
|
|
|
network_name: str
|
2018-05-25 08:03:25 +02:00
|
|
|
|
2018-06-11 15:33:32 +02:00
|
|
|
account_class = baseaccount.BaseAccount
|
|
|
|
database_class = basedatabase.BaseDatabase
|
|
|
|
headers_class = baseheader.BaseHeaders
|
|
|
|
network_class = basenetwork.BaseNetwork
|
|
|
|
transaction_class = basetransaction.BaseTransaction
|
2018-05-25 08:03:25 +02:00
|
|
|
|
2018-06-11 15:33:32 +02:00
|
|
|
secret_prefix = None
|
2018-07-29 02:52:54 +02:00
|
|
|
pubkey_address_prefix: bytes
|
|
|
|
script_address_prefix: bytes
|
|
|
|
extended_public_key_prefix: bytes
|
|
|
|
extended_private_key_prefix: bytes
|
2018-05-25 08:03:25 +02:00
|
|
|
|
2018-06-08 05:47:46 +02:00
|
|
|
default_fee_per_byte = 10
|
2018-05-25 08:03:25 +02:00
|
|
|
|
2018-07-12 04:37:15 +02:00
|
|
|
def __init__(self, config=None):
|
2018-05-25 08:03:25 +02:00
|
|
|
self.config = config or {}
|
2018-07-12 04:37:15 +02:00
|
|
|
self.db = self.config.get('db') or self.database_class(
|
2018-06-14 21:17:59 +02:00
|
|
|
os.path.join(self.path, "blockchain.db")
|
|
|
|
) # type: basedatabase.BaseDatabase
|
2018-07-12 04:37:15 +02:00
|
|
|
self.network = self.config.get('network') or self.network_class(self)
|
2018-05-25 08:03:25 +02:00
|
|
|
self.network.on_header.listen(self.process_header)
|
|
|
|
self.network.on_status.listen(self.process_status)
|
2018-07-17 05:58:29 +02:00
|
|
|
self.accounts = []
|
2018-07-12 04:37:15 +02:00
|
|
|
self.headers = self.config.get('headers') or self.headers_class(self)
|
2018-07-29 02:52:54 +02:00
|
|
|
self.fee_per_byte: int = self.config.get('fee_per_byte', self.default_fee_per_byte)
|
2018-06-08 05:47:46 +02:00
|
|
|
|
|
|
|
self._on_transaction_controller = StreamController()
|
|
|
|
self.on_transaction = self._on_transaction_controller.stream
|
2018-06-26 23:22:05 +02:00
|
|
|
self.on_transaction.listen(
|
2018-07-29 02:52:54 +02:00
|
|
|
lambda e: log.info(
|
|
|
|
'(%s) on_transaction: address=%s, height=%s, is_verified=%s, tx.id=%s',
|
|
|
|
self.get_id(), e.address, e.height, e.is_verified, e.tx.id
|
2018-06-26 23:22:05 +02:00
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
self._on_header_controller = StreamController()
|
|
|
|
self.on_header = self._on_header_controller.stream
|
2018-06-08 05:47:46 +02:00
|
|
|
|
2018-06-25 15:54:35 +02:00
|
|
|
self._transaction_processing_locks = {}
|
2018-07-29 02:52:54 +02:00
|
|
|
self._utxo_reservation_lock = defer.DeferredLock()
|
|
|
|
self._header_processing_lock = defer.DeferredLock()
|
2018-06-25 15:54:35 +02:00
|
|
|
|
2018-06-11 15:33:32 +02:00
|
|
|
@classmethod
|
|
|
|
def get_id(cls):
|
|
|
|
return '{}_{}'.format(cls.symbol.lower(), cls.network_name.lower())
|
|
|
|
|
2018-07-15 22:04:11 +02:00
|
|
|
@classmethod
|
|
|
|
def hash160_to_address(cls, h160):
|
|
|
|
raw_address = cls.pubkey_address_prefix + h160
|
2018-06-11 15:33:32 +02:00
|
|
|
return Base58.encode(bytearray(raw_address + double_sha256(raw_address)[0:4]))
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def address_to_hash160(address):
|
2018-07-29 02:52:54 +02:00
|
|
|
return Base58.decode(address)[1:21]
|
2018-06-11 15:33:32 +02:00
|
|
|
|
2018-07-15 22:04:11 +02:00
|
|
|
@classmethod
|
|
|
|
def public_key_to_address(cls, public_key):
|
|
|
|
return cls.hash160_to_address(hash160(public_key))
|
2018-06-11 15:33:32 +02:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def private_key_to_wif(private_key):
|
|
|
|
return b'\x1c' + private_key + b'\x01'
|
|
|
|
|
2018-06-08 05:47:46 +02:00
|
|
|
@property
|
|
|
|
def path(self):
|
2018-07-05 02:43:25 +02:00
|
|
|
return os.path.join(self.config['data_path'], self.get_id())
|
2018-06-08 05:47:46 +02:00
|
|
|
|
2018-06-14 02:57:57 +02:00
|
|
|
@defer.inlineCallbacks
|
2018-07-29 02:52:54 +02:00
|
|
|
def add_account(self, account: baseaccount.BaseAccount) -> defer.Deferred:
|
2018-07-17 05:58:29 +02:00
|
|
|
self.accounts.append(account)
|
2018-06-14 02:57:57 +02:00
|
|
|
if self.network.is_connected:
|
|
|
|
yield self.update_account(account)
|
|
|
|
|
2018-07-12 18:14:04 +02:00
|
|
|
@defer.inlineCallbacks
|
|
|
|
def get_transaction(self, txhash):
|
2018-07-29 02:52:54 +02:00
|
|
|
raw, _, _ = yield self.db.get_transaction(txhash)
|
2018-07-12 18:14:04 +02:00
|
|
|
if raw is not None:
|
|
|
|
defer.returnValue(self.transaction_class(raw))
|
|
|
|
|
2018-06-11 15:33:32 +02:00
|
|
|
@defer.inlineCallbacks
|
|
|
|
def get_private_key_for_address(self, address):
|
2018-06-12 16:02:04 +02:00
|
|
|
match = yield self.db.get_address(address)
|
2018-06-11 15:33:32 +02:00
|
|
|
if match:
|
|
|
|
for account in self.accounts:
|
2018-07-15 03:34:07 +02:00
|
|
|
if match['account'] == account.public_key.address:
|
2018-06-11 15:33:32 +02:00
|
|
|
defer.returnValue(account.get_private_key(match['chain'], match['position']))
|
|
|
|
|
2018-06-18 05:22:15 +02:00
|
|
|
@defer.inlineCallbacks
|
2018-07-29 02:52:54 +02:00
|
|
|
def get_effective_amount_estimators(self, funding_accounts: Iterable[baseaccount.BaseAccount]):
|
2018-06-18 05:22:15 +02:00
|
|
|
estimators = []
|
|
|
|
for account in funding_accounts:
|
2018-07-10 02:22:04 +02:00
|
|
|
utxos = yield account.get_unspent_outputs()
|
2018-06-18 05:22:15 +02:00
|
|
|
for utxo in utxos:
|
|
|
|
estimators.append(utxo.get_estimator(self))
|
|
|
|
defer.returnValue(estimators)
|
|
|
|
|
2018-07-29 02:52:54 +02:00
|
|
|
@defer.inlineCallbacks
|
|
|
|
def get_spendable_utxos(self, amount: int, funding_accounts):
|
|
|
|
yield self._utxo_reservation_lock.acquire()
|
|
|
|
try:
|
|
|
|
txos = yield self.get_effective_amount_estimators(funding_accounts)
|
|
|
|
selector = CoinSelector(
|
|
|
|
txos, amount,
|
2018-08-03 16:41:40 +02:00
|
|
|
self.transaction_class.output_class.pay_pubkey_hash(COIN, NULL_HASH32).get_fee(self)
|
2018-07-29 02:52:54 +02:00
|
|
|
)
|
|
|
|
spendables = selector.select()
|
|
|
|
if spendables:
|
|
|
|
yield self.reserve_outputs(s.txo for s in spendables)
|
|
|
|
except Exception:
|
|
|
|
log.exception('Failed to get spendable utxos:')
|
|
|
|
raise
|
|
|
|
finally:
|
|
|
|
self._utxo_reservation_lock.release()
|
|
|
|
defer.returnValue(spendables)
|
|
|
|
|
|
|
|
def reserve_outputs(self, txos):
|
|
|
|
return self.db.reserve_outputs(txos)
|
|
|
|
|
|
|
|
def release_outputs(self, txos):
|
|
|
|
return self.db.release_outputs(txos)
|
|
|
|
|
2018-06-12 16:02:04 +02:00
|
|
|
@defer.inlineCallbacks
|
|
|
|
def get_local_status(self, address):
|
|
|
|
address_details = yield self.db.get_address(address)
|
2018-06-26 23:22:05 +02:00
|
|
|
history = address_details['history'] or ''
|
2018-07-29 02:52:54 +02:00
|
|
|
h = sha256(history.encode())
|
|
|
|
defer.returnValue(hexlify(h))
|
2018-06-12 16:02:04 +02:00
|
|
|
|
|
|
|
@defer.inlineCallbacks
|
|
|
|
def get_local_history(self, address):
|
|
|
|
address_details = yield self.db.get_address(address)
|
2018-06-26 23:22:05 +02:00
|
|
|
history = address_details['history'] or ''
|
|
|
|
parts = history.split(':')[:-1]
|
2018-06-12 16:02:04 +02:00
|
|
|
defer.returnValue(list(zip(parts[0::2], map(int, parts[1::2]))))
|
|
|
|
|
2018-06-25 15:54:35 +02:00
|
|
|
@staticmethod
|
|
|
|
def get_root_of_merkle_tree(branches, branch_positions, working_branch):
|
|
|
|
for i, branch in enumerate(branches):
|
|
|
|
other_branch = unhexlify(branch)[::-1]
|
|
|
|
other_branch_on_left = bool((branch_positions >> i) & 1)
|
|
|
|
if other_branch_on_left:
|
|
|
|
combined = other_branch + working_branch
|
|
|
|
else:
|
|
|
|
combined = working_branch + other_branch
|
|
|
|
working_branch = double_sha256(combined)
|
|
|
|
return hexlify(working_branch[::-1])
|
|
|
|
|
|
|
|
@defer.inlineCallbacks
|
|
|
|
def is_valid_transaction(self, tx, height):
|
2018-06-26 23:22:05 +02:00
|
|
|
height <= len(self.headers) or defer.returnValue(False)
|
2018-07-15 03:34:07 +02:00
|
|
|
merkle = yield self.network.get_merkle(tx.id, height)
|
2018-06-25 15:54:35 +02:00
|
|
|
merkle_root = self.get_root_of_merkle_tree(merkle['merkle'], merkle['pos'], tx.hash)
|
|
|
|
header = self.headers[height]
|
|
|
|
defer.returnValue(merkle_root == header['merkle_root'])
|
|
|
|
|
2018-05-25 08:03:25 +02:00
|
|
|
@defer.inlineCallbacks
|
|
|
|
def start(self):
|
2018-06-08 05:47:46 +02:00
|
|
|
if not os.path.exists(self.path):
|
|
|
|
os.mkdir(self.path)
|
|
|
|
yield self.db.start()
|
2018-05-25 08:03:25 +02:00
|
|
|
first_connection = self.network.on_connected.first
|
|
|
|
self.network.start()
|
|
|
|
yield first_connection
|
|
|
|
self.headers.touch()
|
|
|
|
yield self.update_headers()
|
|
|
|
yield self.network.subscribe_headers()
|
|
|
|
yield self.update_accounts()
|
|
|
|
|
2018-06-27 02:41:03 +02:00
|
|
|
@defer.inlineCallbacks
|
2018-05-25 08:03:25 +02:00
|
|
|
def stop(self):
|
2018-06-27 02:41:03 +02:00
|
|
|
yield self.network.stop()
|
|
|
|
yield self.db.stop()
|
2018-05-25 08:03:25 +02:00
|
|
|
|
|
|
|
@defer.inlineCallbacks
|
|
|
|
def update_headers(self):
|
|
|
|
while True:
|
|
|
|
height_sought = len(self.headers)
|
2018-07-01 23:20:17 +02:00
|
|
|
headers = yield self.network.get_headers(height_sought, 2000)
|
2018-05-25 08:03:25 +02:00
|
|
|
if headers['count'] <= 0:
|
|
|
|
break
|
|
|
|
yield self.headers.connect(height_sought, unhexlify(headers['hex']))
|
2018-07-23 04:52:21 +02:00
|
|
|
self._on_header_controller.add(self.headers.height)
|
2018-05-25 08:03:25 +02:00
|
|
|
|
|
|
|
@defer.inlineCallbacks
|
|
|
|
def process_header(self, response):
|
2018-07-29 02:52:54 +02:00
|
|
|
yield self._header_processing_lock.acquire()
|
|
|
|
try:
|
|
|
|
header = response[0]
|
|
|
|
if header['height'] == len(self.headers):
|
|
|
|
# New header from network directly connects after the last local header.
|
|
|
|
yield self.headers.connect(len(self.headers), unhexlify(header['hex']))
|
|
|
|
self._on_header_controller.add(self.headers.height)
|
|
|
|
elif header['height'] > len(self.headers):
|
|
|
|
# New header is several heights ahead of local, do download instead.
|
|
|
|
yield self.update_headers()
|
|
|
|
finally:
|
|
|
|
self._header_processing_lock.release()
|
2018-05-25 08:03:25 +02:00
|
|
|
|
|
|
|
def update_accounts(self):
|
|
|
|
return defer.DeferredList([
|
|
|
|
self.update_account(a) for a in self.accounts
|
|
|
|
])
|
|
|
|
|
|
|
|
@defer.inlineCallbacks
|
2018-06-12 16:02:04 +02:00
|
|
|
def update_account(self, account): # type: (baseaccount.BaseAccount) -> defer.Defferred
|
2018-05-25 08:03:25 +02:00
|
|
|
# Before subscribing, download history for any addresses that don't have any,
|
|
|
|
# this avoids situation where we're getting status updates to addresses we know
|
|
|
|
# need to update anyways. Continue to get history and create more addresses until
|
|
|
|
# all missing addresses are created and history for them is fully restored.
|
2018-06-12 16:02:04 +02:00
|
|
|
yield account.ensure_address_gap()
|
2018-07-14 23:47:18 +02:00
|
|
|
addresses = yield account.get_addresses(max_used_times=0)
|
2018-05-25 08:03:25 +02:00
|
|
|
while addresses:
|
|
|
|
yield defer.DeferredList([
|
|
|
|
self.update_history(a) for a in addresses
|
|
|
|
])
|
2018-06-12 16:02:04 +02:00
|
|
|
addresses = yield account.ensure_address_gap()
|
2018-05-25 08:03:25 +02:00
|
|
|
|
|
|
|
# By this point all of the addresses should be restored and we
|
|
|
|
# can now subscribe all of them to receive updates.
|
2018-06-12 16:02:04 +02:00
|
|
|
all_addresses = yield account.get_addresses()
|
|
|
|
yield defer.DeferredList(
|
|
|
|
list(map(self.subscribe_history, all_addresses))
|
|
|
|
)
|
2018-06-08 05:47:46 +02:00
|
|
|
|
2018-05-25 08:03:25 +02:00
|
|
|
@defer.inlineCallbacks
|
2018-06-12 16:02:04 +02:00
|
|
|
def update_history(self, address):
|
|
|
|
remote_history = yield self.network.get_history(address)
|
2018-06-25 15:54:35 +02:00
|
|
|
local_history = yield self.get_local_history(address)
|
2018-06-12 16:02:04 +02:00
|
|
|
|
2018-06-25 15:54:35 +02:00
|
|
|
synced_history = []
|
2018-06-26 23:22:05 +02:00
|
|
|
for i, (hex_id, remote_height) in enumerate(map(itemgetter('tx_hash', 'height'), remote_history)):
|
2018-06-25 15:54:35 +02:00
|
|
|
|
2018-06-26 23:22:05 +02:00
|
|
|
synced_history.append((hex_id, remote_height))
|
2018-06-25 15:54:35 +02:00
|
|
|
|
2018-07-15 03:34:07 +02:00
|
|
|
if i < len(local_history) and local_history[i] == (hex_id, remote_height):
|
2018-06-12 16:02:04 +02:00
|
|
|
continue
|
|
|
|
|
2018-06-26 23:22:05 +02:00
|
|
|
lock = self._transaction_processing_locks.setdefault(hex_id, defer.DeferredLock())
|
2018-06-25 15:54:35 +02:00
|
|
|
|
|
|
|
yield lock.acquire()
|
|
|
|
|
2018-07-15 06:40:46 +02:00
|
|
|
try:
|
|
|
|
|
|
|
|
# see if we have a local copy of transaction, otherwise fetch it from server
|
2018-07-29 02:52:54 +02:00
|
|
|
raw, _, is_verified = yield self.db.get_transaction(hex_id)
|
2018-07-15 06:40:46 +02:00
|
|
|
save_tx = None
|
|
|
|
if raw is None:
|
|
|
|
_raw = yield self.network.get_transaction(hex_id)
|
|
|
|
tx = self.transaction_class(unhexlify(_raw))
|
|
|
|
save_tx = 'insert'
|
|
|
|
else:
|
|
|
|
tx = self.transaction_class(raw)
|
|
|
|
|
|
|
|
if remote_height > 0 and not is_verified:
|
|
|
|
is_verified = yield self.is_valid_transaction(tx, remote_height)
|
|
|
|
is_verified = 1 if is_verified else 0
|
|
|
|
if save_tx is None:
|
|
|
|
save_tx = 'update'
|
|
|
|
|
|
|
|
yield self.db.save_transaction_io(
|
|
|
|
save_tx, tx, remote_height, is_verified, address, self.address_to_hash160(address),
|
|
|
|
''.join('{}:{}:'.format(tx_id, tx_height) for tx_id, tx_height in synced_history)
|
|
|
|
)
|
|
|
|
|
2018-07-29 02:52:54 +02:00
|
|
|
log.debug(
|
|
|
|
"%s: sync'ed tx %s for address: %s, height: %s, verified: %s",
|
2018-07-15 06:40:46 +02:00
|
|
|
self.get_id(), hex_id, address, remote_height, is_verified
|
2018-07-29 02:52:54 +02:00
|
|
|
)
|
2018-07-17 05:58:29 +02:00
|
|
|
|
2018-07-17 06:09:02 +02:00
|
|
|
self._on_transaction_controller.add(TransactionEvent(address, tx, remote_height, is_verified))
|
2018-07-15 06:40:46 +02:00
|
|
|
|
2018-07-29 02:52:54 +02:00
|
|
|
except Exception:
|
2018-07-15 06:40:46 +02:00
|
|
|
log.exception('Failed to synchronize transaction:')
|
2018-07-29 02:52:54 +02:00
|
|
|
raise
|
2018-07-15 06:40:46 +02:00
|
|
|
|
|
|
|
finally:
|
|
|
|
lock.release()
|
2018-07-15 22:04:11 +02:00
|
|
|
if not lock.locked and hex_id in self._transaction_processing_locks:
|
2018-07-15 06:40:46 +02:00
|
|
|
del self._transaction_processing_locks[hex_id]
|
2018-05-25 08:03:25 +02:00
|
|
|
|
|
|
|
@defer.inlineCallbacks
|
|
|
|
def subscribe_history(self, address):
|
2018-06-08 05:47:46 +02:00
|
|
|
remote_status = yield self.network.subscribe_address(address)
|
2018-06-12 16:02:04 +02:00
|
|
|
local_status = yield self.get_local_status(address)
|
2018-06-08 05:47:46 +02:00
|
|
|
if local_status != remote_status:
|
2018-06-12 16:02:04 +02:00
|
|
|
yield self.update_history(address)
|
2018-05-25 08:03:25 +02:00
|
|
|
|
2018-06-08 05:47:46 +02:00
|
|
|
@defer.inlineCallbacks
|
2018-05-25 08:03:25 +02:00
|
|
|
def process_status(self, response):
|
2018-06-08 05:47:46 +02:00
|
|
|
address, remote_status = response
|
2018-06-12 16:02:04 +02:00
|
|
|
local_status = yield self.get_local_status(address)
|
2018-06-08 05:47:46 +02:00
|
|
|
if local_status != remote_status:
|
2018-06-12 16:02:04 +02:00
|
|
|
yield self.update_history(address)
|
2018-05-25 08:03:25 +02:00
|
|
|
|
|
|
|
def broadcast(self, tx):
|
2018-07-15 03:34:07 +02:00
|
|
|
return self.network.broadcast(hexlify(tx.raw).decode())
|