Skip to content

Commit

Permalink
Merge pull request #154 from levonyak/PAN-2343-node-registration-prer…
Browse files Browse the repository at this point in the history
…equisites-checks

[PAN-2343] Node registration prerequisites checks
  • Loading branch information
markuslevonyak authored Nov 26, 2024
2 parents 80ff76f + f00450b commit 6d3c9c4
Show file tree
Hide file tree
Showing 11 changed files with 377 additions and 25 deletions.
53 changes: 53 additions & 0 deletions pantos/servicenode/blockchains/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,18 @@ def __init__(self):
f'unable to initialize the {self.get_blockchain_name()} '
'utilities')

@abc.abstractmethod
def get_own_address(self) -> BlockchainAddress:
"""Get the service node's own address on the blockchain.
Returns
-------
BlockchainAddress
The service node's own address.
"""
pass # pragma: no cover

@abc.abstractmethod
def is_node_registered(self) -> bool:
"""Determine if the service node is registered at the Pantos Hub
Expand Down Expand Up @@ -171,6 +183,24 @@ def is_valid_recipient_address(self, recipient_address: str) -> bool:
"""
pass # pragma: no cover

@abc.abstractmethod
def read_minimum_deposit(self) -> int:
"""Read the service node's minimum deposit at the Pantos Hub on
the blockchain.
Returns
-------
int
The service node's minimum deposit.
Raises
------
BlockchainClientError
If the service node's minimum deposit cannot be read.
"""
pass # pragma: no cover

@abc.abstractmethod
def read_node_url(self) -> str:
"""Read the service node's URL that is registered at the Pantos
Expand All @@ -189,6 +219,29 @@ def read_node_url(self) -> str:
"""
pass # pragma: no cover

def read_own_pan_balance(self) -> int:
"""Read the service node's own PAN token balance on the
blockchain.
Returns
-------
int
The service node's own PAN token balance.
Raises
------
BlockchainClientError
If the service node's own PAN token balance cannot be read.
"""
try:
return self._get_utilities().get_balance(
self.get_own_address(),
token_address=self._get_config()['pan_token'])
except Exception:
raise self._create_error(
'unable to read the own PAN token balance')

@abc.abstractmethod
def register_node(self, node_url: str, node_deposit: int,
withdrawal_address: BlockchainAddress) -> None:
Expand Down
14 changes: 14 additions & 0 deletions pantos/servicenode/blockchains/ethereum.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ def get_error_class(cls) -> type[BlockchainClientError]:
# Docstring inherited
return EthereumClientError

def get_own_address(self) -> BlockchainAddress:
# Docstring inherited
return self.__address # pragma: no cover

def is_node_registered(self) -> bool:
# Docstring inherited
try:
Expand All @@ -101,6 +105,16 @@ def is_valid_recipient_address(self, recipient_address: str) -> bool:
is_zero_address = int(recipient_address, 0) == 0
return not is_zero_address

def read_minimum_deposit(self) -> int:
# Docstring inherited
try:
node_connections = self.__create_node_connections()
hub_contract = self._create_hub_contract(node_connections)
return hub_contract.caller().\
getCurrentMinimumServiceNodeDeposit().get()
except Exception:
raise self._create_error('unable to read the minimum deposit')

def read_node_url(self) -> str:
# Docstring inherited
try:
Expand Down
8 changes: 8 additions & 0 deletions pantos/servicenode/blockchains/solana.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ def get_error_class(cls) -> type[BlockchainClientError]:
# Docstring inherited
return SolanaClientError

def get_own_address(self) -> BlockchainAddress:
# Docstring inherited
raise NotImplementedError # pragma: no cover

def is_node_registered(self) -> bool:
# Docstring inherited
return False # pragma: no cover
Expand All @@ -43,6 +47,10 @@ def is_valid_recipient_address(self, recipient_address: str) -> bool:
# Docstring inherited
raise NotImplementedError # pragma: no cover

def read_minimum_deposit(self) -> int:
# Docstring inherited
raise NotImplementedError # pragma: no cover

def read_node_url(self) -> str:
# Docstring inherited
raise NotImplementedError # pragma: no cover
Expand Down
48 changes: 45 additions & 3 deletions pantos/servicenode/business/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""Base classes for all business logic interactors and errors.
"""
import abc
import typing

from pantos.common.exceptions import ErrorCreator

from pantos.servicenode.exceptions import ServiceNodeError

Expand All @@ -13,8 +15,48 @@ class InteractorError(ServiceNodeError):
pass


class Interactor(abc.ABC):
class InvalidAmountError(InteractorError):
"""Exception to be raised if an amount is invalid.
"""
def __init__(self, **kwargs: typing.Any):
# Docstring inherited
super().__init__('invalid amount', **kwargs)


class InvalidBlockchainAddressError(InteractorError):
"""Exception to be raised if a blockchain address is invalid.
"""
def __init__(self, **kwargs: typing.Any):
# Docstring inherited
super().__init__('invalid blockchain address', **kwargs)


class InvalidUrlError(InteractorError):
"""Exception to be raised if a URL is invalid.
"""
def __init__(self, **kwargs: typing.Any):
# Docstring inherited
super().__init__('invalid URL', **kwargs)


class Interactor(ErrorCreator[InteractorError]):
"""Base class for all interactors.
"""
pass
def _create_invalid_amount_error(self,
**kwargs: typing.Any) -> InteractorError:
return self._create_error(specialized_error_class=InvalidAmountError,
**kwargs)

def _create_invalid_blockchain_address_error(
self, **kwargs: typing.Any) -> InteractorError:
return self._create_error(
specialized_error_class=InvalidBlockchainAddressError, **kwargs)

def _create_invalid_url_error(self,
**kwargs: typing.Any) -> InteractorError:
return self._create_error(specialized_error_class=InvalidUrlError,
**kwargs)
7 changes: 6 additions & 1 deletion pantos/servicenode/business/bids.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ class BidInteractor(Interactor):
"""Interactor for managing service node bids.
"""
@classmethod
def get_error_class(cls) -> type[InteractorError]:
# Docstring inherited
return BidInteractorError

def get_current_bids(
self, source_blockchain_id: int,
destination_blockchain_id: int) -> list[dict[str, int | str]]:
Expand Down Expand Up @@ -76,7 +81,7 @@ def get_current_bids(
'signature': signature
})
except Exception:
raise BidInteractorError(
raise self._create_error(
'unable to get the current bids',
source_blockchain_id=source_blockchain_id,
destination_blockchain_id=destination_blockchain_id)
Expand Down
64 changes: 58 additions & 6 deletions pantos/servicenode/business/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@
"""
import logging
import urllib.parse

from pantos.common.blockchains.enums import Blockchain

from pantos.servicenode.blockchains.base import BlockchainClient
from pantos.servicenode.blockchains.factory import get_blockchain_client
from pantos.servicenode.business.base import Interactor
from pantos.servicenode.business.base import InteractorError
from pantos.servicenode.configuration import config
from pantos.servicenode.configuration import get_blockchain_config

_VALID_NODE_URL_SCHEMES = ['http', 'https']

_logger = logging.getLogger(__name__)
"""Logger for this module."""

Expand All @@ -26,14 +30,28 @@ class NodeInteractor(Interactor):
"""Interactor for managing the service node itself.
"""
@classmethod
def get_error_class(cls) -> type[InteractorError]:
# Docstring inherited
return NodeInteractorError

def update_node_registrations(self) -> None:
"""Update the service node registrations on all supported
blockchains.
Raises
------
InvalidUrlError
If the configured service node URL is invalid.
InvalidAmountError
If the configured service node deposit is less than the
Pantos Hub's minimum service node deposit or greater than
the service node's own PAN token balance.
InvalidBlockchainAddressError
If the configured withdrawal address is invalid.
NodeInteractorError
If a service node registration cannot be updated.
If a service node registration cannot be updated for any
other reason.
"""
for blockchain in Blockchain:
Expand All @@ -50,6 +68,7 @@ def update_node_registrations(self) -> None:
old_node_url = blockchain_client.read_node_url()
new_node_url = config['application']['url']
if old_node_url != new_node_url:
self.__validate_node_url(new_node_url)
blockchain_client.update_node_url(new_node_url)
elif to_be_registered:
is_unbonding = blockchain_client.is_unbonding()
Expand All @@ -59,17 +78,50 @@ def update_node_registrations(self) -> None:
blockchain_client.cancel_unregistration()
else:
# Not yet registered
withdrawal_address = blockchain_config[
'withdrawal_address']
node_url = config['application']['url']
node_deposit = blockchain_config['deposit']
withdrawal_address = blockchain_config[
'withdrawal_address']
self.__validate_node_url(node_url)
self.__validate_node_deposit(blockchain_client,
node_deposit)
self.__validate_withdrawal_address(
blockchain_client, withdrawal_address)
blockchain_client.register_node(
node_url, node_deposit, withdrawal_address)
elif is_registered:
# Not to be registered anymore
blockchain_client.unregister_node()
# Do nothing if neither registered nor to be registered
except NodeInteractorError:
raise
except Exception:
raise NodeInteractorError(
'unable to update the service node registration on '
'{}'.format(blockchain.name))
raise self._create_error(
'unable to update a service node registration',
blockchain=blockchain)

def __validate_node_deposit(self, blockchain_client: BlockchainClient,
node_deposit: int) -> None:
minimum_deposit = blockchain_client.read_minimum_deposit()
own_pan_balance = blockchain_client.read_own_pan_balance()
if node_deposit < minimum_deposit or node_deposit > own_pan_balance:
raise self._create_invalid_amount_error(
node_deposit=node_deposit, minimum_deposit=minimum_deposit,
own_pan_balance=own_pan_balance)

def __validate_node_url(self, node_url: str) -> None:
try:
parse_result = urllib.parse.urlparse(node_url)
except ValueError:
raise self._create_invalid_url_error(node_url=node_url)
is_scheme_valid = parse_result.scheme in _VALID_NODE_URL_SCHEMES
is_netloc_valid = len(parse_result.netloc) > 0
if not is_scheme_valid or not is_netloc_valid:
raise self._create_invalid_url_error(node_url=node_url)

def __validate_withdrawal_address(self,
blockchain_client: BlockchainClient,
withdrawal_address: str) -> None:
if not blockchain_client.is_valid_address(withdrawal_address):
raise self._create_invalid_blockchain_address_error(
withdrawal_address=withdrawal_address)
5 changes: 5 additions & 0 deletions pantos/servicenode/business/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ class BidPluginInteractor(Interactor):
"""Interactor for handling the bid plugin operations.
"""
@classmethod
def get_error_class(cls) -> type[InteractorError]:
# Docstring inherited
return BidPluginInteractorError

def replace_bids(self, source_blockchain: Blockchain) -> int:
"""Replace the old bids with new bids given by the bid plugin.
Additionally, the Validator fee is added to the bid fee.
Expand Down
19 changes: 12 additions & 7 deletions pantos/servicenode/business/transfers.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ class TransferInteractor(Interactor):
"""Interactor for handling token transfers.
"""
@classmethod
def get_error_class(cls) -> type[InteractorError]:
# Docstring inherited
return TransferInteractorError

@dataclasses.dataclass
class ConfirmTransferRequest:
"""Request data for confirming the inclusion of a token transfer
Expand Down Expand Up @@ -158,7 +163,7 @@ def confirm_transfer(self, request: ConfirmTransferRequest) -> bool:
request.internal_transfer_id, TransferStatus.CONFIRMED)
return True
except Exception:
raise TransferInteractorError(
raise self._create_error(
'unable to determine if a token transfer is confirmed',
request=request)

Expand Down Expand Up @@ -250,8 +255,8 @@ def execute_transfer(self, request: ExecuteTransferRequest) -> uuid.UUID:
except TransferInteractorUnrecoverableError:
raise
except Exception:
raise TransferInteractorError('unable to execute a token transfer',
request=request)
raise self._create_error('unable to execute a token transfer',
request=request)

def __single_chain_transfer(self,
request: ExecuteTransferRequest) -> uuid.UUID:
Expand Down Expand Up @@ -634,8 +639,8 @@ def find_transfer(self, task_id: uuid.UUID) -> FindTransferResponse:
except TransferInteractorResourceNotFoundError:
raise
except Exception:
raise TransferInteractorError(
'unable to search for a token transfer', task_id=task_id)
raise self._create_error('unable to search for a token transfer',
task_id=task_id)

@dataclasses.dataclass
class InitiateTransferRequest:
Expand Down Expand Up @@ -772,8 +777,8 @@ def initiate_transfer(self, request: InitiateTransferRequest) -> uuid.UUID:
except TransferInteractorBidNotAcceptedError:
raise
except Exception:
raise TransferInteractorError(
'unable to initiate a new token transfer', request=request)
raise self._create_error('unable to initiate a new token transfer',
request=request)


@celery.current_app.task(bind=True, max_retries=100)
Expand Down
Loading

0 comments on commit 6d3c9c4

Please sign in to comment.