import asyncio import json import hashlib from bisect import bisect_right from binascii import hexlify, unhexlify from lbry.testcase import CommandTestCase from lbry.wallet.transaction import Transaction, Output from lbry.schema.compat import OldClaimMessage from lbry.crypto.hash import sha256 from lbry.crypto.base58 import Base58 class BaseResolveTestCase(CommandTestCase): async def assertResolvesToClaimId(self, name, claim_id): other = await self.resolve(name) if claim_id is None: self.assertIn('error', other) self.assertEqual(other['error']['name'], 'NOT_FOUND') else: self.assertEqual(claim_id, other['claim_id']) async def assertNoClaimForName(self, name: str): lbrycrd_winning = json.loads(await self.blockchain._cli_cmnd('getvalueforname', name)) stream, channel = await self.conductor.spv_node.server.bp.db.fs_resolve(name) self.assertNotIn('claimId', lbrycrd_winning) if stream is not None: self.assertIsInstance(stream, LookupError) else: self.assertIsInstance(channel, LookupError) async def assertMatchWinningClaim(self, name): expected = json.loads(await self.blockchain._cli_cmnd('getvalueforname', name)) stream, channel = await self.conductor.spv_node.server.bp.db.fs_resolve(name) claim = stream if stream else channel self.assertEqual(expected['claimId'], claim.claim_hash.hex()) self.assertEqual(expected['validAtHeight'], claim.activation_height) self.assertEqual(expected['lastTakeoverHeight'], claim.last_takeover_height) self.assertEqual(expected['txId'], claim.tx_hash[::-1].hex()) self.assertEqual(expected['n'], claim.position) self.assertEqual(expected['amount'], claim.amount) self.assertEqual(expected['effectiveAmount'], claim.effective_amount) return claim async def assertMatchClaim(self, claim_id): expected = json.loads(await self.blockchain._cli_cmnd('getclaimbyid', claim_id)) claim = await self.conductor.spv_node.server.bp.db.fs_getclaimbyid(claim_id) if not expected: self.assertIsNone(claim) return self.assertEqual(expected['claimId'], claim.claim_hash.hex()) self.assertEqual(expected['validAtHeight'], claim.activation_height) self.assertEqual(expected['lastTakeoverHeight'], claim.last_takeover_height) self.assertEqual(expected['txId'], claim.tx_hash[::-1].hex()) self.assertEqual(expected['n'], claim.position) self.assertEqual(expected['amount'], claim.amount) self.assertEqual(expected['effectiveAmount'], claim.effective_amount) return claim async def assertMatchClaimIsWinning(self, name, claim_id): self.assertEqual(claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaim(claim_id) await self.assertMatchClaimsForName(name) async def assertMatchClaimsForName(self, name): expected = json.loads(await self.blockchain._cli_cmnd('getclaimsforname', name)) db = self.conductor.spv_node.server.bp.db def check_supports(claim_id, lbrycrd_supports): for i, (tx_num, position, amount) in enumerate(db.get_supports(bytes.fromhex(claim_id))): support = lbrycrd_supports[i] self.assertEqual(support['txId'], db.total_transactions[tx_num][::-1].hex()) self.assertEqual(support['n'], position) self.assertEqual(support['height'], bisect_right(db.tx_counts, tx_num)) self.assertEqual(support['validAtHeight'], db.get_activation(tx_num, position, is_support=True)) # self.assertEqual(len(expected['claims']), len(db_claims.claims)) # self.assertEqual(expected['lastTakeoverHeight'], db_claims.lastTakeoverHeight) for c in expected['claims']: check_supports(c['claimId'], c['supports']) claim_hash = bytes.fromhex(c['claimId']) self.assertEqual(c['validAtHeight'], db.get_activation( db.transaction_num_mapping[bytes.fromhex(c['txId'])[::-1]], c['n'] )) self.assertEqual(c['effectiveAmount'], db.get_effective_amount(claim_hash)) class ResolveCommand(BaseResolveTestCase): async def test_resolve_response(self): channel_id = self.get_claim_id( await self.channel_create('@abc', '0.01') ) # resolving a channel @abc response = await self.resolve('lbry://@abc') self.assertEqual(response['name'], '@abc') self.assertEqual(response['value_type'], 'channel') self.assertEqual(response['meta']['claims_in_channel'], 0) await self.stream_create('foo', '0.01', channel_id=channel_id) await self.stream_create('foo2', '0.01', channel_id=channel_id) # resolving a channel @abc with some claims in it response['confirmations'] += 2 response['meta']['claims_in_channel'] = 2 self.assertEqual(response, await self.resolve('lbry://@abc')) # resolving claim foo within channel @abc claim = await self.resolve('lbry://@abc/foo') self.assertEqual(claim['name'], 'foo') self.assertEqual(claim['value_type'], 'stream') self.assertEqual(claim['signing_channel']['name'], '@abc') self.assertTrue(claim['is_channel_signature_valid']) self.assertEqual( claim['timestamp'], self.ledger.headers.estimated_timestamp(claim['height']) ) self.assertEqual( claim['signing_channel']['timestamp'], self.ledger.headers.estimated_timestamp(claim['signing_channel']['height']) ) # resolving claim foo by itself self.assertEqual(claim, await self.resolve('lbry://foo')) # resolving from the given permanent url self.assertEqual(claim, await self.resolve(claim['permanent_url'])) # resolving multiple at once response = await self.out(self.daemon.jsonrpc_resolve(['lbry://foo', 'lbry://foo2'])) self.assertSetEqual({'lbry://foo', 'lbry://foo2'}, set(response)) claim = response['lbry://foo2'] self.assertEqual(claim['name'], 'foo2') self.assertEqual(claim['value_type'], 'stream') self.assertEqual(claim['signing_channel']['name'], '@abc') self.assertTrue(claim['is_channel_signature_valid']) # resolve has correct confirmations tx_details = await self.blockchain.get_raw_transaction(claim['txid']) self.assertEqual(claim['confirmations'], json.loads(tx_details)['confirmations']) # resolve handles invalid data await self.blockchain_claim_name("gibberish", hexlify(b"{'invalid':'json'}").decode(), "0.1") await self.generate(1) response = await self.out(self.daemon.jsonrpc_resolve("lbry://gibberish")) self.assertSetEqual({'lbry://gibberish'}, set(response)) claim = response['lbry://gibberish'] self.assertEqual(claim['name'], 'gibberish') self.assertNotIn('value', claim) # resolve retries await self.conductor.spv_node.stop() resolve_task = asyncio.create_task(self.resolve('foo')) await self.conductor.spv_node.start(self.conductor.blockchain_node) self.assertIsNotNone((await resolve_task)['claim_id']) async def test_winning_by_effective_amount(self): # first one remains winner unless something else changes claim_id1 = self.get_claim_id( await self.channel_create('@foo', allow_duplicate_name=True)) await self.assertResolvesToClaimId('@foo', claim_id1) claim_id2 = self.get_claim_id( await self.channel_create('@foo', allow_duplicate_name=True)) await self.assertResolvesToClaimId('@foo', claim_id1) claim_id3 = self.get_claim_id( await self.channel_create('@foo', allow_duplicate_name=True)) await self.assertResolvesToClaimId('@foo', claim_id1) # supports change the winner await self.support_create(claim_id3, '0.09') await self.assertResolvesToClaimId('@foo', claim_id3) await self.support_create(claim_id2, '0.19') await self.assertResolvesToClaimId('@foo', claim_id2) await self.support_create(claim_id1, '0.29') await self.assertResolvesToClaimId('@foo', claim_id1) await self.support_abandon(claim_id1) await self.assertResolvesToClaimId('@foo', claim_id2) async def test_advanced_resolve(self): claim_id1 = self.get_claim_id( await self.stream_create('foo', '0.7', allow_duplicate_name=True)) await self.assertResolvesToClaimId('foo$1', claim_id1) claim_id2 = self.get_claim_id( await self.stream_create('foo', '0.8', allow_duplicate_name=True)) await self.assertResolvesToClaimId('foo$1', claim_id2) await self.assertResolvesToClaimId('foo$2', claim_id1) claim_id3 = self.get_claim_id( await self.stream_create('foo', '0.9', allow_duplicate_name=True)) # plain winning claim await self.assertResolvesToClaimId('foo', claim_id3) # amount order resolution await self.assertResolvesToClaimId('foo$1', claim_id3) await self.assertResolvesToClaimId('foo$2', claim_id2) await self.assertResolvesToClaimId('foo$3', claim_id1) await self.assertResolvesToClaimId('foo$4', None) # async def test_partial_claim_id_resolve(self): # # add some noise # await self.channel_create('@abc', '0.1', allow_duplicate_name=True) # await self.channel_create('@abc', '0.2', allow_duplicate_name=True) # await self.channel_create('@abc', '1.0', allow_duplicate_name=True) # # channel_id = self.get_claim_id(await self.channel_create('@abc', '1.1', allow_duplicate_name=True)) # await self.assertResolvesToClaimId(f'@abc', channel_id) # await self.assertResolvesToClaimId(f'@abc#{channel_id[:10]}', channel_id) # await self.assertResolvesToClaimId(f'@abc#{channel_id}', channel_id) # # channel = await self.claim_get(channel_id) # await self.assertResolvesToClaimId(channel['short_url'], channel_id) # await self.assertResolvesToClaimId(channel['canonical_url'], channel_id) # await self.assertResolvesToClaimId(channel['permanent_url'], channel_id) # # # add some noise # await self.stream_create('foo', '0.1', allow_duplicate_name=True, channel_id=channel['claim_id']) # await self.stream_create('foo', '0.2', allow_duplicate_name=True, channel_id=channel['claim_id']) # await self.stream_create('foo', '0.3', allow_duplicate_name=True, channel_id=channel['claim_id']) # # claim_id1 = self.get_claim_id( # await self.stream_create('foo', '0.7', allow_duplicate_name=True, channel_id=channel['claim_id'])) # claim1 = await self.claim_get(claim_id=claim_id1) # # await self.assertResolvesToClaimId('foo', claim_id1) # await self.assertResolvesToClaimId('@abc/foo', claim_id1) # await self.assertResolvesToClaimId(claim1['short_url'], claim_id1) # await self.assertResolvesToClaimId(claim1['canonical_url'], claim_id1) # await self.assertResolvesToClaimId(claim1['permanent_url'], claim_id1) # # claim_id2 = self.get_claim_id( # await self.stream_create('foo', '0.8', allow_duplicate_name=True, channel_id=channel['claim_id'])) # claim2 = await self.claim_get(claim_id=claim_id2) # await self.assertResolvesToClaimId('foo', claim_id2) # await self.assertResolvesToClaimId('@abc/foo', claim_id2) # await self.assertResolvesToClaimId(claim2['short_url'], claim_id2) # await self.assertResolvesToClaimId(claim2['canonical_url'], claim_id2) # await self.assertResolvesToClaimId(claim2['permanent_url'], claim_id2) async def test_abandoned_channel_with_signed_claims(self): channel = (await self.channel_create('@abc', '1.0'))['outputs'][0] orphan_claim = await self.stream_create('on-channel-claim', '0.0001', channel_id=channel['claim_id']) abandoned_channel_id = channel['claim_id'] await self.channel_abandon(txid=channel['txid'], nout=0) channel = (await self.channel_create('@abc', '1.0'))['outputs'][0] orphan_claim_id = self.get_claim_id(orphan_claim) # Original channel doesn't exists anymore, so the signature is invalid. For invalid signatures, resolution is # only possible outside a channel self.assertEqual( {'error': { 'name': 'NOT_FOUND', 'text': 'Could not find claim at "lbry://@abc/on-channel-claim".', }}, await self.resolve('lbry://@abc/on-channel-claim') ) response = await self.resolve('lbry://on-channel-claim') self.assertFalse(response['is_channel_signature_valid']) self.assertEqual({'channel_id': abandoned_channel_id}, response['signing_channel']) direct_uri = 'lbry://on-channel-claim#' + orphan_claim_id response = await self.resolve(direct_uri) self.assertFalse(response['is_channel_signature_valid']) self.assertEqual({'channel_id': abandoned_channel_id}, response['signing_channel']) await self.stream_abandon(claim_id=orphan_claim_id) uri = 'lbry://@abc/on-channel-claim' # now, claim something on this channel (it will update the invalid claim, but we save and forcefully restore) valid_claim = await self.stream_create('on-channel-claim', '0.00000001', channel_id=channel['claim_id']) # resolves normally response = await self.resolve(uri) self.assertTrue(response['is_channel_signature_valid']) # ooops! claimed a valid conflict! (this happens on the wild, mostly by accident or race condition) await self.stream_create( 'on-channel-claim', '0.00000001', channel_id=channel['claim_id'], allow_duplicate_name=True ) # it still resolves! but to the older claim response = await self.resolve(uri) self.assertTrue(response['is_channel_signature_valid']) self.assertEqual(response['txid'], valid_claim['txid']) claims = [await self.resolve('on-channel-claim'), await self.resolve('on-channel-claim$2')] self.assertEqual(2, len(claims)) self.assertEqual( {channel['claim_id']}, {claim['signing_channel']['claim_id'] for claim in claims} ) async def test_normalization_resolution(self): one = 'ΣίσυφοςfiÆ' two = 'ΣΊΣΥΦΟσFIæ' _ = await self.stream_create(one, '0.1') c = await self.stream_create(two, '0.2') winner_id = self.get_claim_id(c) # winning_one = await self.check_lbrycrd_winning(one) await self.assertMatchClaimIsWinning(two, winner_id) r1 = await self.resolve(f'lbry://{one}') r2 = await self.resolve(f'lbry://{two}') self.assertEqual(winner_id, r1['claim_id']) self.assertEqual(winner_id, r2['claim_id']) async def test_resolve_old_claim(self): channel = await self.daemon.jsonrpc_channel_create('@olds', '1.0') await self.confirm_tx(channel.id) address = channel.outputs[0].get_address(self.account.ledger) claim = generate_signed_legacy(address, channel.outputs[0]) tx = await Transaction.claim_create('example', claim.SerializeToString(), 1, address, [self.account], self.account) await tx.sign([self.account]) await self.broadcast(tx) await self.confirm_tx(tx.id) response = await self.resolve('@olds/example') self.assertTrue(response['is_channel_signature_valid']) claim.publisherSignature.signature = bytes(reversed(claim.publisherSignature.signature)) tx = await Transaction.claim_create( 'bad_example', claim.SerializeToString(), 1, address, [self.account], self.account ) await tx.sign([self.account]) await self.broadcast(tx) await self.confirm_tx(tx.id) response = await self.resolve('bad_example') self.assertFalse(response['is_channel_signature_valid']) self.assertEqual( {'error': { 'name': 'NOT_FOUND', 'text': 'Could not find claim at "@olds/bad_example".', }}, await self.resolve('@olds/bad_example') ) async def test_resolve_with_includes(self): wallet2 = await self.daemon.jsonrpc_wallet_create('wallet2', create_account=True) address2 = await self.daemon.jsonrpc_address_unused(wallet_id=wallet2.id) await self.wallet_send('1.0', address2) stream = await self.stream_create( 'priced', '0.1', wallet_id=wallet2.id, fee_amount='0.5', fee_currency='LBC', fee_address=address2 ) stream_id = self.get_claim_id(stream) resolve = await self.resolve('priced') self.assertNotIn('is_my_output', resolve) self.assertNotIn('purchase_receipt', resolve) self.assertNotIn('sent_supports', resolve) self.assertNotIn('sent_tips', resolve) self.assertNotIn('received_tips', resolve) # is_my_output resolve = await self.resolve('priced', include_is_my_output=True) self.assertFalse(resolve['is_my_output']) resolve = await self.resolve('priced', wallet_id=wallet2.id, include_is_my_output=True) self.assertTrue(resolve['is_my_output']) # purchase receipt resolve = await self.resolve('priced', include_purchase_receipt=True) self.assertNotIn('purchase_receipt', resolve) await self.purchase_create(stream_id) resolve = await self.resolve('priced', include_purchase_receipt=True) self.assertEqual('0.5', resolve['purchase_receipt']['amount']) # my supports and my tips resolve = await self.resolve( 'priced', include_sent_supports=True, include_sent_tips=True, include_received_tips=True ) self.assertEqual('0.0', resolve['sent_supports']) self.assertEqual('0.0', resolve['sent_tips']) self.assertEqual('0.0', resolve['received_tips']) await self.support_create(stream_id, '0.3') await self.support_create(stream_id, '0.2') await self.support_create(stream_id, '0.4', tip=True) await self.support_create(stream_id, '0.5', tip=True) resolve = await self.resolve( 'priced', include_sent_supports=True, include_sent_tips=True, include_received_tips=True ) self.assertEqual('0.5', resolve['sent_supports']) self.assertEqual('0.9', resolve['sent_tips']) self.assertEqual('0.0', resolve['received_tips']) resolve = await self.resolve( 'priced', include_sent_supports=True, include_sent_tips=True, include_received_tips=True, wallet_id=wallet2.id ) self.assertEqual('0.0', resolve['sent_supports']) self.assertEqual('0.0', resolve['sent_tips']) self.assertEqual('0.9', resolve['received_tips']) self.assertEqual('1.4', resolve['meta']['support_amount']) # make sure nothing is leaked between wallets through cached tx/txos resolve = await self.resolve('priced') self.assertNotIn('is_my_output', resolve) self.assertNotIn('purchase_receipt', resolve) self.assertNotIn('sent_supports', resolve) self.assertNotIn('sent_tips', resolve) self.assertNotIn('received_tips', resolve) class ResolveClaimTakeovers(BaseResolveTestCase): async def test_channel_invalidation(self): channel_id = (await self.channel_create('@test', '0.1'))['outputs'][0]['claim_id'] channel_id2 = (await self.channel_create('@other', '0.1'))['outputs'][0]['claim_id'] async def make_claim(name, amount, channel_id=None): return ( await self.stream_create(name, amount, channel_id=channel_id) )['outputs'][0]['claim_id'] unsigned_then_signed = await make_claim('unsigned_then_signed', '0.1') unsigned_then_updated_then_signed = await make_claim('unsigned_then_updated_then_signed', '0.1') signed_then_unsigned = await make_claim( 'signed_then_unsigned', '0.01', channel_id=channel_id ) signed_then_signed_different_chan = await make_claim( 'signed_then_signed_different_chan', '0.01', channel_id=channel_id ) self.assertIn("error", await self.resolve('@test/unsigned_then_signed')) await self.assertMatchClaimIsWinning('unsigned_then_signed', unsigned_then_signed) self.assertIn("error", await self.resolve('@test/unsigned_then_updated_then_signed')) await self.assertMatchClaimIsWinning('unsigned_then_updated_then_signed', unsigned_then_updated_then_signed) self.assertDictEqual( await self.resolve('@test/signed_then_unsigned'), await self.resolve('signed_then_unsigned') ) await self.assertMatchClaimIsWinning('signed_then_unsigned', signed_then_unsigned) # sign 'unsigned_then_signed' and update it await self.ledger.wait(await self.daemon.jsonrpc_stream_update( unsigned_then_signed, '0.09', channel_id=channel_id)) await self.ledger.wait(await self.daemon.jsonrpc_stream_update(unsigned_then_updated_then_signed, '0.09')) await self.ledger.wait(await self.daemon.jsonrpc_stream_update( unsigned_then_updated_then_signed, '0.09', channel_id=channel_id)) await self.ledger.wait(await self.daemon.jsonrpc_stream_update( signed_then_unsigned, '0.09', clear_channel=True)) await self.ledger.wait(await self.daemon.jsonrpc_stream_update( signed_then_signed_different_chan, '0.09', channel_id=channel_id2)) await self.daemon.jsonrpc_txo_spend(type='channel', claim_id=channel_id) signed3 = await make_claim('signed3', '0.01', channel_id=channel_id) signed4 = await make_claim('signed4', '0.01', channel_id=channel_id2) self.assertIn("error", await self.resolve('@test')) self.assertIn("error", await self.resolve('@test/signed1')) self.assertIn("error", await self.resolve('@test/unsigned_then_updated_then_signed')) self.assertIn("error", await self.resolve('@test/unsigned_then_signed')) self.assertIn("error", await self.resolve('@test/signed3')) self.assertIn("error", await self.resolve('@test/signed4')) await self.assertMatchClaimIsWinning('signed_then_unsigned', signed_then_unsigned) await self.assertMatchClaimIsWinning('unsigned_then_signed', unsigned_then_signed) await self.assertMatchClaimIsWinning('unsigned_then_updated_then_signed', unsigned_then_updated_then_signed) await self.assertMatchClaimIsWinning('signed_then_signed_different_chan', signed_then_signed_different_chan) await self.assertMatchClaimIsWinning('signed3', signed3) await self.assertMatchClaimIsWinning('signed4', signed4) self.assertDictEqual(await self.resolve('@other/signed_then_signed_different_chan'), await self.resolve('signed_then_signed_different_chan')) self.assertDictEqual(await self.resolve('@other/signed4'), await self.resolve('signed4')) async def _test_activation_delay(self): name = 'derp' # initially claim the name first_claim_id = (await self.stream_create(name, '0.1', allow_duplicate_name=True))['outputs'][0]['claim_id'] await self.assertMatchClaimIsWinning(name, first_claim_id) await self.generate(320) # a claim of higher amount made now will have a takeover delay of 10 second_claim_id = (await self.stream_create(name, '0.2', allow_duplicate_name=True))['outputs'][0]['claim_id'] # sanity check self.assertNotEqual(first_claim_id, second_claim_id) # takeover should not have happened yet await self.assertMatchClaimIsWinning(name, first_claim_id) await self.generate(9) # not yet await self.assertMatchClaimIsWinning(name, first_claim_id) await self.generate(1) # the new claim should have activated await self.assertMatchClaimIsWinning(name, second_claim_id) return first_claim_id, second_claim_id async def test_activation_delay(self): await self._test_activation_delay() async def test_activation_delay_then_abandon_then_reclaim(self): name = 'derp' first_claim_id, second_claim_id = await self._test_activation_delay() await self.daemon.jsonrpc_txo_spend(type='stream', claim_id=first_claim_id) await self.daemon.jsonrpc_txo_spend(type='stream', claim_id=second_claim_id) await self.generate(1) await self.assertNoClaimForName(name) await self._test_activation_delay() async def test_early_takeover(self): name = 'derp' # block 207 first_claim_id = (await self.stream_create(name, '0.1', allow_duplicate_name=True))['outputs'][0]['claim_id'] await self.assertMatchClaimIsWinning(name, first_claim_id) await self.generate(96) # block 304, activates at 307 second_claim_id = (await self.stream_create(name, '0.2', allow_duplicate_name=True))['outputs'][0]['claim_id'] # block 305, activates at 308 (but gets triggered early by the takeover by the second claim) third_claim_id = (await self.stream_create(name, '0.3', allow_duplicate_name=True))['outputs'][0]['claim_id'] self.assertNotEqual(first_claim_id, second_claim_id) # takeover should not have happened yet await self.assertMatchClaimIsWinning(name, first_claim_id) await self.generate(1) await self.assertMatchClaimIsWinning(name, first_claim_id) await self.generate(1) await self.assertMatchClaimIsWinning(name, third_claim_id) await self.generate(1) await self.assertMatchClaimIsWinning(name, third_claim_id) async def test_early_takeover_zero_delay(self): name = 'derp' # block 207 first_claim_id = (await self.stream_create(name, '0.1', allow_duplicate_name=True))['outputs'][0]['claim_id'] await self.assertMatchClaimIsWinning(name, first_claim_id) await self.generate(96) # block 304, activates at 307 second_claim_id = (await self.stream_create(name, '0.2', allow_duplicate_name=True))['outputs'][0]['claim_id'] await self.assertMatchClaimIsWinning(name, first_claim_id) await self.generate(1) await self.assertMatchClaimIsWinning(name, first_claim_id) await self.generate(1) await self.assertMatchClaimIsWinning(name, first_claim_id) # on block 307 make a third claim with a yet higher amount, it takes over with no delay because the # second claim activates and begins the takeover on this block third_claim_id = (await self.stream_create(name, '0.3', allow_duplicate_name=True))['outputs'][0]['claim_id'] await self.assertMatchClaimIsWinning(name, third_claim_id) await self.generate(1) await self.assertMatchClaimIsWinning(name, third_claim_id) async def test_early_takeover_from_support_zero_delay(self): name = 'derp' # block 207 first_claim_id = (await self.stream_create(name, '0.1', allow_duplicate_name=True))['outputs'][0]['claim_id'] await self.assertMatchClaimIsWinning(name, first_claim_id) await self.generate(96) # block 304, activates at 307 second_claim_id = (await self.stream_create(name, '0.2', allow_duplicate_name=True))['outputs'][0]['claim_id'] await self.assertMatchClaimIsWinning(name, first_claim_id) await self.generate(1) await self.assertMatchClaimIsWinning(name, first_claim_id) third_claim_id = (await self.stream_create(name, '0.19', allow_duplicate_name=True))['outputs'][0]['claim_id'] await self.assertMatchClaimIsWinning(name, first_claim_id) tx = await self.daemon.jsonrpc_support_create(third_claim_id, '0.1') await self.ledger.wait(tx) await self.generate(1) await self.assertMatchClaimIsWinning(name, third_claim_id) await self.generate(1) await self.assertMatchClaimIsWinning(name, third_claim_id) async def test_early_takeover_from_support_and_claim_zero_delay(self): name = 'derp' # block 207 first_claim_id = (await self.stream_create(name, '0.1', allow_duplicate_name=True))['outputs'][0]['claim_id'] await self.assertMatchClaimIsWinning(name, first_claim_id) await self.generate(96) # block 304, activates at 307 second_claim_id = (await self.stream_create(name, '0.2', allow_duplicate_name=True))['outputs'][0]['claim_id'] await self.assertMatchClaimIsWinning(name, first_claim_id) await self.generate(1) await self.assertMatchClaimIsWinning(name, first_claim_id) await self.generate(1) file_path = self.create_upload_file(data=b'hi!') tx = await self.daemon.jsonrpc_stream_create(name, '0.19', file_path=file_path, allow_duplicate_name=True) await self.ledger.wait(tx) third_claim_id = tx.outputs[0].claim_id wallet = self.daemon.wallet_manager.get_wallet_or_default(None) funding_accounts = wallet.get_accounts_or_all(None) amount = self.daemon.get_dewies_or_error("amount", '0.1') account = wallet.get_account_or_default(None) claim_address = await account.receiving.get_or_create_usable_address() tx = await Transaction.support( 'derp', third_claim_id, amount, claim_address, funding_accounts, funding_accounts[0], None ) await tx.sign(funding_accounts) await self.daemon.broadcast_or_release(tx, True) await self.ledger.wait(tx) await self.generate(1) await self.assertMatchClaimIsWinning(name, third_claim_id) await self.generate(1) await self.assertMatchClaimIsWinning(name, third_claim_id) async def test_early_takeover_abandoned_controlling_support(self): name = 'derp' # block 207 first_claim_id = (await self.stream_create(name, '0.1', allow_duplicate_name=True))['outputs'][0][ 'claim_id'] tx = await self.daemon.jsonrpc_support_create(first_claim_id, '0.2') await self.ledger.wait(tx) await self.assertMatchClaimIsWinning(name, first_claim_id) await self.generate(96) # block 304, activates at 307 second_claim_id = (await self.stream_create(name, '0.2', allow_duplicate_name=True))['outputs'][0][ 'claim_id'] # block 305, activates at 308 (but gets triggered early by the takeover by the second claim) third_claim_id = (await self.stream_create(name, '0.3', allow_duplicate_name=True))['outputs'][0][ 'claim_id'] self.assertNotEqual(first_claim_id, second_claim_id) # takeover should not have happened yet await self.assertMatchClaimIsWinning(name, first_claim_id) await self.generate(1) await self.assertMatchClaimIsWinning(name, first_claim_id) await self.daemon.jsonrpc_txo_spend(type='support', txid=tx.id) await self.generate(1) await self.assertMatchClaimIsWinning(name, third_claim_id) await self.generate(1) await self.assertMatchClaimIsWinning(name, third_claim_id) async def test_block_takeover_with_delay_1_support(self): name = 'derp' # initially claim the name first_claim_id = (await self.stream_create(name, '0.1'))['outputs'][0]['claim_id'] self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.generate(320) # a claim of higher amount made now will have a takeover delay of 10 second_claim_id = (await self.stream_create(name, '0.2', allow_duplicate_name=True))['outputs'][0]['claim_id'] # sanity check self.assertNotEqual(first_claim_id, second_claim_id) # takeover should not have happened yet await self.assertMatchClaimIsWinning(name, first_claim_id) await self.generate(8) await self.assertMatchClaimIsWinning(name, first_claim_id) # prevent the takeover by adding a support one block before the takeover happens await self.support_create(first_claim_id, bid='1.0') # one more block until activation await self.generate(1) await self.assertMatchClaimIsWinning(name, first_claim_id) async def test_block_takeover_with_delay_0_support(self): name = 'derp' # initially claim the name first_claim_id = (await self.stream_create(name, '0.1'))['outputs'][0]['claim_id'] await self.assertMatchClaimIsWinning(name, first_claim_id) await self.generate(320) # a claim of higher amount made now will have a takeover delay of 10 second_claim_id = (await self.stream_create(name, '0.2', allow_duplicate_name=True))['outputs'][0]['claim_id'] # sanity check await self.assertMatchClaimIsWinning(name, first_claim_id) # takeover should not have happened yet await self.assertMatchClaimIsWinning(name, first_claim_id) await self.generate(9) await self.assertMatchClaimIsWinning(name, first_claim_id) # prevent the takeover by adding a support on the same block the takeover would happen await self.support_create(first_claim_id, bid='1.0') await self.assertMatchClaimIsWinning(name, first_claim_id) async def _test_almost_prevent_takeover(self, name: str, blocks: int = 9): # initially claim the name first_claim_id = (await self.stream_create(name, '0.1'))['outputs'][0]['claim_id'] await self.assertMatchClaimIsWinning(name, first_claim_id) await self.generate(320) # a claim of higher amount made now will have a takeover delay of 10 second_claim_id = (await self.stream_create(name, '0.2', allow_duplicate_name=True))['outputs'][0]['claim_id'] # sanity check self.assertNotEqual(first_claim_id, second_claim_id) # takeover should not have happened yet await self.assertMatchClaimIsWinning(name, first_claim_id) await self.generate(blocks) await self.assertMatchClaimIsWinning(name, first_claim_id) # prevent the takeover by adding a support on the same block the takeover would happen tx = await self.daemon.jsonrpc_support_create(first_claim_id, '1.0') await self.ledger.wait(tx) return first_claim_id, second_claim_id, tx async def test_almost_prevent_takeover_remove_support_same_block_supported(self): name = 'derp' first_claim_id, second_claim_id, tx = await self._test_almost_prevent_takeover(name, 9) await self.daemon.jsonrpc_txo_spend(type='support', txid=tx.id) await self.generate(1) await self.assertMatchClaimIsWinning(name, second_claim_id) async def test_almost_prevent_takeover_remove_support_one_block_after_supported(self): name = 'derp' first_claim_id, second_claim_id, tx = await self._test_almost_prevent_takeover(name, 8) await self.generate(1) await self.daemon.jsonrpc_txo_spend(type='support', txid=tx.id) await self.generate(1) await self.assertMatchClaimIsWinning(name, second_claim_id) async def test_abandon_before_takeover(self): name = 'derp' # initially claim the name first_claim_id = (await self.stream_create(name, '0.1'))['outputs'][0]['claim_id'] await self.assertMatchClaimIsWinning(name, first_claim_id) await self.generate(320) # a claim of higher amount made now will have a takeover delay of 10 second_claim_id = (await self.stream_create(name, '0.2', allow_duplicate_name=True))['outputs'][0]['claim_id'] # sanity check self.assertNotEqual(first_claim_id, second_claim_id) # takeover should not have happened yet await self.assertMatchClaimIsWinning(name, first_claim_id) await self.generate(8) await self.assertMatchClaimIsWinning(name, first_claim_id) # abandon the winning claim await self.daemon.jsonrpc_txo_spend(type='stream', claim_id=first_claim_id) await self.generate(1) # the takeover and activation should happen a block earlier than they would have absent the abandon await self.assertMatchClaimIsWinning(name, second_claim_id) await self.generate(1) await self.assertMatchClaimIsWinning(name, second_claim_id) async def test_abandon_before_takeover_no_delay_update(self): # TODO: fix race condition line 506 name = 'derp' # initially claim the name first_claim_id = (await self.stream_create(name, '0.1'))['outputs'][0]['claim_id'] await self.assertMatchClaimIsWinning(name, first_claim_id) await self.generate(320) # block 527 # a claim of higher amount made now will have a takeover delay of 10 second_claim_id = (await self.stream_create(name, '0.2', allow_duplicate_name=True))['outputs'][0]['claim_id'] # block 528 # sanity check self.assertNotEqual(first_claim_id, second_claim_id) # takeover should not have happened yet await self.assertMatchClaimIsWinning(name, first_claim_id) await self.assertMatchClaimsForName(name) await self.generate(8) await self.assertMatchClaimIsWinning(name, first_claim_id) await self.assertMatchClaimsForName(name) # abandon the winning claim await self.daemon.jsonrpc_txo_spend(type='stream', claim_id=first_claim_id) await self.daemon.jsonrpc_stream_update(second_claim_id, '0.1') await self.generate(1) # the takeover and activation should happen a block earlier than they would have absent the abandon await self.assertMatchClaimIsWinning(name, second_claim_id) await self.assertMatchClaimsForName(name) await self.generate(1) # await self.ledger.on_header.where(lambda e: e.height == 537) await self.assertMatchClaimIsWinning(name, second_claim_id) await self.assertMatchClaimsForName(name) async def test_abandon_controlling_support_before_pending_takeover(self): name = 'derp' # initially claim the name first_claim_id = (await self.stream_create(name, '0.1'))['outputs'][0]['claim_id'] controlling_support_tx = await self.daemon.jsonrpc_support_create(first_claim_id, '0.9') await self.ledger.wait(controlling_support_tx) self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.generate(321) second_claim_id = (await self.stream_create(name, '0.9', allow_duplicate_name=True))['outputs'][0]['claim_id'] self.assertNotEqual(first_claim_id, second_claim_id) # takeover should not have happened yet await self.assertMatchClaimIsWinning(name, first_claim_id) await self.generate(8) await self.assertMatchClaimIsWinning(name, first_claim_id) # abandon the support that causes the winning claim to have the highest staked tx = await self.daemon.jsonrpc_txo_spend(type='support', txid=controlling_support_tx.id) await self.generate(1) await self.assertMatchClaimIsWinning(name, first_claim_id) # await self.assertMatchClaim(second_claim_id) await self.generate(1) await self.assertMatchClaim(first_claim_id) await self.assertMatchClaimIsWinning(name, second_claim_id) async def test_remove_controlling_support(self): name = 'derp' # initially claim the name first_claim_id = (await self.stream_create(name, '0.2'))['outputs'][0]['claim_id'] first_support_tx = await self.daemon.jsonrpc_support_create(first_claim_id, '0.9') await self.ledger.wait(first_support_tx) await self.assertMatchClaimIsWinning(name, first_claim_id) await self.generate(320) # give the first claim long enough for a 10 block takeover delay await self.assertMatchClaimIsWinning(name, first_claim_id) # make a second claim which will take over the name second_claim_id = (await self.stream_create(name, '0.1', allow_duplicate_name=True))['outputs'][0]['claim_id'] self.assertNotEqual(first_claim_id, second_claim_id) second_claim_support_tx = await self.daemon.jsonrpc_support_create(second_claim_id, '1.5') await self.ledger.wait(second_claim_support_tx) await self.generate(1) # neither the second claim or its support have activated yet await self.assertMatchClaimIsWinning(name, first_claim_id) await self.generate(9) # claim activates, but is not yet winning await self.assertMatchClaimIsWinning(name, first_claim_id) await self.generate(1) # support activates, takeover happens await self.assertMatchClaimIsWinning(name, second_claim_id) await self.daemon.jsonrpc_txo_spend(type='support', claim_id=second_claim_id, blocking=True) await self.generate(1) # support activates, takeover happens await self.assertMatchClaimIsWinning(name, first_claim_id) async def test_claim_expiration(self): name = 'derp' # starts at height 206 vanishing_claim = (await self.stream_create('vanish', '0.1'))['outputs'][0]['claim_id'] await self.generate(493) # in block 701 and 702 first_claim_id = (await self.stream_create(name, '0.3'))['outputs'][0]['claim_id'] await self.assertMatchClaimIsWinning('vanish', vanishing_claim) await self.generate(100) # block 801, expiration fork happened await self.assertNoClaimForName('vanish') # second claim is in block 802 second_claim_id = (await self.stream_create(name, '0.2', allow_duplicate_name=True))['outputs'][0]['claim_id'] await self.assertMatchClaimIsWinning(name, first_claim_id) await self.generate(498) await self.assertMatchClaimIsWinning(name, first_claim_id) await self.generate(1) await self.assertMatchClaimIsWinning(name, second_claim_id) await self.generate(100) await self.assertMatchClaimIsWinning(name, second_claim_id) await self.generate(1) await self.assertNoClaimForName(name) class ResolveAfterReorg(BaseResolveTestCase): async def reorg(self, start): blocks = self.ledger.headers.height - start self.blockchain.block_expected = start - 1 # go back to start await self.blockchain.invalidate_block((await self.ledger.headers.hash(start)).decode()) # go to previous + 1 await self.generate(blocks + 2) async def assertBlockHash(self, height): bp = self.conductor.spv_node.server.bp def get_txids(): return [ bp.db.fs_tx_hash(tx_num)[0][::-1].hex() for tx_num in range(bp.db.tx_counts[height - 1], bp.db.tx_counts[height]) ] block_hash = await self.blockchain.get_block_hash(height) self.assertEqual(block_hash, (await self.ledger.headers.hash(height)).decode()) self.assertEqual(block_hash, (await bp.db.fs_block_hashes(height, 1))[0][::-1].hex()) txids = await asyncio.get_event_loop().run_in_executor(bp.db.executor, get_txids) txs = await bp.db.fs_transactions(txids) block_txs = (await bp.daemon.deserialised_block(block_hash))['tx'] self.assertSetEqual(set(block_txs), set(txs.keys()), msg='leveldb/lbrycrd is missing transactions') self.assertListEqual(block_txs, list(txs.keys()), msg='leveldb/lbrycrd transactions are of order') async def test_reorg(self): self.assertEqual(self.ledger.headers.height, 206) channel_name = '@abc' channel_id = self.get_claim_id( await self.channel_create(channel_name, '0.01') ) self.assertEqual(channel_id, (await self.assertMatchWinningClaim(channel_name)).claim_hash.hex()) await self.reorg(206) self.assertEqual(channel_id, (await self.assertMatchWinningClaim(channel_name)).claim_hash.hex()) # await self.assertNoClaimForName(channel_name) # self.assertNotIn('error', await self.resolve(channel_name)) stream_name = 'foo' stream_id = self.get_claim_id( await self.stream_create(stream_name, '0.01', channel_id=channel_id) ) self.assertEqual(stream_id, (await self.assertMatchWinningClaim(stream_name)).claim_hash.hex()) await self.reorg(206) self.assertEqual(stream_id, (await self.assertMatchWinningClaim(stream_name)).claim_hash.hex()) await self.support_create(stream_id, '0.01') self.assertNotIn('error', await self.resolve(stream_name)) self.assertEqual(stream_id, (await self.assertMatchWinningClaim(stream_name)).claim_hash.hex()) await self.reorg(206) # self.assertNotIn('error', await self.resolve(stream_name)) self.assertEqual(stream_id, (await self.assertMatchWinningClaim(stream_name)).claim_hash.hex()) await self.stream_abandon(stream_id) self.assertNotIn('error', await self.resolve(channel_name)) self.assertIn('error', await self.resolve(stream_name)) self.assertEqual(channel_id, (await self.assertMatchWinningClaim(channel_name)).claim_hash.hex()) await self.assertNoClaimForName(stream_name) # TODO: check @abc/foo too await self.reorg(206) self.assertNotIn('error', await self.resolve(channel_name)) self.assertIn('error', await self.resolve(stream_name)) self.assertEqual(channel_id, (await self.assertMatchWinningClaim(channel_name)).claim_hash.hex()) await self.assertNoClaimForName(stream_name) await self.channel_abandon(channel_id) self.assertIn('error', await self.resolve(channel_name)) self.assertIn('error', await self.resolve(stream_name)) await self.reorg(206) self.assertIn('error', await self.resolve(channel_name)) self.assertIn('error', await self.resolve(stream_name)) async def test_reorg_change_claim_height(self): # sanity check result = await self.resolve('hovercraft') # TODO: do these for claim_search and resolve both self.assertIn('error', result) still_valid = await self.daemon.jsonrpc_stream_create( 'still-valid', '1.0', file_path=self.create_upload_file(data=b'hi!') ) await self.ledger.wait(still_valid) await self.generate(1) # create a claim and verify it's returned by claim_search self.assertEqual(self.ledger.headers.height, 207) await self.assertBlockHash(207) broadcast_tx = await self.daemon.jsonrpc_stream_create( 'hovercraft', '1.0', file_path=self.create_upload_file(data=b'hi!') ) await self.ledger.wait(broadcast_tx) await self.generate(1) await self.ledger.wait(broadcast_tx, self.blockchain.block_expected) self.assertEqual(self.ledger.headers.height, 208) await self.assertBlockHash(208) claim = await self.resolve('hovercraft') self.assertEqual(claim['txid'], broadcast_tx.id) self.assertEqual(claim['height'], 208) # check that our tx is in block 208 as returned by lbrycrdd invalidated_block_hash = (await self.ledger.headers.hash(208)).decode() block_207 = await self.blockchain.get_block(invalidated_block_hash) self.assertIn(claim['txid'], block_207['tx']) self.assertEqual(208, claim['height']) # reorg the last block dropping our claim tx await self.blockchain.invalidate_block(invalidated_block_hash) await self.blockchain.clear_mempool() await self.blockchain.generate(2) # wait for the client to catch up and verify the reorg await asyncio.wait_for(self.on_header(209), 3.0) await self.assertBlockHash(207) await self.assertBlockHash(208) await self.assertBlockHash(209) # verify the claim was dropped from block 208 as returned by lbrycrdd reorg_block_hash = await self.blockchain.get_block_hash(208) self.assertNotEqual(invalidated_block_hash, reorg_block_hash) block_207 = await self.blockchain.get_block(reorg_block_hash) self.assertNotIn(claim['txid'], block_207['tx']) client_reorg_block_hash = (await self.ledger.headers.hash(208)).decode() self.assertEqual(client_reorg_block_hash, reorg_block_hash) # verify the dropped claim is no longer returned by claim search self.assertDictEqual( {'error': {'name': 'NOT_FOUND', 'text': 'Could not find claim at "hovercraft".'}}, await self.resolve('hovercraft') ) # verify the claim published a block earlier wasn't also reverted self.assertEqual(207, (await self.resolve('still-valid'))['height']) # broadcast the claim in a different block new_txid = await self.blockchain.sendrawtransaction(hexlify(broadcast_tx.raw).decode()) self.assertEqual(broadcast_tx.id, new_txid) await self.blockchain.generate(1) # wait for the client to catch up await asyncio.wait_for(self.on_header(210), 1.0) # verify the claim is in the new block and that it is returned by claim_search republished = await self.resolve('hovercraft') self.assertEqual(210, republished['height']) self.assertEqual(claim['claim_id'], republished['claim_id']) # this should still be unchanged self.assertEqual(207, (await self.resolve('still-valid'))['height']) async def test_reorg_drop_claim(self): # sanity check result = await self.resolve('hovercraft') # TODO: do these for claim_search and resolve both self.assertIn('error', result) still_valid = await self.daemon.jsonrpc_stream_create( 'still-valid', '1.0', file_path=self.create_upload_file(data=b'hi!') ) await self.ledger.wait(still_valid) await self.generate(1) # create a claim and verify it's returned by claim_search self.assertEqual(self.ledger.headers.height, 207) await self.assertBlockHash(207) broadcast_tx = await self.daemon.jsonrpc_stream_create( 'hovercraft', '1.0', file_path=self.create_upload_file(data=b'hi!') ) await self.ledger.wait(broadcast_tx) await self.generate(1) await self.ledger.wait(broadcast_tx, self.blockchain.block_expected) self.assertEqual(self.ledger.headers.height, 208) await self.assertBlockHash(208) claim = await self.resolve('hovercraft') self.assertEqual(claim['txid'], broadcast_tx.id) self.assertEqual(claim['height'], 208) # check that our tx is in block 208 as returned by lbrycrdd invalidated_block_hash = (await self.ledger.headers.hash(208)).decode() block_207 = await self.blockchain.get_block(invalidated_block_hash) self.assertIn(claim['txid'], block_207['tx']) self.assertEqual(208, claim['height']) # reorg the last block dropping our claim tx await self.blockchain.invalidate_block(invalidated_block_hash) await self.blockchain.clear_mempool() await self.blockchain.generate(2) # wait for the client to catch up and verify the reorg await asyncio.wait_for(self.on_header(209), 3.0) await self.assertBlockHash(207) await self.assertBlockHash(208) await self.assertBlockHash(209) # verify the claim was dropped from block 208 as returned by lbrycrdd reorg_block_hash = await self.blockchain.get_block_hash(208) self.assertNotEqual(invalidated_block_hash, reorg_block_hash) block_207 = await self.blockchain.get_block(reorg_block_hash) self.assertNotIn(claim['txid'], block_207['tx']) client_reorg_block_hash = (await self.ledger.headers.hash(208)).decode() self.assertEqual(client_reorg_block_hash, reorg_block_hash) # verify the dropped claim is no longer returned by claim search self.assertDictEqual( {'error': {'name': 'NOT_FOUND', 'text': 'Could not find claim at "hovercraft".'}}, await self.resolve('hovercraft') ) # verify the claim published a block earlier wasn't also reverted self.assertEqual(207, (await self.resolve('still-valid'))['height']) # broadcast the claim in a different block new_txid = await self.blockchain.sendrawtransaction(hexlify(broadcast_tx.raw).decode()) self.assertEqual(broadcast_tx.id, new_txid) await self.blockchain.generate(1) # wait for the client to catch up await asyncio.wait_for(self.on_header(210), 1.0) # verify the claim is in the new block and that it is returned by claim_search republished = await self.resolve('hovercraft') self.assertEqual(210, republished['height']) self.assertEqual(claim['claim_id'], republished['claim_id']) # this should still be unchanged self.assertEqual(207, (await self.resolve('still-valid'))['height']) def generate_signed_legacy(address: bytes, output: Output): decoded_address = Base58.decode(address) claim = OldClaimMessage() claim.ParseFromString(unhexlify( '080110011aee04080112a604080410011a2b4865726520617265203520526561736f6e73204920e29da4e' 'fb88f204e657874636c6f7564207c20544c4722920346696e64206f7574206d6f72652061626f7574204e' '657874636c6f75643a2068747470733a2f2f6e657874636c6f75642e636f6d2f0a0a596f752063616e206' '6696e64206d65206f6e20746865736520736f6369616c733a0a202a20466f72756d733a2068747470733a' '2f2f666f72756d2e6865617679656c656d656e742e696f2f0a202a20506f64636173743a2068747470733' 'a2f2f6f6666746f706963616c2e6e65740a202a2050617472656f6e3a2068747470733a2f2f7061747265' '6f6e2e636f6d2f7468656c696e757867616d65720a202a204d657263683a2068747470733a2f2f7465657' '37072696e672e636f6d2f73746f7265732f6f6666696369616c2d6c696e75782d67616d65720a202a2054' '77697463683a2068747470733a2f2f7477697463682e74762f786f6e64616b0a202a20547769747465723' 'a2068747470733a2f2f747769747465722e636f6d2f7468656c696e757867616d65720a0a2e2e2e0a6874' '7470733a2f2f7777772e796f75747562652e636f6d2f77617463683f763d4672546442434f535f66632a0' 'f546865204c696e75782047616d6572321c436f7079726967687465642028636f6e746163742061757468' '6f722938004a2968747470733a2f2f6265726b2e6e696e6a612f7468756d626e61696c732f46725464424' '34f535f666352005a001a41080110011a30040e8ac6e89c061f982528c23ad33829fd7146435bf7a4cc22' 'f0bff70c4fe0b91fd36da9a375e3e1c171db825bf5d1f32209766964656f2f6d70342a5c080110031a406' '2b2dd4c45e364030fbfad1a6fefff695ebf20ea33a5381b947753e2a0ca359989a5cc7d15e5392a0d354c' '0b68498382b2701b22c03beb8dcb91089031b871e72214feb61536c007cdf4faeeaab4876cb397feaf6b51' )) claim.ClearField("publisherSignature") digest = sha256(b''.join([ decoded_address, claim.SerializeToString(), output.claim_hash[::-1] ])) signature = output.private_key.sign_digest_deterministic(digest, hashfunc=hashlib.sha256) claim.publisherSignature.version = 1 claim.publisherSignature.signatureType = 1 claim.publisherSignature.signature = signature claim.publisherSignature.certificateId = output.claim_hash[::-1] return claim