diff --git a/hathor/builder/cli_builder.py b/hathor/builder/cli_builder.py index fc897867b..0e9ea9d04 100644 --- a/hathor/builder/cli_builder.py +++ b/hathor/builder/cli_builder.py @@ -37,7 +37,8 @@ from hathor.pubsub import PubSubManager from hathor.stratum import StratumFactory from hathor.util import Random, Reactor, not_none -from hathor.verification.verification_service import VerificationService, VertexVerifiers +from hathor.verification.verification_service import VerificationService +from hathor.verification.vertex_verifiers import VertexVerifiers from hathor.wallet import BaseWallet, HDWallet, Wallet logger = get_logger() diff --git a/hathor/cli/mining.py b/hathor/cli/mining.py index 8be4e348c..63ab8757c 100644 --- a/hathor/cli/mining.py +++ b/hathor/cli/mining.py @@ -138,11 +138,12 @@ def execute(args: Namespace) -> None: try: from hathor.daa import DifficultyAdjustmentAlgorithm - from hathor.verification.block_verifier import BlockVerifier + from hathor.verification.verification_service import VerificationService, VertexVerifiers settings = get_settings() daa = DifficultyAdjustmentAlgorithm(settings=settings) - verifier = BlockVerifier(settings=settings, daa=daa) - verifier.verify_without_storage(block) + verifiers = VertexVerifiers.create_defaults(settings=settings, daa=daa) + verification_service = VerificationService(verifiers=verifiers) + verification_service.verify_without_storage(block) except HathorError: print('[{}] ERROR: Block has not been pushed because it is not valid.'.format(datetime.datetime.now())) else: diff --git a/hathor/cli/openapi_files/register.py b/hathor/cli/openapi_files/register.py index 733f56848..7ce9afac6 100644 --- a/hathor/cli/openapi_files/register.py +++ b/hathor/cli/openapi_files/register.py @@ -36,6 +36,7 @@ def get_registered_resources() -> list[type[Resource]]: """ import hathor.event.resources.event # noqa: 401 import hathor.feature_activation.resources.feature # noqa: 401 + import hathor.healthcheck.resources.healthcheck # noqa: 401 import hathor.p2p.resources # noqa: 401 import hathor.profiler.resources # noqa: 401 import hathor.stratum.resources # noqa: 401 diff --git a/hathor/healthcheck/models.py b/hathor/healthcheck/models.py deleted file mode 100644 index c75457720..000000000 --- a/hathor/healthcheck/models.py +++ /dev/null @@ -1,116 +0,0 @@ -from abc import ABC, abstractmethod -from dataclasses import dataclass -from datetime import datetime -from enum import Enum -from typing import Any, Optional - - -class ComponentType(str, Enum): - """Enum used to store the component types that can be used in the HealthCheckComponentStatus class.""" - - DATASTORE = 'datastore' - INTERNAL = 'internal' - FULLNODE = 'fullnode' - - -class HealthCheckStatus(str, Enum): - """Enum used to store the component status that can be used in the HealthCheckComponentStatus class.""" - - PASS = 'pass' - WARN = 'warn' - FAIL = 'fail' - - -@dataclass -class ComponentHealthCheck: - """This class is used to store the result of a health check in a specific component.""" - - component_name: str - component_type: ComponentType - status: HealthCheckStatus - output: str - time: Optional[str] = None - component_id: Optional[str] = None - observed_value: Optional[str] = None - observed_unit: Optional[str] = None - - def __post_init__(self) -> None: - self.time = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') - - def to_json(self) -> dict[str, str]: - """Return a dict representation of the object. All field names are converted to camel case.""" - json = { - 'componentType': self.component_type.value, - 'status': self.status.value, - 'output': self.output, - } - - if self.time: - json['time'] = self.time - - if self.component_id: - json['componentId'] = self.component_id - - if self.observed_value: - assert ( - self.observed_unit is not None - ), 'observed_unit must be set if observed_value is set' - - json['observedValue'] = self.observed_value - json['observedUnit'] = self.observed_unit - - return json - - -@dataclass -class ServiceHealthCheck: - """This class is used to store the result of a service health check.""" - - description: str - checks: dict[str, list[ComponentHealthCheck]] - - @property - def status(self) -> HealthCheckStatus: - """Return the status of the health check based on the status of the components.""" - status = HealthCheckStatus.PASS - - for component_checks in self.checks.values(): - for check in component_checks: - if check.status == HealthCheckStatus.FAIL: - return HealthCheckStatus.FAIL - elif check.status == HealthCheckStatus.WARN: - status = HealthCheckStatus.WARN - - return status - - def __post_init__(self) -> None: - """Perform some validations after the object is initialized.""" - # Make sure the checks dict is not empty - if not self.checks: - raise ValueError('checks dict cannot be empty') - - def get_http_status_code(self) -> int: - """Return the HTTP status code for the status.""" - if self.status in [HealthCheckStatus.PASS]: - return 200 - elif self.status in [HealthCheckStatus.WARN, HealthCheckStatus.FAIL]: - return 503 - else: - raise ValueError(f'Missing treatment for status {self.status}') - - def to_json(self) -> dict[str, Any]: - """Return a dict representation of the object. All field names are converted to camel case.""" - return { - 'status': self.status.value, - 'description': self.description, - 'checks': {k: [c.to_json() for c in v] for k, v in self.checks.items()}, - } - - -class ComponentHealthCheckInterface(ABC): - """This is an interface to be used by other classes implementing health checks for components.""" - - @abstractmethod - async def get_health_check(self) -> ComponentHealthCheck: - """Return the health check status for the component.""" - raise NotImplementedError() diff --git a/hathor/healthcheck/resources/healthcheck.py b/hathor/healthcheck/resources/healthcheck.py index 2cdc29cd9..5e9afcb9f 100644 --- a/hathor/healthcheck/resources/healthcheck.py +++ b/hathor/healthcheck/resources/healthcheck.py @@ -1,19 +1,18 @@ -import hathor +import asyncio + +from healthcheck import Healthcheck, HealthcheckCallbackResponse, HealthcheckInternalComponent, HealthcheckStatus + from hathor.api_util import Resource, get_arg_default, get_args from hathor.cli.openapi_files.register import register_resource -from hathor.healthcheck.models import ComponentHealthCheck, ComponentType, HealthCheckStatus, ServiceHealthCheck from hathor.manager import HathorManager from hathor.util import json_dumpb -def build_sync_health_status(manager: HathorManager) -> ComponentHealthCheck: - """Builds the sync health status object.""" +async def sync_healthcheck(manager: HathorManager) -> HealthcheckCallbackResponse: healthy, reason = manager.is_sync_healthy() - return ComponentHealthCheck( - component_name='sync', - component_type=ComponentType.INTERNAL, - status=HealthCheckStatus.PASS if healthy else HealthCheckStatus.FAIL, + return HealthcheckCallbackResponse( + status=HealthcheckStatus.PASS if healthy else HealthcheckStatus.FAIL, output=reason or 'Healthy', ) @@ -38,22 +37,21 @@ def render_GET(self, request): raw_args = get_args(request) strict_status_code = get_arg_default(raw_args, 'strict_status_code', '0') == '1' - components_health_checks = [ - build_sync_health_status(self.manager) - ] - - health_check = ServiceHealthCheck( - description=f'Hathor-core {hathor.__version__}', - checks={c.component_name: [c] for c in components_health_checks}, + sync_component = HealthcheckInternalComponent( + name='sync', ) + sync_component.add_healthcheck(lambda: sync_healthcheck(self.manager)) + + healthcheck = Healthcheck(name='hathor-core', components=[sync_component]) + status = asyncio.get_event_loop().run_until_complete(healthcheck.run()) if strict_status_code: request.setResponseCode(200) else: - status_code = health_check.get_http_status_code() + status_code = status.get_http_status_code() request.setResponseCode(status_code) - return json_dumpb(health_check.to_json()) + return json_dumpb(status.to_json()) HealthcheckResource.openapi = { diff --git a/hathor/p2p/manager.py b/hathor/p2p/manager.py index 1682cd9f9..ad5df083e 100644 --- a/hathor/p2p/manager.py +++ b/hathor/p2p/manager.py @@ -248,7 +248,8 @@ def do_discovery(self) -> None: Do a discovery and connect on all discovery strategies. """ for peer_discovery in self.peer_discoveries: - peer_discovery.discover_and_connect(self.connect_to) + coro = peer_discovery.discover_and_connect(self.connect_to) + Deferred.fromCoroutine(coro) def disable_rate_limiter(self) -> None: """Disable global rate limiter.""" diff --git a/hathor/p2p/peer_discovery.py b/hathor/p2p/peer_discovery.py index 8730b7ecb..a202f6409 100644 --- a/hathor/p2p/peer_discovery.py +++ b/hathor/p2p/peer_discovery.py @@ -14,13 +14,13 @@ import socket from abc import ABC, abstractmethod -from typing import Any, Callable, Generator +from typing import Callable from structlog import get_logger from twisted.internet import defer -from twisted.internet.defer import inlineCallbacks from twisted.names.client import lookupAddress, lookupText from twisted.names.dns import Record_A, Record_TXT, RRHeader +from typing_extensions import override logger = get_logger() @@ -30,7 +30,7 @@ class PeerDiscovery(ABC): """ @abstractmethod - def discover_and_connect(self, connect_to: Callable[[str], None]) -> Any: + async def discover_and_connect(self, connect_to: Callable[[str], None]) -> None: """ This method must discover the peers and call `connect_to` for each of them. :param connect_to: Function which will be called for each discovered peer. @@ -51,7 +51,8 @@ def __init__(self, descriptions: list[str]): self.log = logger.new() self.descriptions = descriptions - def discover_and_connect(self, connect_to: Callable[[str], None]) -> Any: + @override + async def discover_and_connect(self, connect_to: Callable[[str], None]) -> None: for description in self.descriptions: connect_to(description) @@ -70,18 +71,17 @@ def __init__(self, hosts: list[str], default_port: int = 40403, test_mode: int = self.default_port = default_port self.test_mode = test_mode - @inlineCallbacks - def discover_and_connect(self, connect_to: Callable[[str], None]) -> Generator[Any, Any, None]: + @override + async def discover_and_connect(self, connect_to: Callable[[str], None]) -> None: """ Run DNS lookup for host and connect to it This is executed when starting the DNS Peer Discovery and first connecting to the network """ for host in self.hosts: - url_list = yield self.dns_seed_lookup(host) + url_list = await self.dns_seed_lookup(host) for url in url_list: connect_to(url) - @inlineCallbacks - def dns_seed_lookup(self, host: str) -> Generator[Any, Any, list[str]]: + async def dns_seed_lookup(self, host: str) -> list[str]: """ Run a DNS lookup for TXT, A, and AAAA records and return a list of connection strings. """ if self.test_mode: @@ -97,7 +97,7 @@ def dns_seed_lookup(self, host: str) -> Generator[Any, Any, list[str]]: d2.addErrback(self.errback), d = defer.gatherResults([d1, d2]) - results = yield d + results = await d unique_urls: set[str] = set() for urls in results: unique_urls.update(urls) diff --git a/hathor/p2p/peer_id.py b/hathor/p2p/peer_id.py index 711fd1f5c..f3122c34f 100644 --- a/hathor/p2p/peer_id.py +++ b/hathor/p2p/peer_id.py @@ -16,7 +16,7 @@ import hashlib from enum import Enum from math import inf -from typing import TYPE_CHECKING, Any, Generator, Optional, cast +from typing import TYPE_CHECKING, Any, Optional, cast from cryptography import x509 from cryptography.exceptions import InvalidSignature @@ -24,7 +24,6 @@ from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding, rsa from OpenSSL.crypto import X509, PKey -from twisted.internet.defer import inlineCallbacks from twisted.internet.interfaces import ISSLTransport from twisted.internet.ssl import Certificate, CertificateOptions, TLSVersion, trustRootFromCertificates @@ -324,8 +323,7 @@ def _get_certificate_options(self) -> CertificateOptions: ) return certificate_options - @inlineCallbacks - def validate_entrypoint(self, protocol: 'HathorProtocol') -> Generator[Any, Any, bool]: + async def validate_entrypoint(self, protocol: 'HathorProtocol') -> bool: """ Validates if connection entrypoint is one of the peer entrypoints """ found_entrypoint = False @@ -349,7 +347,7 @@ def validate_entrypoint(self, protocol: 'HathorProtocol') -> Generator[Any, Any, host = connection_string_to_host(entrypoint) # TODO: don't use `daa.TEST_MODE` for this test_mode = not_none(DifficultyAdjustmentAlgorithm.singleton).TEST_MODE - result = yield discover_dns(host, test_mode) + result = await discover_dns(host, test_mode) if protocol.connection_string in result: # Found the entrypoint found_entrypoint = True @@ -369,7 +367,7 @@ def validate_entrypoint(self, protocol: 'HathorProtocol') -> Generator[Any, Any, found_entrypoint = True break test_mode = not_none(DifficultyAdjustmentAlgorithm.singleton).TEST_MODE - result = yield discover_dns(host, test_mode) + result = await discover_dns(host, test_mode) if connection_host in [connection_string_to_host(x) for x in result]: # Found the entrypoint found_entrypoint = True diff --git a/hathor/p2p/protocol.py b/hathor/p2p/protocol.py index 3df296466..696ba3c07 100644 --- a/hathor/p2p/protocol.py +++ b/hathor/p2p/protocol.py @@ -14,7 +14,7 @@ import time from enum import Enum -from typing import TYPE_CHECKING, Any, Generator, Optional, cast +from typing import TYPE_CHECKING, Any, Coroutine, Generator, Optional, cast from structlog import get_logger from twisted.internet.defer import Deferred @@ -311,7 +311,8 @@ def recv_message(self, cmd: ProtocolMessages, payload: str) -> Optional[Deferred fn = self.state.cmd_map.get(cmd) if fn is not None: try: - return fn(payload) + result = fn(payload) + return Deferred.fromCoroutine(result) if isinstance(result, Coroutine) else result except Exception: self.log.warn('recv_message processing error', exc_info=True) raise diff --git a/hathor/p2p/states/base.py b/hathor/p2p/states/base.py index ee07bc931..abbc17dd0 100644 --- a/hathor/p2p/states/base.py +++ b/hathor/p2p/states/base.py @@ -12,7 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Callable, Optional, Union +from collections.abc import Coroutine +from typing import TYPE_CHECKING, Any, Callable, Optional from structlog import get_logger from twisted.internet.defer import Deferred @@ -27,7 +28,10 @@ class BaseState: protocol: 'HathorProtocol' - cmd_map: dict[ProtocolMessages, Union[Callable[[str], None], Callable[[str], Deferred[None]]]] + cmd_map: dict[ + ProtocolMessages, + Callable[[str], None] | Callable[[str], Deferred[None]] | Callable[[str], Coroutine[Deferred[None], Any, None]] + ] def __init__(self, protocol: 'HathorProtocol'): self.log = logger.new(**protocol.get_logger_context()) diff --git a/hathor/p2p/states/peer_id.py b/hathor/p2p/states/peer_id.py index 9b91b5b62..b2e1f0a50 100644 --- a/hathor/p2p/states/peer_id.py +++ b/hathor/p2p/states/peer_id.py @@ -12,10 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Any, Generator +from typing import TYPE_CHECKING from structlog import get_logger -from twisted.internet.defer import inlineCallbacks from hathor.conf import HathorSettings from hathor.p2p.messages import ProtocolMessages @@ -77,8 +76,7 @@ def send_peer_id(self) -> None: } self.send_message(ProtocolMessages.PEER_ID, json_dumps(hello)) - @inlineCallbacks - def handle_peer_id(self, payload: str) -> Generator[Any, Any, None]: + async def handle_peer_id(self, payload: str) -> None: """ Executed when a PEER-ID is received. It basically checks the identity of the peer. Only after this step, the peer connection is considered established and ready to communicate. @@ -117,7 +115,7 @@ def handle_peer_id(self, payload: str) -> Generator[Any, Any, None]: protocol.send_error_and_close_connection('We are already connected.') return - entrypoint_valid = yield peer.validate_entrypoint(protocol) + entrypoint_valid = await peer.validate_entrypoint(protocol) if not entrypoint_valid: protocol.send_error_and_close_connection('Connection string is not in the entrypoints.') return diff --git a/hathor/p2p/sync_v1/agent.py b/hathor/p2p/sync_v1/agent.py index 8a53fd962..f9757f7e7 100644 --- a/hathor/p2p/sync_v1/agent.py +++ b/hathor/p2p/sync_v1/agent.py @@ -14,15 +14,13 @@ import base64 import struct -from collections import OrderedDict from math import inf from typing import TYPE_CHECKING, Any, Callable, Generator, Iterator, Optional from weakref import WeakSet from structlog import get_logger from twisted.internet.defer import Deferred, inlineCallbacks -from twisted.internet.interfaces import IConsumer, IDelayedCall, IPushProducer -from zope.interface import implementer +from twisted.internet.interfaces import IDelayedCall from hathor.conf.get_settings import get_settings from hathor.p2p.messages import GetNextPayload, GetTipsPayload, NextPayload, ProtocolMessages, TipsPayload @@ -32,7 +30,6 @@ from hathor.transaction.base_transaction import tx_or_block_from_bytes from hathor.transaction.storage.exceptions import TransactionDoesNotExist from hathor.util import Reactor, json_dumps, json_loads -from hathor.utils.zope import asserted_cast logger = get_logger() @@ -52,126 +49,6 @@ def _get_deps(tx: BaseTransaction) -> Iterator[bytes]: yield txin.tx_id -@implementer(IPushProducer) -class SendDataPush: - """ Prioritize blocks over transactions when pushing data to peers. - """ - def __init__(self, node_sync: 'NodeSyncTimestamp'): - self.node_sync = node_sync - self.protocol: 'HathorProtocol' = node_sync.protocol - assert self.protocol.transport is not None - consumer = asserted_cast(IConsumer, self.protocol.transport) - self.consumer = consumer - self.is_running: bool = False - self.is_producing: bool = False - - self.queue: OrderedDict[bytes, tuple[BaseTransaction, list[bytes]]] = OrderedDict() - self.priority_queue: OrderedDict[bytes, tuple[BaseTransaction, list[bytes]]] = OrderedDict() - - self.delayed_call: Optional[IDelayedCall] = None - - def start(self) -> None: - """ Start pushing data. - """ - if self.is_running: - raise Exception('SendDataPush is already started.') - self.is_running = True - self.consumer.registerProducer(self, True) - self.resumeProducing() - - def stop(self) -> None: - """ Stop pushing data. - """ - if not self.is_running: - raise Exception('SendDataPush is already stopped.') - self.is_running = False - self.pauseProducing() - self.consumer.unregisterProducer() - - def schedule_if_needed(self) -> None: - """ Schedule `send_next` if needed. - """ - if not self.is_running: - return - - if not self.is_producing: - return - - if self.delayed_call and self.delayed_call.active(): - return - - if len(self.queue) > 0 or len(self.priority_queue) > 0: - self.delayed_call = self.node_sync.reactor.callLater(0, self.send_next) - - def add(self, tx: BaseTransaction) -> None: - """ Add a new block/transaction to be pushed. - """ - assert tx.hash is not None - if tx.is_block: - self.add_to_priority(tx) - else: - deps = list(_get_deps(tx)) - self.queue[tx.hash] = (tx, deps) - self.schedule_if_needed() - - def add_to_priority(self, tx: BaseTransaction) -> None: - """ Add a new block/transaction to be pushed with priority. - """ - assert tx.hash is not None - assert tx.hash not in self.queue - if tx.hash in self.priority_queue: - return - deps = list(_get_deps(tx)) - for h in deps: - if h in self.queue: - tx2, _ = self.queue.pop(h) - self.add_to_priority(tx2) - self.priority_queue[tx.hash] = (tx, deps) - self.schedule_if_needed() - - def send_next(self) -> None: - """ Push next block/transaction to peer. - """ - assert self.is_running - assert self.is_producing - - if len(self.priority_queue) > 0: - # Send blocks first. - _, (tx, _) = self.priority_queue.popitem(last=False) - - elif len(self.queue) > 0: - # Otherwise, send in order. - _, (tx, _) = self.queue.popitem(last=False) - - else: - # Nothing to send. - self.delayed_call = None - return - - self.node_sync.send_data(tx) - self.schedule_if_needed() - - def resumeProducing(self) -> None: - """ This method is automatically called to resume pushing data. - """ - self.is_producing = True - self.schedule_if_needed() - - def pauseProducing(self) -> None: - """ This method is automatically called to pause pushing data. - """ - self.is_producing = False - if self.delayed_call and self.delayed_call.active(): - self.delayed_call.cancel() - - def stopProducing(self) -> None: - """ This method is automatically called to stop pushing data. - """ - self.pauseProducing() - self.queue.clear() - self.priority_queue.clear() - - class NodeSyncTimestamp(SyncAgent): """ An algorithm to sync the DAG between two peers using the timestamp of the transactions. @@ -218,8 +95,6 @@ def __init__(self, protocol: 'HathorProtocol', downloader: Downloader, reactor: # This number may decrease if a new transaction/block arrives in a timestamp smaller than it. self.synced_timestamp: int = 0 - self.send_data_queue: SendDataPush = SendDataPush(self) - # Latest data timestamp of the peer. self.previous_timestamp: int = 0 @@ -274,8 +149,6 @@ def start(self) -> None: if self._started: raise Exception('NodeSyncTimestamp is already running') self._started = True - if self.send_data_queue: - self.send_data_queue.start() self.next_step() def stop(self) -> None: @@ -284,8 +157,6 @@ def stop(self) -> None: if not self._started: raise Exception('NodeSyncTimestamp is already stopped') self._started = False - if self.send_data_queue and self.send_data_queue.is_running: - self.send_data_queue.stop() if self.call_later_id and self.call_later_id.active(): self.call_later_id.cancel() for call_later in self._send_tips_call_later: @@ -330,10 +201,7 @@ def send_tx_to_peer_if_possible(self, tx: BaseTransaction) -> None: if parent.timestamp > self.synced_timestamp: return - if self.send_data_queue: - self.send_data_queue.add(tx) - else: - self.send_data(tx) + self.send_data(tx) def get_peer_next(self, timestamp: Optional[int] = None, offset: int = 0) -> Deferred[NextPayload]: """ A helper that returns a deferred that is called when the peer replies. diff --git a/hathor/p2p/utils.py b/hathor/p2p/utils.py index 007a1f7c9..12509ffc4 100644 --- a/hathor/p2p/utils.py +++ b/hathor/p2p/utils.py @@ -14,7 +14,7 @@ import datetime import re -from typing import Any, Generator, Optional +from typing import Any, Optional from urllib.parse import parse_qs, urlparse import requests @@ -25,7 +25,6 @@ from cryptography.hazmat.primitives.serialization import load_pem_private_key from cryptography.x509 import Certificate from cryptography.x509.oid import NameOID -from twisted.internet.defer import inlineCallbacks from twisted.internet.interfaces import IAddress from hathor.conf.get_settings import get_settings @@ -100,15 +99,14 @@ def connection_string_to_host(connection_string: str) -> str: return urlparse(connection_string).netloc.split(':')[0] -@inlineCallbacks -def discover_dns(host: str, test_mode: int = 0) -> Generator[Any, Any, list[str]]: +async def discover_dns(host: str, test_mode: int = 0) -> list[str]: """ Start a DNS peer discovery object and execute a search for the host Returns the DNS string from the requested host E.g., localhost -> tcp://127.0.0.1:40403 """ discovery = DNSPeerDiscovery([], test_mode=test_mode) - result = yield discovery.dns_seed_lookup(host) + result = await discovery.dns_seed_lookup(host) return result diff --git a/hathor/pubsub.py b/hathor/pubsub.py index b9c5506c3..f598f4998 100644 --- a/hathor/pubsub.py +++ b/hathor/pubsub.py @@ -14,9 +14,10 @@ from collections import defaultdict, deque from enum import Enum -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any, Callable, Optional -from twisted.internet.interfaces import IReactorFromThreads +from structlog import get_logger +from twisted.internet.interfaces import IDelayedCall, IReactorFromThreads from twisted.python.threadable import isInIOThread from hathor.util import Reactor @@ -25,6 +26,8 @@ if TYPE_CHECKING: from hathor.transaction import BaseTransaction, Block +logger = get_logger() + class HathorEvents(Enum): """ @@ -170,6 +173,9 @@ def __init__(self, reactor: Reactor) -> None: self._subscribers = defaultdict(list) self.queue: deque[tuple[PubSubCallable, HathorEvents, EventArguments]] = deque() self.reactor = reactor + self.log = logger.new() + + self._call_later_id: Optional[IDelayedCall] = None def subscribe(self, key: HathorEvents, fn: PubSubCallable) -> None: """Subscribe to a specific event. @@ -193,22 +199,36 @@ def _call_next(self) -> None: """Execute next call if it exists.""" if not self.queue: return - fn, key, args = self.queue.popleft() - fn(key, args) - if self.queue: + + self.log.debug('running pubsub call_next', len=len(self.queue)) + + try: + while self.queue: + fn, key, args = self.queue.popleft() + fn(key, args) + except Exception: + self.log.error('event processing failed', key=key, args=args) + raise + finally: self._schedule_call_next() def _schedule_call_next(self) -> None: """Schedule next call's execution.""" assert self.reactor.running + if not self.queue: + return + if not isInIOThread() and (threaded_reactor := verified_cast(IReactorFromThreads, self.reactor)): # We're taking a conservative approach, since not all functions might need to run # on the main thread [yan 2019-02-20] threaded_reactor.callFromThread(self._call_next) return - self.reactor.callLater(0, self._call_next) + if self._call_later_id and self._call_later_id.active(): + return + + self._call_later_id = self.reactor.callLater(0, self._call_next) def publish(self, key: HathorEvents, **kwargs: Any) -> None: """Publish a new event. @@ -224,7 +244,5 @@ def publish(self, key: HathorEvents, **kwargs: Any) -> None: if not self.reactor.running: fn(key, args) else: - is_empty = bool(not self.queue) self.queue.append((fn, key, args)) - if is_empty: - self._schedule_call_next() + self._schedule_call_next() diff --git a/hathor/simulator/patches.py b/hathor/simulator/patches.py index 1f9fdd1d3..3c056249e 100644 --- a/hathor/simulator/patches.py +++ b/hathor/simulator/patches.py @@ -18,41 +18,16 @@ from hathor.mining.cpu_mining_service import CpuMiningService from hathor.transaction import BaseTransaction -from hathor.verification.block_verifier import BlockVerifier -from hathor.verification.merge_mined_block_verifier import MergeMinedBlockVerifier -from hathor.verification.token_creation_transaction_verifier import TokenCreationTransactionVerifier -from hathor.verification.transaction_verifier import TransactionVerifier +from hathor.verification.vertex_verifier import VertexVerifier logger = get_logger() -def _verify_pow(vertex: BaseTransaction) -> None: - assert vertex.hash is not None - logger.new().debug('Skipping VertexVerifier.verify_pow() for simulator') - - -class SimulatorBlockVerifier(BlockVerifier): - @classmethod - def verify_pow(cls, vertex: BaseTransaction, *, override_weight: Optional[float] = None) -> None: - _verify_pow(vertex) - - -class SimulatorMergeMinedBlockVerifier(MergeMinedBlockVerifier): - @classmethod - def verify_pow(cls, vertex: BaseTransaction, *, override_weight: Optional[float] = None) -> None: - _verify_pow(vertex) - - -class SimulatorTransactionVerifier(TransactionVerifier): - @classmethod - def verify_pow(cls, vertex: BaseTransaction, *, override_weight: Optional[float] = None) -> None: - _verify_pow(vertex) - - -class SimulatorTokenCreationTransactionVerifier(TokenCreationTransactionVerifier): +class SimulatorVertexVerifier(VertexVerifier): @classmethod def verify_pow(cls, vertex: BaseTransaction, *, override_weight: Optional[float] = None) -> None: - _verify_pow(vertex) + assert vertex.hash is not None + logger.new().debug('Skipping VertexVerifier.verify_pow() for simulator') class SimulatorCpuMiningService(CpuMiningService): diff --git a/hathor/simulator/simulator.py b/hathor/simulator/simulator.py index f8ae953b7..b6c546a3f 100644 --- a/hathor/simulator/simulator.py +++ b/hathor/simulator/simulator.py @@ -29,16 +29,10 @@ from hathor.p2p.peer_id import PeerId from hathor.simulator.clock import HeapClock, MemoryReactorHeapClock from hathor.simulator.miner.geometric_miner import GeometricMiner -from hathor.simulator.patches import ( - SimulatorBlockVerifier, - SimulatorCpuMiningService, - SimulatorMergeMinedBlockVerifier, - SimulatorTokenCreationTransactionVerifier, - SimulatorTransactionVerifier, -) +from hathor.simulator.patches import SimulatorCpuMiningService, SimulatorVertexVerifier from hathor.simulator.tx_generator import RandomTransactionGenerator from hathor.util import Random -from hathor.verification.verification_service import VertexVerifiers +from hathor.verification.vertex_verifiers import VertexVerifiers from hathor.wallet import HDWallet if TYPE_CHECKING: @@ -257,13 +251,9 @@ def _build_vertex_verifiers( """ A custom VertexVerifiers builder to be used by the simulator. """ - return VertexVerifiers( - block=SimulatorBlockVerifier(settings=settings, daa=daa, feature_service=feature_service), - merge_mined_block=SimulatorMergeMinedBlockVerifier( - settings=settings, - daa=daa, - feature_service=feature_service - ), - tx=SimulatorTransactionVerifier(settings=settings, daa=daa), - token_creation_tx=SimulatorTokenCreationTransactionVerifier(settings=settings, daa=daa), + return VertexVerifiers.create( + settings=settings, + vertex_verifier=SimulatorVertexVerifier(settings=settings, daa=daa), + daa=daa, + feature_service=feature_service, ) diff --git a/hathor/transaction/resources/create_tx.py b/hathor/transaction/resources/create_tx.py index e347cbf78..897bd0ead 100644 --- a/hathor/transaction/resources/create_tx.py +++ b/hathor/transaction/resources/create_tx.py @@ -18,6 +18,7 @@ from hathor.cli.openapi_files.register import register_resource from hathor.crypto.util import decode_address from hathor.exception import InvalidNewTransaction +from hathor.manager import HathorManager from hathor.transaction import Transaction, TxInput, TxOutput from hathor.transaction.scripts import create_output_script from hathor.util import api_catch_exceptions, json_dumpb, json_loadb @@ -49,7 +50,7 @@ class CreateTxResource(Resource): """ isLeaf = True - def __init__(self, manager): + def __init__(self, manager: HathorManager) -> None: # Important to have the manager so we can know the tx_storage self.manager = manager @@ -107,15 +108,17 @@ def render_POST(self, request): def _verify_unsigned_skip_pow(self, tx: Transaction) -> None: """ Same as .verify but skipping pow and signature verification.""" assert type(tx) is Transaction - verifier = self.manager.verification_service.verifiers.tx - verifier.verify_number_of_inputs(tx) - verifier.verify_number_of_outputs(tx) - verifier.verify_outputs(tx) - verifier.verify_sigops_output(tx) - verifier.verify_sigops_input(tx) - verifier.verify_inputs(tx, skip_script=True) # need to run verify_inputs first to check if all inputs exist - verifier.verify_parents(tx) - verifier.verify_sum(tx) + verifiers = self.manager.verification_service.verifiers + verifiers.tx.verify_number_of_inputs(tx) + verifiers.vertex.verify_number_of_outputs(tx) + verifiers.vertex.verify_outputs(tx) + verifiers.tx.verify_output_token_indexes(tx) + verifiers.vertex.verify_sigops_output(tx) + verifiers.tx.verify_sigops_input(tx) + # need to run verify_inputs first to check if all inputs exist + verifiers.tx.verify_inputs(tx, skip_script=True) + verifiers.vertex.verify_parents(tx) + verifiers.tx.verify_sum(tx.get_complete_token_info()) CreateTxResource.openapi = { diff --git a/hathor/transaction/scripts/execute.py b/hathor/transaction/scripts/execute.py index 18af61c10..23109afbc 100644 --- a/hathor/transaction/scripts/execute.py +++ b/hathor/transaction/scripts/execute.py @@ -16,7 +16,7 @@ from typing import NamedTuple, Optional, Union from hathor.transaction import BaseTransaction, Transaction, TxInput -from hathor.transaction.exceptions import DataIndexError, FinalStackInvalid, InvalidScriptError, OutOfData, ScriptError +from hathor.transaction.exceptions import DataIndexError, FinalStackInvalid, InvalidScriptError, OutOfData class ScriptExtras(NamedTuple): @@ -54,21 +54,19 @@ def execute_eval(data: bytes, log: list[str], extras: ScriptExtras) -> None: :raises ScriptError: case opcode is not found :raises FinalStackInvalid: case the evaluation fails """ - from hathor.transaction.scripts.opcode import MAP_OPCODE_TO_FN, Opcode + from hathor.transaction.scripts.opcode import Opcode, execute_op_code + from hathor.transaction.scripts.script_context import ScriptContext stack: Stack = [] + context = ScriptContext(stack=stack, logs=log, extras=extras) data_len = len(data) pos = 0 while pos < data_len: opcode, pos = get_script_op(pos, data, stack) if Opcode.is_pushdata(opcode): continue - # this is an opcode manipulating the stack - fn = MAP_OPCODE_TO_FN.get(opcode, None) - if fn is None: - # throw error - raise ScriptError('unknown opcode') - fn(stack, log, extras) + # this is an opcode manipulating the stack + execute_op_code(Opcode(opcode), context) evaluate_final_stack(stack, log) diff --git a/hathor/transaction/scripts/opcode.py b/hathor/transaction/scripts/opcode.py index 5af5d09e9..3c185f5a5 100644 --- a/hathor/transaction/scripts/opcode.py +++ b/hathor/transaction/scripts/opcode.py @@ -15,7 +15,6 @@ import datetime import struct from enum import IntEnum -from typing import Callable from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives import hashes @@ -38,14 +37,8 @@ TimeLocked, VerifyFailed, ) -from hathor.transaction.scripts.execute import ( - ScriptExtras, - Stack, - binary_to_int, - decode_opn, - get_data_value, - get_script_op, -) +from hathor.transaction.scripts.execute import Stack, binary_to_int, decode_opn, get_data_value, get_script_op +from hathor.transaction.scripts.script_context import ScriptContext class Opcode(IntEnum): @@ -157,7 +150,7 @@ def op_pushdata1(position: int, full_data: bytes, stack: Stack) -> int: return new_pos -def op_dup(stack: Stack, log: list[str], extras: ScriptExtras) -> None: +def op_dup(context: ScriptContext) -> None: """Duplicates item on top of stack :param stack: the stack used when evaluating the script @@ -165,12 +158,12 @@ def op_dup(stack: Stack, log: list[str], extras: ScriptExtras) -> None: :raises MissingStackItems: if there's no element on stack """ - if not len(stack): + if not len(context.stack): raise MissingStackItems('OP_DUP: empty stack') - stack.append(stack[-1]) + context.stack.append(context.stack[-1]) -def op_greaterthan_timestamp(stack: Stack, log: list[str], extras: ScriptExtras) -> None: +def op_greaterthan_timestamp(context: ScriptContext) -> None: """Check whether transaction's timestamp is greater than the top of stack The top of stack must be a big-endian u32int. @@ -180,17 +173,17 @@ def op_greaterthan_timestamp(stack: Stack, log: list[str], extras: ScriptExtras) :raises MissingStackItems: if there's no element on stack """ - if not len(stack): + if not len(context.stack): raise MissingStackItems('OP_GREATERTHAN_TIMESTAMP: empty stack') - buf = stack.pop() + buf = context.stack.pop() assert isinstance(buf, bytes) (timelock,) = struct.unpack('!I', buf) - if extras.tx.timestamp <= timelock: + if context.extras.tx.timestamp <= timelock: raise TimeLocked('The output is locked until {}'.format( datetime.datetime.fromtimestamp(timelock).strftime("%m/%d/%Y %I:%M:%S %p"))) -def op_equalverify(stack: Stack, log: list[str], extras: ScriptExtras) -> None: +def op_equalverify(context: ScriptContext) -> None: """Verifies top 2 elements from stack are equal :param stack: the stack used when evaluating the script @@ -199,15 +192,15 @@ def op_equalverify(stack: Stack, log: list[str], extras: ScriptExtras) -> None: :raises MissingStackItems: if there aren't 2 element on stack :raises EqualVerifyFailed: items don't match """ - if len(stack) < 2: - raise MissingStackItems('OP_EQUALVERIFY: need 2 elements on stack, currently {}'.format(len(stack))) - op_equal(stack, log, extras) - is_equal = stack.pop() + if len(context.stack) < 2: + raise MissingStackItems('OP_EQUALVERIFY: need 2 elements on stack, currently {}'.format(len(context.stack))) + op_equal(context) + is_equal = context.stack.pop() if not is_equal: raise EqualVerifyFailed('Failed to verify if elements are equal') -def op_equal(stack: Stack, log: list[str], extras: ScriptExtras) -> None: +def op_equal(context: ScriptContext) -> None: """Verifies top 2 elements from stack are equal In case they are the same, we push 1 to the stack and push 0 if they are different @@ -215,20 +208,20 @@ def op_equal(stack: Stack, log: list[str], extras: ScriptExtras) -> None: :param stack: the stack used when evaluating the script :type stack: list[] """ - if len(stack) < 2: - raise MissingStackItems('OP_EQUAL: need 2 elements on stack, currently {}'.format(len(stack))) - elem1 = stack.pop() - elem2 = stack.pop() + if len(context.stack) < 2: + raise MissingStackItems('OP_EQUAL: need 2 elements on stack, currently {}'.format(len(context.stack))) + elem1 = context.stack.pop() + elem2 = context.stack.pop() assert isinstance(elem1, bytes) assert isinstance(elem2, bytes) if elem1 == elem2: - stack.append(1) + context.stack.append(1) else: - stack.append(0) - log.append('OP_EQUAL: failed. elements: {} {}'.format(elem1.hex(), elem2.hex())) + context.stack.append(0) + context.logs.append('OP_EQUAL: failed. elements: {} {}'.format(elem1.hex(), elem2.hex())) -def op_checksig(stack: Stack, log: list[str], extras: ScriptExtras) -> None: +def op_checksig(context: ScriptContext) -> None: """Verifies public key and signature match. Expects public key to be on top of stack, followed by signature. If they match, put 1 on stack (meaning True); otherwise, push 0 (False) @@ -241,10 +234,10 @@ def op_checksig(stack: Stack, log: list[str], extras: ScriptExtras) -> None: :return: if they don't match, return error message :rtype: string """ - if len(stack) < 2: - raise MissingStackItems('OP_CHECKSIG: need 2 elements on stack, currently {}'.format(len(stack))) - pubkey = stack.pop() - signature = stack.pop() + if len(context.stack) < 2: + raise MissingStackItems('OP_CHECKSIG: need 2 elements on stack, currently {}'.format(len(context.stack))) + pubkey = context.stack.pop() + signature = context.stack.pop() assert isinstance(pubkey, bytes) assert isinstance(signature, bytes) @@ -256,16 +249,16 @@ def op_checksig(stack: Stack, log: list[str], extras: ScriptExtras) -> None: # pubkey is not compressed public key raise ScriptError('OP_CHECKSIG: pubkey is not a public key') from e try: - public_key.verify(signature, extras.tx.get_sighash_all_data(), ec.ECDSA(hashes.SHA256())) + public_key.verify(signature, context.extras.tx.get_sighash_all_data(), ec.ECDSA(hashes.SHA256())) # valid, push true to stack - stack.append(1) + context.stack.append(1) except InvalidSignature: # invalid, push false to stack - stack.append(0) - log.append('OP_CHECKSIG: failed') + context.stack.append(0) + context.logs.append('OP_CHECKSIG: failed') -def op_hash160(stack: Stack, log: list[str], extras: ScriptExtras) -> None: +def op_hash160(context: ScriptContext) -> None: """Top stack item is hashed twice: first with SHA-256 and then with RIPEMD-160. Result is pushed back to stack. @@ -274,15 +267,15 @@ def op_hash160(stack: Stack, log: list[str], extras: ScriptExtras) -> None: :raises MissingStackItems: if there's no element on stack """ - if not len(stack): + if not len(context.stack): raise MissingStackItems('OP_HASH160: empty stack') - elem1 = stack.pop() + elem1 = context.stack.pop() assert isinstance(elem1, bytes) new_elem = get_hash160(elem1) - stack.append(new_elem) + context.stack.append(new_elem) -def op_checkdatasig(stack: Stack, log: list[str], extras: ScriptExtras) -> None: +def op_checkdatasig(context: ScriptContext) -> None: """Verifies public key, signature and data match. Expects public key to be on top of stack, followed by signature and data. If they match, put data on stack; otherwise, fail. @@ -292,11 +285,11 @@ def op_checkdatasig(stack: Stack, log: list[str], extras: ScriptExtras) -> None: :raises MissingStackItems: if there aren't 3 element on stack :raises OracleChecksigFailed: invalid signature, given data and public key """ - if len(stack) < 3: - raise MissingStackItems('OP_CHECKDATASIG: need 3 elements on stack, currently {}'.format(len(stack))) - pubkey = stack.pop() - signature = stack.pop() - data = stack.pop() + if len(context.stack) < 3: + raise MissingStackItems('OP_CHECKDATASIG: need 3 elements on stack, currently {}'.format(len(context.stack))) + pubkey = context.stack.pop() + signature = context.stack.pop() + data = context.stack.pop() assert isinstance(pubkey, bytes) assert isinstance(signature, bytes) assert isinstance(data, bytes) @@ -311,12 +304,12 @@ def op_checkdatasig(stack: Stack, log: list[str], extras: ScriptExtras) -> None: try: public_key.verify(signature, data, ec.ECDSA(hashes.SHA256())) # valid, push true to stack - stack.append(data) + context.stack.append(data) except InvalidSignature as e: raise OracleChecksigFailed from e -def op_data_strequal(stack: Stack, log: list[str], extras: ScriptExtras) -> None: +def op_data_strequal(context: ScriptContext) -> None: """Equivalent to an OP_GET_DATA_STR followed by an OP_EQUALVERIFY. Consumes three parameters from stack: . Gets the kth value @@ -329,11 +322,11 @@ def op_data_strequal(stack: Stack, log: list[str], extras: ScriptExtras) -> None :raises MissingStackItems: if there aren't 3 element on stack :raises VerifyFailed: verification failed """ - if len(stack) < 3: - raise MissingStackItems('OP_DATA_STREQUAL: need 3 elements on stack, currently {}'.format(len(stack))) - value = stack.pop() - data_k = stack.pop() - data = stack.pop() + if len(context.stack) < 3: + raise MissingStackItems('OP_DATA_STREQUAL: need 3 elements on stack, currently {}'.format(len(context.stack))) + value = context.stack.pop() + data_k = context.stack.pop() + data = context.stack.pop() assert isinstance(value, bytes) assert isinstance(data, bytes) @@ -344,10 +337,10 @@ def op_data_strequal(stack: Stack, log: list[str], extras: ScriptExtras) -> None if data_value != value: raise VerifyFailed('OP_DATA_STREQUAL: {} x {}'.format(data_value.decode('utf-8'), value.decode('utf-8'))) - stack.append(data) + context.stack.append(data) -def op_data_greaterthan(stack: Stack, log: list[str], extras: ScriptExtras) -> None: +def op_data_greaterthan(context: ScriptContext) -> None: """Equivalent to an OP_GET_DATA_INT followed by an OP_GREATERTHAN. Consumes three parameters from stack: . Gets the kth value @@ -359,11 +352,11 @@ def op_data_greaterthan(stack: Stack, log: list[str], extras: ScriptExtras) -> N :raises MissingStackItems: if there aren't 3 element on stack :raises VerifyFailed: verification failed """ - if len(stack) < 3: - raise MissingStackItems('OP_DATA_GREATERTHAN: need 3 elements on stack, currently {}'.format(len(stack))) - value = stack.pop() - data_k = stack.pop() - data = stack.pop() + if len(context.stack) < 3: + raise MissingStackItems(f'OP_DATA_GREATERTHAN: need 3 elements on stack, currently {len(context.stack)}') + value = context.stack.pop() + data_k = context.stack.pop() + data = context.stack.pop() assert isinstance(value, bytes) assert isinstance(data, bytes) @@ -380,10 +373,10 @@ def op_data_greaterthan(stack: Stack, log: list[str], extras: ScriptExtras) -> N if data_int <= value_int: raise VerifyFailed('op_data_greaterthan: {} x {}'.format(data_int, value_int)) - stack.append(data) + context.stack.append(data) -def op_data_match_interval(stack: Stack, log: list[str], extras: ScriptExtras) -> None: +def op_data_match_interval(stack: Stack) -> None: """Equivalent to an OP_GET_DATA_INT followed by an OP_MATCH_INTERVAL. :param stack: the stack used when evaluating the script @@ -435,7 +428,7 @@ def op_data_match_interval(stack: Stack, log: list[str], extras: ScriptExtras) - stack.append(last_pubkey) -def op_data_match_value(stack: Stack, log: list[str], extras: ScriptExtras) -> None: +def op_data_match_value(context: ScriptContext) -> None: """Equivalent to an OP_GET_DATA_STR followed by an OP_MATCH_VALUE. :param stack: the stack used when evaluating the script @@ -444,25 +437,25 @@ def op_data_match_value(stack: Stack, log: list[str], extras: ScriptExtras) -> N :raises MissingStackItems: if there aren't 3 element on stack :raises VerifyFailed: verification failed """ - if len(stack) < 1: + if len(context.stack) < 1: raise MissingStackItems('OP_DATA_MATCH_VALUE: empty stack') - data_n_items = stack.pop() + data_n_items = context.stack.pop() assert isinstance(data_n_items, bytes) # TODO test this can be transformed to integer n_items = data_n_items[0] # number of items in stack that will be used will_use = 2 * n_items + 3 # n data_points, n + 1 keys, k and data - if len(stack) < will_use: + if len(context.stack) < will_use: raise MissingStackItems('OP_DATA_MATCH_VALUE: need {} elements on stack, currently {}'.format( - will_use, len(stack))) + will_use, len(context.stack))) items = {} try: for _ in range(n_items): - pubkey = stack.pop() - buf = stack.pop() + pubkey = context.stack.pop() + buf = context.stack.pop() assert isinstance(pubkey, (str, bytes)) assert isinstance(buf, bytes) value = binary_to_int(buf) @@ -471,20 +464,20 @@ def op_data_match_value(stack: Stack, log: list[str], extras: ScriptExtras) -> N raise VerifyFailed from e # one pubkey is left on stack - last_pubkey = stack.pop() + last_pubkey = context.stack.pop() # next two items are data index and data - data_k = stack.pop() - data = stack.pop() + data_k = context.stack.pop() + data = context.stack.pop() assert isinstance(data_k, int) assert isinstance(data, bytes) data_value = get_data_value(data_k, data) data_int = binary_to_int(data_value) winner_pubkey = items.get(data_int, last_pubkey) assert isinstance(winner_pubkey, (str, bytes)) - stack.append(winner_pubkey) + context.stack.append(winner_pubkey) -def op_find_p2pkh(stack: Stack, log: list[str], extras: ScriptExtras) -> None: +def op_find_p2pkh(context: ScriptContext) -> None: """Checks whether the current transaction has an output with a P2PKH script with the given public key hash and the same amount as the input. @@ -500,28 +493,28 @@ def op_find_p2pkh(stack: Stack, log: list[str], extras: ScriptExtras) -> None: :raises MissingStackItems: if stack is empty :raises VerifyFailed: verification failed """ - if not len(stack): + if not len(context.stack): raise MissingStackItems('OP_FIND_P2PKH: empty stack') from hathor.transaction.scripts import P2PKH - spent_tx = extras.spent_tx - txin = extras.txin - tx = extras.tx + spent_tx = context.extras.spent_tx + txin = context.extras.txin + tx = context.extras.tx contract_value = spent_tx.outputs[txin.index].value - address = stack.pop() + address = context.stack.pop() address_b58 = get_address_b58_from_bytes(address) for output in tx.outputs: p2pkh_out = P2PKH.parse_script(output.script) if p2pkh_out: if p2pkh_out.address == address_b58 and output.value == contract_value: - stack.append(1) + context.stack.append(1) return # didn't find any match raise VerifyFailed -def op_checkmultisig(stack: Stack, log: list[str], extras: ScriptExtras) -> None: +def op_checkmultisig(context: ScriptContext) -> None: """Checks if it has the minimum signatures required and if all of them are valid :param stack: the stack used when evaluating the script @@ -532,11 +525,11 @@ def op_checkmultisig(stack: Stack, log: list[str], extras: ScriptExtras) -> None """ settings = get_settings() - if not len(stack): + if not len(context.stack): raise MissingStackItems('OP_CHECKMULTISIG: empty stack') # Pop the quantity of pubkeys - pubkey_count = stack.pop() + pubkey_count = context.stack.pop() if not isinstance(pubkey_count, int): raise InvalidStackData('OP_CHECKMULTISIG: pubkey count should be an integer') @@ -548,20 +541,20 @@ def op_checkmultisig(stack: Stack, log: list[str], extras: ScriptExtras) -> None ) ) - if len(stack) < pubkey_count: + if len(context.stack) < pubkey_count: raise MissingStackItems('OP_CHECKMULTISIG: not enough public keys on the stack') # Get all pubkeys pubkeys = [] for _ in range(pubkey_count): - pubkey_bytes = stack.pop() + pubkey_bytes = context.stack.pop() pubkeys.append(pubkey_bytes) - if not len(stack): + if not len(context.stack): raise MissingStackItems('OP_CHECKMULTISIG: less elements than should on the stack') # Pop the quantity of signatures required - signatures_count = stack.pop() + signatures_count = context.stack.pop() if not isinstance(signatures_count, int): raise InvalidStackData('OP_CHECKMULTISIG: signatures count should be an integer') @@ -574,13 +567,13 @@ def op_checkmultisig(stack: Stack, log: list[str], extras: ScriptExtras) -> None ) # Error if we don't have the minimum quantity of signatures - if len(stack) < signatures_count: + if len(context.stack) < signatures_count: raise MissingStackItems('OP_CHECKMULTISIG: not enough signatures on the stack') # Get all signatures signatures = [] for _ in range(signatures_count): - signature_bytes = stack.pop() + signature_bytes = context.stack.pop() signatures.append(signature_bytes) # For each signature we check if it's valid with one of the public keys @@ -590,21 +583,21 @@ def op_checkmultisig(stack: Stack, log: list[str], extras: ScriptExtras) -> None while pubkey_index < len(pubkeys): pubkey = pubkeys[pubkey_index] new_stack = [signature, pubkey] - op_checksig(new_stack, log, extras) + op_checksig(ScriptContext(stack=new_stack, logs=context.logs, extras=context.extras)) result = new_stack.pop() pubkey_index += 1 if result == 1: break else: # finished all pubkeys and did not verify all signatures - stack.append(0) + context.stack.append(0) return # If all signatures are valids we push 1 - stack.append(1) + context.stack.append(1) -def op_integer(opcode: int, stack: Stack, log: list[str], extras: ScriptExtras) -> None: +def op_integer(opcode: int, stack: Stack) -> None: """ Appends an integer to the stack We get the opcode comparing to all integers opcodes @@ -624,17 +617,26 @@ def op_integer(opcode: int, stack: Stack, log: list[str], extras: ScriptExtras) raise ScriptError(e) from e -MAP_OPCODE_TO_FN: dict[int, Callable[[Stack, list[str], ScriptExtras], None]] = { - Opcode.OP_DUP: op_dup, - Opcode.OP_EQUAL: op_equal, - Opcode.OP_EQUALVERIFY: op_equalverify, - Opcode.OP_CHECKSIG: op_checksig, - Opcode.OP_HASH160: op_hash160, - Opcode.OP_GREATERTHAN_TIMESTAMP: op_greaterthan_timestamp, - Opcode.OP_CHECKMULTISIG: op_checkmultisig, - Opcode.OP_DATA_STREQUAL: op_data_strequal, - Opcode.OP_DATA_GREATERTHAN: op_data_greaterthan, - Opcode.OP_DATA_MATCH_VALUE: op_data_match_value, - Opcode.OP_CHECKDATASIG: op_checkdatasig, - Opcode.OP_FIND_P2PKH: op_find_p2pkh, -} +def execute_op_code(opcode: Opcode, context: ScriptContext) -> None: + """ + Execute a function opcode. + + Args: + opcode: the opcode to be executed. + context: the script context to be manipulated. + """ + context.logs.append(f'Executing function opcode {opcode.name} ({hex(opcode.value)})') + match opcode: + case Opcode.OP_DUP: op_dup(context) + case Opcode.OP_EQUAL: op_equal(context) + case Opcode.OP_EQUALVERIFY: op_equalverify(context) + case Opcode.OP_CHECKSIG: op_checksig(context) + case Opcode.OP_HASH160: op_hash160(context) + case Opcode.OP_GREATERTHAN_TIMESTAMP: op_greaterthan_timestamp(context) + case Opcode.OP_CHECKMULTISIG: op_checkmultisig(context) + case Opcode.OP_DATA_STREQUAL: op_data_strequal(context) + case Opcode.OP_DATA_GREATERTHAN: op_data_greaterthan(context) + case Opcode.OP_DATA_MATCH_VALUE: op_data_match_value(context) + case Opcode.OP_CHECKDATASIG: op_checkdatasig(context) + case Opcode.OP_FIND_P2PKH: op_find_p2pkh(context) + case _: raise ScriptError(f'unknown opcode: {opcode}') diff --git a/hathor/transaction/scripts/script_context.py b/hathor/transaction/scripts/script_context.py new file mode 100644 index 000000000..925a881f1 --- /dev/null +++ b/hathor/transaction/scripts/script_context.py @@ -0,0 +1,25 @@ +# Copyright 2023 Hathor Labs +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from hathor.transaction.scripts.execute import ScriptExtras, Stack + + +class ScriptContext: + """A context to be manipulated during script execution. A separate instance must be used for each script.""" + __slots__ = ('stack', 'logs', 'extras') + + def __init__(self, *, stack: Stack, logs: list[str], extras: ScriptExtras) -> None: + self.stack = stack + self.logs = logs + self.extras = extras diff --git a/hathor/verification/block_verifier.py b/hathor/verification/block_verifier.py index 7a2e91b84..d919c6bd2 100644 --- a/hathor/verification/block_verifier.py +++ b/hathor/verification/block_verifier.py @@ -15,8 +15,7 @@ from hathor.conf.settings import HathorSettings from hathor.daa import DifficultyAdjustmentAlgorithm from hathor.feature_activation.feature_service import BlockIsMissingSignal, BlockIsSignaling, FeatureService -from hathor.profiler import get_cpu_profiler -from hathor.transaction import BaseTransaction, Block +from hathor.transaction import Block from hathor.transaction.exceptions import ( BlockMustSignalError, BlockWithInputs, @@ -26,13 +25,10 @@ TransactionDataError, WeightError, ) -from hathor.verification.vertex_verifier import VertexVerifier -cpu = get_cpu_profiler() - -class BlockVerifier(VertexVerifier): - __slots__ = ('_feature_service', ) +class BlockVerifier: + __slots__ = ('_settings', '_daa', '_feature_service') def __init__( self, @@ -41,48 +37,10 @@ def __init__( daa: DifficultyAdjustmentAlgorithm, feature_service: FeatureService | None = None ) -> None: - super().__init__(settings=settings, daa=daa) + self._settings = settings + self._daa = daa self._feature_service = feature_service - def verify_basic(self, block: Block, *, skip_block_weight_verification: bool = False) -> None: - """Partially run validations, the ones that need parents/inputs are skipped.""" - if not skip_block_weight_verification: - self.verify_weight(block) - self.verify_reward(block) - - @cpu.profiler(key=lambda _, block: 'block-verify!{}'.format(block.hash.hex())) - def verify(self, block: Block) -> None: - """ - (1) confirms at least two pending transactions and references last block - (2) solves the pow with the correct weight (done in HathorManager) - (3) creates the correct amount of tokens in the output (done in HathorManager) - (4) all parents must exist and have timestamp smaller than ours - (5) data field must contain at most BLOCK_DATA_MAX_SIZE bytes - (6) whether this block must signal feature support - """ - # TODO Should we validate a limit of outputs? - if block.is_genesis: - # TODO do genesis validation - return - - self.verify_without_storage(block) - - # (1) and (4) - self.verify_parents(block) - - self.verify_height(block) - - self.verify_mandatory_signaling(block) - - def verify_without_storage(self, block: Block) -> None: - """ Run all verifications that do not need a storage. - """ - self.verify_pow(block) - self.verify_no_inputs(block) - self.verify_outputs(block) - self.verify_data(block) - self.verify_sigops_output(block) - def verify_height(self, block: Block) -> None: """Validate that the block height is enough to confirm all transactions being confirmed.""" meta = block.get_metadata() @@ -113,9 +71,7 @@ def verify_no_inputs(self, block: Block) -> None: if inputs: raise BlockWithInputs('number of inputs {}'.format(len(inputs))) - def verify_outputs(self, block: BaseTransaction) -> None: - assert isinstance(block, Block) - super().verify_outputs(block) + def verify_output_token_indexes(self, block: Block) -> None: for output in block.outputs: if output.get_token_index() > 0: raise BlockWithTokensError('in output: {}'.format(output.to_human_readable())) diff --git a/hathor/verification/merge_mined_block_verifier.py b/hathor/verification/merge_mined_block_verifier.py index 2090c9119..9314fbb2a 100644 --- a/hathor/verification/merge_mined_block_verifier.py +++ b/hathor/verification/merge_mined_block_verifier.py @@ -12,18 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -from hathor.transaction import Block, MergeMinedBlock -from hathor.verification.block_verifier import BlockVerifier +from hathor.transaction import MergeMinedBlock -class MergeMinedBlockVerifier(BlockVerifier): +class MergeMinedBlockVerifier: __slots__ = () - def verify_without_storage(self, block: Block) -> None: - assert isinstance(block, MergeMinedBlock) - self.verify_aux_pow(block) - super().verify_without_storage(block) - def verify_aux_pow(self, block: MergeMinedBlock) -> None: """ Verify auxiliary proof-of-work (for merged mining). """ diff --git a/hathor/verification/token_creation_transaction_verifier.py b/hathor/verification/token_creation_transaction_verifier.py index e6cca358d..66d96f111 100644 --- a/hathor/verification/token_creation_transaction_verifier.py +++ b/hathor/verification/token_creation_transaction_verifier.py @@ -12,26 +12,22 @@ # See the License for the specific language governing permissions and # limitations under the License. +from hathor.conf.settings import HathorSettings from hathor.transaction.exceptions import InvalidToken, TransactionDataError from hathor.transaction.token_creation_tx import TokenCreationTransaction +from hathor.transaction.transaction import TokenInfo from hathor.transaction.util import clean_token_string +from hathor.types import TokenUid from hathor.util import not_none -from hathor.verification.transaction_verifier import TransactionVerifier -class TokenCreationTransactionVerifier(TransactionVerifier): - __slots__ = () +class TokenCreationTransactionVerifier: + __slots__ = ('_settings',) - def verify(self, tx: TokenCreationTransaction, *, reject_locked_reward: bool = True) -> None: - """ Run all validations as regular transactions plus validation on token info. + def __init__(self, *, settings: HathorSettings) -> None: + self._settings = settings - We also overload verify_sum to make some different checks - """ - super().verify(tx, reject_locked_reward=reject_locked_reward) - self.verify_minted_tokens(tx) - self.verify_token_info(tx) - - def verify_minted_tokens(self, tx: TokenCreationTransaction) -> None: + def verify_minted_tokens(self, tx: TokenCreationTransaction, token_dict: dict[TokenUid, TokenInfo]) -> None: """ Besides all checks made on regular transactions, a few extra ones are made: - only HTR tokens on the inputs; - new tokens are actually being minted; @@ -39,8 +35,6 @@ def verify_minted_tokens(self, tx: TokenCreationTransaction) -> None: :raises InvalidToken: when there's an error in token operations :raises InputOutputMismatch: if sum of inputs is not equal to outputs and there's no mint/melt """ - token_dict = tx.get_complete_token_info() - # make sure tokens are being minted token_info = token_dict[not_none(tx.hash)] if token_info.amount <= 0: diff --git a/hathor/verification/transaction_verifier.py b/hathor/verification/transaction_verifier.py index ad07bf5c7..630c82147 100644 --- a/hathor/verification/transaction_verifier.py +++ b/hathor/verification/transaction_verifier.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from hathor.conf.settings import HathorSettings +from hathor.daa import DifficultyAdjustmentAlgorithm from hathor.profiler import get_cpu_profiler from hathor.transaction import BaseTransaction, Transaction, TxInput from hathor.transaction.exceptions import ( @@ -34,46 +36,16 @@ from hathor.transaction.transaction import TokenInfo from hathor.transaction.util import get_deposit_amount, get_withdraw_amount from hathor.types import TokenUid, VertexId -from hathor.verification.vertex_verifier import VertexVerifier cpu = get_cpu_profiler() -class TransactionVerifier(VertexVerifier): - __slots__ = () +class TransactionVerifier: + __slots__ = ('_settings', '_daa') - def verify_basic(self, tx: Transaction) -> None: - """Partially run validations, the ones that need parents/inputs are skipped.""" - if tx.is_genesis: - # TODO do genesis validation? - return - self.verify_parents_basic(tx) - self.verify_weight(tx) - self.verify_without_storage(tx) - - @cpu.profiler(key=lambda _, tx: 'tx-verify!{}'.format(tx.hash.hex())) - def verify(self, tx: Transaction, *, reject_locked_reward: bool = True) -> None: - """ Common verification for all transactions: - (i) number of inputs is at most 256 - (ii) number of outputs is at most 256 - (iii) confirms at least two pending transactions - (iv) solves the pow (we verify weight is correct in HathorManager) - (v) validates signature of inputs - (vi) validates public key and output (of the inputs) addresses - (vii) validate that both parents are valid - (viii) validate input's timestamps - (ix) validate inputs and outputs sum - """ - if tx.is_genesis: - # TODO do genesis validation - return - self.verify_without_storage(tx) - self.verify_sigops_input(tx) - self.verify_inputs(tx) # need to run verify_inputs first to check if all inputs exist - self.verify_parents(tx) - self.verify_sum(tx) - if reject_locked_reward: - self.verify_reward_locked(tx) + def __init__(self, *, settings: HathorSettings, daa: DifficultyAdjustmentAlgorithm) -> None: + self._settings = settings + self._daa = daa def verify_parents_basic(self, tx: Transaction) -> None: """Verify number and non-duplicity of parents.""" @@ -98,14 +70,6 @@ def verify_weight(self, tx: Transaction) -> None: raise WeightError(f'Invalid new tx {tx.hash_hex}: weight ({tx.weight}) is ' f'greater than the maximum allowed ({max_tx_weight})') - def verify_without_storage(self, tx: Transaction) -> None: - """ Run all verifications that do not need a storage. - """ - self.verify_pow(tx) - self.verify_number_of_inputs(tx) - self.verify_outputs(tx) - self.verify_sigops_output(tx) - def verify_sigops_input(self, tx: Transaction) -> None: """ Count sig operations on all inputs and verify that the total sum is below the limit """ @@ -177,18 +141,6 @@ def verify_script(self, *, tx: Transaction, input_tx: TxInput, spent_tx: BaseTra except ScriptError as e: raise InvalidInputData(e) from e - def verify_sum(self, tx: Transaction) -> None: - """Verify that the sum of outputs is equal of the sum of inputs, for each token. - - If there are authority UTXOs involved, tokens can be minted or melted, so the above rule may - not be respected. - - :raises InvalidToken: when there's an error in token operations - :raises InputOutputMismatch: if sum of inputs is not equal to outputs and there's no mint/melt - """ - token_dict = tx.get_complete_token_info() - self.verify_authorities_and_deposit(token_dict) - def verify_reward_locked(self, tx: Transaction) -> None: """Will raise `RewardLocked` if any reward is spent before the best block height is enough, considering only the block rewards spent by this tx itself, and not the inherited `min_height`.""" @@ -205,19 +157,17 @@ def verify_number_of_inputs(self, tx: Transaction) -> None: if not tx.is_genesis: raise NoInputError('Transaction must have at least one input') - def verify_outputs(self, tx: BaseTransaction) -> None: + def verify_output_token_indexes(self, tx: Transaction) -> None: """Verify outputs reference an existing token uid in the tokens list :raises InvalidToken: output references non existent token uid """ - assert isinstance(tx, Transaction) - super().verify_outputs(tx) for output in tx.outputs: # check index is valid if output.get_token_index() > len(tx.tokens): raise InvalidToken('token uid index not available: index {}'.format(output.get_token_index())) - def verify_authorities_and_deposit(self, token_dict: dict[TokenUid, TokenInfo]) -> None: + def verify_sum(self, token_dict: dict[TokenUid, TokenInfo]) -> None: """Verify that the sum of outputs is equal of the sum of inputs, for each token. If sum of inputs and outputs is not 0, make sure inputs have mint/melt authority. diff --git a/hathor/verification/verification_service.py b/hathor/verification/verification_service.py index 3248d6516..efa18c6f6 100644 --- a/hathor/verification/verification_service.py +++ b/hathor/verification/verification_service.py @@ -12,47 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import NamedTuple - from typing_extensions import assert_never -from hathor.conf.settings import HathorSettings -from hathor.daa import DifficultyAdjustmentAlgorithm -from hathor.feature_activation.feature_service import FeatureService +from hathor.profiler import get_cpu_profiler from hathor.transaction import BaseTransaction, Block, MergeMinedBlock, Transaction, TxVersion from hathor.transaction.token_creation_tx import TokenCreationTransaction +from hathor.transaction.transaction import TokenInfo from hathor.transaction.validation_state import ValidationState -from hathor.verification.block_verifier import BlockVerifier -from hathor.verification.merge_mined_block_verifier import MergeMinedBlockVerifier -from hathor.verification.token_creation_transaction_verifier import TokenCreationTransactionVerifier -from hathor.verification.transaction_verifier import TransactionVerifier - - -class VertexVerifiers(NamedTuple): - """A group of verifier instances, one for each vertex type.""" - block: BlockVerifier - merge_mined_block: MergeMinedBlockVerifier - tx: TransactionVerifier - token_creation_tx: TokenCreationTransactionVerifier - - @classmethod - def create_defaults( - cls, - *, - settings: HathorSettings, - daa: DifficultyAdjustmentAlgorithm, - feature_service: FeatureService | None = None, - ) -> 'VertexVerifiers': - """ - Create a VertexVerifiers instance using the default verifier for each vertex type, - from all required dependencies. - """ - return VertexVerifiers( - block=BlockVerifier(settings=settings, daa=daa, feature_service=feature_service), - merge_mined_block=MergeMinedBlockVerifier(settings=settings, daa=daa, feature_service=feature_service), - tx=TransactionVerifier(settings=settings, daa=daa), - token_creation_tx=TokenCreationTransactionVerifier(settings=settings, daa=daa), - ) +from hathor.types import TokenUid +from hathor.verification.vertex_verifiers import VertexVerifiers + +cpu = get_cpu_profiler() class VerificationService: @@ -116,25 +86,40 @@ def verify_basic(self, vertex: BaseTransaction, *, skip_block_weight_verificatio match vertex.version: case TxVersion.REGULAR_BLOCK: assert type(vertex) is Block - self.verifiers.block.verify_basic( - vertex, - skip_block_weight_verification=skip_block_weight_verification - ) + self._verify_basic_block(vertex, skip_weight_verification=skip_block_weight_verification) case TxVersion.MERGE_MINED_BLOCK: assert type(vertex) is MergeMinedBlock - self.verifiers.merge_mined_block.verify_basic( - vertex, - skip_block_weight_verification=skip_block_weight_verification - ) + self._verify_basic_merge_mined_block(vertex, skip_weight_verification=skip_block_weight_verification) case TxVersion.REGULAR_TRANSACTION: assert type(vertex) is Transaction - self.verifiers.tx.verify_basic(vertex) + self._verify_basic_tx(vertex) case TxVersion.TOKEN_CREATION_TRANSACTION: assert type(vertex) is TokenCreationTransaction - self.verifiers.token_creation_tx.verify_basic(vertex) + self._verify_basic_token_creation_tx(vertex) case _: assert_never(vertex.version) + def _verify_basic_block(self, block: Block, *, skip_weight_verification: bool) -> None: + """Partially run validations, the ones that need parents/inputs are skipped.""" + if not skip_weight_verification: + self.verifiers.block.verify_weight(block) + self.verifiers.block.verify_reward(block) + + def _verify_basic_merge_mined_block(self, block: MergeMinedBlock, *, skip_weight_verification: bool) -> None: + self._verify_basic_block(block, skip_weight_verification=skip_weight_verification) + + def _verify_basic_tx(self, tx: Transaction) -> None: + """Partially run validations, the ones that need parents/inputs are skipped.""" + if tx.is_genesis: + # TODO do genesis validation? + return + self.verifiers.tx.verify_parents_basic(tx) + self.verifiers.tx.verify_weight(tx) + self.verify_without_storage(tx) + + def _verify_basic_token_creation_tx(self, tx: TokenCreationTransaction) -> None: + self._verify_basic_tx(tx) + def verify(self, vertex: BaseTransaction, *, reject_locked_reward: bool = True) -> None: """Run all verifications. Raises on error. @@ -143,33 +128,126 @@ def verify(self, vertex: BaseTransaction, *, reject_locked_reward: bool = True) match vertex.version: case TxVersion.REGULAR_BLOCK: assert type(vertex) is Block - self.verifiers.block.verify(vertex) + self._verify_block(vertex) case TxVersion.MERGE_MINED_BLOCK: assert type(vertex) is MergeMinedBlock - self.verifiers.merge_mined_block.verify(vertex) + self._verify_merge_mined_block(vertex) case TxVersion.REGULAR_TRANSACTION: assert type(vertex) is Transaction - self.verifiers.tx.verify(vertex, reject_locked_reward=reject_locked_reward) + self._verify_tx(vertex, reject_locked_reward=reject_locked_reward) case TxVersion.TOKEN_CREATION_TRANSACTION: assert type(vertex) is TokenCreationTransaction - self.verifiers.token_creation_tx.verify(vertex, reject_locked_reward=reject_locked_reward) + self._verify_token_creation_tx(vertex, reject_locked_reward=reject_locked_reward) case _: assert_never(vertex.version) + @cpu.profiler(key=lambda _, block: 'block-verify!{}'.format(block.hash.hex())) + def _verify_block(self, block: Block) -> None: + """ + (1) confirms at least two pending transactions and references last block + (2) solves the pow with the correct weight (done in HathorManager) + (3) creates the correct amount of tokens in the output (done in HathorManager) + (4) all parents must exist and have timestamp smaller than ours + (5) data field must contain at most BLOCK_DATA_MAX_SIZE bytes + (6) whether this block must signal feature support + """ + # TODO Should we validate a limit of outputs? + if block.is_genesis: + # TODO do genesis validation + return + + self.verify_without_storage(block) + + # (1) and (4) + self.verifiers.vertex.verify_parents(block) + + self.verifiers.block.verify_height(block) + + self.verifiers.block.verify_mandatory_signaling(block) + + def _verify_merge_mined_block(self, block: MergeMinedBlock) -> None: + self._verify_block(block) + + @cpu.profiler(key=lambda _, tx: 'tx-verify!{}'.format(tx.hash.hex())) + def _verify_tx( + self, + tx: Transaction, + *, + reject_locked_reward: bool, + token_dict: dict[TokenUid, TokenInfo] | None = None + ) -> None: + """ Common verification for all transactions: + (i) number of inputs is at most 256 + (ii) number of outputs is at most 256 + (iii) confirms at least two pending transactions + (iv) solves the pow (we verify weight is correct in HathorManager) + (v) validates signature of inputs + (vi) validates public key and output (of the inputs) addresses + (vii) validate that both parents are valid + (viii) validate input's timestamps + (ix) validate inputs and outputs sum + """ + if tx.is_genesis: + # TODO do genesis validation + return + self.verify_without_storage(tx) + self.verifiers.tx.verify_sigops_input(tx) + self.verifiers.tx.verify_inputs(tx) # need to run verify_inputs first to check if all inputs exist + self.verifiers.vertex.verify_parents(tx) + self.verifiers.tx.verify_sum(token_dict or tx.get_complete_token_info()) + if reject_locked_reward: + self.verifiers.tx.verify_reward_locked(tx) + + def _verify_token_creation_tx(self, tx: TokenCreationTransaction, *, reject_locked_reward: bool) -> None: + """ Run all validations as regular transactions plus validation on token info. + + We also overload verify_sum to make some different checks + """ + token_dict = tx.get_complete_token_info() + self._verify_tx(tx, reject_locked_reward=reject_locked_reward, token_dict=token_dict) + self.verifiers.token_creation_tx.verify_minted_tokens(tx, token_dict) + self.verifiers.token_creation_tx.verify_token_info(tx) + def verify_without_storage(self, vertex: BaseTransaction) -> None: # We assert with type() instead of isinstance() because each subclass has a specific branch. match vertex.version: case TxVersion.REGULAR_BLOCK: assert type(vertex) is Block - self.verifiers.block.verify_without_storage(vertex) + self._verify_without_storage_block(vertex) case TxVersion.MERGE_MINED_BLOCK: assert type(vertex) is MergeMinedBlock - self.verifiers.merge_mined_block.verify_without_storage(vertex) + self._verify_without_storage_merge_mined_block(vertex) case TxVersion.REGULAR_TRANSACTION: assert type(vertex) is Transaction - self.verifiers.tx.verify_without_storage(vertex) + self._verify_without_storage_tx(vertex) case TxVersion.TOKEN_CREATION_TRANSACTION: assert type(vertex) is TokenCreationTransaction - self.verifiers.token_creation_tx.verify_without_storage(vertex) + self._verify_without_storage_token_creation_tx(vertex) case _: assert_never(vertex.version) + + def _verify_without_storage_block(self, block: Block) -> None: + """ Run all verifications that do not need a storage. + """ + self.verifiers.vertex.verify_pow(block) + self.verifiers.block.verify_no_inputs(block) + self.verifiers.vertex.verify_outputs(block) + self.verifiers.block.verify_output_token_indexes(block) + self.verifiers.block.verify_data(block) + self.verifiers.vertex.verify_sigops_output(block) + + def _verify_without_storage_merge_mined_block(self, block: MergeMinedBlock) -> None: + self.verifiers.merge_mined_block.verify_aux_pow(block) + self._verify_without_storage_block(block) + + def _verify_without_storage_tx(self, tx: Transaction) -> None: + """ Run all verifications that do not need a storage. + """ + self.verifiers.vertex.verify_pow(tx) + self.verifiers.tx.verify_number_of_inputs(tx) + self.verifiers.vertex.verify_outputs(tx) + self.verifiers.tx.verify_output_token_indexes(tx) + self.verifiers.vertex.verify_sigops_output(tx) + + def _verify_without_storage_token_creation_tx(self, tx: TokenCreationTransaction) -> None: + self._verify_without_storage_tx(tx) diff --git a/hathor/verification/vertex_verifiers.py b/hathor/verification/vertex_verifiers.py new file mode 100644 index 000000000..eed2ca74f --- /dev/null +++ b/hathor/verification/vertex_verifiers.py @@ -0,0 +1,79 @@ +# Copyright 2023 Hathor Labs +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import NamedTuple + +from hathor.conf.settings import HathorSettings +from hathor.daa import DifficultyAdjustmentAlgorithm +from hathor.feature_activation.feature_service import FeatureService +from hathor.verification.block_verifier import BlockVerifier +from hathor.verification.merge_mined_block_verifier import MergeMinedBlockVerifier +from hathor.verification.token_creation_transaction_verifier import TokenCreationTransactionVerifier +from hathor.verification.transaction_verifier import TransactionVerifier +from hathor.verification.vertex_verifier import VertexVerifier + + +class VertexVerifiers(NamedTuple): + """A group of verifier instances, one for each vertex type.""" + vertex: VertexVerifier + block: BlockVerifier + merge_mined_block: MergeMinedBlockVerifier + tx: TransactionVerifier + token_creation_tx: TokenCreationTransactionVerifier + + @classmethod + def create_defaults( + cls, + *, + settings: HathorSettings, + daa: DifficultyAdjustmentAlgorithm, + feature_service: FeatureService | None = None, + ) -> 'VertexVerifiers': + """ + Create a VertexVerifiers instance using the default verifier for each vertex type, + from all required dependencies. + """ + vertex_verifier = VertexVerifier(settings=settings, daa=daa) + + return cls.create( + settings=settings, + vertex_verifier=vertex_verifier, + daa=daa, + feature_service=feature_service + ) + + @classmethod + def create( + cls, + *, + settings: HathorSettings, + vertex_verifier: VertexVerifier, + daa: DifficultyAdjustmentAlgorithm, + feature_service: FeatureService | None = None, + ) -> 'VertexVerifiers': + """ + Create a VertexVerifiers instance using a custom vertex_verifier. + """ + block_verifier = BlockVerifier(settings=settings, daa=daa, feature_service=feature_service) + merge_mined_block_verifier = MergeMinedBlockVerifier() + tx_verifier = TransactionVerifier(settings=settings, daa=daa) + token_creation_tx_verifier = TokenCreationTransactionVerifier(settings=settings) + + return VertexVerifiers( + vertex=vertex_verifier, + block=block_verifier, + merge_mined_block=merge_mined_block_verifier, + tx=tx_verifier, + token_creation_tx=token_creation_tx_verifier, + ) diff --git a/poetry.lock b/poetry.lock index e327ad4fb..53b74c73a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1374,6 +1374,17 @@ psutil = ["psutil (>=3.0)"] setproctitle = ["setproctitle"] testing = ["filelock"] +[[package]] +name = "python-healthchecklib" +version = "0.1.0" +description = "Opinionated healthcheck library" +optional = false +python-versions = ">=3.8.1,<4.0.0" +files = [ + {file = "python_healthchecklib-0.1.0-py3-none-any.whl", hash = "sha256:95d94fcae7f281adf16624014ae789dfa38d1be327cc38b02ee82bad70671f2f"}, + {file = "python_healthchecklib-0.1.0.tar.gz", hash = "sha256:afa0572d37902c50232d99acf0065836082bb027109c9c98e8d5acfefd381595"}, +] + [[package]] name = "pywin32" version = "305" @@ -2135,4 +2146,4 @@ sentry = ["sentry-sdk", "structlog-sentry"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<4" -content-hash = "1a2830d269a9d5a6fe449b5e884438b5f17a5dacd89110b7ada5af2026c4ab97" +content-hash = "2b20a90cf75e75bd32568e722489db53b4a4b490f4e3f084ff5734ea8137c37e" diff --git a/pyproject.toml b/pyproject.toml index f6b8e838f..f40746b0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,6 +79,7 @@ hathorlib = "0.3.0" pydantic = "~1.10.13" pyyaml = "^6.0.1" typing-extensions = "~4.8.0" +python-healthchecklib = "^0.1.0" [tool.poetry.extras] sentry = ["sentry-sdk", "structlog-sentry"] diff --git a/tests/p2p/test_peer_id.py b/tests/p2p/test_peer_id.py index 8cb20dca8..b9add5faa 100644 --- a/tests/p2p/test_peer_id.py +++ b/tests/p2p/test_peer_id.py @@ -2,8 +2,6 @@ import shutil import tempfile -from twisted.internet.defer import inlineCallbacks - from hathor.conf import HathorSettings from hathor.p2p.peer_id import InvalidPeerIdException, PeerId from hathor.p2p.peer_storage import PeerStorage @@ -212,8 +210,7 @@ def test_retry_logic(self): class BasePeerIdTest(unittest.TestCase): __test__ = False - @inlineCallbacks - def test_validate_entrypoint(self): + async def test_validate_entrypoint(self): manager = self.create_peer('testnet', unlock_wallet=False) peer_id = manager.my_peer peer_id.entrypoints = ['tcp://127.0.0.1:40403'] @@ -221,15 +218,15 @@ def test_validate_entrypoint(self): # we consider that we are starting the connection to the peer protocol = manager.connections.client_factory.buildProtocol('127.0.0.1') protocol.connection_string = 'tcp://127.0.0.1:40403' - result = yield peer_id.validate_entrypoint(protocol) + result = await peer_id.validate_entrypoint(protocol) self.assertTrue(result) # if entrypoint is an URI peer_id.entrypoints = ['uri_name'] - result = yield peer_id.validate_entrypoint(protocol) + result = await peer_id.validate_entrypoint(protocol) self.assertTrue(result) # test invalid. DNS in test mode will resolve to '127.0.0.1:40403' protocol.connection_string = 'tcp://45.45.45.45:40403' - result = yield peer_id.validate_entrypoint(protocol) + result = await peer_id.validate_entrypoint(protocol) self.assertFalse(result) # now test when receiving the connection - i.e. the peer starts it @@ -242,11 +239,11 @@ def getPeer(self): Peer = namedtuple('Peer', 'host') return Peer(host='127.0.0.1') protocol.transport = FakeTransport() - result = yield peer_id.validate_entrypoint(protocol) + result = await peer_id.validate_entrypoint(protocol) self.assertTrue(result) # if entrypoint is an URI peer_id.entrypoints = ['uri_name'] - result = yield peer_id.validate_entrypoint(protocol) + result = await peer_id.validate_entrypoint(protocol) self.assertTrue(result) diff --git a/tests/pubsub/test_pubsub.py b/tests/pubsub/test_pubsub.py index fc41fca44..2d3d1ef62 100644 --- a/tests/pubsub/test_pubsub.py +++ b/tests/pubsub/test_pubsub.py @@ -1,47 +1,8 @@ -import threading -import time - -from twisted.internet import threads -from twisted.python import threadable - from hathor.pubsub import HathorEvents, PubSubManager -from hathor.util import reactor -from tests import unittest - - -class PubSubTestCase(unittest.TestCase): - def _waitForThread(self): - """ - The reactor's threadpool is only available when the reactor is running, - so to have a sane behavior during the tests we make a dummy - L{threads.deferToThread} call. - """ - # copied from twisted/test/test_threads.py [yan] - return threads.deferToThread(time.sleep, 0) - - def test_pubsub_thread(self): - """ Test pubsub function is always called in reactor thread. - """ - def _on_new_event(*args): - self.assertTrue(threadable.isInIOThread()) - - pubsub = PubSubManager(reactor) - pubsub.subscribe(HathorEvents.NETWORK_NEW_TX_ACCEPTED, _on_new_event) - - def cb(_ignore): - waiter = threading.Event() - - def threadedFunc(): - self.assertFalse(threadable.isInIOThread()) - pubsub.publish(HathorEvents.NETWORK_NEW_TX_ACCEPTED) - waiter.set() - - reactor.callInThread(threadedFunc) - waiter.wait(20) - self.assertTrue(waiter.isSet()) +from tests.unittest import TestCase - return self._waitForThread().addCallback(cb) +class PubSubTestCase(TestCase): def test_duplicate_subscribe(self): def noop(): pass diff --git a/tests/resources/healthcheck/test_healthcheck.py b/tests/resources/healthcheck/test_healthcheck.py index 888aac2af..e40fb2a76 100644 --- a/tests/resources/healthcheck/test_healthcheck.py +++ b/tests/resources/healthcheck/test_healthcheck.py @@ -31,6 +31,7 @@ def test_get_no_recent_activity(self): 'checks': { 'sync': [{ 'componentType': 'internal', + 'componentName': 'sync', 'status': 'fail', 'output': HathorManager.UnhealthinessReason.NO_RECENT_ACTIVITY, 'time': ANY @@ -53,6 +54,7 @@ def test_strict_status_code(self): 'checks': { 'sync': [{ 'componentType': 'internal', + 'componentName': 'sync', 'status': 'fail', 'output': HathorManager.UnhealthinessReason.NO_RECENT_ACTIVITY, 'time': ANY @@ -79,6 +81,7 @@ def test_get_no_connected_peer(self): 'checks': { 'sync': [{ 'componentType': 'internal', + 'componentName': 'sync', 'status': 'fail', 'output': HathorManager.UnhealthinessReason.NO_SYNCED_PEER, 'time': ANY @@ -111,6 +114,7 @@ def test_get_peer_out_of_sync(self): 'checks': { 'sync': [{ 'componentType': 'internal', + 'componentName': 'sync', 'status': 'fail', 'output': HathorManager.UnhealthinessReason.NO_SYNCED_PEER, 'time': ANY @@ -143,6 +147,7 @@ def test_get_ready(self): 'checks': { 'sync': [{ 'componentType': 'internal', + 'componentName': 'sync', 'status': 'pass', 'output': 'Healthy', 'time': ANY diff --git a/tests/tx/test_genesis.py b/tests/tx/test_genesis.py index eecabd7fa..885395fa7 100644 --- a/tests/tx/test_genesis.py +++ b/tests/tx/test_genesis.py @@ -1,8 +1,9 @@ from hathor.conf import HathorSettings from hathor.daa import DifficultyAdjustmentAlgorithm, TestMode from hathor.transaction.storage import TransactionMemoryStorage -from hathor.verification.verification_service import VerificationService, VertexVerifiers +from hathor.verification.verification_service import VerificationService from hathor.verification.vertex_verifier import VertexVerifier +from hathor.verification.vertex_verifiers import VertexVerifiers from tests import unittest settings = HathorSettings() diff --git a/tests/tx/test_scripts.py b/tests/tx/test_scripts.py index 853be2ac4..b6cf99566 100644 --- a/tests/tx/test_scripts.py +++ b/tests/tx/test_scripts.py @@ -1,4 +1,5 @@ import struct +from unittest.mock import Mock from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec @@ -54,6 +55,7 @@ op_pushdata, op_pushdata1, ) +from hathor.transaction.scripts.script_context import ScriptContext from hathor.transaction.storage import TransactionMemoryStorage from hathor.wallet import HDWallet from tests import unittest @@ -174,22 +176,22 @@ def test_pushdata1(self): def test_dup(self): with self.assertRaises(MissingStackItems): - op_dup([], log=[], extras=None) + op_dup(ScriptContext(stack=[], logs=[], extras=Mock())) stack = [1] - op_dup(stack, log=[], extras=None) + op_dup(ScriptContext(stack=stack, logs=[], extras=Mock())) self.assertEqual(stack[-1], stack[-2]) def test_equalverify(self): elem = b'a' with self.assertRaises(MissingStackItems): - op_equalverify([elem], log=[], extras=None) + op_equalverify(ScriptContext(stack=[elem], logs=[], extras=Mock())) # no exception should be raised - op_equalverify([elem, elem], log=[], extras=None) + op_equalverify(ScriptContext(stack=[elem, elem], logs=[], extras=Mock())) with self.assertRaises(EqualVerifyFailed): - op_equalverify([elem, b'aaaa'], log=[], extras=None) + op_equalverify(ScriptContext(stack=[elem, b'aaaa'], logs=[], extras=Mock())) def test_checksig_raise_on_uncompressed_pubkey(self): """ Uncompressed pubkeys shoud not be accepted, even if they solve the signature @@ -211,11 +213,11 @@ def test_checksig_raise_on_uncompressed_pubkey(self): # ScriptError if pubkey is not a valid compressed public key # with wrong signature with self.assertRaises(ScriptError): - op_checksig([b'123', pubkey_uncompressed], log=[], extras=None) + op_checksig(ScriptContext(stack=[b'123', pubkey_uncompressed], logs=[], extras=Mock())) # or with rigth one # this will make sure the signature is not made when parameters are wrong with self.assertRaises(ScriptError): - op_checksig([signature, pubkey_uncompressed], log=[], extras=None) + op_checksig(ScriptContext(stack=[signature, pubkey_uncompressed], logs=[], extras=Mock())) def test_checksig_check_for_compressed_pubkey(self): """ Compressed pubkeys bytes representation always start with a byte 2 or 3 @@ -224,19 +226,19 @@ def test_checksig_check_for_compressed_pubkey(self): """ # ScriptError if pubkey is not a public key but starts with 2 or 3 with self.assertRaises(ScriptError): - op_checksig([b'\x0233', b'\x0233'], log=[], extras=None) + op_checksig(ScriptContext(stack=[b'\x0233', b'\x0233'], logs=[], extras=Mock())) with self.assertRaises(ScriptError): - op_checksig([b'\x0321', b'\x0321'], log=[], extras=None) + op_checksig(ScriptContext(stack=[b'\x0321', b'\x0321'], logs=[], extras=Mock())) # ScriptError if pubkey does not start with 2 or 3 with self.assertRaises(ScriptError): - op_checksig([b'\x0123', b'\x0123'], log=[], extras=None) + op_checksig(ScriptContext(stack=[b'\x0123', b'\x0123'], logs=[], extras=Mock())) with self.assertRaises(ScriptError): - op_checksig([b'\x0423', b'\x0423'], log=[], extras=None) + op_checksig(ScriptContext(stack=[b'\x0423', b'\x0423'], logs=[], extras=Mock())) def test_checksig(self): with self.assertRaises(MissingStackItems): - op_checksig([1], log=[], extras=None) + op_checksig(ScriptContext(stack=[1], logs=[], extras=Mock())) block = self.genesis_blocks[0] @@ -251,15 +253,15 @@ def test_checksig(self): signature = self.genesis_private_key.sign(hashed_data, ec.ECDSA(hashes.SHA256())) pubkey_bytes = get_public_key_bytes_compressed(self.genesis_public_key) - extras = ScriptExtras(tx=tx, txin=None, spent_tx=None) + extras = ScriptExtras(tx=tx, txin=Mock(), spent_tx=Mock()) # wrong signature puts False (0) on stack stack = [b'aaaaaaaaa', pubkey_bytes] - op_checksig(stack, log=[], extras=extras) + op_checksig(ScriptContext(stack=stack, logs=[], extras=extras)) self.assertEqual(0, stack.pop()) stack = [signature, pubkey_bytes] - op_checksig(stack, log=[], extras=extras) + op_checksig(ScriptContext(stack=stack, logs=[], extras=extras)) self.assertEqual(1, stack.pop()) def test_checksig_cache(self): @@ -276,22 +278,22 @@ def test_checksig_cache(self): signature = self.genesis_private_key.sign(hashed_data, ec.ECDSA(hashes.SHA256())) pubkey_bytes = get_public_key_bytes_compressed(self.genesis_public_key) - extras = ScriptExtras(tx=tx, txin=None, spent_tx=None) + extras = ScriptExtras(tx=tx, txin=Mock(), spent_tx=Mock()) stack = [signature, pubkey_bytes] self.assertIsNone(tx._sighash_data_cache) - op_checksig(stack, log=[], extras=extras) + op_checksig(ScriptContext(stack=stack, logs=[], extras=extras)) self.assertIsNotNone(tx._sighash_data_cache) self.assertEqual(1, stack.pop()) def test_hash160(self): with self.assertRaises(MissingStackItems): - op_hash160([], log=[], extras=None) + op_hash160(ScriptContext(stack=[], logs=[], extras=Mock())) elem = b'aaaaaaaa' hash160 = get_hash160(elem) stack = [elem] - op_hash160(stack, log=[], extras=None) + op_hash160(ScriptContext(stack=stack, logs=[], extras=Mock())) self.assertEqual(hash160, stack.pop()) def test_checkdatasig_raise_on_uncompressed_pubkey(self): @@ -314,27 +316,27 @@ def test_checkdatasig_raise_on_uncompressed_pubkey(self): # with wrong signature stack = [data, b'123', pubkey_uncompressed] with self.assertRaises(ScriptError): - op_checkdatasig(stack, log=[], extras=None) + op_checkdatasig(ScriptContext(stack=stack, logs=[], extras=Mock())) # or with rigth one # this will make sure the signature is not made when parameters are wrong stack = [data, signature, pubkey_uncompressed] with self.assertRaises(ScriptError): - op_checkdatasig(stack, log=[], extras=None) + op_checkdatasig(ScriptContext(stack=stack, logs=[], extras=Mock())) def test_checkdatasig_check_for_compressed_pubkey(self): # ScriptError if pubkey is not a public key but starts with 2 or 3 with self.assertRaises(ScriptError): - op_checkdatasig([b'\x0233', b'\x0233', b'\x0233'], log=[], extras=None) + op_checkdatasig(ScriptContext(stack=[b'\x0233', b'\x0233', b'\x0233'], logs=[], extras=Mock())) with self.assertRaises(ScriptError): - op_checkdatasig([b'\x0321', b'\x0321', b'\x0321'], log=[], extras=None) + op_checkdatasig(ScriptContext(stack=[b'\x0321', b'\x0321', b'\x0321'], logs=[], extras=Mock())) # ScriptError if pubkey is not a public key with self.assertRaises(ScriptError): - op_checkdatasig([b'\x0123', b'\x0123', b'\x0123'], log=[], extras=None) + op_checkdatasig(ScriptContext(stack=[b'\x0123', b'\x0123', b'\x0123'], logs=[], extras=Mock())) def test_checkdatasig(self): with self.assertRaises(MissingStackItems): - op_checkdatasig([1, 1], log=[], extras=None) + op_checkdatasig(ScriptContext(stack=[1, 1], logs=[], extras=Mock())) data = b'some_random_data' signature = self.genesis_private_key.sign(data, ec.ECDSA(hashes.SHA256())) @@ -342,12 +344,12 @@ def test_checkdatasig(self): stack = [data, signature, pubkey_bytes] # no exception should be raised and data is left on stack - op_checkdatasig(stack, log=[], extras=None) + op_checkdatasig(ScriptContext(stack=stack, logs=[], extras=Mock())) self.assertEqual(data, stack.pop()) stack = [b'data_not_matching', signature, pubkey_bytes] with self.assertRaises(OracleChecksigFailed): - op_checkdatasig(stack, log=[], extras=None) + op_checkdatasig(ScriptContext(stack=stack, logs=[], extras=Mock())) def test_get_data_value(self): value0 = b'value0' @@ -368,7 +370,7 @@ def test_get_data_value(self): def test_data_strequal(self): with self.assertRaises(MissingStackItems): - op_data_strequal([1, 1], log=[], extras=None) + op_data_strequal(ScriptContext(stack=[1, 1], logs=[], extras=Mock())) value0 = b'value0' value1 = b'vvvalue1' @@ -377,20 +379,20 @@ def test_data_strequal(self): data = (bytes([len(value0)]) + value0 + bytes([len(value1)]) + value1 + bytes([len(value2)]) + value2) stack = [data, 0, value0] - op_data_strequal(stack, log=[], extras=None) + op_data_strequal(ScriptContext(stack=stack, logs=[], extras=Mock())) self.assertEqual(stack.pop(), data) stack = [data, 1, value0] with self.assertRaises(VerifyFailed): - op_data_strequal(stack, log=[], extras=None) + op_data_strequal(ScriptContext(stack=stack, logs=[], extras=Mock())) stack = [data, b'\x00', value0] with self.assertRaises(VerifyFailed): - op_data_strequal(stack, log=[], extras=None) + op_data_strequal(ScriptContext(stack=stack, logs=[], extras=Mock())) def test_data_greaterthan(self): with self.assertRaises(MissingStackItems): - op_data_greaterthan([1, 1], log=[], extras=None) + op_data_greaterthan(ScriptContext(stack=[1, 1], logs=[], extras=Mock())) value0 = struct.pack('!I', 1000) value1 = struct.pack('!I', 1) @@ -398,93 +400,93 @@ def test_data_greaterthan(self): data = (bytes([len(value0)]) + value0 + bytes([len(value1)]) + value1) stack = [data, 0, struct.pack('!I', 999)] - op_data_greaterthan(stack, log=[], extras=None) + op_data_greaterthan(ScriptContext(stack=stack, logs=[], extras=Mock())) self.assertEqual(stack.pop(), data) stack = [data, 1, struct.pack('!I', 0)] - op_data_greaterthan(stack, log=[], extras=None) + op_data_greaterthan(ScriptContext(stack=stack, logs=[], extras=Mock())) self.assertEqual(stack.pop(), data) with self.assertRaises(VerifyFailed): stack = [data, 1, struct.pack('!I', 1)] - op_data_greaterthan(stack, log=[], extras=None) + op_data_greaterthan(ScriptContext(stack=stack, logs=[], extras=Mock())) stack = [data, 1, b'not_an_int'] with self.assertRaises(VerifyFailed): - op_data_greaterthan(stack, log=[], extras=None) + op_data_greaterthan(ScriptContext(stack=stack, logs=[], extras=Mock())) stack = [data, b'\x00', struct.pack('!I', 0)] with self.assertRaises(VerifyFailed): - op_data_greaterthan(stack, log=[], extras=None) + op_data_greaterthan(ScriptContext(stack=stack, logs=[], extras=Mock())) def test_data_match_interval(self): with self.assertRaises(MissingStackItems): - op_data_match_interval([1, b'2'], log=[], extras=None) + op_data_match_interval([1, b'2']) value0 = struct.pack('!I', 1000) data = (bytes([len(value0)]) + value0) stack = [data, 0, 'key1', struct.pack('!I', 1000), 'key2', struct.pack('!I', 1005), 'key3', bytes([2])] - op_data_match_interval(stack, log=[], extras=None) + op_data_match_interval(stack) self.assertEqual(stack.pop(), 'key1') self.assertEqual(len(stack), 0) stack = [data, 0, 'key1', struct.pack('!I', 100), 'key2', struct.pack('!I', 1005), 'key3', bytes([2])] - op_data_match_interval(stack, log=[], extras=None) + op_data_match_interval(stack) self.assertEqual(stack.pop(), 'key2') self.assertEqual(len(stack), 0) stack = [data, 0, 'key1', struct.pack('!I', 100), 'key2', struct.pack('!I', 900), 'key3', bytes([2])] - op_data_match_interval(stack, log=[], extras=None) + op_data_match_interval(stack) self.assertEqual(stack.pop(), 'key3') self.assertEqual(len(stack), 0) # missing 1 item on stack stack = [data, 0, struct.pack('!I', 100), 'key2', struct.pack('!I', 900), 'key3', bytes([2])] with self.assertRaises(MissingStackItems): - op_data_match_interval(stack, log=[], extras=None) + op_data_match_interval(stack) # value should be an integer stack = [data, 0, 'key1', struct.pack('!I', 100), 'key2', b'not_an_int', 'key3', bytes([2])] with self.assertRaises(VerifyFailed): - op_data_match_interval(stack, log=[], extras=None) + op_data_match_interval(stack) def test_data_match_value(self): with self.assertRaises(MissingStackItems): - op_data_match_value([1, b'2'], log=[], extras=None) + op_data_match_value(ScriptContext(stack=[1, b'2'], logs=[], extras=Mock())) value0 = struct.pack('!I', 1000) data = (bytes([len(value0)]) + value0) stack = [data, 0, 'key1', struct.pack('!I', 1000), 'key2', struct.pack('!I', 1005), 'key3', bytes([2])] - op_data_match_value(stack, log=[], extras=None) + op_data_match_value(ScriptContext(stack=stack, logs=[], extras=Mock())) self.assertEqual(stack.pop(), 'key2') self.assertEqual(len(stack), 0) stack = [data, 0, 'key1', struct.pack('!I', 999), 'key2', struct.pack('!I', 1000), 'key3', bytes([2])] - op_data_match_value(stack, log=[], extras=None) + op_data_match_value(ScriptContext(stack=stack, logs=[], extras=Mock())) self.assertEqual(stack.pop(), 'key3') self.assertEqual(len(stack), 0) # missing 1 item on stack stack = [data, 0, 'key1', struct.pack('!I', 1000), 'key2', struct.pack('!I', 1000), bytes([2])] with self.assertRaises(MissingStackItems): - op_data_match_value(stack, log=[], extras=None) + op_data_match_value(ScriptContext(stack=stack, logs=[], extras=Mock())) # no value matches stack = [data, 0, 'key1', struct.pack('!I', 999), 'key2', struct.pack('!I', 1111), 'key3', bytes([2])] - op_data_match_value(stack, log=[], extras=None) + op_data_match_value(ScriptContext(stack=stack, logs=[], extras=Mock())) self.assertEqual(stack.pop(), 'key1') self.assertEqual(len(stack), 0) # value should be an integer stack = [data, 0, 'key1', struct.pack('!I', 100), 'key2', b'not_an_int', 'key3', bytes([2])] with self.assertRaises(VerifyFailed): - op_data_match_value(stack, log=[], extras=None) + op_data_match_value(ScriptContext(stack=stack, logs=[], extras=Mock())) def test_find_p2pkh(self): with self.assertRaises(MissingStackItems): - op_find_p2pkh([], log=[], extras=None) + op_find_p2pkh(ScriptContext(stack=[], logs=[], extras=Mock())) addr1 = '15d14K5jMqsN2uwUEFqiPG5SoD7Vr1BfnH' addr2 = '1K35zJQeYrVzQAW7X3s7vbPKmngj5JXTBc' @@ -507,14 +509,14 @@ def test_find_p2pkh(self): stack = [genesis_address] tx = Transaction(outputs=[TxOutput(1, out_genesis)]) extras = ScriptExtras(tx=tx, txin=txin, spent_tx=spent_tx) - op_find_p2pkh(stack, log=[], extras=extras) + op_find_p2pkh(ScriptContext(stack=stack, logs=[], extras=extras)) self.assertEqual(stack.pop(), 1) # several outputs and correct output among them stack = [genesis_address] tx = Transaction(outputs=[TxOutput(1, out1), TxOutput(1, out2), TxOutput(1, out_genesis), TxOutput(1, out3)]) extras = ScriptExtras(tx=tx, txin=txin, spent_tx=spent_tx) - op_find_p2pkh(stack, log=[], extras=extras) + op_find_p2pkh(ScriptContext(stack=stack, logs=[], extras=extras)) self.assertEqual(stack.pop(), 1) # several outputs without correct amount output @@ -522,18 +524,18 @@ def test_find_p2pkh(self): tx = Transaction(outputs=[TxOutput(1, out1), TxOutput(1, out2), TxOutput(2, out_genesis), TxOutput(1, out3)]) extras = ScriptExtras(tx=tx, txin=txin, spent_tx=spent_tx) with self.assertRaises(VerifyFailed): - op_find_p2pkh(stack, log=[], extras=extras) + op_find_p2pkh(ScriptContext(stack=stack, logs=[], extras=extras)) # several outputs without correct address output stack = [genesis_address] tx = Transaction(outputs=[TxOutput(1, out1), TxOutput(1, out2), TxOutput(1, out3)]) extras = ScriptExtras(tx=tx, txin=txin, spent_tx=spent_tx) with self.assertRaises(VerifyFailed): - op_find_p2pkh(stack, log=[], extras=extras) + op_find_p2pkh(ScriptContext(stack=stack, logs=[], extras=extras)) def test_greaterthan_timestamp(self): with self.assertRaises(MissingStackItems): - op_greaterthan_timestamp([], log=[], extras=None) + op_greaterthan_timestamp(ScriptContext(stack=[], logs=[], extras=Mock())) timestamp = 1234567 @@ -541,23 +543,23 @@ def test_greaterthan_timestamp(self): tx = Transaction() stack = [struct.pack('!I', timestamp)] - extras = ScriptExtras(tx=tx, txin=None, spent_tx=None) + extras = ScriptExtras(tx=tx, txin=Mock(), spent_tx=Mock()) with self.assertRaises(TimeLocked): tx.timestamp = timestamp - 1 - op_greaterthan_timestamp(list(stack), log=[], extras=extras) + op_greaterthan_timestamp(ScriptContext(stack=list(stack), logs=[], extras=extras)) with self.assertRaises(TimeLocked): tx.timestamp = timestamp - op_greaterthan_timestamp(list(stack), log=[], extras=extras) + op_greaterthan_timestamp(ScriptContext(stack=list(stack), logs=[], extras=extras)) tx.timestamp = timestamp + 1 - op_greaterthan_timestamp(stack, log=[], extras=extras) + op_greaterthan_timestamp(ScriptContext(stack=stack, logs=[], extras=extras)) self.assertEqual(len(stack), 0) def test_checkmultisig(self): with self.assertRaises(MissingStackItems): - op_checkmultisig([], log=[], extras=None) + op_checkmultisig(ScriptContext(stack=[], logs=[], extras=Mock())) block = self.genesis_blocks[0] @@ -567,7 +569,7 @@ def test_checkmultisig(self): tx = Transaction(inputs=[txin], outputs=[txout]) data_to_sign = tx.get_sighash_all() - extras = ScriptExtras(tx=tx, txin=None, spent_tx=None) + extras = ScriptExtras(tx=tx, txin=Mock(), spent_tx=Mock()) wallet = HDWallet() wallet._manually_initialize() @@ -596,107 +598,107 @@ def test_checkmultisig(self): stack = [ keys[0]['signature'], keys[2]['signature'], 2, keys[0]['pubkey'], keys[1]['pubkey'], keys[2]['pubkey'], 3 ] - op_checkmultisig(stack, log=[], extras=extras) + op_checkmultisig(ScriptContext(stack=stack, logs=[], extras=extras)) self.assertEqual(1, stack.pop()) # New set of valid signatures stack = [ keys[0]['signature'], keys[1]['signature'], 2, keys[0]['pubkey'], keys[1]['pubkey'], keys[2]['pubkey'], 3 ] - op_checkmultisig(stack, log=[], extras=extras) + op_checkmultisig(ScriptContext(stack=stack, logs=[], extras=extras)) self.assertEqual(1, stack.pop()) # Changing the signatures but they match stack = [ keys[1]['signature'], keys[2]['signature'], 2, keys[0]['pubkey'], keys[1]['pubkey'], keys[2]['pubkey'], 3 ] - op_checkmultisig(stack, log=[], extras=extras) + op_checkmultisig(ScriptContext(stack=stack, logs=[], extras=extras)) self.assertEqual(1, stack.pop()) # Signatures are valid but in wrong order stack = [ keys[1]['signature'], keys[0]['signature'], 2, keys[0]['pubkey'], keys[1]['pubkey'], keys[2]['pubkey'], 3 ] - op_checkmultisig(stack, log=[], extras=extras) + op_checkmultisig(ScriptContext(stack=stack, logs=[], extras=extras)) self.assertEqual(0, stack.pop()) # Adding wrong signature, so we get error stack = [ keys[0]['signature'], wrong_key['signature'], 2, keys[0]['pubkey'], keys[1]['pubkey'], keys[2]['pubkey'], 3 ] - op_checkmultisig(stack, log=[], extras=extras) + op_checkmultisig(ScriptContext(stack=stack, logs=[], extras=extras)) self.assertEqual(0, stack.pop()) # Adding same signature twice, so we get error stack = [ keys[0]['signature'], keys[0]['signature'], 2, keys[0]['pubkey'], keys[1]['pubkey'], keys[2]['pubkey'], 3 ] - op_checkmultisig(stack, log=[], extras=extras) + op_checkmultisig(ScriptContext(stack=stack, logs=[], extras=extras)) self.assertEqual(0, stack.pop()) # Adding less signatures than required, so we get error stack = [keys[0]['signature'], 2, keys[0]['pubkey'], keys[1]['pubkey'], keys[2]['pubkey'], 3] with self.assertRaises(MissingStackItems): - op_checkmultisig(stack, log=[], extras=extras) + op_checkmultisig(ScriptContext(stack=stack, logs=[], extras=extras)) # Quantity of signatures is more than it should stack = [ keys[0]['signature'], keys[1]['signature'], 3, keys[0]['pubkey'], keys[1]['pubkey'], keys[2]['pubkey'], 3 ] with self.assertRaises(MissingStackItems): - op_checkmultisig(stack, log=[], extras=extras) + op_checkmultisig(ScriptContext(stack=stack, logs=[], extras=extras)) # Quantity of pubkeys is more than it should stack = [ keys[0]['signature'], keys[1]['signature'], 2, keys[0]['pubkey'], keys[1]['pubkey'], keys[2]['pubkey'], 4 ] with self.assertRaises(InvalidStackData): - op_checkmultisig(stack, log=[], extras=extras) + op_checkmultisig(ScriptContext(stack=stack, logs=[], extras=extras)) # Exception pubkey_count should be integer stack = [ keys[0]['signature'], keys[1]['signature'], 2, keys[0]['pubkey'], keys[1]['pubkey'], keys[2]['pubkey'], '3' ] with self.assertRaises(InvalidStackData): - op_checkmultisig(stack, log=[], extras=extras) + op_checkmultisig(ScriptContext(stack=stack, logs=[], extras=extras)) # Exception not enough pub keys stack = [keys[0]['pubkey'], keys[1]['pubkey'], 3] with self.assertRaises(MissingStackItems): - op_checkmultisig(stack, log=[], extras=extras) + op_checkmultisig(ScriptContext(stack=stack, logs=[], extras=extras)) # Exception stack empty after pubkeys stack = [keys[0]['pubkey'], keys[1]['pubkey'], keys[2]['pubkey'], 3] with self.assertRaises(MissingStackItems): - op_checkmultisig(stack, log=[], extras=extras) + op_checkmultisig(ScriptContext(stack=stack, logs=[], extras=extras)) def test_equal(self): elem = b'a' with self.assertRaises(MissingStackItems): - op_equal([elem], log=[], extras=None) + op_equal(ScriptContext(stack=[elem], logs=[], extras=Mock())) # no exception should be raised stack = [elem, elem] - op_equal(stack, log=[], extras=None) + op_equal(ScriptContext(stack=stack, logs=[], extras=Mock())) self.assertEqual(stack.pop(), 1) stack = [elem, b'aaaa'] - op_equal(stack, log=[], extras=None) + op_equal(ScriptContext(stack=stack, logs=[], extras=Mock())) self.assertEqual(stack.pop(), 0) def test_integer_opcode(self): # We have opcodes from OP_0 to OP_16 for i in range(0, 17): stack = [] - op_integer(getattr(Opcode, 'OP_{}'.format(i)), stack, [], None) + op_integer(getattr(Opcode, 'OP_{}'.format(i)), stack) self.assertEqual(stack, [i]) stack = [] with self.assertRaises(ScriptError): - op_integer(0, stack, [], None) + op_integer(0, stack) with self.assertRaises(ScriptError): - op_integer(0x61, stack, [], None) + op_integer(0x61, stack) def test_decode_opn(self): for i in range(0, 17): diff --git a/tests/tx/test_tx.py b/tests/tx/test_tx.py index 8e748b17c..fd802c7f5 100644 --- a/tests/tx/test_tx.py +++ b/tests/tx/test_tx.py @@ -75,7 +75,7 @@ def test_input_output_match(self): _input.data = P2PKH.create_input_data(public_bytes, signature) with self.assertRaises(InputOutputMismatch): - self._verifiers.tx.verify_sum(tx) + self._verifiers.tx.verify_sum(tx.get_complete_token_info()) def test_validation(self): # add 100 blocks and check that walking through get_next_block_best_chain yields the same blocks @@ -143,7 +143,7 @@ def test_too_many_outputs(self): tx = Transaction(outputs=outputs, storage=self.tx_storage) with self.assertRaises(TooManyOutputs): - self._verifiers.tx.verify_number_of_outputs(tx) + self._verifiers.vertex.verify_number_of_outputs(tx) def _gen_tx_spending_genesis_block(self): parents = [tx.hash for tx in self.genesis_txs] @@ -360,7 +360,7 @@ def test_block_outputs(self): storage=self.tx_storage) with self.assertRaises(TooManyOutputs): - self._verifiers.block.verify_outputs(block) + self._verifiers.vertex.verify_outputs(block) def test_tx_number_parents(self): genesis_block = self.genesis_blocks[0] @@ -677,17 +677,17 @@ def test_tx_methods(self): self.assertFalse(tx_equal.is_genesis) # Pow error - self._verifiers.tx.verify_pow(tx2) + self._verifiers.vertex.verify_pow(tx2) tx2.weight = 100 with self.assertRaises(PowError): - self._verifiers.tx.verify_pow(tx2) + self._verifiers.vertex.verify_pow(tx2) # Verify parent timestamps - self._verifiers.tx.verify_parents(tx2) + self._verifiers.vertex.verify_parents(tx2) tx2_timestamp = tx2.timestamp tx2.timestamp = 2 with self.assertRaises(TimestampError): - self._verifiers.tx.verify_parents(tx2) + self._verifiers.vertex.verify_parents(tx2) tx2.timestamp = tx2_timestamp # Verify inputs timestamps @@ -701,10 +701,10 @@ def test_tx_methods(self): block = blocks[0] block2 = blocks[1] block2.timestamp = block.timestamp + self._settings.MAX_DISTANCE_BETWEEN_BLOCKS - self._verifiers.block.verify_parents(block2) + self._verifiers.vertex.verify_parents(block2) block2.timestamp += 1 with self.assertRaises(TimestampError): - self._verifiers.block.verify_parents(block2) + self._verifiers.vertex.verify_parents(block2) def test_block_big_nonce(self): block = self.genesis_blocks[0] @@ -881,7 +881,8 @@ def _test_txout_script_limit(self, offset): _output = TxOutput(value, script) tx = Transaction(inputs=[_input], outputs=[_output], storage=self.tx_storage) - self._verifiers.tx.verify_outputs(tx) + self._verifiers.vertex.verify_outputs(tx) + self._verifiers.tx.verify_output_token_indexes(tx) def test_txout_script_limit_exceeded(self): with self.assertRaises(InvalidOutputScriptSize): @@ -1058,7 +1059,7 @@ def test_sigops_output_single_below_limit(self) -> None: output3 = TxOutput(value, hscript) tx = Transaction(inputs=[_input], outputs=[output3], storage=self.tx_storage) tx.update_hash() - self._verifiers.tx.verify_sigops_output(tx) + self._verifiers.vertex.verify_sigops_output(tx) def test_sigops_output_multi_below_limit(self) -> None: genesis_block = self.genesis_blocks[0] @@ -1070,7 +1071,7 @@ def test_sigops_output_multi_below_limit(self) -> None: output4 = TxOutput(value, hscript) tx = Transaction(inputs=[_input], outputs=[output4]*num_outputs, storage=self.tx_storage) tx.update_hash() - self._verifiers.tx.verify_sigops_output(tx) + self._verifiers.vertex.verify_sigops_output(tx) def test_sigops_input_single_above_limit(self) -> None: genesis_block = self.genesis_blocks[0] diff --git a/tests/tx/test_tx_deserialization.py b/tests/tx/test_tx_deserialization.py index c45603b54..4e878c802 100644 --- a/tests/tx/test_tx_deserialization.py +++ b/tests/tx/test_tx_deserialization.py @@ -1,7 +1,8 @@ from hathor.daa import DifficultyAdjustmentAlgorithm from hathor.transaction import Block, MergeMinedBlock, Transaction, TxVersion from hathor.transaction.token_creation_tx import TokenCreationTransaction -from hathor.verification.verification_service import VerificationService, VertexVerifiers +from hathor.verification.verification_service import VerificationService +from hathor.verification.vertex_verifiers import VertexVerifiers from tests import unittest diff --git a/tests/tx/test_verification.py b/tests/tx/test_verification.py index 864a0e6a2..336d54510 100644 --- a/tests/tx/test_verification.py +++ b/tests/tx/test_verification.py @@ -24,6 +24,7 @@ from hathor.verification.merge_mined_block_verifier import MergeMinedBlockVerifier from hathor.verification.token_creation_transaction_verifier import TokenCreationTransactionVerifier from hathor.verification.transaction_verifier import TransactionVerifier +from hathor.verification.vertex_verifier import VertexVerifier from tests import unittest from tests.utils import add_blocks_unlock_reward, create_tokens, get_genesis_key @@ -122,27 +123,33 @@ def test_block_verify_basic(self) -> None: def test_block_verify_without_storage(self) -> None: block = self._get_valid_block() - verify_pow_wrapped = Mock(wraps=self.verifiers.block.verify_pow) + verify_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_outputs) + + verify_pow_wrapped = Mock(wraps=self.verifiers.vertex.verify_pow) verify_no_inputs_wrapped = Mock(wraps=self.verifiers.block.verify_no_inputs) - verify_outputs_wrapped = Mock(wraps=self.verifiers.block.verify_outputs) - verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.block.verify_number_of_outputs) + verify_output_token_indexes_wrapped = Mock(wraps=self.verifiers.block.verify_output_token_indexes) + verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_number_of_outputs) verify_data_wrapped = Mock(wraps=self.verifiers.block.verify_data) - verify_sigops_output_wrapped = Mock(wraps=self.verifiers.block.verify_sigops_output) + verify_sigops_output_wrapped = Mock(wraps=self.verifiers.vertex.verify_sigops_output) with ( - patch.object(BlockVerifier, 'verify_pow', verify_pow_wrapped), + patch.object(VertexVerifier, 'verify_outputs', verify_outputs_wrapped), + patch.object(VertexVerifier, 'verify_pow', verify_pow_wrapped), patch.object(BlockVerifier, 'verify_no_inputs', verify_no_inputs_wrapped), - patch.object(BlockVerifier, 'verify_outputs', verify_outputs_wrapped), - patch.object(BlockVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped), + patch.object(BlockVerifier, 'verify_output_token_indexes', verify_output_token_indexes_wrapped), + patch.object(VertexVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped), patch.object(BlockVerifier, 'verify_data', verify_data_wrapped), - patch.object(BlockVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), + patch.object(VertexVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), ): - self.verifiers.block.verify_without_storage(block) + self.manager.verification_service.verify_without_storage(block) + + # Vertex methods + verify_outputs_wrapped.assert_called_once() # Block methods verify_pow_wrapped.assert_called_once() verify_no_inputs_wrapped.assert_called_once() - verify_outputs_wrapped.assert_called_once() + verify_output_token_indexes_wrapped.assert_called_once() verify_number_of_outputs_wrapped.assert_called_once() verify_data_wrapped.assert_called_once() verify_sigops_output_wrapped.assert_called_once() @@ -150,33 +157,39 @@ def test_block_verify_without_storage(self) -> None: def test_block_verify(self) -> None: block = self._get_valid_block() - verify_pow_wrapped = Mock(wraps=self.verifiers.block.verify_pow) + verify_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_outputs) + + verify_pow_wrapped = Mock(wraps=self.verifiers.vertex.verify_pow) verify_no_inputs_wrapped = Mock(wraps=self.verifiers.block.verify_no_inputs) - verify_outputs_wrapped = Mock(wraps=self.verifiers.block.verify_outputs) - verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.block.verify_number_of_outputs) + verify_output_token_indexes_wrapped = Mock(wraps=self.verifiers.block.verify_output_token_indexes) + verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_number_of_outputs) verify_data_wrapped = Mock(wraps=self.verifiers.block.verify_data) - verify_sigops_output_wrapped = Mock(wraps=self.verifiers.block.verify_sigops_output) - verify_parents_wrapped = Mock(wraps=self.verifiers.block.verify_parents) + verify_sigops_output_wrapped = Mock(wraps=self.verifiers.vertex.verify_sigops_output) + verify_parents_wrapped = Mock(wraps=self.verifiers.vertex.verify_parents) verify_height_wrapped = Mock(wraps=self.verifiers.block.verify_height) verify_mandatory_signaling_wrapped = Mock(wraps=self.verifiers.block.verify_mandatory_signaling) with ( - patch.object(BlockVerifier, 'verify_pow', verify_pow_wrapped), + patch.object(VertexVerifier, 'verify_outputs', verify_outputs_wrapped), + patch.object(VertexVerifier, 'verify_pow', verify_pow_wrapped), patch.object(BlockVerifier, 'verify_no_inputs', verify_no_inputs_wrapped), - patch.object(BlockVerifier, 'verify_outputs', verify_outputs_wrapped), - patch.object(BlockVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped), + patch.object(BlockVerifier, 'verify_output_token_indexes', verify_output_token_indexes_wrapped), + patch.object(VertexVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped), patch.object(BlockVerifier, 'verify_data', verify_data_wrapped), - patch.object(BlockVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), - patch.object(BlockVerifier, 'verify_parents', verify_parents_wrapped), + patch.object(VertexVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), + patch.object(VertexVerifier, 'verify_parents', verify_parents_wrapped), patch.object(BlockVerifier, 'verify_height', verify_height_wrapped), patch.object(BlockVerifier, 'verify_mandatory_signaling', verify_mandatory_signaling_wrapped), ): self.manager.verification_service.verify(block) + # Vertex methods + verify_outputs_wrapped.assert_called_once() + # Block methods verify_pow_wrapped.assert_called_once() verify_no_inputs_wrapped.assert_called_once() - verify_outputs_wrapped.assert_called_once() + verify_output_token_indexes_wrapped.assert_called_once() verify_number_of_outputs_wrapped.assert_called_once() verify_data_wrapped.assert_called_once() verify_sigops_output_wrapped.assert_called_once() @@ -227,26 +240,29 @@ def test_block_validate_basic(self) -> None: def test_block_validate_full(self) -> None: block = self._get_valid_block() - verify_pow_wrapped = Mock(wraps=self.verifiers.block.verify_pow) + verify_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_outputs) + + verify_pow_wrapped = Mock(wraps=self.verifiers.vertex.verify_pow) verify_no_inputs_wrapped = Mock(wraps=self.verifiers.block.verify_no_inputs) - verify_outputs_wrapped = Mock(wraps=self.verifiers.block.verify_outputs) - verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.block.verify_number_of_outputs) + verify_output_token_indexes_wrapped = Mock(wraps=self.verifiers.block.verify_output_token_indexes) + verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_number_of_outputs) verify_data_wrapped = Mock(wraps=self.verifiers.block.verify_data) - verify_sigops_output_wrapped = Mock(wraps=self.verifiers.block.verify_sigops_output) - verify_parents_wrapped = Mock(wraps=self.verifiers.block.verify_parents) + verify_sigops_output_wrapped = Mock(wraps=self.verifiers.vertex.verify_sigops_output) + verify_parents_wrapped = Mock(wraps=self.verifiers.vertex.verify_parents) verify_height_wrapped = Mock(wraps=self.verifiers.block.verify_height) verify_weight_wrapped = Mock(wraps=self.verifiers.block.verify_weight) verify_reward_wrapped = Mock(wraps=self.verifiers.block.verify_reward) verify_mandatory_signaling_wrapped = Mock(wraps=self.verifiers.block.verify_mandatory_signaling) with ( - patch.object(BlockVerifier, 'verify_pow', verify_pow_wrapped), + patch.object(VertexVerifier, 'verify_outputs', verify_outputs_wrapped), + patch.object(VertexVerifier, 'verify_pow', verify_pow_wrapped), patch.object(BlockVerifier, 'verify_no_inputs', verify_no_inputs_wrapped), - patch.object(BlockVerifier, 'verify_outputs', verify_outputs_wrapped), - patch.object(BlockVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped), + patch.object(BlockVerifier, 'verify_output_token_indexes', verify_output_token_indexes_wrapped), + patch.object(VertexVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped), patch.object(BlockVerifier, 'verify_data', verify_data_wrapped), - patch.object(BlockVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), - patch.object(BlockVerifier, 'verify_parents', verify_parents_wrapped), + patch.object(VertexVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), + patch.object(VertexVerifier, 'verify_parents', verify_parents_wrapped), patch.object(BlockVerifier, 'verify_height', verify_height_wrapped), patch.object(BlockVerifier, 'verify_weight', verify_weight_wrapped), patch.object(BlockVerifier, 'verify_reward', verify_reward_wrapped), @@ -254,10 +270,13 @@ def test_block_validate_full(self) -> None: ): self.manager.verification_service.validate_full(block) + # Vertex methods + verify_outputs_wrapped.assert_called_once() + # Block methods verify_pow_wrapped.assert_called_once() verify_no_inputs_wrapped.assert_called_once() - verify_outputs_wrapped.assert_called_once() + verify_output_token_indexes_wrapped.assert_called_once() verify_number_of_outputs_wrapped.assert_called_once() verify_data_wrapped.assert_called_once() verify_sigops_output_wrapped.assert_called_once() @@ -270,12 +289,12 @@ def test_block_validate_full(self) -> None: def test_merge_mined_block_verify_basic(self) -> None: block = self._get_valid_merge_mined_block() - verify_weight_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_weight) - verify_reward_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_reward) + verify_weight_wrapped = Mock(wraps=self.verifiers.block.verify_weight) + verify_reward_wrapped = Mock(wraps=self.verifiers.block.verify_reward) with ( - patch.object(MergeMinedBlockVerifier, 'verify_weight', verify_weight_wrapped), - patch.object(MergeMinedBlockVerifier, 'verify_reward', verify_reward_wrapped), + patch.object(BlockVerifier, 'verify_weight', verify_weight_wrapped), + patch.object(BlockVerifier, 'verify_reward', verify_reward_wrapped), ): self.manager.verification_service.verify_basic(block) @@ -286,30 +305,36 @@ def test_merge_mined_block_verify_basic(self) -> None: def test_merge_mined_block_verify_without_storage(self) -> None: block = self._get_valid_merge_mined_block() - verify_pow_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_pow) - verify_no_inputs_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_no_inputs) - verify_outputs_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_outputs) - verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_number_of_outputs) - verify_data_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_data) - verify_sigops_output_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_sigops_output) + verify_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_outputs) + + verify_pow_wrapped = Mock(wraps=self.verifiers.vertex.verify_pow) + verify_no_inputs_wrapped = Mock(wraps=self.verifiers.block.verify_no_inputs) + verify_output_token_indexes_wrapped = Mock(wraps=self.verifiers.block.verify_output_token_indexes) + verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_number_of_outputs) + verify_data_wrapped = Mock(wraps=self.verifiers.block.verify_data) + verify_sigops_output_wrapped = Mock(wraps=self.verifiers.vertex.verify_sigops_output) verify_aux_pow_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_aux_pow) with ( - patch.object(MergeMinedBlockVerifier, 'verify_pow', verify_pow_wrapped), - patch.object(MergeMinedBlockVerifier, 'verify_no_inputs', verify_no_inputs_wrapped), - patch.object(MergeMinedBlockVerifier, 'verify_outputs', verify_outputs_wrapped), - patch.object(MergeMinedBlockVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped), - patch.object(MergeMinedBlockVerifier, 'verify_data', verify_data_wrapped), - patch.object(MergeMinedBlockVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), + patch.object(VertexVerifier, 'verify_outputs', verify_outputs_wrapped), + patch.object(VertexVerifier, 'verify_pow', verify_pow_wrapped), + patch.object(BlockVerifier, 'verify_no_inputs', verify_no_inputs_wrapped), + patch.object(BlockVerifier, 'verify_output_token_indexes', verify_output_token_indexes_wrapped), + patch.object(VertexVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped), + patch.object(BlockVerifier, 'verify_data', verify_data_wrapped), + patch.object(VertexVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), patch.object(MergeMinedBlockVerifier, 'verify_aux_pow', verify_aux_pow_wrapped), ): - self.verifiers.merge_mined_block.verify_without_storage(block) + self.manager.verification_service.verify_without_storage(block) + + # Vertex methods + verify_outputs_wrapped.assert_called_once() # Block methods verify_pow_wrapped.assert_called_once() verify_no_inputs_wrapped.assert_called_once() - verify_outputs_wrapped.assert_called_once() + verify_output_token_indexes_wrapped.assert_called_once() verify_number_of_outputs_wrapped.assert_called_once() verify_data_wrapped.assert_called_once() verify_sigops_output_wrapped.assert_called_once() @@ -320,36 +345,42 @@ def test_merge_mined_block_verify_without_storage(self) -> None: def test_merge_mined_block_verify(self) -> None: block = self._get_valid_merge_mined_block() - verify_pow_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_pow) - verify_no_inputs_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_no_inputs) - verify_outputs_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_outputs) - verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_number_of_outputs) - verify_data_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_data) - verify_sigops_output_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_sigops_output) - verify_parents_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_parents) - verify_height_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_height) - verify_mandatory_signaling_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_mandatory_signaling) + verify_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_outputs) + + verify_pow_wrapped = Mock(wraps=self.verifiers.vertex.verify_pow) + verify_no_inputs_wrapped = Mock(wraps=self.verifiers.block.verify_no_inputs) + verify_output_token_indexes_wrapped = Mock(wraps=self.verifiers.block.verify_output_token_indexes) + verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_number_of_outputs) + verify_data_wrapped = Mock(wraps=self.verifiers.block.verify_data) + verify_sigops_output_wrapped = Mock(wraps=self.verifiers.vertex.verify_sigops_output) + verify_parents_wrapped = Mock(wraps=self.verifiers.vertex.verify_parents) + verify_height_wrapped = Mock(wraps=self.verifiers.block.verify_height) + verify_mandatory_signaling_wrapped = Mock(wraps=self.verifiers.block.verify_mandatory_signaling) verify_aux_pow_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_aux_pow) with ( - patch.object(MergeMinedBlockVerifier, 'verify_pow', verify_pow_wrapped), - patch.object(MergeMinedBlockVerifier, 'verify_no_inputs', verify_no_inputs_wrapped), - patch.object(MergeMinedBlockVerifier, 'verify_outputs', verify_outputs_wrapped), - patch.object(MergeMinedBlockVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped), - patch.object(MergeMinedBlockVerifier, 'verify_data', verify_data_wrapped), - patch.object(MergeMinedBlockVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), - patch.object(MergeMinedBlockVerifier, 'verify_parents', verify_parents_wrapped), - patch.object(MergeMinedBlockVerifier, 'verify_height', verify_height_wrapped), + patch.object(VertexVerifier, 'verify_outputs', verify_outputs_wrapped), + patch.object(VertexVerifier, 'verify_pow', verify_pow_wrapped), + patch.object(BlockVerifier, 'verify_no_inputs', verify_no_inputs_wrapped), + patch.object(BlockVerifier, 'verify_output_token_indexes', verify_output_token_indexes_wrapped), + patch.object(VertexVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped), + patch.object(BlockVerifier, 'verify_data', verify_data_wrapped), + patch.object(VertexVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), + patch.object(VertexVerifier, 'verify_parents', verify_parents_wrapped), + patch.object(BlockVerifier, 'verify_height', verify_height_wrapped), + patch.object(BlockVerifier, 'verify_mandatory_signaling', verify_mandatory_signaling_wrapped), patch.object(MergeMinedBlockVerifier, 'verify_aux_pow', verify_aux_pow_wrapped), - patch.object(MergeMinedBlockVerifier, 'verify_mandatory_signaling', verify_mandatory_signaling_wrapped), ): self.manager.verification_service.verify(block) + # Vertex methods + verify_outputs_wrapped.assert_called_once() + # Block methods verify_pow_wrapped.assert_called_once() verify_no_inputs_wrapped.assert_called_once() - verify_outputs_wrapped.assert_called_once() + verify_output_token_indexes_wrapped.assert_called_once() verify_number_of_outputs_wrapped.assert_called_once() verify_data_wrapped.assert_called_once() verify_sigops_output_wrapped.assert_called_once() @@ -363,12 +394,12 @@ def test_merge_mined_block_verify(self) -> None: def test_merge_mined_block_validate_basic(self) -> None: block = self._get_valid_merge_mined_block() - verify_weight_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_weight) - verify_reward_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_reward) + verify_weight_wrapped = Mock(wraps=self.verifiers.block.verify_weight) + verify_reward_wrapped = Mock(wraps=self.verifiers.block.verify_reward) with ( - patch.object(MergeMinedBlockVerifier, 'verify_weight', verify_weight_wrapped), - patch.object(MergeMinedBlockVerifier, 'verify_reward', verify_reward_wrapped), + patch.object(BlockVerifier, 'verify_weight', verify_weight_wrapped), + patch.object(BlockVerifier, 'verify_reward', verify_reward_wrapped), ): self.manager.verification_service.validate_basic(block) @@ -403,40 +434,46 @@ def test_merge_mined_block_validate_basic(self) -> None: def test_merge_mined_block_validate_full(self) -> None: block = self._get_valid_merge_mined_block() - verify_pow_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_pow) - verify_no_inputs_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_no_inputs) - verify_outputs_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_outputs) - verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_number_of_outputs) - verify_data_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_data) - verify_sigops_output_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_sigops_output) - verify_parents_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_parents) - verify_height_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_height) - verify_weight_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_weight) - verify_reward_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_reward) - verify_mandatory_signaling_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_mandatory_signaling) + verify_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_outputs) + + verify_pow_wrapped = Mock(wraps=self.verifiers.vertex.verify_pow) + verify_no_inputs_wrapped = Mock(wraps=self.verifiers.block.verify_no_inputs) + verify_output_token_indexes_wrapped = Mock(wraps=self.verifiers.block.verify_output_token_indexes) + verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_number_of_outputs) + verify_data_wrapped = Mock(wraps=self.verifiers.block.verify_data) + verify_sigops_output_wrapped = Mock(wraps=self.verifiers.vertex.verify_sigops_output) + verify_parents_wrapped = Mock(wraps=self.verifiers.vertex.verify_parents) + verify_height_wrapped = Mock(wraps=self.verifiers.block.verify_height) + verify_weight_wrapped = Mock(wraps=self.verifiers.block.verify_weight) + verify_reward_wrapped = Mock(wraps=self.verifiers.block.verify_reward) + verify_mandatory_signaling_wrapped = Mock(wraps=self.verifiers.block.verify_mandatory_signaling) verify_aux_pow_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_aux_pow) with ( - patch.object(MergeMinedBlockVerifier, 'verify_pow', verify_pow_wrapped), - patch.object(MergeMinedBlockVerifier, 'verify_no_inputs', verify_no_inputs_wrapped), - patch.object(MergeMinedBlockVerifier, 'verify_outputs', verify_outputs_wrapped), - patch.object(MergeMinedBlockVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped), - patch.object(MergeMinedBlockVerifier, 'verify_data', verify_data_wrapped), - patch.object(MergeMinedBlockVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), - patch.object(MergeMinedBlockVerifier, 'verify_parents', verify_parents_wrapped), - patch.object(MergeMinedBlockVerifier, 'verify_height', verify_height_wrapped), - patch.object(MergeMinedBlockVerifier, 'verify_weight', verify_weight_wrapped), - patch.object(MergeMinedBlockVerifier, 'verify_reward', verify_reward_wrapped), + patch.object(VertexVerifier, 'verify_outputs', verify_outputs_wrapped), + patch.object(VertexVerifier, 'verify_pow', verify_pow_wrapped), + patch.object(BlockVerifier, 'verify_no_inputs', verify_no_inputs_wrapped), + patch.object(BlockVerifier, 'verify_output_token_indexes', verify_output_token_indexes_wrapped), + patch.object(VertexVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped), + patch.object(BlockVerifier, 'verify_data', verify_data_wrapped), + patch.object(VertexVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), + patch.object(VertexVerifier, 'verify_parents', verify_parents_wrapped), + patch.object(BlockVerifier, 'verify_height', verify_height_wrapped), + patch.object(BlockVerifier, 'verify_weight', verify_weight_wrapped), + patch.object(BlockVerifier, 'verify_reward', verify_reward_wrapped), + patch.object(BlockVerifier, 'verify_mandatory_signaling', verify_mandatory_signaling_wrapped), patch.object(MergeMinedBlockVerifier, 'verify_aux_pow', verify_aux_pow_wrapped), - patch.object(MergeMinedBlockVerifier, 'verify_mandatory_signaling', verify_mandatory_signaling_wrapped), ): self.manager.verification_service.validate_full(block) + # Vertex methods + verify_outputs_wrapped.assert_called_once() + # Block methods verify_pow_wrapped.assert_called_once() verify_no_inputs_wrapped.assert_called_once() - verify_outputs_wrapped.assert_called_once() + verify_output_token_indexes_wrapped.assert_called_once() verify_number_of_outputs_wrapped.assert_called_once() verify_data_wrapped.assert_called_once() verify_sigops_output_wrapped.assert_called_once() @@ -452,56 +489,68 @@ def test_merge_mined_block_validate_full(self) -> None: def test_transaction_verify_basic(self) -> None: tx = self._get_valid_tx() + verify_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_outputs) + verify_parents_basic_wrapped = Mock(wraps=self.verifiers.tx.verify_parents_basic) verify_weight_wrapped = Mock(wraps=self.verifiers.tx.verify_weight) - verify_pow_wrapped = Mock(wraps=self.verifiers.tx.verify_pow) + verify_pow_wrapped = Mock(wraps=self.verifiers.vertex.verify_pow) verify_number_of_inputs_wrapped = Mock(wraps=self.verifiers.tx.verify_number_of_inputs) - verify_outputs_wrapped = Mock(wraps=self.verifiers.tx.verify_outputs) - verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.tx.verify_number_of_outputs) - verify_sigops_output_wrapped = Mock(wraps=self.verifiers.tx.verify_sigops_output) + verify_output_token_indexes_wrapped = Mock(wraps=self.verifiers.tx.verify_output_token_indexes) + verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_number_of_outputs) + verify_sigops_output_wrapped = Mock(wraps=self.verifiers.vertex.verify_sigops_output) with ( + patch.object(VertexVerifier, 'verify_outputs', verify_outputs_wrapped), patch.object(TransactionVerifier, 'verify_parents_basic', verify_parents_basic_wrapped), patch.object(TransactionVerifier, 'verify_weight', verify_weight_wrapped), - patch.object(TransactionVerifier, 'verify_pow', verify_pow_wrapped), + patch.object(VertexVerifier, 'verify_pow', verify_pow_wrapped), patch.object(TransactionVerifier, 'verify_number_of_inputs', verify_number_of_inputs_wrapped), - patch.object(TransactionVerifier, 'verify_outputs', verify_outputs_wrapped), - patch.object(TransactionVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped), - patch.object(TransactionVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), + patch.object(TransactionVerifier, 'verify_output_token_indexes', verify_output_token_indexes_wrapped), + patch.object(VertexVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped), + patch.object(VertexVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), ): self.manager.verification_service.verify_basic(tx) + # Vertex methods + verify_outputs_wrapped.assert_called_once() + # Transaction methods verify_parents_basic_wrapped.assert_called_once() verify_weight_wrapped.assert_called_once() verify_pow_wrapped.assert_called_once() verify_number_of_inputs_wrapped.assert_called_once() - verify_outputs_wrapped.assert_called_once() + verify_output_token_indexes_wrapped.assert_called_once() verify_number_of_outputs_wrapped.assert_called_once() verify_sigops_output_wrapped.assert_called_once() def test_transaction_verify_without_storage(self) -> None: tx = self._get_valid_tx() - verify_pow_wrapped = Mock(wraps=self.verifiers.tx.verify_pow) + verify_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_outputs) + + verify_pow_wrapped = Mock(wraps=self.verifiers.vertex.verify_pow) verify_number_of_inputs_wrapped = Mock(wraps=self.verifiers.tx.verify_number_of_inputs) - verify_outputs_wrapped = Mock(wraps=self.verifiers.tx.verify_outputs) - verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.tx.verify_number_of_outputs) - verify_sigops_output_wrapped = Mock(wraps=self.verifiers.tx.verify_sigops_output) + verify_output_token_indexes_wrapped = Mock(wraps=self.verifiers.tx.verify_output_token_indexes) + verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_number_of_outputs) + verify_sigops_output_wrapped = Mock(wraps=self.verifiers.vertex.verify_sigops_output) with ( - patch.object(TransactionVerifier, 'verify_pow', verify_pow_wrapped), + patch.object(VertexVerifier, 'verify_outputs', verify_outputs_wrapped), + patch.object(VertexVerifier, 'verify_pow', verify_pow_wrapped), patch.object(TransactionVerifier, 'verify_number_of_inputs', verify_number_of_inputs_wrapped), - patch.object(TransactionVerifier, 'verify_outputs', verify_outputs_wrapped), - patch.object(TransactionVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped), - patch.object(TransactionVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), + patch.object(TransactionVerifier, 'verify_output_token_indexes', verify_output_token_indexes_wrapped), + patch.object(VertexVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped), + patch.object(VertexVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), ): - self.verifiers.tx.verify_without_storage(tx) + self.manager.verification_service.verify_without_storage(tx) + + # Vertex methods + verify_outputs_wrapped.assert_called_once() # Transaction methods verify_pow_wrapped.assert_called_once() verify_number_of_inputs_wrapped.assert_called_once() - verify_outputs_wrapped.assert_called_once() + verify_output_token_indexes_wrapped.assert_called_once() verify_number_of_outputs_wrapped.assert_called_once() verify_sigops_output_wrapped.assert_called_once() @@ -509,37 +558,43 @@ def test_transaction_verify(self) -> None: add_blocks_unlock_reward(self.manager) tx = self._get_valid_tx() - verify_pow_wrapped = Mock(wraps=self.verifiers.tx.verify_pow) + verify_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_outputs) + + verify_pow_wrapped = Mock(wraps=self.verifiers.vertex.verify_pow) verify_number_of_inputs_wrapped = Mock(wraps=self.verifiers.tx.verify_number_of_inputs) - verify_outputs_wrapped = Mock(wraps=self.verifiers.tx.verify_outputs) - verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.tx.verify_number_of_outputs) - verify_sigops_output_wrapped = Mock(wraps=self.verifiers.tx.verify_sigops_output) + verify_output_token_indexes_wrapped = Mock(wraps=self.verifiers.tx.verify_output_token_indexes) + verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_number_of_outputs) + verify_sigops_output_wrapped = Mock(wraps=self.verifiers.vertex.verify_sigops_output) verify_sigops_input_wrapped = Mock(wraps=self.verifiers.tx.verify_sigops_input) verify_inputs_wrapped = Mock(wraps=self.verifiers.tx.verify_inputs) verify_script_wrapped = Mock(wraps=self.verifiers.tx.verify_script) - verify_parents_wrapped = Mock(wraps=self.verifiers.tx.verify_parents) + verify_parents_wrapped = Mock(wraps=self.verifiers.vertex.verify_parents) verify_sum_wrapped = Mock(wraps=self.verifiers.tx.verify_sum) verify_reward_locked_wrapped = Mock(wraps=self.verifiers.tx.verify_reward_locked) with ( - patch.object(TransactionVerifier, 'verify_pow', verify_pow_wrapped), + patch.object(VertexVerifier, 'verify_outputs', verify_outputs_wrapped), + patch.object(VertexVerifier, 'verify_pow', verify_pow_wrapped), patch.object(TransactionVerifier, 'verify_number_of_inputs', verify_number_of_inputs_wrapped), - patch.object(TransactionVerifier, 'verify_outputs', verify_outputs_wrapped), - patch.object(TransactionVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped), - patch.object(TransactionVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), + patch.object(TransactionVerifier, 'verify_output_token_indexes', verify_output_token_indexes_wrapped), + patch.object(VertexVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped), + patch.object(VertexVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), patch.object(TransactionVerifier, 'verify_sigops_input', verify_sigops_input_wrapped), patch.object(TransactionVerifier, 'verify_inputs', verify_inputs_wrapped), patch.object(TransactionVerifier, 'verify_script', verify_script_wrapped), - patch.object(TransactionVerifier, 'verify_parents', verify_parents_wrapped), + patch.object(VertexVerifier, 'verify_parents', verify_parents_wrapped), patch.object(TransactionVerifier, 'verify_sum', verify_sum_wrapped), patch.object(TransactionVerifier, 'verify_reward_locked', verify_reward_locked_wrapped), ): self.manager.verification_service.verify(tx) + # Vertex methods + verify_outputs_wrapped.assert_called_once() + # Transaction methods verify_pow_wrapped.assert_called_once() verify_number_of_inputs_wrapped.assert_called_once() - verify_outputs_wrapped.assert_called_once() + verify_output_token_indexes_wrapped.assert_called_once() verify_number_of_outputs_wrapped.assert_called_once() verify_sigops_output_wrapped.assert_called_once() verify_sigops_input_wrapped.assert_called_once() @@ -554,31 +609,37 @@ def test_transaction_validate_basic(self) -> None: add_blocks_unlock_reward(self.manager) tx = self._get_valid_tx() + verify_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_outputs) + verify_parents_basic_wrapped = Mock(wraps=self.verifiers.tx.verify_parents_basic) verify_weight_wrapped = Mock(wraps=self.verifiers.tx.verify_weight) - verify_pow_wrapped = Mock(wraps=self.verifiers.tx.verify_pow) + verify_pow_wrapped = Mock(wraps=self.verifiers.vertex.verify_pow) verify_number_of_inputs_wrapped = Mock(wraps=self.verifiers.tx.verify_number_of_inputs) - verify_outputs_wrapped = Mock(wraps=self.verifiers.tx.verify_outputs) - verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.tx.verify_number_of_outputs) - verify_sigops_output_wrapped = Mock(wraps=self.verifiers.tx.verify_sigops_output) + verify_output_token_indexes_wrapped = Mock(wraps=self.verifiers.tx.verify_output_token_indexes) + verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_number_of_outputs) + verify_sigops_output_wrapped = Mock(wraps=self.verifiers.vertex.verify_sigops_output) with ( + patch.object(VertexVerifier, 'verify_outputs', verify_outputs_wrapped), patch.object(TransactionVerifier, 'verify_parents_basic', verify_parents_basic_wrapped), patch.object(TransactionVerifier, 'verify_weight', verify_weight_wrapped), - patch.object(TransactionVerifier, 'verify_pow', verify_pow_wrapped), + patch.object(VertexVerifier, 'verify_pow', verify_pow_wrapped), patch.object(TransactionVerifier, 'verify_number_of_inputs', verify_number_of_inputs_wrapped), - patch.object(TransactionVerifier, 'verify_outputs', verify_outputs_wrapped), - patch.object(TransactionVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped), - patch.object(TransactionVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), + patch.object(TransactionVerifier, 'verify_output_token_indexes', verify_output_token_indexes_wrapped), + patch.object(VertexVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped), + patch.object(VertexVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), ): self.manager.verification_service.validate_basic(tx) + # Vertex methods + verify_outputs_wrapped.assert_called_once() + # Transaction methods verify_parents_basic_wrapped.assert_called_once() verify_weight_wrapped.assert_called_once() verify_pow_wrapped.assert_called_once() verify_number_of_inputs_wrapped.assert_called_once() - verify_outputs_wrapped.assert_called_once() + verify_output_token_indexes_wrapped.assert_called_once() verify_number_of_outputs_wrapped.assert_called_once() verify_sigops_output_wrapped.assert_called_once() @@ -592,20 +653,20 @@ def test_transaction_validate_basic(self) -> None: # and if running basic validation again it shouldn't validate or change the validation state verify_parents_basic_wrapped2 = Mock(wraps=self.verifiers.tx.verify_parents_basic) verify_weight_wrapped2 = Mock(wraps=self.verifiers.tx.verify_weight) - verify_pow_wrapped2 = Mock(wraps=self.verifiers.tx.verify_pow) + verify_pow_wrapped2 = Mock(wraps=self.verifiers.vertex.verify_pow) verify_number_of_inputs_wrapped2 = Mock(wraps=self.verifiers.tx.verify_number_of_inputs) - verify_outputs_wrapped2 = Mock(wraps=self.verifiers.tx.verify_outputs) - verify_number_of_outputs_wrapped2 = Mock(wraps=self.verifiers.tx.verify_number_of_outputs) - verify_sigops_output_wrapped2 = Mock(wraps=self.verifiers.tx.verify_sigops_output) + verify_outputs_wrapped2 = Mock(wraps=self.verifiers.vertex.verify_outputs) + verify_number_of_outputs_wrapped2 = Mock(wraps=self.verifiers.vertex.verify_number_of_outputs) + verify_sigops_output_wrapped2 = Mock(wraps=self.verifiers.vertex.verify_sigops_output) with ( patch.object(TransactionVerifier, 'verify_parents_basic', verify_parents_basic_wrapped2), patch.object(TransactionVerifier, 'verify_weight', verify_weight_wrapped2), - patch.object(TransactionVerifier, 'verify_pow', verify_pow_wrapped2), + patch.object(VertexVerifier, 'verify_pow', verify_pow_wrapped2), patch.object(TransactionVerifier, 'verify_number_of_inputs', verify_number_of_inputs_wrapped2), - patch.object(TransactionVerifier, 'verify_outputs', verify_outputs_wrapped2), - patch.object(TransactionVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped2), - patch.object(TransactionVerifier, 'verify_sigops_output', verify_sigops_output_wrapped2), + patch.object(VertexVerifier, 'verify_outputs', verify_outputs_wrapped2), + patch.object(VertexVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped2), + patch.object(VertexVerifier, 'verify_sigops_output', verify_sigops_output_wrapped2), ): self.manager.verification_service.validate_basic(tx) @@ -625,43 +686,49 @@ def test_transaction_validate_full(self) -> None: add_blocks_unlock_reward(self.manager) tx = self._get_valid_tx() + verify_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_outputs) + verify_parents_basic_wrapped = Mock(wraps=self.verifiers.tx.verify_parents_basic) verify_weight_wrapped = Mock(wraps=self.verifiers.tx.verify_weight) - verify_pow_wrapped = Mock(wraps=self.verifiers.tx.verify_pow) + verify_pow_wrapped = Mock(wraps=self.verifiers.vertex.verify_pow) verify_number_of_inputs_wrapped = Mock(wraps=self.verifiers.tx.verify_number_of_inputs) - verify_outputs_wrapped = Mock(wraps=self.verifiers.tx.verify_outputs) - verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.tx.verify_number_of_outputs) - verify_sigops_output_wrapped = Mock(wraps=self.verifiers.tx.verify_sigops_output) + verify_output_token_indexes_wrapped = Mock(wraps=self.verifiers.tx.verify_output_token_indexes) + verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_number_of_outputs) + verify_sigops_output_wrapped = Mock(wraps=self.verifiers.vertex.verify_sigops_output) verify_sigops_input_wrapped = Mock(wraps=self.verifiers.tx.verify_sigops_input) verify_inputs_wrapped = Mock(wraps=self.verifiers.tx.verify_inputs) verify_script_wrapped = Mock(wraps=self.verifiers.tx.verify_script) - verify_parents_wrapped = Mock(wraps=self.verifiers.tx.verify_parents) + verify_parents_wrapped = Mock(wraps=self.verifiers.vertex.verify_parents) verify_sum_wrapped = Mock(wraps=self.verifiers.tx.verify_sum) verify_reward_locked_wrapped = Mock(wraps=self.verifiers.tx.verify_reward_locked) with ( + patch.object(VertexVerifier, 'verify_outputs', verify_outputs_wrapped), patch.object(TransactionVerifier, 'verify_parents_basic', verify_parents_basic_wrapped), patch.object(TransactionVerifier, 'verify_weight', verify_weight_wrapped), - patch.object(TransactionVerifier, 'verify_pow', verify_pow_wrapped), + patch.object(VertexVerifier, 'verify_pow', verify_pow_wrapped), patch.object(TransactionVerifier, 'verify_number_of_inputs', verify_number_of_inputs_wrapped), - patch.object(TransactionVerifier, 'verify_outputs', verify_outputs_wrapped), - patch.object(TransactionVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped), - patch.object(TransactionVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), + patch.object(TransactionVerifier, 'verify_output_token_indexes', verify_output_token_indexes_wrapped), + patch.object(VertexVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped), + patch.object(VertexVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), patch.object(TransactionVerifier, 'verify_sigops_input', verify_sigops_input_wrapped), patch.object(TransactionVerifier, 'verify_inputs', verify_inputs_wrapped), patch.object(TransactionVerifier, 'verify_script', verify_script_wrapped), - patch.object(TransactionVerifier, 'verify_parents', verify_parents_wrapped), + patch.object(VertexVerifier, 'verify_parents', verify_parents_wrapped), patch.object(TransactionVerifier, 'verify_sum', verify_sum_wrapped), patch.object(TransactionVerifier, 'verify_reward_locked', verify_reward_locked_wrapped), ): self.manager.verification_service.validate_full(tx) + # Vertex methods + assert verify_outputs_wrapped.call_count == 2 + # Transaction methods verify_parents_basic_wrapped.assert_called_once() verify_weight_wrapped.assert_called_once() assert verify_pow_wrapped.call_count == 2 assert verify_number_of_inputs_wrapped.call_count == 2 - assert verify_outputs_wrapped.call_count == 2 + assert verify_output_token_indexes_wrapped.call_count == 2 assert verify_number_of_outputs_wrapped.call_count == 2 assert verify_sigops_output_wrapped.call_count == 2 verify_sigops_input_wrapped.assert_called_once() @@ -677,20 +744,20 @@ def test_transaction_validate_full(self) -> None: # and if running full validation again it shouldn't validate or change the validation state verify_parents_basic_wrapped2 = Mock(wraps=self.verifiers.tx.verify_parents_basic) verify_weight_wrapped2 = Mock(wraps=self.verifiers.tx.verify_weight) - verify_pow_wrapped2 = Mock(wraps=self.verifiers.tx.verify_pow) + verify_pow_wrapped2 = Mock(wraps=self.verifiers.vertex.verify_pow) verify_number_of_inputs_wrapped2 = Mock(wraps=self.verifiers.tx.verify_number_of_inputs) - verify_outputs_wrapped2 = Mock(wraps=self.verifiers.tx.verify_outputs) - verify_number_of_outputs_wrapped2 = Mock(wraps=self.verifiers.tx.verify_number_of_outputs) - verify_sigops_output_wrapped2 = Mock(wraps=self.verifiers.tx.verify_sigops_output) + verify_outputs_wrapped2 = Mock(wraps=self.verifiers.vertex.verify_outputs) + verify_number_of_outputs_wrapped2 = Mock(wraps=self.verifiers.vertex.verify_number_of_outputs) + verify_sigops_output_wrapped2 = Mock(wraps=self.verifiers.vertex.verify_sigops_output) with ( patch.object(TransactionVerifier, 'verify_parents_basic', verify_parents_basic_wrapped2), patch.object(TransactionVerifier, 'verify_weight', verify_weight_wrapped2), - patch.object(TransactionVerifier, 'verify_pow', verify_pow_wrapped2), + patch.object(VertexVerifier, 'verify_pow', verify_pow_wrapped2), patch.object(TransactionVerifier, 'verify_number_of_inputs', verify_number_of_inputs_wrapped2), - patch.object(TransactionVerifier, 'verify_outputs', verify_outputs_wrapped2), - patch.object(TransactionVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped2), - patch.object(TransactionVerifier, 'verify_sigops_output', verify_sigops_output_wrapped2), + patch.object(VertexVerifier, 'verify_outputs', verify_outputs_wrapped2), + patch.object(VertexVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped2), + patch.object(VertexVerifier, 'verify_sigops_output', verify_sigops_output_wrapped2), ): self.manager.verification_service.validate_basic(tx) @@ -709,101 +776,116 @@ def test_transaction_validate_full(self) -> None: def test_token_creation_transaction_verify_basic(self) -> None: tx = self._get_valid_token_creation_tx() - verify_parents_basic_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_parents_basic) - verify_weight_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_weight) - verify_pow_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_pow) - verify_number_of_inputs_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_number_of_inputs) - verify_outputs_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_outputs) - verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_number_of_outputs) - verify_sigops_output_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_sigops_output) + verify_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_outputs) + + verify_parents_basic_wrapped = Mock(wraps=self.verifiers.tx.verify_parents_basic) + verify_weight_wrapped = Mock(wraps=self.verifiers.tx.verify_weight) + verify_pow_wrapped = Mock(wraps=self.verifiers.vertex.verify_pow) + verify_number_of_inputs_wrapped = Mock(wraps=self.verifiers.tx.verify_number_of_inputs) + verify_output_token_indexes_wrapped = Mock(wraps=self.verifiers.tx.verify_output_token_indexes) + verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_number_of_outputs) + verify_sigops_output_wrapped = Mock(wraps=self.verifiers.vertex.verify_sigops_output) with ( - patch.object(TokenCreationTransactionVerifier, 'verify_parents_basic', verify_parents_basic_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_weight', verify_weight_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_pow', verify_pow_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_number_of_inputs', verify_number_of_inputs_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_outputs', verify_outputs_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_number_of_outputs', - verify_number_of_outputs_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), + patch.object(VertexVerifier, 'verify_outputs', verify_outputs_wrapped), + patch.object(TransactionVerifier, 'verify_parents_basic', verify_parents_basic_wrapped), + patch.object(TransactionVerifier, 'verify_weight', verify_weight_wrapped), + patch.object(VertexVerifier, 'verify_pow', verify_pow_wrapped), + patch.object(TransactionVerifier, 'verify_number_of_inputs', verify_number_of_inputs_wrapped), + patch.object(TransactionVerifier, 'verify_output_token_indexes', verify_output_token_indexes_wrapped), + patch.object(VertexVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped), + patch.object(VertexVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), ): self.manager.verification_service.verify_basic(tx) + # Vertex methods + verify_outputs_wrapped.assert_called_once() + # Transaction methods verify_parents_basic_wrapped.assert_called_once() verify_weight_wrapped.assert_called_once() verify_pow_wrapped.assert_called_once() verify_number_of_inputs_wrapped.assert_called_once() - verify_outputs_wrapped.assert_called_once() + verify_output_token_indexes_wrapped.assert_called_once() verify_number_of_outputs_wrapped.assert_called_once() verify_sigops_output_wrapped.assert_called_once() def test_token_creation_transaction_verify_without_storage(self) -> None: tx = self._get_valid_token_creation_tx() - verify_pow_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_pow) - verify_number_of_inputs_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_number_of_inputs) - verify_outputs_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_outputs) - verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_number_of_outputs) - verify_sigops_output_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_sigops_output) + verify_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_outputs) + + verify_pow_wrapped = Mock(wraps=self.verifiers.vertex.verify_pow) + verify_number_of_inputs_wrapped = Mock(wraps=self.verifiers.tx.verify_number_of_inputs) + verify_output_token_indexes_wrapped = Mock(wraps=self.verifiers.tx.verify_output_token_indexes) + verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_number_of_outputs) + verify_sigops_output_wrapped = Mock(wraps=self.verifiers.vertex.verify_sigops_output) with ( - patch.object(TokenCreationTransactionVerifier, 'verify_pow', verify_pow_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_number_of_inputs', verify_number_of_inputs_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_outputs', verify_outputs_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_number_of_outputs', - verify_number_of_outputs_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), + patch.object(VertexVerifier, 'verify_outputs', verify_outputs_wrapped), + patch.object(VertexVerifier, 'verify_pow', verify_pow_wrapped), + patch.object(TransactionVerifier, 'verify_number_of_inputs', verify_number_of_inputs_wrapped), + patch.object(TransactionVerifier, 'verify_output_token_indexes', verify_output_token_indexes_wrapped), + patch.object(VertexVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped), + patch.object(VertexVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), ): - self.verifiers.token_creation_tx.verify_without_storage(tx) + self.manager.verification_service.verify_without_storage(tx) + + # Vertex methods + verify_outputs_wrapped.assert_called_once() # Transaction methods verify_pow_wrapped.assert_called_once() verify_number_of_inputs_wrapped.assert_called_once() - verify_outputs_wrapped.assert_called_once() + verify_output_token_indexes_wrapped.assert_called_once() verify_number_of_outputs_wrapped.assert_called_once() verify_sigops_output_wrapped.assert_called_once() def test_token_creation_transaction_verify(self) -> None: tx = self._get_valid_token_creation_tx() - verify_pow_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_pow) - verify_number_of_inputs_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_number_of_inputs) - verify_outputs_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_outputs) - verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_number_of_outputs) - verify_sigops_output_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_sigops_output) - verify_sigops_input_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_sigops_input) - verify_inputs_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_inputs) - verify_script_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_script) - verify_parents_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_parents) - verify_sum_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_sum) - verify_reward_locked_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_reward_locked) + verify_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_outputs) + + verify_pow_wrapped = Mock(wraps=self.verifiers.vertex.verify_pow) + verify_number_of_inputs_wrapped = Mock(wraps=self.verifiers.tx.verify_number_of_inputs) + verify_output_token_indexes_wrapped = Mock(wraps=self.verifiers.tx.verify_output_token_indexes) + verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_number_of_outputs) + verify_sigops_output_wrapped = Mock(wraps=self.verifiers.vertex.verify_sigops_output) + verify_sigops_input_wrapped = Mock(wraps=self.verifiers.tx.verify_sigops_input) + verify_inputs_wrapped = Mock(wraps=self.verifiers.tx.verify_inputs) + verify_script_wrapped = Mock(wraps=self.verifiers.tx.verify_script) + verify_parents_wrapped = Mock(wraps=self.verifiers.vertex.verify_parents) + verify_sum_wrapped = Mock(wraps=self.verifiers.tx.verify_sum) + verify_reward_locked_wrapped = Mock(wraps=self.verifiers.tx.verify_reward_locked) verify_token_info_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_token_info) verify_minted_tokens_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_minted_tokens) with ( - patch.object(TokenCreationTransactionVerifier, 'verify_pow', verify_pow_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_number_of_inputs', verify_number_of_inputs_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_outputs', verify_outputs_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_number_of_outputs', - verify_number_of_outputs_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_sigops_input', verify_sigops_input_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_inputs', verify_inputs_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_script', verify_script_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_parents', verify_parents_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_sum', verify_sum_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_reward_locked', verify_reward_locked_wrapped), + patch.object(VertexVerifier, 'verify_outputs', verify_outputs_wrapped), + patch.object(VertexVerifier, 'verify_pow', verify_pow_wrapped), + patch.object(TransactionVerifier, 'verify_number_of_inputs', verify_number_of_inputs_wrapped), + patch.object(TransactionVerifier, 'verify_output_token_indexes', verify_output_token_indexes_wrapped), + patch.object(VertexVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped), + patch.object(VertexVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), + patch.object(TransactionVerifier, 'verify_sigops_input', verify_sigops_input_wrapped), + patch.object(TransactionVerifier, 'verify_inputs', verify_inputs_wrapped), + patch.object(TransactionVerifier, 'verify_script', verify_script_wrapped), + patch.object(VertexVerifier, 'verify_parents', verify_parents_wrapped), + patch.object(TransactionVerifier, 'verify_sum', verify_sum_wrapped), + patch.object(TransactionVerifier, 'verify_reward_locked', verify_reward_locked_wrapped), patch.object(TokenCreationTransactionVerifier, 'verify_token_info', verify_token_info_wrapped), patch.object(TokenCreationTransactionVerifier, 'verify_minted_tokens', verify_minted_tokens_wrapped), ): self.manager.verification_service.verify(tx) + # Vertex methods + verify_outputs_wrapped.assert_called_once() + # Transaction methods verify_pow_wrapped.assert_called_once() verify_number_of_inputs_wrapped.assert_called_once() - verify_outputs_wrapped.assert_called_once() + verify_output_token_indexes_wrapped.assert_called_once() verify_number_of_outputs_wrapped.assert_called_once() verify_sigops_output_wrapped.assert_called_once() verify_sigops_input_wrapped.assert_called_once() @@ -821,32 +903,37 @@ def test_token_creation_transaction_validate_basic(self) -> None: tx = self._get_valid_token_creation_tx() tx.get_metadata().validation = ValidationState.INITIAL - verify_parents_basic_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_parents_basic) - verify_weight_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_weight) - verify_pow_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_pow) - verify_number_of_inputs_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_number_of_inputs) - verify_outputs_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_outputs) - verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_number_of_outputs) - verify_sigops_output_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_sigops_output) + verify_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_outputs) + + verify_parents_basic_wrapped = Mock(wraps=self.verifiers.tx.verify_parents_basic) + verify_weight_wrapped = Mock(wraps=self.verifiers.tx.verify_weight) + verify_pow_wrapped = Mock(wraps=self.verifiers.vertex.verify_pow) + verify_number_of_inputs_wrapped = Mock(wraps=self.verifiers.tx.verify_number_of_inputs) + verify_output_token_indexes_wrapped = Mock(wraps=self.verifiers.tx.verify_output_token_indexes) + verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_number_of_outputs) + verify_sigops_output_wrapped = Mock(wraps=self.verifiers.vertex.verify_sigops_output) with ( - patch.object(TokenCreationTransactionVerifier, 'verify_parents_basic', verify_parents_basic_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_weight', verify_weight_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_pow', verify_pow_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_number_of_inputs', verify_number_of_inputs_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_outputs', verify_outputs_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_number_of_outputs', - verify_number_of_outputs_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), + patch.object(VertexVerifier, 'verify_outputs', verify_outputs_wrapped), + patch.object(TransactionVerifier, 'verify_parents_basic', verify_parents_basic_wrapped), + patch.object(TransactionVerifier, 'verify_weight', verify_weight_wrapped), + patch.object(VertexVerifier, 'verify_pow', verify_pow_wrapped), + patch.object(TransactionVerifier, 'verify_number_of_inputs', verify_number_of_inputs_wrapped), + patch.object(TransactionVerifier, 'verify_output_token_indexes', verify_output_token_indexes_wrapped), + patch.object(VertexVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped), + patch.object(VertexVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), ): self.manager.verification_service.validate_basic(tx) + # Vertex methods + verify_outputs_wrapped.assert_called_once() + # Transaction methods verify_parents_basic_wrapped.assert_called_once() verify_weight_wrapped.assert_called_once() verify_pow_wrapped.assert_called_once() verify_number_of_inputs_wrapped.assert_called_once() - verify_outputs_wrapped.assert_called_once() + verify_output_token_indexes_wrapped.assert_called_once() verify_number_of_outputs_wrapped.assert_called_once() verify_sigops_output_wrapped.assert_called_once() @@ -858,24 +945,22 @@ def test_token_creation_transaction_validate_basic(self) -> None: self.assertEqual(tx.get_metadata().validation, ValidationState.FULL) # and if running basic validation again it shouldn't validate or change the validation state - verify_parents_basic_wrapped2 = Mock(wraps=self.verifiers.token_creation_tx.verify_parents_basic) - verify_weight_wrapped2 = Mock(wraps=self.verifiers.token_creation_tx.verify_weight) - verify_pow_wrapped2 = Mock(wraps=self.verifiers.token_creation_tx.verify_pow) - verify_number_of_inputs_wrapped2 = Mock(wraps=self.verifiers.token_creation_tx.verify_number_of_inputs) - verify_outputs_wrapped2 = Mock(wraps=self.verifiers.token_creation_tx.verify_outputs) - verify_number_of_outputs_wrapped2 = Mock(wraps=self.verifiers.token_creation_tx.verify_number_of_outputs) - verify_sigops_output_wrapped2 = Mock(wraps=self.verifiers.token_creation_tx.verify_sigops_output) + verify_parents_basic_wrapped2 = Mock(wraps=self.verifiers.tx.verify_parents_basic) + verify_weight_wrapped2 = Mock(wraps=self.verifiers.tx.verify_weight) + verify_pow_wrapped2 = Mock(wraps=self.verifiers.vertex.verify_pow) + verify_number_of_inputs_wrapped2 = Mock(wraps=self.verifiers.tx.verify_number_of_inputs) + verify_outputs_wrapped2 = Mock(wraps=self.verifiers.vertex.verify_outputs) + verify_number_of_outputs_wrapped2 = Mock(wraps=self.verifiers.vertex.verify_number_of_outputs) + verify_sigops_output_wrapped2 = Mock(wraps=self.verifiers.vertex.verify_sigops_output) with ( - patch.object(TokenCreationTransactionVerifier, 'verify_parents_basic', verify_parents_basic_wrapped2), - patch.object(TokenCreationTransactionVerifier, 'verify_weight', verify_weight_wrapped2), - patch.object(TokenCreationTransactionVerifier, 'verify_pow', verify_pow_wrapped2), - patch.object(TokenCreationTransactionVerifier, 'verify_number_of_inputs', - verify_number_of_inputs_wrapped2), - patch.object(TokenCreationTransactionVerifier, 'verify_outputs', verify_outputs_wrapped2), - patch.object(TokenCreationTransactionVerifier, 'verify_number_of_outputs', - verify_number_of_outputs_wrapped2), - patch.object(TokenCreationTransactionVerifier, 'verify_sigops_output', verify_sigops_output_wrapped2), + patch.object(TransactionVerifier, 'verify_parents_basic', verify_parents_basic_wrapped2), + patch.object(TransactionVerifier, 'verify_weight', verify_weight_wrapped2), + patch.object(VertexVerifier, 'verify_pow', verify_pow_wrapped2), + patch.object(TransactionVerifier, 'verify_number_of_inputs', verify_number_of_inputs_wrapped2), + patch.object(VertexVerifier, 'verify_outputs', verify_outputs_wrapped2), + patch.object(VertexVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped2), + patch.object(VertexVerifier, 'verify_sigops_output', verify_sigops_output_wrapped2), ): self.manager.verification_service.validate_basic(tx) @@ -895,49 +980,54 @@ def test_token_creation_transaction_validate_full(self) -> None: tx = self._get_valid_token_creation_tx() tx.get_metadata().validation = ValidationState.INITIAL - verify_parents_basic_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_parents_basic) - verify_weight_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_weight) - verify_pow_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_pow) - verify_number_of_inputs_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_number_of_inputs) - verify_outputs_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_outputs) - verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_number_of_outputs) - verify_sigops_output_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_sigops_output) - verify_sigops_input_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_sigops_input) - verify_inputs_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_inputs) - verify_script_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_script) - verify_parents_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_parents) - verify_sum_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_sum) - verify_reward_locked_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_reward_locked) + verify_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_outputs) + + verify_parents_basic_wrapped = Mock(wraps=self.verifiers.tx.verify_parents_basic) + verify_weight_wrapped = Mock(wraps=self.verifiers.tx.verify_weight) + verify_pow_wrapped = Mock(wraps=self.verifiers.vertex.verify_pow) + verify_number_of_inputs_wrapped = Mock(wraps=self.verifiers.tx.verify_number_of_inputs) + verify_output_token_indexes_wrapped = Mock(wraps=self.verifiers.tx.verify_output_token_indexes) + verify_number_of_outputs_wrapped = Mock(wraps=self.verifiers.vertex.verify_number_of_outputs) + verify_sigops_output_wrapped = Mock(wraps=self.verifiers.vertex.verify_sigops_output) + verify_sigops_input_wrapped = Mock(wraps=self.verifiers.tx.verify_sigops_input) + verify_inputs_wrapped = Mock(wraps=self.verifiers.tx.verify_inputs) + verify_script_wrapped = Mock(wraps=self.verifiers.tx.verify_script) + verify_parents_wrapped = Mock(wraps=self.verifiers.vertex.verify_parents) + verify_sum_wrapped = Mock(wraps=self.verifiers.tx.verify_sum) + verify_reward_locked_wrapped = Mock(wraps=self.verifiers.tx.verify_reward_locked) verify_token_info_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_token_info) verify_minted_tokens_wrapped = Mock(wraps=self.verifiers.token_creation_tx.verify_minted_tokens) with ( - patch.object(TokenCreationTransactionVerifier, 'verify_parents_basic', verify_parents_basic_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_weight', verify_weight_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_pow', verify_pow_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_number_of_inputs', verify_number_of_inputs_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_outputs', verify_outputs_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_number_of_outputs', - verify_number_of_outputs_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_sigops_input', verify_sigops_input_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_inputs', verify_inputs_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_script', verify_script_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_parents', verify_parents_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_sum', verify_sum_wrapped), - patch.object(TokenCreationTransactionVerifier, 'verify_reward_locked', verify_reward_locked_wrapped), + patch.object(VertexVerifier, 'verify_outputs', verify_outputs_wrapped), + patch.object(TransactionVerifier, 'verify_parents_basic', verify_parents_basic_wrapped), + patch.object(TransactionVerifier, 'verify_weight', verify_weight_wrapped), + patch.object(VertexVerifier, 'verify_pow', verify_pow_wrapped), + patch.object(TransactionVerifier, 'verify_number_of_inputs', verify_number_of_inputs_wrapped), + patch.object(TransactionVerifier, 'verify_output_token_indexes', verify_output_token_indexes_wrapped), + patch.object(VertexVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped), + patch.object(VertexVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), + patch.object(TransactionVerifier, 'verify_sigops_input', verify_sigops_input_wrapped), + patch.object(TransactionVerifier, 'verify_inputs', verify_inputs_wrapped), + patch.object(TransactionVerifier, 'verify_script', verify_script_wrapped), + patch.object(VertexVerifier, 'verify_parents', verify_parents_wrapped), + patch.object(TransactionVerifier, 'verify_sum', verify_sum_wrapped), + patch.object(TransactionVerifier, 'verify_reward_locked', verify_reward_locked_wrapped), patch.object(TokenCreationTransactionVerifier, 'verify_token_info', verify_token_info_wrapped), patch.object(TokenCreationTransactionVerifier, 'verify_minted_tokens', verify_minted_tokens_wrapped), ): self.manager.verification_service.validate_full(tx) + # Vertex methods + assert verify_outputs_wrapped.call_count == 2 + # Transaction methods verify_parents_basic_wrapped.assert_called_once() verify_weight_wrapped.assert_called_once() assert verify_pow_wrapped.call_count == 2 assert verify_number_of_inputs_wrapped.call_count == 2 - assert verify_outputs_wrapped.call_count == 2 + assert verify_output_token_indexes_wrapped.call_count == 2 assert verify_number_of_outputs_wrapped.call_count == 2 assert verify_sigops_output_wrapped.call_count == 2 verify_sigops_input_wrapped.assert_called_once()