diff --git a/tests/integration/blockchain/test_blockchain_reorganization.py b/tests/integration/blockchain/test_blockchain_reorganization.py index 40a748e9d..b7fef197d 100644 --- a/tests/integration/blockchain/test_blockchain_reorganization.py +++ b/tests/integration/blockchain/test_blockchain_reorganization.py @@ -154,3 +154,82 @@ class BlockchainReorganizationTests(CommandTestCase): # 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']) diff --git a/tests/integration/blockchain/test_resolve_command.py b/tests/integration/blockchain/test_resolve_command.py index a0987a7a6..49eda2fe0 100644 --- a/tests/integration/blockchain/test_resolve_command.py +++ b/tests/integration/blockchain/test_resolve_command.py @@ -19,6 +19,56 @@ class BaseResolveTestCase(CommandTestCase): 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)) + resolved, _ = await self.conductor.spv_node.server.bp.db.fs_getclaimbyid(claim_id) + print(expected) + print(resolved) + self.assertDictEqual({ + 'claim_id': expected['claimId'], + 'activation_height': expected['validAtHeight'], + 'last_takeover_height': expected['lastTakeoverHeight'], + 'txid': expected['txId'], + 'nout': expected['n'], + 'amount': expected['amount'], + 'effective_amount': expected['effectiveAmount'] + }, { + 'claim_id': resolved.claim_hash.hex(), + 'activation_height': resolved.activation_height, + 'last_takeover_height': resolved.last_takeover_height, + 'txid': resolved.tx_hash[::-1].hex(), + 'nout': resolved.position, + 'amount': resolved.amount, + 'effective_amount': resolved.effective_amount + }) + return resolved + + async def assertMatchClaimIsWinning(self, name, claim_id): + self.assertEqual(claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + await self.assertMatchClaim(claim_id) + class ResolveCommand(BaseResolveTestCase): @@ -126,45 +176,45 @@ class ResolveCommand(BaseResolveTestCase): 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_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] @@ -224,6 +274,11 @@ class ResolveCommand(BaseResolveTestCase): winner_id = self.get_claim_id(c) + # winning_one = await self.check_lbrycrd_winning(one) + winning_two = await self.assertMatchWinningClaim(two) + + self.assertEqual(winner_id, winning_two.claim_hash.hex()) + r1 = await self.resolve(f'lbry://{one}') r2 = await self.resolve(f'lbry://{two}') @@ -329,6 +384,201 @@ class ResolveCommand(BaseResolveTestCase): self.assertNotIn('received_tips', resolve) +class ResolveClaimTakeovers(BaseResolveTestCase): + async def test_activation_delay(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 + self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + await self.generate(9) + # not yet + self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + await self.generate(1) + # the new claim should have activated + self.assertEqual(second_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + + 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 + self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + await self.generate(8) + self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + # 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) + self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + + 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'] + 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 + self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + await self.generate(9) + self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + # 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') + self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + + 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'] + 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 + self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + await self.generate(blocks) + self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + # 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) + self.assertEqual(second_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + + 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) + self.assertEqual(second_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + + 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'] + 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 + self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + await self.generate(8) + self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + # 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 + self.assertEqual(second_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + await self.generate(1) + self.assertEqual(second_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + + 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'] + self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + 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 + self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + await self.generate(8) + self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + # 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 + self.assertEqual(second_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + await self.generate(1) + # await self.ledger.on_header.where(lambda e: e.height == 537) + self.assertEqual(second_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + + 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, '1.1', allow_duplicate_name=True))['outputs'][0]['claim_id'] + self.assertNotEqual(first_claim_id, second_claim_id) + # takeover should not have happened yet + self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + await self.generate(8) + self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + # 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) + self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + await self.generate(1) + self.assertEqual(second_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + + async def test_remove_controlling_support(self): + name = 'derp' + # initially claim the name + first_claim_id = (await self.stream_create(name, '0.1'))['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) + self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + + await self.generate(321) # give the first claim long enough for a 10 block takeover delay + + # make a second claim which will take over the name + second_claim_id = (await self.stream_create(name, '0.2', allow_duplicate_name=True))['outputs'][0]['claim_id'] + second_claim_support_tx = await self.daemon.jsonrpc_support_create(second_claim_id, '1.0') + await self.ledger.wait(second_claim_support_tx) + self.assertNotEqual(first_claim_id, second_claim_id) + + # the name resolves to the first claim + self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + await self.generate(9) + # still resolves to the first claim + self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + await self.generate(1) # second claim takes over + self.assertEqual(second_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + await self.generate(33) # give the second claim long enough for a 1 block takeover delay + self.assertEqual(second_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + # abandon the support that causes the winning claim to have the highest staked + await self.daemon.jsonrpc_txo_spend(type='support', txid=second_claim_support_tx.id) + await self.generate(1) + self.assertEqual(second_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + await self.generate(1) # first claim takes over + self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) + + class ResolveAfterReorg(BaseResolveTestCase): async def reorg(self, start): @@ -339,6 +589,26 @@ class ResolveAfterReorg(BaseResolveTestCase): # 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) @@ -346,29 +616,40 @@ class ResolveAfterReorg(BaseResolveTestCase): channel_id = self.get_claim_id( await self.channel_create(channel_name, '0.01') ) - self.assertNotIn('error', await self.resolve(channel_name)) + self.assertEqual(channel_id, (await self.assertMatchWinningClaim(channel_name)).claim_hash.hex()) await self.reorg(206) - self.assertNotIn('error', await self.resolve(channel_name)) + 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.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.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.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)) @@ -377,6 +658,164 @@ class ResolveAfterReorg(BaseResolveTestCase): 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)