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.
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*
408 | Resolve | Failed to resolve '{url}'.
409 | ResolveTimeout | Failed to resolve '{url}' within the timeout.
410 | KeyFeeAboveMaxAllowed | {message}
411 | InvalidPassword | Password is invalid.
412 | IncompatibleWalletServer | '{server}:{port}' has an incompatibly old version.
410 | Resolve | Failed to resolve '{url}'.
411 | ResolveTimeout | Failed to resolve '{url}' within the timeout.
411 | ResolveCensored | Resolve of '{url}' was censored by channel with claim id '{censor_id}'.
420 | KeyFeeAboveMaxAllowed | {message}
421 | InvalidPassword | Password is invalid.
422 | IncompatibleWalletServer | '{server}:{port}' has an incompatibly old version.
**5xx** | Blob | **Blobs**
500 | BlobNotFound | Blob not found.
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.")
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):
def __init__(self, message):

View file

@ -50,7 +50,7 @@ class ErrorClass:
def get_arguments(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)
return args

View file

@ -4,7 +4,13 @@ from typing import List
from binascii import hexlify
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 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:
@ -73,7 +79,12 @@ class Outputs:
def message_to_txo(self, txo_message, tx_map):
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]
if txo_message.WhichOneof('meta') == 'claim':
claim = txo_message.claim
@ -146,9 +157,11 @@ class Outputs:
if isinstance(txo, Exception):
txo_message.error.text = txo.args[0]
if isinstance(txo, ValueError):
txo_message.error.code = txo_message.error.INVALID
txo_message.error.code = ErrorMessage.INVALID
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
txo_message.tx_hash = 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
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.crypto.hash import hash160, double_sha256, sha256
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."
result = {}
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:
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:
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
async def claim_search(self, accounts, **kwargs) -> Tuple[List[Output], dict, int, int]:

View file

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

View file

@ -763,29 +763,48 @@ class StreamCommands(ClaimTestCase):
bad_content_id = self.get_claim_id(
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')
)
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')
# search for blocked content directly
result = await self.out(self.daemon.jsonrpc_claim_search(name='bad_content'))
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
result = await self.out(self.daemon.jsonrpc_claim_search(channel='@some_channel'))
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
result = await self.out(self.daemon.jsonrpc_claim_search(channel='@some_channel', not_tags=["good", "bad"]))
self.assertEqual(0, len(result['items']))
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):
tx = await self.stream_create(title='created')
txo = tx['outputs'][0]

View file

@ -72,6 +72,7 @@ class BasicTransactionTest(IntegrationTestCase):
self.assertIn('error', response['lbry://@bar/foo'])
# checks for expected format in inexistent URIs
response = await self.ledger.resolve([], ['lbry://404', 'lbry://@404'])
self.assertEqual('lbry://404 did not resolve to a claim', response['lbry://404']['error'])
self.assertEqual('lbry://@404 did not resolve to a claim', response['lbry://@404']['error'])
response = await self.ledger.resolve([], ['lbry://404', 'lbry://@404', 'lbry://@404/404'])
self.assertEqual('Could not find claim at "lbry://404".', response['lbry://404']['error']['text'])
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]
if claim_id is None:
self.assertIn('error', other)
self.assertEqual(other['error']['name'], 'not_found')
else:
self.assertEqual(claim_id, other['claim_id'])
@ -183,7 +184,12 @@ class ResolveCommand(BaseResolveTestCase):
# only possible outside a channel
response = await self.resolve('lbry://@abc/on-channel-claim')
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']
self.assertFalse(response['is_channel_signature_valid'])
@ -257,7 +263,12 @@ class ResolveCommand(BaseResolveTestCase):
self.assertFalse(response['bad_example']['is_channel_signature_valid'])
response = await self.resolve('@olds/bad_example')
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')
self.assertEqual(
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
@ -186,4 +189,10 @@ class EpicAdventuresOfChris45(CommandTestCase):
# He them checks that the claim doesn't resolve anymore.
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({blocking_channel.claim_hash: 1}, censor.censored)
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([
claim2.claim_name, regular_channel.claim_name # claim2 and channel still resolved
])
@ -654,8 +657,14 @@ class TestContentBlocking(TestSQLDB):
results, _ = reader.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('Could not find channel in "@channel1".', results[1].args[0])
self.assertEqual(
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
self.assertEqual(claim3.claim_hash, results[0]['claim_hash'])