lbry-sdk/lbry/blockchain/lbrycrd.py

281 lines
9.4 KiB
Python
Raw Normal View History

2020-02-14 18:19:55 +01:00
import os
import struct
import shutil
import asyncio
import logging
import zipfile
import tempfile
import urllib.request
from typing import Optional
from binascii import hexlify
import aiohttp
import zmq
import zmq.asyncio
2020-05-01 15:28:51 +02:00
from lbry.conf import Config
from lbry.event import EventController
2020-02-14 18:19:55 +01:00
2020-04-12 02:15:04 +02:00
from .database import BlockchainDB
2020-05-01 15:28:51 +02:00
from .ledger import Ledger, RegTestLedger
2020-04-12 02:15:04 +02:00
2020-02-14 18:19:55 +01:00
log = logging.getLogger(__name__)
2020-04-12 02:15:04 +02:00
DOWNLOAD_URL = (
'https://github.com/lbryio/lbrycrd/releases/download/v0.17.4.4/lbrycrd-linux-1744.zip'
2020-02-14 18:19:55 +01:00
)
class Process(asyncio.SubprocessProtocol):
IGNORE_OUTPUT = [
b'keypool keep',
b'keypool reserve',
b'keypool return',
]
def __init__(self):
self.ready = asyncio.Event()
self.stopped = asyncio.Event()
self.log = log.getChild('blockchain')
def pipe_data_received(self, fd, data):
if self.log and not any(ignore in data for ignore in self.IGNORE_OUTPUT):
if b'Error:' in data:
self.log.error(data.decode())
else:
self.log.info(data.decode())
if b'Error:' in data:
self.ready.set()
raise SystemError(data.decode())
if b'Done loading' in data:
self.ready.set()
def process_exited(self):
self.stopped.set()
self.ready.set()
class Lbrycrd:
2020-05-01 15:28:51 +02:00
def __init__(self, ledger: Ledger):
self.ledger = ledger
self.data_dir = self.actual_data_dir = ledger.conf.lbrycrd_dir
if self.is_regtest:
2020-02-28 05:52:18 +01:00
self.actual_data_dir = os.path.join(self.data_dir, 'regtest')
self.blocks_dir = os.path.join(self.actual_data_dir, 'blocks')
2020-02-14 18:19:55 +01:00
self.bin_dir = os.path.join(os.path.dirname(__file__), 'bin')
self.daemon_bin = os.path.join(self.bin_dir, 'lbrycrdd')
self.cli_bin = os.path.join(self.bin_dir, 'lbrycrd-cli')
self.protocol = None
self.transport = None
self.hostname = 'localhost'
self.peerport = 9246 + 2 # avoid conflict with default peer port
self.rpcport = 9245 + 2 # avoid conflict with default rpc port
self.rpcuser = 'rpcuser'
self.rpcpassword = 'rpcpassword'
self.subscribed = False
self.subscription: Optional[asyncio.Task] = None
2020-02-28 05:52:18 +01:00
self.subscription_url = 'tcp://127.0.0.1:29000'
self.default_generate_address = None
2020-05-01 15:28:51 +02:00
self._on_block_controller = EventController()
2020-02-14 18:19:55 +01:00
self.on_block = self._on_block_controller.stream
self.on_block.listen(lambda e: log.info('%s %s', hexlify(e['hash']), e['msg']))
2020-04-12 02:15:04 +02:00
self.db = BlockchainDB(self.actual_data_dir)
2020-05-01 15:28:51 +02:00
self.session: Optional[aiohttp.ClientSession] = None
@classmethod
def temp_regtest(cls):
return cls(RegTestLedger(Config.with_same_dir(tempfile.mkdtemp())))
2020-04-12 02:15:04 +02:00
def get_block_file_path_from_number(self, block_file_number):
return os.path.join(self.actual_data_dir, 'blocks', f'blk{block_file_number:05}.dat')
2020-05-01 15:28:51 +02:00
@property
def is_regtest(self):
return isinstance(self.ledger, RegTestLedger)
2020-02-14 18:19:55 +01:00
@property
def rpc_url(self):
return f'http://{self.rpcuser}:{self.rpcpassword}@{self.hostname}:{self.rpcport}/'
@property
def exists(self):
return (
os.path.exists(self.cli_bin) and
os.path.exists(self.daemon_bin)
)
async def download(self):
downloaded_file = os.path.join(
2020-04-12 02:15:04 +02:00
self.bin_dir, DOWNLOAD_URL[DOWNLOAD_URL.rfind('/')+1:]
2020-02-14 18:19:55 +01:00
)
if not os.path.exists(self.bin_dir):
os.mkdir(self.bin_dir)
if not os.path.exists(downloaded_file):
2020-04-12 02:15:04 +02:00
log.info('Downloading: %s', DOWNLOAD_URL)
2020-02-14 18:19:55 +01:00
async with aiohttp.ClientSession() as session:
2020-04-12 02:15:04 +02:00
async with session.get(DOWNLOAD_URL) as response:
2020-02-14 18:19:55 +01:00
with open(downloaded_file, 'wb') as out_file:
while True:
chunk = await response.content.read(4096)
if not chunk:
break
out_file.write(chunk)
2020-04-12 02:15:04 +02:00
with urllib.request.urlopen(DOWNLOAD_URL) as response:
2020-02-14 18:19:55 +01:00
with open(downloaded_file, 'wb') as out_file:
shutil.copyfileobj(response, out_file)
log.info('Extracting: %s', downloaded_file)
with zipfile.ZipFile(downloaded_file) as dotzip:
dotzip.extractall(self.bin_dir)
# zipfile bug https://bugs.python.org/issue15795
os.chmod(self.cli_bin, 0o755)
os.chmod(self.daemon_bin, 0o755)
return self.exists
async def ensure(self):
return self.exists or await self.download()
2020-02-28 05:52:18 +01:00
def get_start_command(self, *args):
2020-05-01 15:28:51 +02:00
if self.is_regtest:
2020-04-12 02:15:04 +02:00
args += ('-regtest',)
2020-02-28 05:52:18 +01:00
return (
self.daemon_bin,
f'-datadir={self.data_dir}',
f'-port={self.peerport}',
f'-rpcport={self.rpcport}',
f'-rpcuser={self.rpcuser}',
f'-rpcpassword={self.rpcpassword}',
f'-zmqpubhashblock={self.subscription_url}',
'-server', '-printtoconsole',
*args
)
2020-05-01 15:28:51 +02:00
async def open(self):
self.session = aiohttp.ClientSession()
await self.db.open()
async def close(self):
await self.db.close()
await self.session.close()
2020-02-14 18:19:55 +01:00
async def start(self, *args):
loop = asyncio.get_event_loop()
2020-02-28 05:52:18 +01:00
command = self.get_start_command(*args)
2020-02-14 18:19:55 +01:00
log.info(' '.join(command))
2020-02-28 05:52:18 +01:00
self.transport, self.protocol = await loop.subprocess_exec(Process, *command)
2020-02-14 18:19:55 +01:00
await self.protocol.ready.wait()
assert not self.protocol.stopped.is_set()
2020-05-01 15:28:51 +02:00
await self.open()
2020-02-14 18:19:55 +01:00
async def stop(self, cleanup=True):
try:
2020-05-01 15:28:51 +02:00
await self.close()
2020-02-14 18:19:55 +01:00
self.transport.terminate()
await self.protocol.stopped.wait()
2020-02-28 05:52:18 +01:00
assert self.transport.get_returncode() == 0, "lbrycrd daemon exit with error"
2020-02-14 18:19:55 +01:00
finally:
if cleanup:
await self.cleanup()
async def cleanup(self):
await asyncio.get_running_loop().run_in_executor(
2020-02-28 05:52:18 +01:00
None, shutil.rmtree, self.data_dir, True
2020-02-14 18:19:55 +01:00
)
def subscribe(self):
if not self.subscribed:
self.subscribed = True
ctx = zmq.asyncio.Context.instance()
2020-04-12 02:15:04 +02:00
sock = ctx.socket(zmq.SUB) # pylint: disable=no-member
2020-02-28 05:52:18 +01:00
sock.connect(self.subscription_url)
2020-02-14 18:19:55 +01:00
sock.subscribe("hashblock")
self.subscription = asyncio.create_task(self.subscription_handler(sock))
async def subscription_handler(self, sock):
try:
while self.subscribed:
msg = await sock.recv_multipart()
2020-05-01 15:28:51 +02:00
await self._on_block_controller.add({
2020-02-14 18:19:55 +01:00
'hash': msg[1],
'msg': struct.unpack('<I', msg[2])[0]
})
except asyncio.CancelledError:
sock.close()
raise
def unsubscribe(self):
if self.subscribed:
self.subscribed = False
self.subscription.cancel()
self.subscription = None
async def rpc(self, method, params=None):
message = {
"jsonrpc": "1.0",
"id": "1",
"method": method,
"params": params or []
}
async with self.session.post(self.rpc_url, json=message) as resp:
try:
result = await resp.json()
except aiohttp.ContentTypeError as e:
raise Exception(await resp.text()) from e
if not result['error']:
return result['result']
else:
result['error'].update(method=method, params=params)
raise Exception(result['error'])
async def generate(self, blocks):
2020-02-28 05:52:18 +01:00
if self.default_generate_address is None:
self.default_generate_address = await self.get_new_address()
return await self.generate_to_address(blocks, self.default_generate_address)
async def get_new_address(self):
return await self.rpc("getnewaddress")
async def generate_to_address(self, blocks, address):
return await self.rpc("generatetoaddress", [blocks, address])
2020-05-01 15:28:51 +02:00
async def send_to_address(self, address, amount):
return await self.rpc("sendtoaddress", [address, amount])
async def get_block(self, block_hash):
return await self.rpc("getblock", [block_hash])
async def get_raw_transaction(self, txid):
return await self.rpc("getrawtransaction", [txid])
2020-02-28 05:52:18 +01:00
async def fund_raw_transaction(self, tx):
return await self.rpc("fundrawtransaction", [tx])
async def sign_raw_transaction_with_wallet(self, tx):
return await self.rpc("signrawtransactionwithwallet", [tx])
async def send_raw_transaction(self, tx):
return await self.rpc("sendrawtransaction", [tx])
2020-02-14 18:19:55 +01:00
async def claim_name(self, name, data, amount):
return await self.rpc("claimname", [name, data, amount])
2020-05-01 15:28:51 +02:00
async def update_claim(self, txid, data, amount):
return await self.rpc("updateclaim", [txid, data, amount])
async def abandon_claim(self, txid, address):
return await self.rpc("abandonclaim", [txid, address])
async def support_claim(self, name, claim_id, amount, value="", istip=False):
return await self.rpc("supportclaim", [name, claim_id, amount, value, istip])
async def abandon_support(self, txid, address):
return await self.rpc("abandonsupport", [txid, address])