tests/integration/blockchain/test_blockchain.py

This commit is contained in:
Lex Berezhny 2020-06-21 19:54:59 -04:00
parent db5a33dc3f
commit 2306edebf7

View file

@ -2,17 +2,19 @@ import os
import time import time
import asyncio import asyncio
import tempfile import tempfile
from unittest import skip
from binascii import hexlify, unhexlify from binascii import hexlify, unhexlify
from typing import List, Optional
from distutils.dir_util import copy_tree, remove_tree from distutils.dir_util import copy_tree, remove_tree
from lbry import Config, Database, RegTestLedger, Transaction, Output from lbry import Config, Database, RegTestLedger, Transaction, Output, Input
from lbry.crypto.base58 import Base58 from lbry.crypto.base58 import Base58
from lbry.schema.claim import Stream, Channel from lbry.schema.claim import Stream, Channel
from lbry.schema.support import Support from lbry.schema.support import Support
from lbry.blockchain.lbrycrd import Lbrycrd from lbry.blockchain.lbrycrd import Lbrycrd
from lbry.blockchain.sync import BlockchainSync from lbry.blockchain.sync import BlockchainSync
from lbry.blockchain.dewies import dewies_to_lbc from lbry.blockchain.dewies import dewies_to_lbc, lbc_to_dewies
from lbry.constants import CENT from lbry.constants import CENT, COIN
from lbry.testcase import AsyncioTestCase from lbry.testcase import AsyncioTestCase
import logging import logging
@ -61,7 +63,7 @@ class SyncingBlockchainTestCase(BasicBlockchainTestCase):
self.channel_keys = {} self.channel_keys = {}
async def generate(self, blocks, wait=True): async def generate(self, blocks, wait=True) -> List[str]:
block_hashes = await self.chain.generate(blocks) block_hashes = await self.chain.generate(blocks)
self.current_height += blocks self.current_height += blocks
self.last_block_hash = block_hashes[-1] self.last_block_hash = block_hashes[-1]
@ -69,23 +71,37 @@ class SyncingBlockchainTestCase(BasicBlockchainTestCase):
await self.sync.on_block.where(lambda b: self.current_height == b.height) await self.sync.on_block.where(lambda b: self.current_height == b.height)
return block_hashes return block_hashes
async def get_transaction(self, txid: str) -> Transaction: async def get_last_block(self):
return await self.chain.get_block(self.last_block_hash)
@staticmethod
def find_claim_txo(tx) -> Optional[Output]:
for txo in tx.outputs:
if txo.is_claim:
return txo
async def get_claim(self, txid: str) -> Output:
raw = await self.chain.get_raw_transaction(txid) raw = await self.chain.get_raw_transaction(txid)
tx = Transaction(unhexlify(raw)) tx = Transaction(unhexlify(raw))
txo = self.find_claim_txo(tx) txo = self.find_claim_txo(tx)
if txo and txo.is_claim and txo.claim.is_channel: if txo and txo.is_claim and txo.claim.is_channel:
txo.private_key = self.channel_keys.get(txo.claim_hash) txo.private_key = self.channel_keys.get(txo.claim_hash)
return tx
async def get_last_block(self):
return await self.chain.get_block(self.last_block_hash)
def find_claim_txo(self, tx):
for txo in tx.outputs:
if txo.is_claim or txo.is_support:
return txo return txo
async def create_claim(self, title='', amount=CENT, name='foo', claim_id_startswith='', sign=None, is_channel=False): @staticmethod
def find_support_txo(tx) -> Optional[Output]:
for txo in tx.outputs:
if txo.is_support:
return txo
async def get_support(self, txid: str) -> Output:
raw = await self.chain.get_raw_transaction(txid)
return self.find_support_txo(Transaction(unhexlify(raw)))
async def create_claim(
self, title='', amount='0.01', name=None,
claim_id_startswith='', sign=None, is_channel=False) -> str:
name = name or ('@foo' if is_channel else 'foo')
if not claim_id_startswith and sign is None and not is_channel: if not claim_id_startswith and sign is None and not is_channel:
claim = Stream().update(title=title).claim claim = Stream().update(title=title).claim
return await self.chain.claim_name( return await self.chain.claim_name(
@ -94,8 +110,9 @@ class SyncingBlockchainTestCase(BasicBlockchainTestCase):
meta_class = Channel if is_channel else Stream meta_class = Channel if is_channel else Stream
tx = Transaction().add_outputs([ tx = Transaction().add_outputs([
Output.pay_claim_name_pubkey_hash( Output.pay_claim_name_pubkey_hash(
CENT, name, meta_class().update(title='claim #001').claim, lbc_to_dewies(amount), name,
Base58.decode(self.address) meta_class().update(title='claim #001').claim,
self.chain.ledger.address_to_hash160(self.address)
) )
]) ])
private_key = None private_key = None
@ -104,11 +121,6 @@ class SyncingBlockchainTestCase(BasicBlockchainTestCase):
funded = await self.chain.fund_raw_transaction(hexlify(tx.raw).decode()) funded = await self.chain.fund_raw_transaction(hexlify(tx.raw).decode())
tx = Transaction(unhexlify(funded['hex'])) tx = Transaction(unhexlify(funded['hex']))
i = 1 i = 1
if '!' in claim_id_startswith:
claim_id_startswith, not_after_startswith = claim_id_startswith.split('!')
not_after_startswith = tuple(not_after_startswith)
else:
claim_id_startswith, not_after_startswith = claim_id_startswith, ()
while True: while True:
if sign: if sign:
self.find_claim_txo(tx).sign(sign) self.find_claim_txo(tx).sign(sign)
@ -118,7 +130,6 @@ class SyncingBlockchainTestCase(BasicBlockchainTestCase):
txo = self.find_claim_txo(tx) txo = self.find_claim_txo(tx)
claim = txo.claim.channel if is_channel else txo.claim.stream claim = txo.claim.channel if is_channel else txo.claim.stream
if txo.claim_id.startswith(claim_id_startswith): if txo.claim_id.startswith(claim_id_startswith):
if txo.claim_id[len(claim_id_startswith)] not in not_after_startswith:
break break
i += 1 i += 1
claim.update(title=f'claim #{i:03}') claim.update(title=f'claim #{i:03}')
@ -127,19 +138,58 @@ class SyncingBlockchainTestCase(BasicBlockchainTestCase):
self.channel_keys[self.find_claim_txo(tx).claim_hash] = private_key self.channel_keys[self.find_claim_txo(tx).claim_hash] = private_key
return await self.chain.send_raw_transaction(hexlify(tx.raw).decode()) return await self.chain.send_raw_transaction(hexlify(tx.raw).decode())
async def update_claim(self, txo, amount): async def update_claim(self, txo: Output, amount='0.01', reset_channel_key=False, sign=None) -> str:
if reset_channel_key:
self.channel_keys[txo.claim_hash] = await txo.generate_channel_private_key()
if sign is None:
return await self.chain.update_claim( return await self.chain.update_claim(
txo.tx_ref.id, hexlify(txo.claim.to_bytes()).decode(), amount txo.tx_ref.id, hexlify(txo.claim.to_bytes()).decode(), amount
) )
tx = (
Transaction()
.add_inputs([Input.spend(txo)])
.add_outputs([
Output.pay_update_claim_pubkey_hash(
lbc_to_dewies(amount), txo.claim_name, txo.claim_id, txo.claim,
self.chain.ledger.address_to_hash160(self.address)
)
])
)
funded = await self.chain.fund_raw_transaction(hexlify(tx.raw).decode())
tx = Transaction(unhexlify(funded['hex']))
self.find_claim_txo(tx).sign(sign)
tx._reset()
signed = await self.chain.sign_raw_transaction_with_wallet(hexlify(tx.raw).decode())
tx = Transaction(unhexlify(signed['hex']))
return await self.chain.send_raw_transaction(signed['hex'])
async def abandon_claim(self, txid): async def abandon_claim(self, txid: str) -> str:
return await self.chain.abandon_claim(txid, self.address) return await self.chain.abandon_claim(txid, self.address)
async def support_claim(self, txo, amount): async def support_claim(self, txo: Output, amount='0.01', sign=None) -> str:
if not sign:
response = await self.chain.support_claim( response = await self.chain.support_claim(
txo.claim_name, txo.claim_id, amount txo.claim_name, txo.claim_id, amount
) )
return response['txId'] return response['txId']
tx = (
Transaction()
.add_outputs([
Output.pay_support_data_pubkey_hash(
lbc_to_dewies(amount), txo.claim_name, txo.claim_id, Support(),
self.chain.ledger.address_to_hash160(self.address)
)
])
)
funded = await self.chain.fund_raw_transaction(hexlify(tx.raw).decode())
tx = Transaction(unhexlify(funded['hex']))
self.find_support_txo(tx).sign(sign)
tx._reset()
signed = await self.chain.sign_raw_transaction_with_wallet(hexlify(tx.raw).decode())
return await self.chain.send_raw_transaction(signed['hex'])
async def abandon_support(self, txid: str) -> str:
return await self.chain.abandon_support(txid, self.address)
async def get_takeovers(self): async def get_takeovers(self):
takeovers = [] takeovers = []
@ -155,7 +205,7 @@ class SyncingBlockchainTestCase(BasicBlockchainTestCase):
blocks = (new_height-self.current_height)-1 blocks = (new_height-self.current_height)-1
if blocks > 0: if blocks > 0:
await self.generate(blocks) await self.generate(blocks)
txs = [] claims = []
for op in ops: for op in ops:
if len(op) == 3: if len(op) == 3:
op_type, value, amount = op op_type, value, amount = op
@ -171,15 +221,15 @@ class SyncingBlockchainTestCase(BasicBlockchainTestCase):
txid = await self.support_claim(value, amount) txid = await self.support_claim(value, amount)
else: else:
raise ValueError(f'"{op_type}" is unknown operation') raise ValueError(f'"{op_type}" is unknown operation')
txs.append(await self.get_transaction(txid)) claims.append(await self.get_claim(txid))
await self.generate(1) await self.generate(1)
return [self.find_claim_txo(tx) for tx in txs] return claims
async def get_controlling(self): async def get_controlling(self):
for txo in await self.db.search_claims(is_controlling=True): for txo in await self.db.search_claims(is_controlling=True):
return ( return (
txo.claim.stream.title, dewies_to_lbc(txo.amount), txo.claim.stream.title, dewies_to_lbc(txo.amount),
dewies_to_lbc(txo.meta['effective_amount']), txo.meta['takeover_height'] dewies_to_lbc(txo.meta['staked_amount']), txo.meta['takeover_height']
) )
async def get_active(self): async def get_active(self):
@ -192,7 +242,7 @@ class SyncingBlockchainTestCase(BasicBlockchainTestCase):
continue continue
active.append(( active.append((
txo.claim.stream.title, dewies_to_lbc(txo.amount), txo.claim.stream.title, dewies_to_lbc(txo.amount),
dewies_to_lbc(txo.meta['effective_amount']), txo.meta['activation_height'] dewies_to_lbc(txo.meta['staked_amount']), txo.meta['activation_height']
)) ))
return active return active
@ -203,7 +253,7 @@ class SyncingBlockchainTestCase(BasicBlockchainTestCase):
expiration_height__gt=self.current_height): expiration_height__gt=self.current_height):
accepted.append(( accepted.append((
txo.claim.stream.title, dewies_to_lbc(txo.amount), txo.claim.stream.title, dewies_to_lbc(txo.amount),
dewies_to_lbc(txo.meta['effective_amount']), txo.meta['activation_height'] dewies_to_lbc(txo.meta['staked_amount']), txo.meta['activation_height']
)) ))
return accepted return accepted
@ -213,7 +263,7 @@ class SyncingBlockchainTestCase(BasicBlockchainTestCase):
self.assertEqual(accepted or [], await self.get_accepted()) self.assertEqual(accepted or [], await self.get_accepted())
class TestBlockchainEvents(BasicBlockchainTestCase): class TestLbrycrdEvents(BasicBlockchainTestCase):
async def test_block_event(self): async def test_block_event(self):
msgs = [] msgs = []
@ -269,7 +319,7 @@ class TestMultiBlockFileSyncing(BasicBlockchainTestCase):
await self.chain.generate(101) await self.chain.generate(101)
address = Base58.decode(self.chain.get_new_address()) address = Base58.decode(await self.chain.get_new_address())
start = time.perf_counter() start = time.perf_counter()
for _ in range(190): for _ in range(190):
@ -339,18 +389,18 @@ class TestMultiBlockFileSyncing(BasicBlockchainTestCase):
self.assertEqual( self.assertEqual(
[(1, 29, 58)], [(1, 29, 58)],
[(file['file_number'], file['blocks'], file['txs']) [(file['file_number'], file['blocks'], file['txs'])
for file in await db.get_block_files(1, 250)] for file in await db.get_block_files(1, 251)]
) )
# get_blocks_in_file # get_blocks_in_file
self.assertEqual(279, (await db.get_blocks_in_file(1))[88]['height']) self.assertEqual(279, (await db.get_blocks_in_file(1))[88]['height'])
self.assertEqual(279, (await db.get_blocks_in_file(1, 250))[28]['height']) self.assertEqual(279, (await db.get_blocks_in_file(1, 251))[28]['height'])
# get_takeover_count # get_takeover_count
self.assertEqual(0, await db.get_takeover_count(0, 101)) self.assertEqual(0, await db.get_takeover_count(0, 101))
self.assertEqual(2, await db.get_takeover_count(101, 102)) self.assertEqual(2, await db.get_takeover_count(101, 102))
self.assertEqual(1, await db.get_takeover_count(102, 250)) self.assertEqual(1, await db.get_takeover_count(103, 251))
self.assertEqual(0, await db.get_takeover_count(250, 291)) self.assertEqual(0, await db.get_takeover_count(252, 291))
# get_takeovers # get_takeovers
self.assertEqual( self.assertEqual(
@ -359,7 +409,7 @@ class TestMultiBlockFileSyncing(BasicBlockchainTestCase):
{'height': 102, 'name': b'two'}, {'height': 102, 'name': b'two'},
{'height': 250, 'name': b''} # normalization on regtest kicks-in {'height': 250, 'name': b''} # normalization on regtest kicks-in
], ],
[{'name': takeover['normalized'], 'height': takeover['height']} [{'name': takeover['name'], 'height': takeover['height']}
for takeover in await db.get_takeovers(0, 291)] for takeover in await db.get_takeovers(0, 291)]
) )
@ -370,15 +420,13 @@ class TestMultiBlockFileSyncing(BasicBlockchainTestCase):
# get_claim_metadata # get_claim_metadata
self.assertEqual( self.assertEqual(
[ [
{'name': b'two', 'activation_height': 102, 'takeover_height': 102, 'is_controlling': True},
{'name': b'one', 'activation_height': 102, 'takeover_height': 102, 'is_controlling': True}, {'name': b'one', 'activation_height': 102, 'takeover_height': 102, 'is_controlling': True},
{'name': b'two', 'activation_height': 102, 'takeover_height': None, 'is_controlling': False}, {'name': b'two', 'activation_height': 102, 'takeover_height': 102, 'is_controlling': True},
{'name': b'one', 'activation_height': 102, 'takeover_height': None, 'is_controlling': False},
], ],
[{ [{
'name': c['name'], 'is_controlling': bool(c['is_controlling']), 'name': r['name'], 'is_controlling': bool(r['is_controlling']),
'activation_height': c['activation_height'], 'takeover_height': c['takeover_height'], 'activation_height': r['activation_height'], 'takeover_height': r['takeover_height'],
} for c in (await db.get_claim_metadata(0, 103))[:4]] } for r in sorted(await db.get_claim_metadata(102, 102), key=lambda r: r['name']) if r['is_controlling']]
) )
# get_support_metadata_count # get_support_metadata_count
@ -405,7 +453,7 @@ class TestMultiBlockFileSyncing(BasicBlockchainTestCase):
events[0], { events[0], {
"event": "blockchain.sync.start", "event": "blockchain.sync.start",
"data": { "data": {
"starting_height": -1, "starting_height": 0,
"ending_height": 292, "ending_height": 292,
"files": 3, "files": 3,
"blocks": 293, "blocks": 293,
@ -465,7 +513,7 @@ class TestMultiBlockFileSyncing(BasicBlockchainTestCase):
events[0], { events[0], {
"event": "blockchain.sync.start", "event": "blockchain.sync.start",
"data": { "data": {
"starting_height": 292, "starting_height": 293,
"ending_height": 293, "ending_height": 293,
"files": 1, "files": 1,
"blocks": 1, "blocks": 1,
@ -487,7 +535,7 @@ class TestMultiBlockFileSyncing(BasicBlockchainTestCase):
) )
class TestBasicBlockchainSync(SyncingBlockchainTestCase): class TestGeneralBlockchainSync(SyncingBlockchainTestCase):
async def test_sync_advances(self): async def test_sync_advances(self):
blocks = [] blocks = []
@ -503,25 +551,236 @@ class TestBasicBlockchainSync(SyncingBlockchainTestCase):
self.assertEqual(110, self.current_height) self.assertEqual(110, self.current_height)
async def test_claim_create_update_and_delete(self): async def test_claim_create_update_and_delete(self):
search = self.db.search_claims
await self.create_claim('foo', '0.01') await self.create_claim('foo', '0.01')
await self.generate(1) await self.generate(1)
claims = await self.db.search_claims() claims = await search()
self.assertEqual(1, len(claims)) self.assertEqual(1, len(claims))
self.assertEqual(claims[0].claim_name, 'foo') self.assertEqual(claims[0].claim_name, 'foo')
self.assertEqual(dewies_to_lbc(claims[0].amount), '0.01') self.assertEqual(dewies_to_lbc(claims[0].amount), '0.01')
await self.support_claim(claims[0], '0.08') await self.support_claim(claims[0], '0.08')
await self.support_claim(claims[0], '0.03')
await self.update_claim(claims[0], '0.02') await self.update_claim(claims[0], '0.02')
await self.generate(1) await self.generate(1)
claims = await self.db.search_claims() claims = await search()
self.assertEqual(1, len(claims)) self.assertEqual(1, len(claims))
self.assertEqual(claims[0].claim_name, 'foo') self.assertEqual(claims[0].claim_name, 'foo')
self.assertEqual(dewies_to_lbc(claims[0].amount), '0.02') self.assertEqual(dewies_to_lbc(claims[0].amount), '0.02')
self.assertEqual(dewies_to_lbc(claims[0].meta['effective_amount']), '0.1') self.assertEqual(dewies_to_lbc(claims[0].meta['staked_amount']), '0.13')
self.assertEqual(dewies_to_lbc(claims[0].meta['staked_support_amount']), '0.11')
await self.abandon_claim(claims[0].tx_ref.id) await self.abandon_claim(claims[0].tx_ref.id)
await self.generate(1) await self.generate(1)
claims = await self.db.search_claims() claims = await search()
self.assertEqual(0, len(claims)) self.assertEqual(0, len(claims))
async def test_short_and_canonical_urls(self):
search = self.db.search_claims
# same block (no claim gets preference, therefore both end up with short hash of len 2)
await self.create_claim(claim_id_startswith='a1')
await self.create_claim(claim_id_startswith='a2')
await self.generate(1)
a2, a1 = await search(order_by=['claim_id'], limit=2)
self.assertEqual("foo#a1", a1.meta['short_url'])
self.assertEqual("foo#a2", a2.meta['short_url'])
self.assertIsNone(a1.meta['canonical_url'])
self.assertIsNone(a2.meta['canonical_url'])
# separate blocks (first claim had no competition, so it got very short url, second got longer)
await self.create_claim(claim_id_startswith='b1')
await self.generate(1)
await self.create_claim(claim_id_startswith='b2')
await self.generate(1)
b2, b1 = await search(order_by=['claim_id'], limit=2)
self.assertEqual("foo#b", b1.meta['short_url'])
self.assertEqual("foo#b2", b2.meta['short_url'])
self.assertIsNone(b1.meta['canonical_url'])
self.assertIsNone(b2.meta['canonical_url'])
# channels also have urls
channel_a1 = await self.get_claim(
await self.create_claim(claim_id_startswith='a1', is_channel=True))
await self.generate(1)
channel_a2 = await self.get_claim(
await self.create_claim(claim_id_startswith='a2', is_channel=True))
await self.generate(1)
chan_a2, chan_a1 = await search(order_by=['claim_id'], claim_type="channel", limit=2)
self.assertEqual("@foo#a", chan_a1.meta['short_url'])
self.assertEqual("@foo#a2", chan_a2.meta['short_url'])
self.assertIsNone(chan_a1.meta['canonical_url'])
self.assertIsNone(chan_a2.meta['canonical_url'])
# signing a new claim and signing as an update
await self.create_claim(claim_id_startswith='c', sign=channel_a1)
await self.update_claim(b2, sign=channel_a2)
await self.generate(1)
c, b2 = await search(order_by=['claim_id'], claim_type='stream', limit=2)
self.assertEqual("@foo#a/foo#c", c.meta['canonical_url'])
self.assertEqual("@foo#a2/foo#b2", b2.meta['canonical_url'])
# changing previously set channel
await self.update_claim(c, sign=channel_a2)
await self.generate(1)
c, = await search(order_by=['claim_id'], claim_type='stream', limit=1)
self.assertEqual("@foo#a2/foo#c", c.meta['canonical_url'])
async def assert_channel_stream1_stream2_support(
self,
signed_claim_count=0,
signed_support_count=0,
stream1_valid=False,
stream1_channel=None,
stream2_valid=False,
stream2_channel=None,
support_valid=False,
support_channel=None):
search = self.db.search_claims
r, = await search(claim_id=self.stream1.claim_id)
self.assertEqual(r.meta['is_signature_valid'], stream1_valid)
if stream1_channel is None:
self.assertIsNone(r.claim.signing_channel_id)
else:
self.assertEqual(r.claim.signing_channel_id, stream1_channel.claim_id)
r, = await search(claim_id=self.stream2.claim_id)
self.assertEqual(r.meta['is_signature_valid'], stream2_valid)
if stream2_channel is None:
self.assertIsNone(r.claim.signing_channel_id)
else:
self.assertEqual(r.claim.signing_channel_id, stream2_channel.claim_id)
r, = await search(claim_id=self.channel.claim_id)
self.assertEqual(signed_claim_count, r.meta['signed_claim_count'])
self.assertEqual(signed_support_count, r.meta['signed_support_count'])
if support_channel is not None:
r, = await self.db.search_supports()
self.assertEqual(r.meta['is_signature_valid'], support_valid)
self.assertEqual(r.support.signing_channel_id, support_channel.claim_id)
async def test_claim_and_support_signing(self):
search = self.db.search_claims
# create a stream not in channel, should not have signature
self.stream1 = await self.get_claim(
await self.create_claim())
await self.generate(1)
r, = await search(claim_type='stream')
self.assertFalse(r.claim.is_signed)
self.assertFalse(r.meta['is_signature_valid'])
self.assertIsNone(r.claim.signing_channel_id)
# create a new channel, should not have claims or supports
self.channel = await self.get_claim(
await self.create_claim(is_channel=True))
await self.generate(1)
r, = await search(claim_type='channel')
self.assertEqual(0, r.meta['signed_claim_count'])
self.assertEqual(0, r.meta['signed_support_count'])
# create a signed claim, update unsigned claim to have signature and create a signed support
self.stream2 = await self.get_claim(
await self.create_claim(sign=self.channel))
await self.update_claim(self.stream1, sign=self.channel)
self.support = await self.get_support(
await self.support_claim(self.stream1, sign=self.channel))
await self.generate(1)
await self.assert_channel_stream1_stream2_support(
signed_claim_count=2, signed_support_count=1,
stream1_valid=True, stream1_channel=self.channel,
stream2_valid=True, stream2_channel=self.channel,
support_valid=True, support_channel=self.channel
)
# resetting channel key doesn't invalidate previously published streams
await self.update_claim(self.channel, reset_channel_key=True)
await self.generate(1)
await self.assert_channel_stream1_stream2_support(
signed_claim_count=2, signed_support_count=1,
stream1_valid=True, stream1_channel=self.channel,
stream2_valid=True, stream2_channel=self.channel,
support_valid=True, support_channel=self.channel
)
# updating a claim with an invalid signature marks signature invalid
await self.channel.generate_channel_private_key() # new key but no broadcast of change
self.stream2 = await self.get_claim(
await self.update_claim(self.stream2, sign=self.channel))
await self.generate(1)
await self.assert_channel_stream1_stream2_support(
signed_claim_count=1, signed_support_count=1, # channel lost one signed claim
stream1_valid=True, stream1_channel=self.channel,
stream2_valid=False, stream2_channel=self.channel, # sig invalid
support_valid=True, support_channel=self.channel
)
# updating it again with correct signature fixes it
self.channel = await self.get_claim(self.channel.tx_ref.id) # get original channel
self.stream2 = await self.get_claim(
await self.update_claim(self.stream2, sign=self.channel))
await self.generate(1)
await self.assert_channel_stream1_stream2_support(
signed_claim_count=2, signed_support_count=1, # channel re-gained claim
stream1_valid=True, stream1_channel=self.channel,
stream2_valid=True, stream2_channel=self.channel, # sig valid now
support_valid=True, support_channel=self.channel
)
# sign stream with a different channel
self.channel2 = await self.get_claim(
await self.create_claim(is_channel=True))
self.stream2 = await self.get_claim(
await self.update_claim(self.stream2, sign=self.channel2))
await self.generate(1)
await self.assert_channel_stream1_stream2_support(
signed_claim_count=1, signed_support_count=1, # channel1 lost a claim
stream1_valid=True, stream1_channel=self.channel,
stream2_valid=True, stream2_channel=self.channel2, # new channel is the valid signer
support_valid=True, support_channel=self.channel
)
r, = await search(claim_id=self.channel2.claim_id)
self.assertEqual(1, r.meta['signed_claim_count']) # channel2 gained a claim
self.assertEqual(0, r.meta['signed_support_count'])
# deleting claim and support
await self.abandon_claim(self.stream2.tx_ref.id)
await self.abandon_support(self.support.tx_ref.id)
await self.generate(1)
r, = await search(claim_id=self.channel.claim_id)
self.assertEqual(1, r.meta['signed_claim_count'])
self.assertEqual(0, r.meta['signed_support_count']) # channel1 lost abandoned support
r, = await search(claim_id=self.channel2.claim_id)
self.assertEqual(0, r.meta['signed_claim_count']) # channel2 lost abandoned claim
self.assertEqual(0, r.meta['signed_support_count'])
async def resolve_to_claim_id(self, url):
return (await self.db.resolve(url))[url].claim_id
async def test_resolve(self):
chan_a = await self.get_claim(
await self.create_claim(claim_id_startswith='a', is_channel=True))
await self.generate(1)
chan_ab = await self.get_claim(
await self.create_claim(claim_id_startswith='ab', is_channel=True))
await self.generate(1)
self.assertEqual(chan_a.claim_id, await self.resolve_to_claim_id("@foo#a"))
self.assertEqual(chan_ab.claim_id, await self.resolve_to_claim_id("@foo#ab"))
stream_c = await self.get_claim(
await self.create_claim(claim_id_startswith='c', sign=chan_a))
await self.generate(1)
stream_cd = await self.get_claim(
await self.create_claim(claim_id_startswith='cd', sign=chan_ab))
await self.generate(1)
self.assertEqual(stream_c.claim_id, await self.resolve_to_claim_id("@foo#a/foo#c"))
self.assertEqual(stream_cd.claim_id, await self.resolve_to_claim_id("@foo#ab/foo#cd"))
class TestClaimtrieSync(SyncingBlockchainTestCase): class TestClaimtrieSync(SyncingBlockchainTestCase):
@ -538,35 +797,35 @@ class TestClaimtrieSync(SyncingBlockchainTestCase):
await state( await state(
controlling=('Claim A', '10.0', '10.0', 113), controlling=('Claim A', '10.0', '10.0', 113),
active=[], active=[],
accepted=[('Claim B', '20.0', '0.0', 513)] accepted=[('Claim B', '20.0', '20.0', 513)]
) )
await advance(510, [('support', stream, '14.0')]) await advance(510, [('support', stream, '14.0')])
await state( await state(
controlling=('Claim A', '10.0', '24.0', 113), controlling=('Claim A', '10.0', '24.0', 113),
active=[], active=[],
accepted=[('Claim B', '20.0', '0.0', 513)] accepted=[('Claim B', '20.0', '20.0', 513)]
) )
await advance(512, [('claim', 'Claim C', '50.0')]) await advance(512, [('claim', 'Claim C', '50.0')])
await state( await state(
controlling=('Claim A', '10.0', '24.0', 113), controlling=('Claim A', '10.0', '24.0', 113),
active=[], active=[],
accepted=[ accepted=[
('Claim B', '20.0', '0.0', 513), ('Claim B', '20.0', '20.0', 513),
('Claim C', '50.0', '0.0', 524)] ('Claim C', '50.0', '50.0', 524)]
) )
await advance(513, []) await advance(513, [])
await state( await state(
controlling=('Claim A', '10.0', '24.0', 113), controlling=('Claim A', '10.0', '24.0', 113),
active=[('Claim B', '20.0', '20.0', 513)], active=[('Claim B', '20.0', '20.0', 513)],
accepted=[('Claim C', '50.0', '0.0', 524)] accepted=[('Claim C', '50.0', '50.0', 524)]
) )
await advance(520, [('claim', 'Claim D', '60.0')]) await advance(520, [('claim', 'Claim D', '60.0')])
await state( await state(
controlling=('Claim A', '10.0', '24.0', 113), controlling=('Claim A', '10.0', '24.0', 113),
active=[('Claim B', '20.0', '20.0', 513)], active=[('Claim B', '20.0', '20.0', 513)],
accepted=[ accepted=[
('Claim C', '50.0', '0.0', 524), ('Claim C', '50.0', '50.0', 524),
('Claim D', '60.0', '0.0', 532)] ('Claim D', '60.0', '60.0', 532)]
) )
await advance(524, []) await advance(524, [])
await state( await state(
@ -702,8 +961,8 @@ class TestClaimtrieSync(SyncingBlockchainTestCase):
async def test_create_and_multiple_updates_in_same_block(self): async def test_create_and_multiple_updates_in_same_block(self):
await self.generate(10) await self.generate(10)
txid = await self.create_claim('Claim A', '1.0') txid = await self.create_claim('Claim A', '1.0')
txid = await self.update_claim(self.find_claim_txo(await self.get_transaction(txid)), '2.0') txid = await self.update_claim(await self.get_claim(txid), '2.0')
await self.update_claim(self.find_claim_txo(await self.get_transaction(txid)), '3.0') await self.update_claim(await self.get_claim(txid), '3.0')
await self.generate(1) await self.generate(1)
await self.sync.advance() await self.sync.advance()
await self.state( await self.state(
@ -725,134 +984,7 @@ class TestClaimtrieSync(SyncingBlockchainTestCase):
) )
class TestResolve(SyncingBlockchainTestCase): @skip
async def create_claim_txo(self, title='', amount=CENT, name=None, claim_id_startswith=None, sign=None, is_channel=False):
tx = await self.get_transaction(await self.create_claim(
title=title, amount=amount, name=name or ('@foo' if is_channel else 'foo'),
claim_id_startswith=claim_id_startswith, sign=sign, is_channel=is_channel
))
return self.find_claim_txo(tx)
async def test_canonical_url_and_channel_validation(self):
advance, search = self.advance, self.db.search_claims
txo_chan_a = await self.create_claim_txo(claim_id_startswith='a!b', is_channel=True)
await self.generate(1)
txo_chan_ab = await self.create_claim_txo(claim_id_startswith='ab', is_channel=True)
await self.generate(1)
(r_ab, r_a) = await search(order_by=['creation_height'], limit=2)
self.assertEqual("@foo#a", r_a.meta['short_url'])
self.assertEqual("@foo#ab", r_ab.meta['short_url'])
self.assertIsNone(r_a.meta['canonical_url'])
self.assertIsNone(r_ab.meta['canonical_url'])
self.assertEqual(0, r_a.meta['claims_in_channel_count'])
self.assertEqual(0, r_ab.meta['claims_in_channel_count'])
await self.create_claim_txo(claim_id_startswith='a!b')
await self.generate(1)
await self.create_claim_txo(claim_id_startswith='ab!c')
await self.generate(1)
await self.create_claim_txo(claim_id_startswith='abc')
await self.generate(1)
(r_abc, r_ab, r_a) = await search(order_by=['creation_height'], limit=3)
self.assertEqual("foo#a", r_a.meta['short_url'])
self.assertEqual("foo#ab", r_ab.meta['short_url'])
self.assertEqual("foo#abc", r_abc.meta['short_url'])
self.assertIsNone(r_a.meta['canonical_url'])
self.assertIsNone(r_ab.meta['canonical_url'])
self.assertIsNone(r_abc.meta['canonical_url'])
a2_claim = await self.create_claim_txo(claim_id_startswith='a!b'+r_a.claim_id[1], sign=txo_chan_a)
await self.generate(1)
ab2_claim = await self.create_claim_txo(claim_id_startswith='ab!c'+r_ab.claim_id[2], sign=txo_chan_a)
await self.generate(1)
r_ab2, r_a2 = await search(order_by=['creation_height'], limit=2)
r_a2url, r_ab2url = f"foo#{a2_claim.claim_id[:2]}", f"foo#{ab2_claim.claim_id[:3]}"
self.assertEqual(r_a2url, r_a2.meta['short_url'])
self.assertEqual(r_ab2url, r_ab2.meta['short_url'])
self.assertEqual(f"@foo#a/{r_a2url}", r_a2.meta['canonical_url'])
self.assertEqual(f"@foo#a/{r_ab2url}", r_ab2.meta['canonical_url'])
self.assertEqual(2, (await search(claim_id=txo_chan_a.claim_id, limit=1))[0].meta['claims_in_channel_count'])
# change channel public key, invaliding stream claim signatures
await advance(8, [self.get_channel_update(txo_chan_a, COIN, key=b'a')])
(r_ab2, r_a2) = search(order_by=['creation_height'], limit=2)
self.assertEqual(f"foo#{a2_claim.claim_id[:2]}", r_a2['short_url'])
self.assertEqual(f"foo#{ab2_claim.claim_id[:4]}", r_ab2['short_url'])
self.assertIsNone(r_a2['canonical_url'])
self.assertIsNone(r_ab2['canonical_url'])
self.assertEqual(0, search(claim_id=txo_chan_a.claim_id, limit=1)[0]['claims_in_channel'])
# reinstate previous channel public key (previous stream claim signatures become valid again)
channel_update = self.get_channel_update(txo_chan_a, COIN, key=b'c')
await advance(9, [channel_update])
(r_ab2, r_a2) = search(order_by=['creation_height'], limit=2)
self.assertEqual(f"foo#{a2_claim.claim_id[:2]}", r_a2['short_url'])
self.assertEqual(f"foo#{ab2_claim.claim_id[:4]}", r_ab2['short_url'])
self.assertEqual("@foo#a/foo#a", r_a2['canonical_url'])
self.assertEqual("@foo#a/foo#ab", r_ab2['canonical_url'])
self.assertEqual(2, search(claim_id=txo_chan_a.claim_id, limit=1)[0]['claims_in_channel'])
self.assertEqual(0, search(claim_id=txo_chan_ab.claim_id, limit=1)[0]['claims_in_channel'])
# change channel of stream
self.assertEqual("@foo#a/foo#ab", search(claim_id=ab2_claim.claim_id, limit=1)[0]['canonical_url'])
tx_ab2 = self.get_stream_update(tx_ab2, COIN, txo_chan_ab)
await advance(10, [tx_ab2])
self.assertEqual("@foo#ab/foo#a", search(claim_id=ab2_claim.claim_id, limit=1)[0]['canonical_url'])
# TODO: currently there is a bug where stream leaving a channel does not update that channels claims count
self.assertEqual(2, search(claim_id=txo_chan_a.claim_id, limit=1)[0]['claims_in_channel'])
# TODO: after bug is fixed remove test above and add test below
#self.assertEqual(1, search(claim_id=txo_chan_a.claim_id, limit=1)[0]['claims_in_channel'])
self.assertEqual(1, search(claim_id=txo_chan_ab.claim_id, limit=1)[0]['claims_in_channel'])
# claim abandon updates claims_in_channel
await advance(11, [self.get_abandon(tx_ab2)])
self.assertEqual(0, search(claim_id=txo_chan_ab.claim_id, limit=1)[0]['claims_in_channel'])
# delete channel, invaliding stream claim signatures
await advance(12, [self.get_abandon(channel_update)])
(r_a2,) = search(order_by=['creation_height'], limit=1)
self.assertEqual(f"foo#{a2_claim.claim_id[:2]}", r_a2['short_url'])
self.assertIsNone(r_a2['canonical_url'])
def test_resolve_issue_2448(self):
advance = self.advance
tx_chan_a = self.get_channel_with_claim_id_prefix('a', 1, key=b'c')
tx_chan_ab = self.get_channel_with_claim_id_prefix('ab', 72, key=b'c')
txo_chan_a = tx_chan_a[0].outputs[0]
txo_chan_ab = tx_chan_ab[0].outputs[0]
advance(1, [tx_chan_a])
advance(2, [tx_chan_ab])
self.assertEqual(reader.resolve_url("@foo#a")['claim_hash'], txo_chan_a.claim_hash)
self.assertEqual(reader.resolve_url("@foo#ab")['claim_hash'], txo_chan_ab.claim_hash)
# update increase last height change of channel
advance(9, [self.get_channel_update(txo_chan_a, COIN, key=b'c')])
# make sure that activation_height is used instead of height (issue #2448)
self.assertEqual(reader.resolve_url("@foo#a")['claim_hash'], txo_chan_a.claim_hash)
self.assertEqual(reader.resolve_url("@foo#ab")['claim_hash'], txo_chan_ab.claim_hash)
def test_canonical_find_shortest_id(self):
new_hash = 'abcdef0123456789beef'
other0 = '1bcdef0123456789beef'
other1 = 'ab1def0123456789beef'
other2 = 'abc1ef0123456789beef'
other3 = 'abcdef0123456789bee1'
f = FindShortestID()
f.step(other0, new_hash)
self.assertEqual('#a', f.finalize())
f.step(other1, new_hash)
self.assertEqual('#abc', f.finalize())
f.step(other2, new_hash)
self.assertEqual('#abcd', f.finalize())
f.step(other3, new_hash)
self.assertEqual('#abcdef0123456789beef', f.finalize())
class TestTrending(SyncingBlockchainTestCase): class TestTrending(SyncingBlockchainTestCase):
def test_trending(self): def test_trending(self):
@ -884,6 +1016,7 @@ class TestTrending(SyncingBlockchainTestCase):
self.advance(zscore.TRENDING_WINDOW * 2, [self.get_support(problematic, 500000000)]) self.advance(zscore.TRENDING_WINDOW * 2, [self.get_support(problematic, 500000000)])
@skip
class TestContentBlocking(SyncingBlockchainTestCase): class TestContentBlocking(SyncingBlockchainTestCase):
def test_blocking_and_filtering(self): def test_blocking_and_filtering(self):