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:
commit
638e3e6b3d
11 changed files with 105 additions and 32 deletions
|
@ -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.
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:])
|
||||
|
|
|
@ -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]:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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".',
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
|
|
@ -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}".'
|
||||
}}
|
||||
)
|
||||
|
|
|
@ -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'])
|
||||
|
||||
|
|
Loading…
Reference in a new issue