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.
|
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.
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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:])
|
||||||
|
|
|
@ -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]:
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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'])
|
||||||
|
|
|
@ -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".',
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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}".'
|
||||||
|
}}
|
||||||
|
)
|
||||||
|
|
|
@ -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'])
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue