Merge pull request #2756 from lbryio/blocked_resolve

resolve errors make distinction between truely not found claims and claims which were censored by wallet server
This commit is contained in:
Lex Berezhny 2020-02-01 14:00:37 -05:00 committed by GitHub
commit 638e3e6b3d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 105 additions and 32 deletions

View file

@ -51,11 +51,12 @@ Code | Name | Message
405 | ChannelKeyNotFound | Channel signing key not found. 405 | ChannelKeyNotFound | Channel signing key not found.
406 | ChannelKeyInvalid | Channel signing key is out of date. -- For example, channel was updated but you don't have the updated key. 406 | ChannelKeyInvalid | Channel signing key is out of date. -- For example, channel was updated but you don't have the updated key.
407 | DataDownload | Failed to download blob. *generic* 407 | DataDownload | Failed to download blob. *generic*
408 | Resolve | Failed to resolve '{url}'. 410 | Resolve | Failed to resolve '{url}'.
409 | ResolveTimeout | Failed to resolve '{url}' within the timeout. 411 | ResolveTimeout | Failed to resolve '{url}' within the timeout.
410 | KeyFeeAboveMaxAllowed | {message} 411 | ResolveCensored | Resolve of '{url}' was censored by channel with claim id '{censor_id}'.
411 | InvalidPassword | Password is invalid. 420 | KeyFeeAboveMaxAllowed | {message}
412 | IncompatibleWalletServer | '{server}:{port}' has an incompatibly old version. 421 | InvalidPassword | Password is invalid.
422 | IncompatibleWalletServer | '{server}:{port}' has an incompatibly old version.
**5xx** | Blob | **Blobs** **5xx** | Blob | **Blobs**
500 | BlobNotFound | Blob not found. 500 | BlobNotFound | Blob not found.
501 | BlobPermissionDenied | Permission denied to read blob. 501 | BlobPermissionDenied | Permission denied to read blob.

View file

@ -197,6 +197,12 @@ class ResolveTimeoutError(WalletError):
super().__init__(f"Failed to resolve '{url}' within the timeout.") super().__init__(f"Failed to resolve '{url}' within the timeout.")
class ResolveCensoredError(WalletError):
def __init__(self, url, censor_id):
super().__init__(f"Resolve of '{url}' was censored by channel with claim id '{censor_id}'.")
class KeyFeeAboveMaxAllowedError(WalletError): class KeyFeeAboveMaxAllowedError(WalletError):
def __init__(self, message): def __init__(self, message):

View file

@ -50,7 +50,7 @@ class ErrorClass:
def get_arguments(self): def get_arguments(self):
args = ['self'] args = ['self']
for arg in re.findall('{([a-z0-1]+)}', self.message): for arg in re.findall('{([a-z0-1_]+)}', self.message):
args.append(arg) args.append(arg)
return args return args

View file

@ -4,7 +4,13 @@ from typing import List
from binascii import hexlify from binascii import hexlify
from itertools import chain from itertools import chain
from lbry.error import ResolveCensoredError
from lbry.schema.types.v2.result_pb2 import Outputs as OutputsMessage from lbry.schema.types.v2.result_pb2 import Outputs as OutputsMessage
from lbry.schema.types.v2.result_pb2 import Error as ErrorMessage
INVALID = ErrorMessage.Code.Name(ErrorMessage.INVALID)
NOT_FOUND = ErrorMessage.Code.Name(ErrorMessage.NOT_FOUND)
BLOCKED = ErrorMessage.Code.Name(ErrorMessage.BLOCKED)
class Censor: class Censor:
@ -73,7 +79,12 @@ class Outputs:
def message_to_txo(self, txo_message, tx_map): def message_to_txo(self, txo_message, tx_map):
if txo_message.WhichOneof('meta') == 'error': if txo_message.WhichOneof('meta') == 'error':
return None return {
'error': {
'name': txo_message.error.Code.Name(txo_message.error.code).lower(),
'text': txo_message.error.text,
}
}
txo = tx_map[txo_message.tx_hash].outputs[txo_message.nout] txo = tx_map[txo_message.tx_hash].outputs[txo_message.nout]
if txo_message.WhichOneof('meta') == 'claim': if txo_message.WhichOneof('meta') == 'claim':
claim = txo_message.claim claim = txo_message.claim
@ -146,9 +157,11 @@ class Outputs:
if isinstance(txo, Exception): if isinstance(txo, Exception):
txo_message.error.text = txo.args[0] txo_message.error.text = txo.args[0]
if isinstance(txo, ValueError): if isinstance(txo, ValueError):
txo_message.error.code = txo_message.error.INVALID txo_message.error.code = ErrorMessage.INVALID
elif isinstance(txo, LookupError): elif isinstance(txo, LookupError):
txo_message.error.code = txo_message.error.NOT_FOUND txo_message.error.code = ErrorMessage.NOT_FOUND
elif isinstance(txo, ResolveCensoredError):
txo_message.error.code = ErrorMessage.BLOCKED
return return
txo_message.tx_hash = txo['txo_hash'][:32] txo_message.tx_hash = txo['txo_hash'][:32]
txo_message.nout, = struct.unpack('<I', txo['txo_hash'][32:]) txo_message.nout, = struct.unpack('<I', txo['txo_hash'][32:])

View file

@ -12,7 +12,7 @@ from binascii import hexlify, unhexlify
from typing import Dict, Tuple, Type, Iterable, List, Optional, DefaultDict from typing import Dict, Tuple, Type, Iterable, List, Optional, DefaultDict
import pylru import pylru
from lbry.schema.result import Outputs from lbry.schema.result import Outputs, INVALID, NOT_FOUND
from lbry.schema.url import URL from lbry.schema.url import URL
from lbry.crypto.hash import hash160, double_sha256, sha256 from lbry.crypto.hash import hash160, double_sha256, sha256
from lbry.crypto.base58 import Base58 from lbry.crypto.base58 import Base58
@ -661,13 +661,13 @@ class Ledger(metaclass=LedgerRegistry):
assert len(urls) == len(txos), "Mismatch between urls requested for resolve and responses received." assert len(urls) == len(txos), "Mismatch between urls requested for resolve and responses received."
result = {} result = {}
for url, txo in zip(urls, txos): for url, txo in zip(urls, txos):
if txo and URL.parse(url).has_stream_in_channel:
if not txo.channel or not txo.is_signed_by(txo.channel, self):
txo = None
if txo: if txo:
result[url] = txo if isinstance(txo, Output) and URL.parse(url).has_stream_in_channel:
if not txo.channel or not txo.is_signed_by(txo.channel, self):
txo = {'error': {'name': INVALID, 'text': f'{url} has invalid channel signature'}}
else: else:
result[url] = {'error': f'{url} did not resolve to a claim'} txo = {'error': {'name': NOT_FOUND, 'text': f'{url} did not resolve to a claim'}}
result[url] = txo
return result return result
async def claim_search(self, accounts, **kwargs) -> Tuple[List[Output], dict, int, int]: async def claim_search(self, accounts, **kwargs) -> Tuple[List[Output], dict, int, int]:

View file

@ -4,14 +4,14 @@ import apsw
import logging import logging
from operator import itemgetter from operator import itemgetter
from typing import Tuple, List, Dict, Union, Type, Optional from typing import Tuple, List, Dict, Union, Type, Optional
from binascii import unhexlify from binascii import unhexlify, hexlify
from decimal import Decimal from decimal import Decimal
from contextvars import ContextVar from contextvars import ContextVar
from functools import wraps from functools import wraps
from dataclasses import dataclass from dataclasses import dataclass
from lbry.wallet.database import query, interpolate from lbry.wallet.database import query, interpolate
from lbry.error import ResolveCensoredError
from lbry.schema.url import URL, normalize_name from lbry.schema.url import URL, normalize_name
from lbry.schema.tags import clean_tags from lbry.schema.tags import clean_tags
from lbry.schema.result import Outputs, Censor from lbry.schema.result import Outputs, Censor
@ -451,6 +451,8 @@ def resolve_url(raw_url):
matches = search_claims(censor, **query, limit=1) matches = search_claims(censor, **query, limit=1)
if matches: if matches:
channel = matches[0] channel = matches[0]
elif censor.censored:
return ResolveCensoredError(raw_url, hexlify(next(iter(censor.censored))[::-1]).decode())
else: else:
return LookupError(f'Could not find channel in "{raw_url}".') return LookupError(f'Could not find channel in "{raw_url}".')
@ -469,8 +471,10 @@ def resolve_url(raw_url):
matches = search_claims(censor, **query, limit=1) matches = search_claims(censor, **query, limit=1)
if matches: if matches:
return matches[0] return matches[0]
elif censor.censored:
return ResolveCensoredError(raw_url, hexlify(next(iter(censor.censored))[::-1]).decode())
else: else:
return LookupError(f'Could not find stream in "{raw_url}".') return LookupError(f'Could not find claim at "{raw_url}".')
return channel return channel

View file

@ -763,29 +763,48 @@ class StreamCommands(ClaimTestCase):
bad_content_id = self.get_claim_id( bad_content_id = self.get_claim_id(
await self.stream_create('bad_content', '1.1', channel_name='@some_channel', tags=['bad']) await self.stream_create('bad_content', '1.1', channel_name='@some_channel', tags=['bad'])
) )
blocking_channel_id = self.get_claim_id( filtering_channel_id = self.get_claim_id(
await self.channel_create('@filtering', '1.0') await self.channel_create('@filtering', '1.0')
) )
self.conductor.spv_node.server.db.sql.filtering_channel_hashes.add( self.conductor.spv_node.server.db.sql.filtering_channel_hashes.add(
unhexlify(blocking_channel_id)[::-1] unhexlify(filtering_channel_id)[::-1]
) )
await self.stream_repost(bad_content_id, 'filter1', '1.1', channel_name='@filtering') await self.stream_repost(bad_content_id, 'filter1', '1.1', channel_name='@filtering')
# search for blocked content directly # search for blocked content directly
result = await self.out(self.daemon.jsonrpc_claim_search(name='bad_content')) result = await self.out(self.daemon.jsonrpc_claim_search(name='bad_content'))
self.assertEqual([], result['items']) self.assertEqual([], result['items'])
self.assertEqual({"channels": {blocking_channel_id: 1}, "total": 1}, result['blocked']) self.assertEqual({"channels": {filtering_channel_id: 1}, "total": 1}, result['blocked'])
# search channel containing blocked content # search channel containing blocked content
result = await self.out(self.daemon.jsonrpc_claim_search(channel='@some_channel')) result = await self.out(self.daemon.jsonrpc_claim_search(channel='@some_channel'))
self.assertEqual(1, len(result['items'])) self.assertEqual(1, len(result['items']))
self.assertEqual({"channels": {blocking_channel_id: 1}, "total": 1}, result['blocked']) self.assertEqual({"channels": {filtering_channel_id: 1}, "total": 1}, result['blocked'])
# content was filtered by not_tag before censoring # content was filtered by not_tag before censoring
result = await self.out(self.daemon.jsonrpc_claim_search(channel='@some_channel', not_tags=["good", "bad"])) result = await self.out(self.daemon.jsonrpc_claim_search(channel='@some_channel', not_tags=["good", "bad"]))
self.assertEqual(0, len(result['items'])) self.assertEqual(0, len(result['items']))
self.assertEqual({"channels": {}, "total": 0}, result['blocked']) self.assertEqual({"channels": {}, "total": 0}, result['blocked'])
blocking_channel_id = self.get_claim_id(
await self.channel_create('@blocking', '1.0')
)
self.conductor.spv_node.server.db.sql.blocking_channel_hashes.add(
unhexlify(blocking_channel_id)[::-1]
)
# filtered content can still be resolved
result = await self.out(self.daemon.jsonrpc_resolve('lbry://@some_channel/bad_content'))
self.assertEqual(bad_content_id, result['lbry://@some_channel/bad_content']['claim_id'])
await self.stream_repost(bad_content_id, 'block1', '1.1', channel_name='@blocking')
# blocked content is not resolveable
result = await self.out(self.daemon.jsonrpc_resolve('lbry://@some_channel/bad_content'))
error = result['lbry://@some_channel/bad_content']['error']
self.assertTrue(error['name'], 'blocked')
self.assertTrue(error['text'].startswith("Resolve of 'lbry://@some_channel/bad_content' was censored"))
async def test_publish_updates_file_list(self): async def test_publish_updates_file_list(self):
tx = await self.stream_create(title='created') tx = await self.stream_create(title='created')
txo = tx['outputs'][0] txo = tx['outputs'][0]

View file

@ -72,6 +72,7 @@ class BasicTransactionTest(IntegrationTestCase):
self.assertIn('error', response['lbry://@bar/foo']) self.assertIn('error', response['lbry://@bar/foo'])
# checks for expected format in inexistent URIs # checks for expected format in inexistent URIs
response = await self.ledger.resolve([], ['lbry://404', 'lbry://@404']) response = await self.ledger.resolve([], ['lbry://404', 'lbry://@404', 'lbry://@404/404'])
self.assertEqual('lbry://404 did not resolve to a claim', response['lbry://404']['error']) self.assertEqual('Could not find claim at "lbry://404".', response['lbry://404']['error']['text'])
self.assertEqual('lbry://@404 did not resolve to a claim', response['lbry://@404']['error']) self.assertEqual('Could not find channel in "lbry://@404".', response['lbry://@404']['error']['text'])
self.assertEqual('Could not find channel in "lbry://@404/404".', response['lbry://@404/404']['error']['text'])

View file

@ -15,6 +15,7 @@ class BaseResolveTestCase(CommandTestCase):
other = (await self.resolve(name))[name] other = (await self.resolve(name))[name]
if claim_id is None: if claim_id is None:
self.assertIn('error', other) self.assertIn('error', other)
self.assertEqual(other['error']['name'], 'not_found')
else: else:
self.assertEqual(claim_id, other['claim_id']) self.assertEqual(claim_id, other['claim_id'])
@ -183,7 +184,12 @@ class ResolveCommand(BaseResolveTestCase):
# only possible outside a channel # only possible outside a channel
response = await self.resolve('lbry://@abc/on-channel-claim') response = await self.resolve('lbry://@abc/on-channel-claim')
self.assertEqual(response, { self.assertEqual(response, {
'lbry://@abc/on-channel-claim': {'error': 'lbry://@abc/on-channel-claim did not resolve to a claim'} 'lbry://@abc/on-channel-claim': {
'error': {
'name': 'not_found',
'text': 'Could not find claim at "lbry://@abc/on-channel-claim".',
}
}
}) })
response = (await self.resolve('lbry://on-channel-claim'))['lbry://on-channel-claim'] response = (await self.resolve('lbry://on-channel-claim'))['lbry://on-channel-claim']
self.assertFalse(response['is_channel_signature_valid']) self.assertFalse(response['is_channel_signature_valid'])
@ -257,7 +263,12 @@ class ResolveCommand(BaseResolveTestCase):
self.assertFalse(response['bad_example']['is_channel_signature_valid']) self.assertFalse(response['bad_example']['is_channel_signature_valid'])
response = await self.resolve('@olds/bad_example') response = await self.resolve('@olds/bad_example')
self.assertEqual(response, { self.assertEqual(response, {
'@olds/bad_example': {'error': '@olds/bad_example did not resolve to a claim'} '@olds/bad_example': {
'error': {
'name': 'not_found',
'text': 'Could not find claim at "@olds/bad_example".',
}
}
}) })

View file

@ -87,7 +87,10 @@ class EpicAdventuresOfChris45(CommandTestCase):
response = await self.resolve('lbry://@spam/hovercraft') response = await self.resolve('lbry://@spam/hovercraft')
self.assertEqual( self.assertEqual(
response['lbry://@spam/hovercraft'], response['lbry://@spam/hovercraft'],
{'error': 'lbry://@spam/hovercraft did not resolve to a claim'} {'error': {
'name': 'not_found',
'text': 'Could not find claim at "lbry://@spam/hovercraft".'
}}
) )
# After abandoning he just waits for his LBCs to be returned to his account # After abandoning he just waits for his LBCs to be returned to his account
@ -186,4 +189,10 @@ class EpicAdventuresOfChris45(CommandTestCase):
# He them checks that the claim doesn't resolve anymore. # He them checks that the claim doesn't resolve anymore.
response = await self.resolve(uri) response = await self.resolve(uri)
self.assertEqual(response[uri], {'error': f'{uri} did not resolve to a claim'}) self.assertEqual(
response[uri],
{'error': {
'name': 'not_found',
'text': f'Could not find claim at "{uri}".'
}}
)

View file

@ -617,7 +617,10 @@ class TestContentBlocking(TestSQLDB):
self.assertEqual(1, censor.total) self.assertEqual(1, censor.total)
self.assertEqual({blocking_channel.claim_hash: 1}, censor.censored) self.assertEqual({blocking_channel.claim_hash: 1}, censor.censored)
results, _ = reader.resolve([claim1.claim_name]) results, _ = reader.resolve([claim1.claim_name])
self.assertEqual('Could not find stream in "claim1".', results[0].args[0]) self.assertEqual(
f"Resolve of 'claim1' was censored by channel with claim id '{blocking_channel.claim_id}'.",
results[0].args[0]
)
results, _ = reader.resolve([ results, _ = reader.resolve([
claim2.claim_name, regular_channel.claim_name # claim2 and channel still resolved claim2.claim_name, regular_channel.claim_name # claim2 and channel still resolved
]) ])
@ -654,8 +657,14 @@ class TestContentBlocking(TestSQLDB):
results, _ = reader.resolve([ results, _ = reader.resolve([
claim2.claim_name, regular_channel.claim_name # claim2 and channel don't resolve claim2.claim_name, regular_channel.claim_name # claim2 and channel don't resolve
]) ])
self.assertEqual('Could not find stream in "claim2".', results[0].args[0]) self.assertEqual(
self.assertEqual('Could not find channel in "@channel1".', results[1].args[0]) f"Resolve of 'claim2' was censored by channel with claim id '{blocking_channel.claim_id}'.",
results[0].args[0]
)
self.assertEqual(
f"Resolve of '@channel1' was censored by channel with claim id '{blocking_channel.claim_id}'.",
results[1].args[0]
)
results, _ = reader.resolve([claim3.claim_name]) # claim3 still resolved results, _ = reader.resolve([claim3.claim_name]) # claim3 still resolved
self.assertEqual(claim3.claim_hash, results[0]['claim_hash']) self.assertEqual(claim3.claim_hash, results[0]['claim_hash'])