lbry-sdk/tests/integration/blockchain/test_blockchain.py

512 lines
18 KiB
Python
Raw Normal View History

2020-04-11 23:27:41 +02:00
import os
import time
import asyncio
2020-05-01 15:28:51 +02:00
import shutil
import tempfile
from binascii import hexlify, unhexlify
2020-04-11 23:27:41 +02:00
from random import choice
2020-05-01 15:28:51 +02:00
from lbry.conf import Config
2020-04-11 23:27:41 +02:00
from lbry.db import Database
2020-05-01 15:28:51 +02:00
from lbry.crypto.base58 import Base58
2020-04-11 23:27:41 +02:00
from lbry.schema.claim import Stream
2020-05-01 15:28:51 +02:00
from lbry.blockchain.lbrycrd import Lbrycrd
from lbry.blockchain.dewies import dewies_to_lbc, lbc_to_dewies
from lbry.blockchain.transaction import Transaction, Output
from lbry.constants import CENT
from lbry.blockchain.ledger import RegTestLedger
from lbry.testcase import AsyncioTestCase
2020-04-11 23:27:41 +02:00
2020-05-01 15:28:51 +02:00
from lbry.service.full_node import FullNode
from lbry.service.light_client import LightClient
from lbry.service.daemon import Daemon
from lbry.service.api import Client
2020-04-11 23:27:41 +02:00
2020-05-01 15:28:51 +02:00
class BlockchainTestCase(AsyncioTestCase):
2020-04-11 23:27:41 +02:00
async def asyncSetUp(self):
await super().asyncSetUp()
2020-05-01 15:28:51 +02:00
self.chain = Lbrycrd.temp_regtest()
self.ledger = self.chain.ledger
2020-04-11 23:27:41 +02:00
await self.chain.ensure()
await self.chain.start('-maxblockfilesize=8', '-rpcworkqueue=128')
2020-05-01 15:28:51 +02:00
self.addCleanup(self.chain.stop)
class TestEvents(BlockchainTestCase):
2020-04-11 23:27:41 +02:00
async def test_block_event(self):
msgs = []
self.chain.subscribe()
self.chain.on_block.listen(lambda e: msgs.append(e['msg']))
res = await self.chain.generate(5)
await self.chain.on_block.where(lambda e: e['msg'] == 4)
self.assertEqual([0, 1, 2, 3, 4], msgs)
self.assertEqual(5, len(res))
self.chain.unsubscribe()
res = await self.chain.generate(2)
self.assertEqual(2, len(res))
await asyncio.sleep(0.1) # give some time to "miss" the new block events
self.chain.subscribe()
res = await self.chain.generate(3)
await self.chain.on_block.where(lambda e: e['msg'] == 9)
self.assertEqual(3, len(res))
2020-05-01 15:28:51 +02:00
self.assertEqual([0, 1, 2, 3, 4, 7, 8, 9], msgs) # 5, 6 "missed"
class TestBlockchainSync(BlockchainTestCase):
async def asyncSetUp(self):
await super().asyncSetUp()
self.service = FullNode(
self.chain.ledger, f'sqlite:///{self.chain.data_dir}/lbry.db', self.chain
)
self.service.conf.spv_address_filters = False
self.sync = self.service.sync
self.db = self.service.db
await self.db.open()
self.addCleanup(self.db.close)
async def test_multi_block_file_sync(self):
names = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten']
await self.chain.generate(101)
address = Base58.decode(await self.chain.get_new_address())
start = time.perf_counter()
for _ in range(190):
tx = Transaction().add_outputs([
Output.pay_claim_name_pubkey_hash(
CENT, f'{choice(names)}{i}',
Stream().update(
title='a claim title',
description='Lorem ipsum '*400,
tags=['crypto', 'health', 'space'],
).claim,
address)
for i in range(1, 20)
])
funded = await self.chain.fund_raw_transaction(hexlify(tx.raw).decode())
signed = await self.chain.sign_raw_transaction_with_wallet(funded['hex'])
await self.chain.send_raw_transaction(signed['hex'])
await self.chain.generate(1)
print(f'generating {190*20} transactions took {time.perf_counter()-start}s')
2020-04-11 23:27:41 +02:00
self.assertEqual(
[(0, 191, 280), (1, 89, 178), (2, 12, 24)],
[(file['file_number'], file['blocks'], file['txs'])
for file in await self.chain.get_block_files()]
)
self.assertEqual(191, len(await self.chain.get_file_details(0)))
2020-05-01 15:28:51 +02:00
await self.sync.advance()
class FullNodeTestCase(BlockchainTestCase):
async def asyncSetUp(self):
await super().asyncSetUp()
self.current_height = 0
await self.generate(101, wait=False)
self.service = FullNode(self.ledger, f'sqlite:///{self.chain.data_dir}/lbry.db')
self.service.conf.spv_address_filters = False
self.sync = self.service.sync
self.db = self.service.db
2020-04-11 23:27:41 +02:00
2020-05-01 15:28:51 +02:00
self.daemon = Daemon(self.service)
self.api = self.daemon.api
self.addCleanup(self.daemon.stop)
await self.daemon.start()
if False: #os.environ.get('TEST_LBRY_API', 'light_client') == 'light_client':
light_dir = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, light_dir, True)
ledger = RegTestLedger(Config(
data_dir=light_dir,
wallet_dir=light_dir,
api='localhost:5389',
))
self.light_client = self.service = LightClient(
ledger, f'sqlite:///{light_dir}/light_client.db'
)
self.light_api = Daemon(self.service)
await self.light_api.start()
self.addCleanup(self.light_api.stop)
#else:
# self.service = self.full_node
#self.client = Client(self.service, self.ledger.conf.api_connection_url)
async def generate(self, blocks, wait=True):
block_hashes = await self.chain.generate(blocks)
self.current_height += blocks
if wait:
await self.service.sync.on_block.where(
lambda b: self.current_height == b.height
)
return block_hashes
class TestFullNode(FullNodeTestCase):
async def test_foo(self):
await self.generate(10)
wallet = self.service.wallet_manager.default_wallet #create_wallet('test_wallet')
account = wallet.accounts[0]
addresses = await account.ensure_address_gap()
await self.chain.send_to_address(addresses[0], '5.0')
await self.generate(1)
self.assertEqual(await account.get_balance(), lbc_to_dewies('5.0'))
#self.assertEqual((await self.client.account_balance())['total'], '5.0')
tx = await wallet.create_channel('@foo', lbc_to_dewies('1.0'), account, [account], addresses[0])
await self.service.broadcast(tx)
await self.generate(1)
channels = await wallet.get_channels()
print(channels)
class TestClaimtrieSync(FullNodeTestCase):
async def asyncSetUp(self):
await super().asyncSetUp()
self.last_block_hash = None
self.address = await self.chain.get_new_address()
def find_claim_txo(self, tx):
for txo in tx.outputs:
if txo.is_claim:
return txo
async def get_transaction(self, txid):
raw = await self.chain.get_raw_transaction(txid)
return Transaction(unhexlify(raw))
async def claim_name(self, title, amount):
claim = Stream().update(title=title).claim
return await self.chain.claim_name(
'foo', hexlify(claim.to_bytes()).decode(), amount
)
async def claim_update(self, tx, amount):
claim = self.find_claim_txo(tx).claim
return await self.chain.update_claim(
tx.outputs[0].tx_ref.id, hexlify(claim.to_bytes()).decode(), amount
)
async def claim_abandon(self, tx):
return await self.chain.abandon_claim(tx.id, self.address)
async def support_claim(self, tx, amount):
txo = self.find_claim_txo(tx)
response = await self.chain.support_claim(
txo.claim_name, txo.claim_id, amount
)
return response['txId']
async def advance(self, new_height, ops):
blocks = (new_height-self.current_height)-1
if blocks > 0:
await self.generate(blocks)
txs = []
for op in ops:
if len(op) == 3:
op_type, value, amount = op
else:
(op_type, value), amount = op, None
if op_type == 'claim':
txid = await self.claim_name(value, amount)
elif op_type == 'update':
txid = await self.claim_update(value, amount)
elif op_type == 'abandon':
txid = await self.claim_abandon(value)
elif op_type == 'support':
txid = await self.support_claim(value, amount)
else:
raise ValueError(f'"{op_type}" is unknown operation')
txs.append(await self.get_transaction(txid))
self.last_block_hash, = await self.generate(1)
self.current_height = new_height
return txs
async def get_last_block(self):
return await self.chain.get_block(self.last_block_hash)
async def get_controlling(self):
sql = f"""
select
tx.height, tx.raw, txo.position, effective_amount, activation_height
from claimtrie
join claim using (claim_hash)
join txo using (txo_hash)
join tx using (tx_hash)
where
txo.txo_type in (1, 2) and
expiration_height > {self.current_height}
"""
for claim in await self.db.execute_fetchall(sql):
tx = Transaction(claim['raw'], height=claim['height'])
txo = tx.outputs[claim['position']]
return (
txo.claim.stream.title, dewies_to_lbc(txo.amount),
dewies_to_lbc(claim['effective_amount']), claim['activation_height']
)
async def get_active(self):
controlling = await self.get_controlling()
active = []
sql = f"""
select tx.height, tx.raw, txo.position, effective_amount, activation_height
from txo
join tx using (tx_hash)
join claim using (claim_hash)
where
txo.txo_type in (1, 2) and
activation_height <= {self.current_height} and
expiration_height > {self.current_height}
"""
for claim in await self.db.execute_fetchall(sql):
tx = Transaction(claim['raw'], height=claim['height'])
txo = tx.outputs[claim['position']]
if controlling and controlling[0] == txo.claim.stream.title:
continue
active.append((
txo.claim.stream.title, dewies_to_lbc(txo.amount),
dewies_to_lbc(claim['effective_amount']), claim['activation_height']
))
return active
async def get_accepted(self):
accepted = []
sql = f"""
select tx.height, tx.raw, txo.position, effective_amount, activation_height
from txo
join tx using (tx_hash)
join claim using (claim_hash)
where
txo.txo_type in (1, 2) and
activation_height > {self.current_height} and
expiration_height > {self.current_height}
"""
for claim in await self.db.execute_fetchall(sql):
tx = Transaction(claim['raw'], height=claim['height'])
txo = tx.outputs[claim['position']]
accepted.append((
txo.claim.stream.title, dewies_to_lbc(txo.amount),
dewies_to_lbc(claim['effective_amount']), claim['activation_height']
))
return accepted
async def state(self, controlling=None, active=None, accepted=None):
self.assertEqual(controlling, await self.get_controlling())
self.assertEqual(active or [], await self.get_active())
self.assertEqual(accepted or [], await self.get_accepted())
async def test_example_from_spec(self):
# https://spec.lbry.com/#claim-activation-example
advance, state = self.advance, self.state
stream, = await advance(113, [('claim', 'Claim A', '10.0')])
await state(
controlling=('Claim A', '10.0', '10.0', 113),
active=[],
accepted=[]
)
await advance(501, [('claim', 'Claim B', '20.0')])
await state(
controlling=('Claim A', '10.0', '10.0', 113),
active=[],
accepted=[('Claim B', '20.0', '0.0', 513)]
)
await advance(510, [('support', stream, '14')])
await state(
controlling=('Claim A', '10.0', '24.0', 113),
active=[],
accepted=[('Claim B', '20.0', '0.0', 513)]
)
await advance(512, [('claim', 'Claim C', '50.0')])
await state(
controlling=('Claim A', '10.0', '24.0', 113),
active=[],
accepted=[
('Claim B', '20.0', '0.0', 513),
('Claim C', '50.0', '0.0', 524)]
)
await advance(513, [])
await state(
controlling=('Claim A', '10.0', '24.0', 113),
active=[('Claim B', '20.0', '20.0', 513)],
accepted=[('Claim C', '50.0', '0.0', 524)]
)
await advance(520, [('claim', 'Claim D', '60.0')])
await state(
controlling=('Claim A', '10.0', '24.0', 113),
active=[('Claim B', '20.0', '20.0', 513)],
accepted=[
('Claim C', '50.0', '0.0', 524),
('Claim D', '60.0', '0.0', 532)]
)
await advance(524, [])
await state(
controlling=('Claim D', '60.0', '60.0', 524),
active=[
('Claim A', '10.0', '24.0', 113),
('Claim B', '20.0', '20.0', 513),
('Claim C', '50.0', '50.0', 524)],
accepted=[]
)
# beyond example
await advance(525, [('update', stream, '70.0')])
await state(
controlling=('Claim A', '70.0', '84.0', 525),
active=[
('Claim B', '20.0', '20.0', 513),
('Claim C', '50.0', '50.0', 524),
('Claim D', '60.0', '60.0', 524),
],
accepted=[]
)
async def test_competing_claims_subsequent_blocks_height_wins(self):
advance, state = self.advance, self.state
await advance(113, [('claim', 'Claim A', '1.0')])
await state(
controlling=('Claim A', '1.0', '1.0', 113),
active=[],
accepted=[]
)
await advance(114, [('claim', 'Claim B', '1.0')])
await state(
controlling=('Claim A', '1.0', '1.0', 113),
active=[('Claim B', '1.0', '1.0', 114)],
accepted=[]
)
await advance(115, [('claim', 'Claim C', '1.0')])
await state(
controlling=('Claim A', '1.0', '1.0', 113),
active=[
('Claim B', '1.0', '1.0', 114),
('Claim C', '1.0', '1.0', 115)],
accepted=[]
)
async def test_competing_claims_in_single_block_position_wins(self):
claim_a, claim_b = await self.advance(113, [
('claim', 'Claim A', '1.0'),
('claim', 'Claim B', '1.0')
])
block = await self.get_last_block()
# order of tx in block is non-deterministic,
# figure out what ordered we ended up with
if block['tx'][1] == claim_a.id:
winner, other = 'Claim A', 'Claim B'
else:
winner, other = 'Claim B', 'Claim A'
await self.state(
controlling=(winner, '1.0', '1.0', 113),
active=[(other, '1.0', '1.0', 113)],
accepted=[]
)
async def test_competing_claims_in_single_block_effective_amount_wins(self):
await self.advance(113, [
('claim', 'Claim A', '1.0'),
('claim', 'Claim B', '2.0')
])
await self.state(
controlling=('Claim B', '2.0', '2.0', 113),
active=[('Claim A', '1.0', '1.0', 113)],
accepted=[]
)
async def test_winning_claim_deleted(self):
claim1, claim2 = await self.advance(113, [
('claim', 'Claim A', '1.0'),
('claim', 'Claim B', '2.0')
])
await self.state(
controlling=('Claim B', '2.0', '2.0', 113),
active=[('Claim A', '1.0', '1.0', 113)],
accepted=[]
)
await self.advance(114, [('abandon', claim2)])
await self.state(
controlling=('Claim A', '1.0', '1.0', 113),
active=[],
accepted=[]
)
async def test_winning_claim_deleted_and_new_claim_becomes_winner(self):
claim1, claim2 = await self.advance(113, [
('claim', 'Claim A', '1.0'),
('claim', 'Claim B', '2.0')
])
await self.state(
controlling=('Claim B', '2.0', '2.0', 113),
active=[('Claim A', '1.0', '1.0', 113)],
accepted=[]
)
await self.advance(115, [
('abandon', claim2),
('claim', 'Claim C', '3.0')
])
await self.state(
controlling=('Claim C', '3.0', '3.0', 115),
active=[('Claim A', '1.0', '1.0', 113)],
accepted=[]
)
async def test_winning_claim_expires_and_another_takes_over(self):
await self.advance(110, [('claim', 'Claim A', '2.0')])
await self.advance(120, [('claim', 'Claim B', '1.0')])
await self.state(
controlling=('Claim A', '2.0', '2.0', 110),
active=[('Claim B', '1.0', '1.0', 120)],
accepted=[]
)
await self.advance(610, [])
await self.state(
controlling=('Claim B', '1.0', '1.0', 120),
active=[],
accepted=[]
)
await self.advance(620, [])
await self.state(
controlling=None,
active=[],
accepted=[]
)
async def test_create_and_multiple_updates_in_same_block(self):
await self.chain.generate(10)
txid = await self.claim_name('Claim A', '1.0')
txid = await self.claim_update(await self.get_transaction(txid), '2.0')
await self.claim_update(await self.get_transaction(txid), '3.0')
await self.chain.generate(1)
await self.sync.advance()
self.current_height += 11
await self.state(
controlling=('Claim A', '3.0', '3.0', 112),
active=[],
accepted=[]
)
async def test_create_and_abandon_in_same_block(self):
await self.chain.generate(10)
txid = await self.claim_name('Claim A', '1.0')
await self.claim_abandon(await self.get_transaction(txid))
await self.chain.generate(1)
await self.sync.advance()
self.current_height += 11
await self.state(
controlling=None,
active=[],
accepted=[]
)