diff --git a/lbry/testcase.py b/lbry/testcase.py index 54244f5c8..1845eade4 100644 --- a/lbry/testcase.py +++ b/lbry/testcase.py @@ -490,13 +490,15 @@ class CommandTestCase(IntegrationTestCase): """ Synchronous version of `out` method. """ return json.loads(jsonrpc_dumps_pretty(value, ledger=self.ledger))['result'] - async def confirm_and_render(self, awaitable, confirm) -> Transaction: + async def confirm_and_render(self, awaitable, confirm, return_tx=False) -> Transaction: tx = await awaitable if confirm: await self.ledger.wait(tx) await self.generate(1) await self.ledger.wait(tx, self.blockchain.block_expected) - return self.sout(tx) + if not return_tx: + return self.sout(tx) + return tx def create_upload_file(self, data, prefix=None, suffix=None): file_path = tempfile.mktemp(prefix=prefix or "tmp", suffix=suffix or "", dir=self.daemon.conf.upload_dir) @@ -507,19 +509,19 @@ class CommandTestCase(IntegrationTestCase): async def stream_create( self, name='hovercraft', bid='1.0', file_path=None, - data=b'hi!', confirm=True, prefix=None, suffix=None, **kwargs): + data=b'hi!', confirm=True, prefix=None, suffix=None, return_tx=False, **kwargs): if file_path is None and data is not None: file_path = self.create_upload_file(data=data, prefix=prefix, suffix=suffix) return await self.confirm_and_render( - self.daemon.jsonrpc_stream_create(name, bid, file_path=file_path, **kwargs), confirm + self.daemon.jsonrpc_stream_create(name, bid, file_path=file_path, **kwargs), confirm, return_tx ) async def stream_update( - self, claim_id, data=None, prefix=None, suffix=None, confirm=True, **kwargs): + self, claim_id, data=None, prefix=None, suffix=None, confirm=True, return_tx=False, **kwargs): if data is not None: file_path = self.create_upload_file(data=data, prefix=prefix, suffix=suffix) return await self.confirm_and_render( - self.daemon.jsonrpc_stream_update(claim_id, file_path=file_path, **kwargs), confirm + self.daemon.jsonrpc_stream_update(claim_id, file_path=file_path, **kwargs), confirm, return_tx ) return await self.confirm_and_render( self.daemon.jsonrpc_stream_update(claim_id, **kwargs), confirm diff --git a/lbry/wallet/server/block_processor.py b/lbry/wallet/server/block_processor.py index 5fe9663a0..b1648a615 100644 --- a/lbry/wallet/server/block_processor.py +++ b/lbry/wallet/server/block_processor.py @@ -614,7 +614,8 @@ class BlockProcessor: self.db_op_stack.extend(claim.get_invalidate_signature_ops()) for staged in list(self.txo_to_claim.values()): - if staged.signing_hash == claim_hash and staged.claim_hash not in self.doesnt_have_valid_signature: + needs_invalidate = staged.claim_hash not in self.doesnt_have_valid_signature + if staged.signing_hash == claim_hash and needs_invalidate: self.db_op_stack.extend(staged.get_invalidate_signature_ops()) self.txo_to_claim[self.claim_hash_to_txo[staged.claim_hash]] = staged.invalidate_signature() self.signatures_changed.add(staged.claim_hash) @@ -1173,8 +1174,18 @@ class BlockProcessor: ) # Handle abandoned claims + abandoned_channels = {} + # abandon the channels last to handle abandoned signed claims in the same tx, + # see test_abandon_channel_and_claims_in_same_tx for abandoned_claim_hash, (tx_num, nout, name) in spent_claims.items(): - # print(f"\tabandon {abandoned_claim_hash.hex()} {tx_num} {nout}") + if name.startswith('@'): + abandoned_channels[abandoned_claim_hash] = (tx_num, nout, name) + else: + # print(f"\tabandon {name} {abandoned_claim_hash.hex()} {tx_num} {nout}") + self._abandon_claim(abandoned_claim_hash, tx_num, nout, name) + + for abandoned_claim_hash, (tx_num, nout, name) in abandoned_channels.items(): + # print(f"\tabandon {name} {abandoned_claim_hash.hex()} {tx_num} {nout}") self._abandon_claim(abandoned_claim_hash, tx_num, nout, name) self.db.total_transactions.append(tx_hash) diff --git a/lbry/wallet/server/leveldb.py b/lbry/wallet/server/leveldb.py index 20aa5cf65..432ba0a77 100644 --- a/lbry/wallet/server/leveldb.py +++ b/lbry/wallet/server/leveldb.py @@ -219,9 +219,6 @@ class LevelDB: return f'{name}#{k.partial_claim_id}' break print(f"{claim_id} has a collision") - # FIXME: there are a handful of claims that appear to have short id collisions but really do not - # these claims are actually abandoned, but are not handled correctly because they are abandoned in the - # same tx as their channel. return f'{name}#{claim_id}' def _prepare_resolve_result(self, tx_num: int, position: int, claim_hash: bytes, name: str, root_tx_num: int, @@ -246,8 +243,10 @@ class LevelDB: if channel_hash: channel_vals = self.get_claim_txo(channel_hash) if channel_vals: - channel_name = channel_vals.name - canonical_url = f'{channel_name}#{channel_hash.hex()}/{name}#{claim_hash.hex()}' + channel_short_url = self.get_short_claim_id_url( + channel_vals.name, channel_hash, channel_vals.root_tx_num, channel_vals.root_position + ) + canonical_url = f'{channel_short_url}/{short_url}' return ResolveResult( name, claim_hash, tx_num, position, tx_hash, height, claim_amount, short_url=short_url, is_controlling=controlling_claim.claim_hash == claim_hash, canonical_url=canonical_url, @@ -552,11 +551,6 @@ class LevelDB: ) tags = list(set(claim_tags).union(set(reposted_tags))) languages = list(set(claim_languages).union(set(reposted_languages))) - canonical_url = f'{claim.name}#{claim.claim_hash.hex()}' - if metadata.is_signed: - channel = self.get_claim_txo(metadata.signing_channel_hash[::-1]) - if channel: - canonical_url = f'{channel.name}#{metadata.signing_channel_hash[::-1].hex()}/{canonical_url}' value = { 'claim_hash': claim_hash[::-1], # 'claim_id': claim_hash.hex(), @@ -576,10 +570,8 @@ class LevelDB: 'support_amount': claim.support_amount, 'is_controlling': claim.is_controlling, 'last_take_over_height': claim.last_takeover_height, - - 'short_url': f'{claim.name}#{claim.claim_hash.hex()}', # TODO: fix - 'canonical_url': canonical_url, - + 'short_url': claim.short_url, + 'canonical_url': claim.canonical_url, 'title': None if not metadata.is_stream else metadata.stream.title, 'author': None if not metadata.is_stream else metadata.stream.author, 'description': None if not metadata.is_stream else metadata.stream.description, diff --git a/tests/integration/blockchain/test_resolve_command.py b/tests/integration/blockchain/test_resolve_command.py index 2c443944e..702d39a1d 100644 --- a/tests/integration/blockchain/test_resolve_command.py +++ b/tests/integration/blockchain/test_resolve_command.py @@ -130,6 +130,27 @@ class ResolveCommand(BaseResolveTestCase): await self.assertResolvesToClaimId(f'@abc#{colliding_claim_ids[1][:17]}', colliding_claim_ids[1]) await self.assertResolvesToClaimId(f'@abc#{colliding_claim_ids[1]}', colliding_claim_ids[1]) + async def test_abandon_channel_and_claims_in_same_tx(self): + channel_id = self.get_claim_id( + await self.channel_create('@abc', '0.01') + ) + await self.stream_create('foo', '0.01', channel_id=channel_id) + await self.channel_update(channel_id, bid='0.001') + foo2_id = self.get_claim_id(await self.stream_create('foo2', '0.01', channel_id=channel_id)) + await self.stream_update(foo2_id, bid='0.0001', channel_id=channel_id, confirm=False) + tx = await self.stream_create('foo3', '0.01', channel_id=channel_id, confirm=False, return_tx=True) + await self.ledger.wait(tx) + + # db = self.conductor.spv_node.server.bp.db + # claims = list(db.all_claims_producer()) + # print("claims", claims) + await self.daemon.jsonrpc_txo_spend(blocking=True) + await self.generate(1) + await self.assertNoClaimForName('@abc') + await self.assertNoClaimForName('foo') + await self.assertNoClaimForName('foo2') + await self.assertNoClaimForName('foo3') + async def test_resolve_response(self): channel_id = self.get_claim_id( await self.channel_create('@abc', '0.01')