lbry-sdk/lbry/wallet/ledger.py

1179 lines
50 KiB
Python
Raw Normal View History

2020-01-03 04:18:49 +01:00
import os
import copy
import time
2018-10-15 23:16:43 +02:00
import asyncio
2018-07-10 06:30:13 +02:00
import logging
from datetime import datetime
2020-01-03 04:18:49 +01:00
from functools import partial
from operator import itemgetter
2020-02-21 04:11:25 +01:00
from collections import defaultdict
2020-01-03 04:18:49 +01:00
from binascii import hexlify, unhexlify
2020-02-21 04:11:25 +01:00
from typing import Dict, Tuple, Type, Iterable, List, Optional, DefaultDict, NamedTuple
2018-06-12 17:53:29 +02:00
2020-01-03 04:50:27 +01:00
import pylru
from lbry.schema.result import Outputs, INVALID, NOT_FOUND
2019-06-21 02:55:47 +02:00
from lbry.schema.url import URL
2020-01-03 04:18:49 +01:00
from lbry.crypto.hash import hash160, double_sha256, sha256
from lbry.crypto.base58 import Base58
from .tasks import TaskGroup
from .database import Database
from .stream import StreamController
from .dewies import dewies_to_lbc
from .account import Account, AddressManager, SingleKey
from .network import Network
from .transaction import Transaction, Output
from .header import Headers, UnvalidatedHeaders
2020-04-02 20:31:03 +02:00
from .checkpoints import HASHES
from .constants import TXO_TYPES, CLAIM_TYPES, COIN, NULL_HASH32
2020-01-03 04:18:49 +01:00
from .bip32 import PubKey, PrivateKey
from .coinselection import CoinSelector
2018-07-10 06:30:13 +02:00
log = logging.getLogger(__name__)
2020-01-03 04:18:49 +01:00
LedgerType = Type['BaseLedger']
class LedgerRegistry(type):
ledgers: Dict[str, LedgerType] = {}
def __new__(mcs, name, bases, attrs):
cls: LedgerType = super().__new__(mcs, name, bases, attrs)
if not (name == 'BaseLedger' and not bases):
ledger_id = cls.get_id()
assert ledger_id not in mcs.ledgers, \
f'Ledger with id "{ledger_id}" already registered.'
mcs.ledgers[ledger_id] = cls
return cls
@classmethod
def get_ledger_class(mcs, ledger_id: str) -> LedgerType:
return mcs.ledgers[ledger_id]
2020-02-21 04:11:25 +01:00
class TransactionEvent(NamedTuple):
address: str
tx: Transaction
2020-01-03 04:18:49 +01:00
2020-02-21 04:11:25 +01:00
class AddressesGeneratedEvent(NamedTuple):
address_manager: AddressManager
addresses: List[str]
2020-01-03 04:18:49 +01:00
2020-02-21 04:11:25 +01:00
class BlockHeightEvent(NamedTuple):
height: int
change: int
2020-01-03 04:18:49 +01:00
class TransactionCacheItem:
2020-03-25 18:17:08 +01:00
__slots__ = '_tx', 'lock', 'has_tx', 'pending_verifications'
2020-01-03 04:18:49 +01:00
def __init__(self, tx: Optional[Transaction] = None, lock: Optional[asyncio.Lock] = None):
self.has_tx = asyncio.Event()
self.lock = lock or asyncio.Lock()
self._tx = self.tx = tx
2020-03-25 18:17:08 +01:00
self.pending_verifications = 0
2018-07-10 06:30:13 +02:00
2020-01-03 04:18:49 +01:00
@property
def tx(self) -> Optional[Transaction]:
return self._tx
@tx.setter
def tx(self, tx: Transaction):
self._tx = tx
if tx is not None:
self.has_tx.set()
class Ledger(metaclass=LedgerRegistry):
2018-06-12 17:53:29 +02:00
name = 'LBRY Credits'
symbol = 'LBC'
2018-06-14 06:53:38 +02:00
network_name = 'mainnet'
2018-06-12 17:53:29 +02:00
headers_class = Headers
2019-03-24 21:55:04 +01:00
2018-10-15 23:16:43 +02:00
secret_prefix = bytes((0x1c,))
pubkey_address_prefix = bytes((0x55,))
script_address_prefix = bytes((0x7a,))
2018-07-01 23:21:18 +02:00
extended_public_key_prefix = unhexlify('0488b21e')
extended_private_key_prefix = unhexlify('0488ade4')
2018-06-14 06:53:38 +02:00
max_target = 0x0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
genesis_hash = '9c89283ba0f3227f6c03b70216b9f665f0118d5e0fa729cedf4fb34d6a34f463'
genesis_bits = 0x1f00ffff
target_timespan = 150
2018-06-12 17:53:29 +02:00
default_fee_per_byte = 50
default_fee_per_name_char = 200000
2020-04-02 20:31:03 +02:00
checkpoints = HASHES
2020-01-03 04:18:49 +01:00
def __init__(self, config=None):
self.config = config or {}
self.db: Database = self.config.get('db') or Database(
os.path.join(self.path, "blockchain.db")
)
self.db.ledger = self
self.headers: Headers = self.config.get('headers') or self.headers_class(
os.path.join(self.path, "headers")
)
2020-04-02 20:31:03 +02:00
self.headers.checkpoints = self.checkpoints
2020-01-03 04:18:49 +01:00
self.network: Network = self.config.get('network') or Network(self)
self.network.on_header.listen(self.receive_header)
self.network.on_status.listen(self.process_status_update)
self.accounts = []
self.fee_per_byte: int = self.config.get('fee_per_byte', self.default_fee_per_byte)
self._on_transaction_controller = StreamController()
self.on_transaction = self._on_transaction_controller.stream
self.on_transaction.listen(
2020-07-15 02:31:40 +02:00
lambda e: log.info(
2020-01-03 04:18:49 +01:00
'(%s) on_transaction: address=%s, height=%s, is_verified=%s, tx.id=%s',
self.get_id(), e.address, e.tx.height, e.tx.is_verified, e.tx.id
)
)
self._on_address_controller = StreamController()
self.on_address = self._on_address_controller.stream
self.on_address.listen(
lambda e: log.info('(%s) on_address: %s', self.get_id(), e.addresses)
)
self._on_header_controller = StreamController()
self.on_header = self._on_header_controller.stream
self.on_header.listen(
lambda change: log.info(
'%s: added %s header blocks, final height %s',
self.get_id(), change, self.headers.height
)
)
self._download_height = 0
self._on_ready_controller = StreamController()
self.on_ready = self._on_ready_controller.stream
self._tx_cache = pylru.lrucache(self.config.get("tx_cache_size", 1024))
2020-01-03 04:18:49 +01:00
self._update_tasks = TaskGroup()
self._other_tasks = TaskGroup() # that we dont need to start
2020-01-03 04:18:49 +01:00
self._utxo_reservation_lock = asyncio.Lock()
self._header_processing_lock = asyncio.Lock()
2020-01-10 05:02:58 +01:00
self._address_update_locks: DefaultDict[str, asyncio.Lock] = defaultdict(asyncio.Lock)
2020-07-08 18:15:26 +02:00
self._history_lock = asyncio.Lock()
2020-01-03 04:18:49 +01:00
self.coin_selection_strategy = None
self._known_addresses_out_of_sync = set()
2018-06-12 17:53:29 +02:00
self.fee_per_name_char = self.config.get('fee_per_name_char', self.default_fee_per_name_char)
self._balance_cache = pylru.lrucache(100000)
2020-01-03 04:18:49 +01:00
@classmethod
def get_id(cls):
return '{}_{}'.format(cls.symbol.lower(), cls.network_name.lower())
@classmethod
def hash160_to_address(cls, h160):
raw_address = cls.pubkey_address_prefix + h160
return Base58.encode(bytearray(raw_address + double_sha256(raw_address)[0:4]))
@staticmethod
def address_to_hash160(address):
return Base58.decode(address)[1:21]
@classmethod
def is_valid_address(cls, address):
decoded = Base58.decode_check(address)
return decoded[0] == cls.pubkey_address_prefix[0]
@classmethod
def public_key_to_address(cls, public_key):
return cls.hash160_to_address(hash160(public_key))
@staticmethod
def private_key_to_wif(private_key):
return b'\x1c' + private_key + b'\x01'
@property
def path(self):
return os.path.join(self.config['data_path'], self.get_id())
def add_account(self, account: Account):
self.accounts.append(account)
async def _get_account_and_address_info_for_address(self, wallet, address):
match = await self.db.get_address(accounts=wallet.accounts, address=address)
if match:
for account in wallet.accounts:
if match['account'] == account.public_key.address:
return account, match
async def get_private_key_for_address(self, wallet, address) -> Optional[PrivateKey]:
match = await self._get_account_and_address_info_for_address(wallet, address)
if match:
account, address_info = match
return account.get_private_key(address_info['chain'], address_info['pubkey'].n)
return None
async def get_public_key_for_address(self, wallet, address) -> Optional[PubKey]:
match = await self._get_account_and_address_info_for_address(wallet, address)
if match:
_, address_info = match
return address_info['pubkey']
return None
async def get_account_for_address(self, wallet, address):
match = await self._get_account_and_address_info_for_address(wallet, address)
if match:
return match[0]
async def get_effective_amount_estimators(self, funding_accounts: Iterable[Account]):
estimators = []
for account in funding_accounts:
utxos = await account.get_utxos()
for utxo in utxos:
estimators.append(utxo.get_estimator(self))
return estimators
async def get_addresses(self, **constraints):
return await self.db.get_addresses(**constraints)
def get_address_count(self, **constraints):
return self.db.get_address_count(**constraints)
2020-06-15 16:20:55 +02:00
async def get_spendable_utxos(self, amount: int, funding_accounts: Optional[Iterable['Account']], min_amount=1):
2020-06-04 16:18:04 +02:00
min_amount = min(amount // 10, min_amount)
fee = Output.pay_pubkey_hash(COIN, NULL_HASH32).get_fee(self)
selector = CoinSelector(amount, fee)
2020-01-03 04:18:49 +01:00
async with self._utxo_reservation_lock:
2020-06-04 16:18:04 +02:00
if self.coin_selection_strategy == 'sqlite':
return await self.db.get_spendable_utxos(self, amount + fee, funding_accounts, min_amount=min_amount,
fee_per_byte=self.fee_per_byte)
2020-01-03 04:18:49 +01:00
txos = await self.get_effective_amount_estimators(funding_accounts)
spendables = selector.select(txos, self.coin_selection_strategy)
if spendables:
await self.reserve_outputs(s.txo for s in spendables)
return spendables
def reserve_outputs(self, txos):
return self.db.reserve_outputs(txos)
def release_outputs(self, txos):
return self.db.release_outputs(txos)
def release_tx(self, tx):
return self.release_outputs([txi.txo_ref.txo for txi in tx.inputs])
def get_utxos(self, **constraints):
self.constraint_spending_utxos(constraints)
return self.db.get_utxos(**constraints)
def get_utxo_count(self, **constraints):
self.constraint_spending_utxos(constraints)
return self.db.get_utxo_count(**constraints)
async def get_txos(self, resolve=False, **constraints) -> List[Output]:
2020-03-07 06:34:47 +01:00
txos = await self.db.get_txos(**constraints)
if resolve:
return await self._resolve_for_local_results(constraints.get('accounts', []), txos)
return txos
def get_txo_count(self, **constraints):
return self.db.get_txo_count(**constraints)
2020-03-21 00:07:16 +01:00
def get_txo_sum(self, **constraints):
return self.db.get_txo_sum(**constraints)
def get_txo_plot(self, **constraints):
return self.db.get_txo_plot(**constraints)
2020-01-03 04:18:49 +01:00
def get_transactions(self, **constraints):
return self.db.get_transactions(**constraints)
def get_transaction_count(self, **constraints):
return self.db.get_transaction_count(**constraints)
async def get_local_status_and_history(self, address, history=None):
if not history:
address_details = await self.db.get_address(address=address)
history = (address_details['history'] if address_details else '') or ''
2020-01-03 04:18:49 +01:00
parts = history.split(':')[:-1]
return (
hexlify(sha256(history.encode())).decode() if history else None,
list(zip(parts[0::2], map(int, parts[1::2])))
)
@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])
async def start(self):
if not os.path.exists(self.path):
os.mkdir(self.path)
await asyncio.wait([
self.db.open(),
self.headers.open()
])
2020-03-30 23:02:08 +02:00
fully_synced = self.on_ready.first
asyncio.create_task(self.network.start())
await self.network.on_connected.first
2020-03-21 08:32:03 +01:00
async with self._header_processing_lock:
await self._update_tasks.add(self.initial_headers_sync())
self.network.on_connected.listen(self.join_network)
asyncio.ensure_future(self.join_network())
2020-03-30 23:02:08 +02:00
await fully_synced
2020-08-04 21:02:43 +02:00
await self.db.release_all_outputs()
2020-01-03 04:18:49 +01:00
await asyncio.gather(*(a.maybe_migrate_certificates() for a in self.accounts))
await asyncio.gather(*(a.save_max_gap() for a in self.accounts))
if len(self.accounts) > 10:
log.info("Loaded %i accounts", len(self.accounts))
else:
await self._report_state()
self.on_transaction.listen(self._reset_balance_cache)
async def join_network(self, *_):
log.info("Subscribing and updating accounts.")
2020-03-24 04:54:55 +01:00
await self._update_tasks.add(self.subscribe_accounts())
2020-01-03 04:18:49 +01:00
await self._update_tasks.done.wait()
self._on_ready_controller.add(True)
async def stop(self):
self._update_tasks.cancel()
self._other_tasks.cancel()
2020-01-03 04:18:49 +01:00
await self._update_tasks.done.wait()
await self._other_tasks.done.wait()
2020-01-03 04:18:49 +01:00
await self.network.stop()
await self.db.close()
await self.headers.close()
@property
def local_height_including_downloaded_height(self):
return max(self.headers.height, self._download_height)
async def initial_headers_sync(self):
2020-03-18 08:10:55 +01:00
get_chunk = partial(self.network.retriable_call, self.network.get_headers, count=1000, b64=True)
self.headers.chunk_getter = get_chunk
async def doit():
for height in reversed(sorted(self.headers.known_missing_checkpointed_chunks)):
async with self._header_processing_lock:
2020-03-23 04:05:36 +01:00
await self.headers.ensure_chunk_at(height)
self._other_tasks.add(doit())
2020-03-21 08:32:03 +01:00
await self.update_headers()
2020-01-03 04:18:49 +01:00
async def update_headers(self, height=None, headers=None, subscription_update=False):
rewound = 0
while True:
if height is None or height > len(self.headers):
# sometimes header subscription updates are for a header in the future
# which can't be connected, so we do a normal header sync instead
height = len(self.headers)
headers = None
subscription_update = False
if not headers:
header_response = await self.network.retriable_call(self.network.get_headers, height, 2001)
headers = header_response['hex']
if not headers:
# Nothing to do, network thinks we're already at the latest height.
return
added = await self.headers.connect(height, unhexlify(headers))
if added > 0:
height += added
self._on_header_controller.add(
BlockHeightEvent(self.headers.height, added))
if rewound > 0:
# we started rewinding blocks and apparently found
# a new chain
rewound = 0
await self.db.rewind_blockchain(height)
if subscription_update:
# subscription updates are for latest header already
# so we don't need to check if there are newer / more
# on another loop of update_headers(), just return instead
return
elif added == 0:
# we had headers to connect but none got connected, probably a reorganization
height -= 1
rewound += 1
log.warning(
"Blockchain Reorganization: attempting rewind to height %s from starting height %s",
height, height+rewound
)
self._tx_cache.clear()
2020-01-03 04:18:49 +01:00
else:
raise IndexError(f"headers.connect() returned negative number ({added})")
if height < 0:
raise IndexError(
"Blockchain reorganization rewound all the way back to genesis hash. "
"Something is very wrong. Maybe you are on the wrong blockchain?"
)
if rewound >= 100:
raise IndexError(
"Blockchain reorganization dropped {} headers. This is highly unusual. "
"Will not continue to attempt reorganizing. Please, delete the ledger "
"synchronization directory inside your wallet directory (folder: '{}') and "
"restart the program to synchronize from scratch."
2020-01-03 04:50:27 +01:00
.format(rewound, self.get_id())
2020-01-03 04:18:49 +01:00
)
headers = None # ready to download some more headers
# if we made it this far and this was a subscription_update
# it means something went wrong and now we're doing a more
# robust sync, turn off subscription update shortcut
subscription_update = False
async def receive_header(self, response):
async with self._header_processing_lock:
header = response[0]
await self.update_headers(
height=header['height'], headers=header['hex'], subscription_update=True
)
async def subscribe_accounts(self):
if self.network.is_connected and self.accounts:
2020-01-10 05:05:49 +01:00
log.info("Subscribe to %i accounts", len(self.accounts))
2020-01-03 04:18:49 +01:00
await asyncio.wait([
self.subscribe_account(a) for a in self.accounts
])
async def subscribe_account(self, account: Account):
for address_manager in account.address_managers.values():
await self.subscribe_addresses(address_manager, await address_manager.get_addresses())
await account.ensure_address_gap()
async def unsubscribe_account(self, account: Account):
for address in await account.get_addresses():
await self.network.unsubscribe_address(address)
async def announce_addresses(self, address_manager: AddressManager, addresses: List[str]):
await self.subscribe_addresses(address_manager, addresses)
await self._on_address_controller.add(
AddressesGeneratedEvent(address_manager, addresses)
)
2020-01-10 05:05:49 +01:00
async def subscribe_addresses(self, address_manager: AddressManager, addresses: List[str], batch_size: int = 1000):
2020-01-03 04:18:49 +01:00
if self.network.is_connected and addresses:
2020-01-10 05:05:49 +01:00
addresses_remaining = list(addresses)
while addresses_remaining:
batch = addresses_remaining[:batch_size]
2020-01-10 19:57:52 +01:00
results = await self.network.subscribe_address(*batch)
for address, remote_status in zip(batch, results):
self._update_tasks.add(self.update_history(address, remote_status, address_manager))
addresses_remaining = addresses_remaining[batch_size:]
2020-11-04 21:33:19 +01:00
if self.network.client and self.network.client.server_address_and_port:
log.info("subscribed to %i/%i addresses on %s:%i", len(addresses) - len(addresses_remaining),
len(addresses), *self.network.client.server_address_and_port)
if self.network.client and self.network.client.server_address_and_port:
log.info(
"finished subscribing to %i addresses on %s:%i", len(addresses),
*self.network.client.server_address_and_port
)
2020-01-03 04:18:49 +01:00
def process_status_update(self, update):
address, remote_status = update
self._update_tasks.add(self.update_history(address, remote_status))
2020-07-15 03:08:46 +02:00
async def update_history(self, address, remote_status, address_manager: AddressManager = None,
reattempt_update: bool = True):
2020-01-10 05:02:58 +01:00
async with self._address_update_locks[address]:
2020-01-03 04:18:49 +01:00
self._known_addresses_out_of_sync.discard(address)
local_status, local_history = await self.get_local_status_and_history(address)
if local_status == remote_status:
return True
remote_history = await self.network.retriable_call(self.network.get_history, address)
remote_history = list(map(itemgetter('tx_hash', 'height'), remote_history))
we_need = set(remote_history) - set(local_history)
if not we_need:
2020-07-09 19:34:08 +02:00
remote_missing = set(local_history) - set(remote_history)
if remote_missing:
log.warning(
"%i transactions we have for %s are not in the remote address history",
len(remote_missing), address
)
2020-01-03 04:18:49 +01:00
return True
2020-07-08 18:15:26 +02:00
to_request = {}
pending_synced_history = {}
already_synced = set()
2020-01-03 04:18:49 +01:00
2020-07-09 19:34:08 +02:00
already_synced_offset = 0
2020-07-08 18:15:26 +02:00
for i, (txid, remote_height) in enumerate(remote_history):
2020-07-09 19:34:08 +02:00
if i == already_synced_offset and i < len(local_history) and local_history[i] == (txid, remote_height):
2020-07-08 18:15:26 +02:00
pending_synced_history[i] = f'{txid}:{remote_height}:'
already_synced.add((txid, remote_height))
2020-07-09 19:34:08 +02:00
already_synced_offset += 1
2020-07-08 18:15:26 +02:00
continue
2020-07-09 19:34:08 +02:00
2020-07-08 18:15:26 +02:00
tx_indexes = {}
2020-01-03 04:18:49 +01:00
2020-07-08 18:15:26 +02:00
for i, (txid, remote_height) in enumerate(remote_history):
tx_indexes[txid] = i
if (txid, remote_height) in already_synced:
continue
to_request[i] = (txid, remote_height)
2020-07-15 03:08:46 +02:00
log.debug(
2020-12-23 22:30:52 +01:00
"request %i transactions, %i/%i for %s are already synced", len(to_request), len(already_synced),
2020-12-20 04:50:48 +01:00
len(remote_history), address
2020-07-15 03:08:46 +02:00
)
2020-12-20 03:39:01 +01:00
remote_history_txids = set(txid for txid, _ in remote_history)
2020-12-20 07:12:11 +01:00
async for tx in self.request_synced_transactions(to_request, remote_history_txids, address):
2020-07-08 18:15:26 +02:00
pending_synced_history[tx_indexes[tx.id]] = f"{tx.id}:{tx.height}:"
2020-12-23 22:30:52 +01:00
if len(pending_synced_history) % 100 == 0:
2020-12-20 07:12:11 +01:00
log.info("Syncing address %s: %d/%d", address, len(pending_synced_history), len(to_request))
log.info("Sync finished for address %s: %d/%d", address, len(pending_synced_history), len(to_request))
2020-07-08 18:15:26 +02:00
2020-07-15 03:08:46 +02:00
assert len(pending_synced_history) == len(remote_history), \
f"{len(pending_synced_history)} vs {len(remote_history)}"
2020-07-08 18:15:26 +02:00
synced_history = ""
for remote_i, i in zip(range(len(remote_history)), sorted(pending_synced_history.keys())):
assert i == remote_i, f"{i} vs {remote_i}"
txid, height = remote_history[remote_i]
if f"{txid}:{height}:" != pending_synced_history[i]:
2020-07-15 03:08:46 +02:00
log.warning("history mismatch: %s vs %s", remote_history[remote_i], pending_synced_history[i])
2020-07-08 18:15:26 +02:00
synced_history += pending_synced_history[i]
2020-12-23 22:30:52 +01:00
await self.db.set_address_history(address, synced_history)
2020-07-08 18:15:26 +02:00
2020-01-03 04:18:49 +01:00
if address_manager is None:
address_manager = await self.get_address_manager_for_address(address)
if address_manager is not None:
await address_manager.ensure_address_gap()
local_status, local_history = \
2020-07-08 18:15:26 +02:00
await self.get_local_status_and_history(address, synced_history)
2020-01-03 04:18:49 +01:00
if local_status != remote_status:
if local_history == remote_history:
2020-06-04 16:19:24 +02:00
log.warning(
"%s has a synced history but a mismatched status", address
)
2020-01-03 04:18:49 +01:00
return True
2020-06-04 16:19:24 +02:00
remote_set = set(remote_history)
local_set = set(local_history)
2020-01-03 04:18:49 +01:00
log.warning(
2020-06-04 16:19:24 +02:00
"%s is out of sync after syncing.\n"
"Remote: %s with %d items (%i unique), local: %s with %d items (%i unique).\n"
"Histories are mismatched on %i items.\n"
"Local is missing\n"
"%s\n"
"Remote is missing\n"
"%s\n"
"******",
address, remote_status, len(remote_history), len(remote_set),
local_status, len(local_history), len(local_set), len(remote_set.symmetric_difference(local_set)),
"\n".join([f"{txid} - {height}" for txid, height in local_set.difference(remote_set)]),
"\n".join([f"{txid} - {height}" for txid, height in remote_set.difference(local_set)])
2020-01-03 04:18:49 +01:00
)
self._known_addresses_out_of_sync.add(address)
return False
else:
2020-07-15 02:31:40 +02:00
log.debug("finished syncing transaction history for %s, %i known txs", address, len(local_history))
2020-01-03 04:18:49 +01:00
return True
async def maybe_verify_transaction(self, tx, remote_height, merkle=None):
2020-01-03 04:18:49 +01:00
tx.height = remote_height
2020-12-20 17:35:43 +01:00
if 0 < remote_height < len(self.headers):
2020-03-25 18:29:10 +01:00
# can't be tx.pending_verifications == 1 because we have to handle the transaction_show case
if not merkle:
merkle = await self.network.retriable_call(self.network.get_merkle, tx.id, remote_height)
2020-12-20 17:35:43 +01:00
if 'merkle' not in merkle:
return
2020-01-03 04:18:49 +01:00
merkle_root = self.get_root_of_merkle_tree(merkle['merkle'], merkle['pos'], tx.hash)
2020-03-16 15:28:11 +01:00
header = await self.headers.get(remote_height)
2020-01-03 04:18:49 +01:00
tx.position = merkle['pos']
tx.is_verified = merkle_root == header['merkle_root']
2020-12-23 23:10:19 +01:00
return tx
2020-01-03 04:18:49 +01:00
2020-12-22 02:37:48 +01:00
async def request_transactions(self, to_request: Tuple[Tuple[str, int], ...], cached=False):
batches = [[]]
remote_heights = {}
2020-12-24 01:37:20 +01:00
cache_hits = set()
for txid, height in sorted(to_request, key=lambda x: x[1]):
2020-12-22 02:37:48 +01:00
if cached:
if txid in self._tx_cache:
if self._tx_cache[txid].tx is not None and self._tx_cache[txid].tx.is_verified:
2020-12-24 01:37:20 +01:00
cache_hits.add(txid)
2020-12-22 02:37:48 +01:00
continue
else:
self._tx_cache[txid] = TransactionCacheItem()
remote_heights[txid] = height
2020-12-20 07:12:11 +01:00
if len(batches[-1]) == 100:
batches.append([])
batches[-1].append(txid)
if not batches[-1]:
batches.pop()
2020-12-24 01:37:20 +01:00
if cached and cache_hits:
yield {txid: self._tx_cache[txid].tx for txid in cache_hits}
for batch in batches:
2020-12-23 23:10:19 +01:00
txs = await self._single_batch(batch, remote_heights)
if cached:
for txid, tx in txs.items():
self._tx_cache[txid].tx = tx
yield txs
2020-12-20 04:50:48 +01:00
async def request_synced_transactions(self, to_request, remote_history, address):
2020-12-23 23:10:19 +01:00
async for txs in self.request_transactions(((txid, height) for txid, height in to_request.values())):
for tx in txs.values():
yield tx
await self._sync_and_save_batch(address, remote_history, txs)
2020-07-08 18:15:26 +02:00
2020-12-20 07:12:11 +01:00
async def _single_batch(self, batch, remote_heights):
heights = {remote_heights[txid] for txid in batch}
unrestriced = 0 < min(heights) < max(heights) < max(self.headers.checkpoints or [0])
batch_result = await self.network.retriable_call(self.network.get_transaction_batch, batch, not unrestriced)
2020-12-23 23:10:19 +01:00
txs = {}
2020-12-20 04:50:48 +01:00
for txid, (raw, merkle) in batch_result.items():
remote_height = remote_heights[txid]
tx = Transaction(unhexlify(raw), height=remote_height)
2020-12-23 23:10:19 +01:00
txs[tx.id] = tx
2020-12-20 07:12:11 +01:00
await self.maybe_verify_transaction(tx, remote_height, merkle)
2020-12-23 23:10:19 +01:00
return txs
2020-07-09 19:34:08 +02:00
2020-12-23 22:30:52 +01:00
async def _sync_and_save_batch(self, address, remote_history, pending_txs):
await asyncio.gather(*(self._sync(tx, remote_history, pending_txs) for tx in pending_txs.values()))
await self.db.save_transaction_io_batch(
pending_txs.values(), address, self.address_to_hash160(address), ""
)
while pending_txs:
self._on_transaction_controller.add(TransactionEvent(address, pending_txs.popitem()[1]))
2020-12-22 02:37:48 +01:00
async def _sync(self, tx, remote_history, pending_txs):
2020-12-20 07:12:11 +01:00
check_db_for_txos = {}
2020-12-20 04:50:48 +01:00
for txi in tx.inputs:
if txi.txo_ref.txo is not None:
continue
2020-12-20 07:12:11 +01:00
wanted_txid = txi.txo_ref.tx_ref.id
if wanted_txid not in remote_history:
2020-12-20 04:50:48 +01:00
continue
2020-12-22 02:37:48 +01:00
if wanted_txid in pending_txs:
2020-12-22 20:05:37 +01:00
txi.txo_ref = pending_txs[wanted_txid].outputs[txi.txo_ref.position].ref
2020-12-20 04:50:48 +01:00
else:
2020-12-20 07:12:11 +01:00
check_db_for_txos[txi] = txi.txo_ref.id
2020-12-04 00:31:23 +01:00
2020-12-20 04:50:48 +01:00
referenced_txos = {} if not check_db_for_txos else {
txo.id: txo for txo in await self.db.get_txos(
2020-12-20 17:35:43 +01:00
txoid__in=list(check_db_for_txos.values()), order_by='txo.txoid', no_tx=True
2020-12-20 04:50:48 +01:00
)
}
2020-12-20 07:12:11 +01:00
for txi in check_db_for_txos:
if txi.txo_ref.id in referenced_txos:
txi.txo_ref = referenced_txos[txi.txo_ref.id].ref
else:
tx_from_db = await self.db.get_transaction(txid=txi.txo_ref.tx_ref.id)
if tx_from_db is None:
2020-12-22 02:37:48 +01:00
log.warning("%s not on db, not on cache, but on remote history!", txi.txo_ref.id)
else:
txi.txo_ref = tx_from_db.outputs[txi.txo_ref.position].ref
return tx
2020-07-08 18:15:26 +02:00
2020-01-03 04:18:49 +01:00
async def get_address_manager_for_address(self, address) -> Optional[AddressManager]:
details = await self.db.get_address(address=address)
for account in self.accounts:
if account.id == details['account']:
return account.address_managers[details['chain']]
return None
def broadcast(self, tx):
# broadcast can't be a retriable call yet
return self.network.broadcast(hexlify(tx.raw).decode())
async def wait(self, tx: Transaction, height=-1, timeout=1):
timeout = timeout or 600 # after 10 minutes there is almost 0 hope
2020-01-03 04:18:49 +01:00
addresses = set()
for txi in tx.inputs:
if txi.txo_ref.txo is not None:
addresses.add(
self.hash160_to_address(txi.txo_ref.txo.pubkey_hash)
)
for txo in tx.outputs:
if txo.has_address:
addresses.add(self.hash160_to_address(txo.pubkey_hash))
start = int(time.perf_counter())
while timeout and (int(time.perf_counter()) - start) <= timeout:
if await self._wait_round(tx, height, addresses):
return
raise asyncio.TimeoutError('Timed out waiting for transaction.')
async def _wait_round(self, tx: Transaction, height: int, addresses: Iterable[str]):
2020-01-03 04:18:49 +01:00
records = await self.db.get_addresses(address__in=addresses)
_, pending = await asyncio.wait([
self.on_transaction.where(partial(
lambda a, e: a == e.address and e.tx.height >= height and e.tx.id == tx.id,
address_record['address']
)) for address_record in records
], timeout=1)
2020-06-16 21:38:33 +02:00
if not pending:
return True
records = await self.db.get_addresses(address__in=addresses)
for record in records:
local_history = (await self.get_local_status_and_history(
record['address'], history=record['history']
))[1] if record['history'] else []
for txid, local_height in local_history:
if txid == tx.id:
if local_height >= height:
return True
log.warning(
"local history has higher height than remote for %s (%i vs %i)", txid,
local_height, height
)
return False
2020-07-07 20:21:16 +02:00
log.warning(
2020-06-16 21:38:33 +02:00
"local history does not contain %s, requested height %i", tx.id, height
)
return False
2020-01-03 04:18:49 +01:00
async def _inflate_outputs(
self, query, accounts,
include_purchase_receipt=False,
include_is_my_output=False,
2020-03-22 06:13:26 +01:00
include_sent_supports=False,
include_sent_tips=False,
2020-12-21 01:06:08 +01:00
include_received_tips=False) -> Tuple[List[Output], dict, int, int]:
encoded_outputs = await query
outputs = Outputs.from_base64(encoded_outputs or b'') # TODO: why is the server returning None?
2020-12-21 01:06:08 +01:00
txs: List[Transaction] = []
2019-04-29 06:38:58 +02:00
if len(outputs.txs) > 0:
2020-12-22 02:37:48 +01:00
async for tx in self.request_transactions(tuple(outputs.txs), cached=True):
2020-12-23 23:10:19 +01:00
txs.extend(tx.values())
_txos, blocked = outputs.inflate(txs)
txos = []
for txo in _txos:
if isinstance(txo, Output):
# transactions and outputs are cached and shared between wallets
# we don't want to leak informaion between wallet so we add the
# wallet specific metadata on throw away copies of the txos
txo = copy.copy(txo)
2020-05-18 18:27:22 +02:00
channel = txo.channel
txo.purchase_receipt = None
txo.update_annotations(None)
2020-05-18 18:27:22 +02:00
txo.channel = channel
txos.append(txo)
includes = (
include_purchase_receipt, include_is_my_output,
include_sent_supports, include_sent_tips
)
if accounts and any(includes):
receipts = {}
if include_purchase_receipt:
priced_claims = []
for txo in txos:
if isinstance(txo, Output) and txo.has_price:
priced_claims.append(txo)
if priced_claims:
receipts = {
txo.purchased_claim_id: txo for txo in
await self.db.get_purchases(
accounts=accounts,
purchased_claim_id__in=[c.claim_id for c in priced_claims]
)
}
for txo in txos:
if isinstance(txo, Output) and txo.can_decode_claim:
if include_purchase_receipt:
txo.purchase_receipt = receipts.get(txo.claim_id)
if include_is_my_output:
mine = await self.db.get_txo_count(
claim_id=txo.claim_id, txo_type__in=CLAIM_TYPES, is_my_output=True,
2020-04-02 02:44:34 +02:00
is_spent=False, accounts=accounts
)
if mine:
txo.is_my_output = True
else:
txo.is_my_output = False
2020-03-22 06:13:26 +01:00
if include_sent_supports:
supports = await self.db.get_txo_sum(
claim_id=txo.claim_id, txo_type=TXO_TYPES['support'],
is_my_input=True, is_my_output=True,
2020-04-02 02:44:34 +02:00
is_spent=False, accounts=accounts
)
txo.sent_supports = supports
2020-03-22 06:13:26 +01:00
if include_sent_tips:
tips = await self.db.get_txo_sum(
claim_id=txo.claim_id, txo_type=TXO_TYPES['support'],
2020-03-22 06:13:26 +01:00
is_my_input=True, is_my_output=False,
accounts=accounts
)
txo.sent_tips = tips
2020-03-22 06:13:26 +01:00
if include_received_tips:
tips = await self.db.get_txo_sum(
claim_id=txo.claim_id, txo_type=TXO_TYPES['support'],
is_my_input=False, is_my_output=True,
accounts=accounts
)
txo.received_tips = tips
return txos, blocked, outputs.offset, outputs.total
2018-10-15 23:16:43 +02:00
2020-08-04 18:11:02 +02:00
async def resolve(self, accounts, urls, new_sdk_server=None, **kwargs):
txos = []
urls_copy = list(urls)
2020-08-04 18:11:02 +02:00
if new_sdk_server:
2020-11-03 22:23:31 +01:00
resolve = partial(self.network.new_resolve, new_sdk_server)
2020-08-04 17:33:39 +02:00
else:
2020-12-21 01:06:08 +01:00
resolve = partial(self.network.retriable_call, self.network.resolve)
while urls_copy:
batch, urls_copy = urls_copy[:100], urls_copy[100:]
txos.extend(
(await self._inflate_outputs(
resolve(batch), accounts, **kwargs
))[0]
)
2019-04-29 06:38:58 +02:00
assert len(urls) == len(txos), "Mismatch between urls requested for resolve and responses received."
result = {}
for url, txo in zip(urls, txos):
if txo:
if isinstance(txo, Output) and URL.parse(url).has_stream_in_channel:
if not txo.channel or not txo.is_signed_by(txo.channel, self):
txo = {'error': {'name': INVALID, 'text': f'{url} has invalid channel signature'}}
else:
txo = {'error': {'name': NOT_FOUND, 'text': f'{url} did not resolve to a claim'}}
result[url] = txo
return result
2018-10-15 23:16:43 +02:00
2020-10-13 21:51:59 +02:00
async def sum_supports(self, new_sdk_server, **kwargs) -> List[Dict]:
return await self.network.sum_supports(new_sdk_server, **kwargs)
2020-03-22 06:13:26 +01:00
async def claim_search(
self, accounts, include_purchase_receipt=False, include_is_my_output=False,
2020-08-04 18:11:02 +02:00
new_sdk_server=None, **kwargs) -> Tuple[List[Output], dict, int, int]:
if new_sdk_server:
claim_search = partial(self.network.new_claim_search, new_sdk_server)
2020-12-21 01:06:08 +01:00
else:
claim_search = self.network.claim_search
return await self._inflate_outputs(
claim_search(**kwargs), accounts,
include_purchase_receipt=include_purchase_receipt,
include_is_my_output=include_is_my_output,
)
2019-03-31 00:40:01 +01:00
2020-03-22 17:23:21 +01:00
async def get_claim_by_claim_id(self, accounts, claim_id, **kwargs) -> Output:
for claim in (await self.claim_search(accounts, claim_id=claim_id, **kwargs))[0]:
2019-04-29 06:38:58 +02:00
return claim
2018-06-12 17:53:29 +02:00
2018-10-15 23:16:43 +02:00
async def _report_state(self):
try:
for account in self.accounts:
balance = dewies_to_lbc(await account.get_balance(include_claims=True))
channel_count = await account.get_channel_count()
claim_count = await account.get_claim_count()
if isinstance(account.receiving, SingleKey):
log.info("Loaded single key account %s with %s LBC. "
"%d channels, %d certificates and %d claims",
account.id, balance, channel_count, len(account.channel_keys), claim_count)
else:
total_receiving = len(await account.receiving.get_addresses())
total_change = len(await account.change.get_addresses())
log.info("Loaded account %s with %s LBC, %d receiving addresses (gap: %d), "
"%d change addresses (gap: %d), %d channels, %d certificates and %d claims. ",
account.id, balance, total_receiving, account.receiving.gap, total_change,
account.change.gap, channel_count, len(account.channel_keys), claim_count)
2020-01-10 18:27:56 +01:00
except Exception as err:
if isinstance(err, asyncio.CancelledError): # TODO: remove when updated to 3.8
raise
log.exception(
'Failed to display wallet state, please file issue '
'for this bug along with the traceback you see below:')
2018-07-12 05:18:59 +02:00
async def _reset_balance_cache(self, e: TransactionEvent):
account_ids = [
r['account'] for r in await self.db.get_addresses(('account',), address=e.address)
]
for account_id in account_ids:
if account_id in self._balance_cache:
del self._balance_cache[account_id]
2019-08-12 07:16:15 +02:00
@staticmethod
def constraint_spending_utxos(constraints):
constraints['txo_type__in'] = (0, TXO_TYPES['purchase'])
async def get_purchases(self, resolve=False, **constraints):
purchases = await self.db.get_purchases(**constraints)
if resolve:
claim_ids = [p.purchased_claim_id for p in purchases]
try:
2020-01-19 03:43:10 +01:00
resolved, _, _, _ = await self.claim_search([], claim_ids=claim_ids)
2020-01-10 18:27:56 +01:00
except Exception as err:
if isinstance(err, asyncio.CancelledError): # TODO: remove when updated to 3.8
raise
log.exception("Resolve failed while looking up purchased claim ids:")
resolved = []
lookup = {claim.claim_id: claim for claim in resolved}
for purchase in purchases:
purchase.purchased_claim = lookup.get(purchase.purchased_claim_id)
return purchases
def get_purchase_count(self, resolve=False, **constraints):
return self.db.get_purchase_count(**constraints)
async def _resolve_for_local_results(self, accounts, txos):
results = []
2020-03-07 06:34:47 +01:00
response = await self.resolve(
accounts, [txo.permanent_url for txo in txos if txo.can_decode_claim]
)
for txo in txos:
2020-03-07 06:34:47 +01:00
resolved = response.get(txo.permanent_url) if txo.can_decode_claim else None
if isinstance(resolved, Output):
resolved.update_annotations(txo)
results.append(resolved)
else:
if isinstance(resolved, dict) and 'error' in resolved:
txo.meta['error'] = resolved['error']
results.append(txo)
return results
async def get_claims(self, resolve=False, **constraints):
claims = await self.db.get_claims(**constraints)
if resolve:
return await self._resolve_for_local_results(constraints.get('accounts', []), claims)
return claims
def get_claim_count(self, **constraints):
return self.db.get_claim_count(**constraints)
async def get_streams(self, resolve=False, **constraints):
streams = await self.db.get_streams(**constraints)
if resolve:
return await self._resolve_for_local_results(constraints.get('accounts', []), streams)
return streams
def get_stream_count(self, **constraints):
return self.db.get_stream_count(**constraints)
async def get_channels(self, resolve=False, **constraints):
channels = await self.db.get_channels(**constraints)
if resolve:
return await self._resolve_for_local_results(constraints.get('accounts', []), channels)
return channels
def get_channel_count(self, **constraints):
return self.db.get_channel_count(**constraints)
2019-11-14 03:16:27 +01:00
async def resolve_collection(self, collection, offset=0, page_size=1):
2020-12-21 01:06:08 +01:00
claim_ids = collection.claim.collection.claims.ids[offset:page_size + offset]
2019-11-14 03:16:27 +01:00
try:
2020-01-19 02:58:30 +01:00
resolve_results, _, _, _ = await self.claim_search([], claim_ids=claim_ids)
2020-01-10 18:27:56 +01:00
except Exception as err:
if isinstance(err, asyncio.CancelledError): # TODO: remove when updated to 3.8
raise
2019-11-14 03:16:27 +01:00
log.exception("Resolve failed while looking up collection claim ids:")
return []
claims = []
for claim_id in claim_ids:
found = False
for txo in resolve_results:
if txo.claim_id == claim_id:
claims.append(txo)
found = True
break
if not found:
claims.append(None)
return claims
async def get_collections(self, resolve_claims=0, **constraints):
2019-11-13 23:50:35 +01:00
collections = await self.db.get_collections(**constraints)
2019-11-14 03:16:27 +01:00
if resolve_claims > 0:
2019-11-13 23:50:35 +01:00
for collection in collections:
2019-11-14 03:16:27 +01:00
collection.claims = await self.resolve_collection(collection, page_size=resolve_claims)
2019-11-13 23:50:35 +01:00
return collections
2019-11-14 03:16:27 +01:00
def get_collection_count(self, resolve_claims=0, **constraints):
2019-11-12 18:17:35 +01:00
return self.db.get_collection_count(**constraints)
def get_supports(self, **constraints):
return self.db.get_supports(**constraints)
def get_support_count(self, **constraints):
return self.db.get_support_count(**constraints)
2020-03-17 15:29:26 +01:00
async def get_transaction_history(self, read_only=False, **constraints):
txs: List[Transaction] = await self.db.get_transactions(
include_is_my_output=True, include_is_spent=True,
read_only=read_only, **constraints
)
headers = self.headers
history = []
2020-01-03 04:50:27 +01:00
for tx in txs: # pylint: disable=too-many-nested-blocks
2020-03-21 08:32:03 +01:00
ts = headers.estimated_timestamp(tx.height)
item = {
'txid': tx.id,
'timestamp': ts,
'date': datetime.fromtimestamp(ts).isoformat(' ')[:-3] if tx.height > 0 else None,
2020-12-21 01:06:08 +01:00
'confirmations': (headers.height + 1) - tx.height if tx.height > 0 else 0,
'claim_info': [],
'update_info': [],
'support_info': [],
'abandon_info': [],
'purchase_info': []
}
is_my_inputs = all([txi.is_my_input for txi in tx.inputs])
if is_my_inputs:
# fees only matter if we are the ones paying them
2020-12-21 01:06:08 +01:00
item['value'] = dewies_to_lbc(tx.net_account_balance + tx.fee)
item['fee'] = dewies_to_lbc(-tx.fee)
else:
# someone else paid the fees
item['value'] = dewies_to_lbc(tx.net_account_balance)
item['fee'] = '0.0'
for txo in tx.my_claim_outputs:
item['claim_info'].append({
'address': txo.get_address(self),
'balance_delta': dewies_to_lbc(-txo.amount),
'amount': dewies_to_lbc(txo.amount),
'claim_id': txo.claim_id,
'claim_name': txo.claim_name,
'nout': txo.position,
'is_spent': txo.is_spent,
})
for txo in tx.my_update_outputs:
if is_my_inputs: # updating my own claim
previous = None
for txi in tx.inputs:
if txi.txo_ref.txo is not None:
other_txo = txi.txo_ref.txo
if (other_txo.is_claim or other_txo.script.is_support_claim) \
2020-12-21 01:06:08 +01:00
and other_txo.claim_id == txo.claim_id:
previous = other_txo
break
if previous is not None:
item['update_info'].append({
'address': txo.get_address(self),
2020-12-21 01:06:08 +01:00
'balance_delta': dewies_to_lbc(previous.amount - txo.amount),
'amount': dewies_to_lbc(txo.amount),
'claim_id': txo.claim_id,
'claim_name': txo.claim_name,
'nout': txo.position,
'is_spent': txo.is_spent,
})
else: # someone sent us their claim
item['update_info'].append({
'address': txo.get_address(self),
'balance_delta': dewies_to_lbc(0),
'amount': dewies_to_lbc(txo.amount),
'claim_id': txo.claim_id,
'claim_name': txo.claim_name,
'nout': txo.position,
'is_spent': txo.is_spent,
})
for txo in tx.my_support_outputs:
item['support_info'].append({
'address': txo.get_address(self),
'balance_delta': dewies_to_lbc(txo.amount if not is_my_inputs else -txo.amount),
'amount': dewies_to_lbc(txo.amount),
'claim_id': txo.claim_id,
'claim_name': txo.claim_name,
'is_tip': not is_my_inputs,
'nout': txo.position,
'is_spent': txo.is_spent,
})
if is_my_inputs:
for txo in tx.other_support_outputs:
item['support_info'].append({
'address': txo.get_address(self),
'balance_delta': dewies_to_lbc(-txo.amount),
'amount': dewies_to_lbc(txo.amount),
'claim_id': txo.claim_id,
'claim_name': txo.claim_name,
'is_tip': is_my_inputs,
'nout': txo.position,
'is_spent': txo.is_spent,
})
for txo in tx.my_abandon_outputs:
item['abandon_info'].append({
'address': txo.get_address(self),
'balance_delta': dewies_to_lbc(txo.amount),
'amount': dewies_to_lbc(txo.amount),
'claim_id': txo.claim_id,
'claim_name': txo.claim_name,
'nout': txo.position
})
for txo in tx.any_purchase_outputs:
item['purchase_info'].append({
'address': txo.get_address(self),
'balance_delta': dewies_to_lbc(txo.amount if not is_my_inputs else -txo.amount),
'amount': dewies_to_lbc(txo.amount),
'claim_id': txo.purchased_claim_id,
'nout': txo.position,
'is_spent': txo.is_spent,
})
history.append(item)
return history
2020-03-17 15:29:26 +01:00
def get_transaction_history_count(self, read_only=False, **constraints):
2020-02-21 04:11:25 +01:00
return self.db.get_transaction_count(read_only=read_only, **constraints)
async def get_detailed_balance(self, accounts, confirmations=0):
result = {
'total': 0,
'available': 0,
'reserved': 0,
'reserved_subtotals': {
'claims': 0,
'supports': 0,
'tips': 0
}
}
2019-10-14 01:32:10 +02:00
for account in accounts:
balance = self._balance_cache.get(account.id)
if not balance:
2020-12-21 01:06:08 +01:00
balance = self._balance_cache[account.id] = \
await account.get_detailed_balance(confirmations, reserved_subtotals=True)
for key, value in balance.items():
if key == 'reserved_subtotals':
for subkey, subvalue in value.items():
result['reserved_subtotals'][subkey] += subvalue
else:
result[key] += value
2019-10-14 01:32:10 +02:00
return result
2020-01-03 04:18:49 +01:00
class TestNetLedger(Ledger):
2018-06-12 17:53:29 +02:00
network_name = 'testnet'
2018-10-15 23:16:43 +02:00
pubkey_address_prefix = bytes((111,))
script_address_prefix = bytes((196,))
2018-06-12 17:53:29 +02:00
extended_public_key_prefix = unhexlify('043587cf')
extended_private_key_prefix = unhexlify('04358394')
2020-04-02 20:31:03 +02:00
checkpoints = {}
2020-01-03 04:18:49 +01:00
class RegTestLedger(Ledger):
2018-06-12 17:53:29 +02:00
network_name = 'regtest'
2018-08-16 07:38:28 +02:00
headers_class = UnvalidatedHeaders
2018-10-15 23:16:43 +02:00
pubkey_address_prefix = bytes((111,))
script_address_prefix = bytes((196,))
2018-06-12 17:53:29 +02:00
extended_public_key_prefix = unhexlify('043587cf')
extended_private_key_prefix = unhexlify('04358394')
max_target = 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
genesis_hash = '6e3fcf1299d4ec5d79c3a4c91d624a4acf9e2e173d95a1a0504f677669687556'
genesis_bits = 0x207fffff
target_timespan = 1
2020-04-02 20:31:03 +02:00
checkpoints = {}