Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions hathor/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@
from hathor.feature_activation.feature_service import FeatureService
from hathor.mining import BlockTemplate, BlockTemplates
from hathor.mining.cpu_mining_service import CpuMiningService
from hathor.nanocontracts.exception import NanoContractDoesNotExist
from hathor.nanocontracts.runner import Runner
from hathor.nanocontracts.runner.runner import RunnerFactory
from hathor.nanocontracts.storage import NCBlockStorage, NCContractStorage
Expand Down Expand Up @@ -414,11 +413,7 @@ def get_nc_storage(self, block: Block, nc_id: VertexId) -> NCContractStorage:
"""Return a contract storage with the contract state at a given block."""
from hathor.nanocontracts.types import ContractId, VertexId as NCVertexId
block_storage = self.get_nc_block_storage(block)
try:
contract_storage = block_storage.get_contract_storage(ContractId(NCVertexId(nc_id)))
except KeyError:
raise NanoContractDoesNotExist(nc_id.hex())
return contract_storage
return block_storage.get_contract_storage(ContractId(NCVertexId(nc_id)))

def get_best_block_nc_storage(self, nc_id: VertexId) -> NCContractStorage:
"""Return a contract storage with the contract state at the best block."""
Expand Down
48 changes: 24 additions & 24 deletions hathor/nanocontracts/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,35 +15,35 @@
from hathor.exception import HathorError
from hathor.transaction.exceptions import TxValidationError

"""
All exceptions in this module MUST inherit from NCFail so they're
correctly caught by the block consensus to fail NC transactions.
"""

class BlueprintSyntaxError(SyntaxError):
"""Raised when a blueprint contains invalid syntax."""
pass


class NCError(HathorError):
"""Base exception for nano contract's exceptions."""
pass
class NCFail(HathorError):
"""Raised by Blueprint's methods to fail execution."""


class NCTxValidationError(TxValidationError):
class BlueprintSyntaxError(SyntaxError, NCFail):
"""Raised when a blueprint contains invalid syntax."""
pass


class NCInvalidSignature(NCTxValidationError):
class NCTxValidationError(TxValidationError, NCFail):
pass


class NCInvalidPubKey(NCTxValidationError):
class NCInvalidSignature(NCTxValidationError, NCFail):
pass


class NCInvalidSeqnum(NCTxValidationError):
class NCInvalidPubKey(NCTxValidationError, NCFail):
pass


class NCFail(NCError):
"""Raised by Blueprint's methods to fail execution."""
class NCInvalidSeqnum(NCTxValidationError, NCFail):
pass


class NanoContractDoesNotExist(NCFail):
Expand Down Expand Up @@ -71,7 +71,7 @@ class NCViewMethodError(NCFail):
pass


class NCMethodNotFound(NCFail, NCTxValidationError):
class NCMethodNotFound(NCTxValidationError):
"""Raised when a method is not found in a nano contract."""
pass

Expand Down Expand Up @@ -102,7 +102,7 @@ class NCInvalidContractId(NCFail):
"""Raised when a contract call is invalid."""


class NCInvalidMethodCall(NCFail, NCTxValidationError):
class NCInvalidMethodCall(NCTxValidationError):
"""Raised when a contract calls another contract's invalid method."""


Expand Down Expand Up @@ -155,12 +155,12 @@ class NCForbiddenReentrancy(NCFail):
pass


class UnknownFieldType(NCError):
class UnknownFieldType(NCFail):
"""Raised when there is no field available for a given type."""
pass


class NCContractCreationNotFound(NCError):
class NCContractCreationNotFound(NCFail):
"""Raised when a nano contract creation transaction is not found.

This error might also happen when the transaction is at the mempool or when it fails execution."""
Expand All @@ -181,38 +181,38 @@ class NCContractCreationVoided(NCContractCreationNotFound):
pass


class OCBInvalidScript(NCError):
class OCBInvalidScript(NCFail):
"""Raised when an On-Chain Blueprint script does not pass our script restrictions check.
"""
pass


class OCBInvalidBlueprintVertexType(NCError):
class OCBInvalidBlueprintVertexType(NCFail):
"""Raised when a vertex that is not an OnChainBlueprint is used as a blueprint-id.
"""
pass


class OCBBlueprintNotConfirmed(NCError):
class OCBBlueprintNotConfirmed(NCFail):
"""Raised when trying to use an OnChainBlueprint that is not confirmed by a block in the current best chain.
"""


class OCBPubKeyNotAllowed(NCError):
class OCBPubKeyNotAllowed(NCFail):
"""Raised when an OnChainBlueprint transaction uses a pubkey that is not explicitly allowed in the settings.
"""


class OCBOutOfFuelDuringLoading(NCError):
class OCBOutOfFuelDuringLoading(NCFail):
"""Raised when loading an On-chain Blueprint and the execution exceeds the fuel limit.
"""


class OCBOutOfMemoryDuringLoading(NCError):
class OCBOutOfMemoryDuringLoading(NCFail):
"""Raised when loading an On-chain Blueprint and the execution exceeds the memory limit.
"""


class NCDisabledBuiltinError(NCError):
class NCDisabledBuiltinError(NCFail):
"""Raised when a disabled builtin is used during creation or execution of a nanocontract.
"""
12 changes: 11 additions & 1 deletion hathor/nanocontracts/metered_exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ def exec(self, source: str, /) -> dict[str, Any]:
def call(self, func: Callable[_P, _T], /, *, args: _P.args) -> _T:
""" This is equivalent to `func(*args, **kwargs)` but with execution metering and memory limiting.
"""
from hathor import NCFail
from hathor.nanocontracts.custom_builtins import EXEC_BUILTINS

env: dict[str, object] = {
'__builtins__': EXEC_BUILTINS,
'__func__': func,
Expand All @@ -97,5 +99,13 @@ def call(self, func: Callable[_P, _T], /, *, args: _P.args) -> _T:
optimize=0,
_feature_version=PYTHON_CODE_COMPAT_VERSION[1],
)
exec(code, env)

try:
exec(code, env)
except NCFail:
raise
except Exception as e:
# Convert any other exception to NCFail.
raise NCFail from e

return cast(_T, env['__result__'])
2 changes: 1 addition & 1 deletion hathor/nanocontracts/nc_exec_logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ def error(self, message: str, **kwargs: Any) -> None:
def __emit_event__(self, data: bytes) -> None:
"""Emit a custom event from a Nano Contract."""
if len(data) > MAX_EVENT_SIZE:
raise ValueError(f'event data cannot be larger than {MAX_EVENT_SIZE} bytes, is {len(data)}')
raise NCFail(f'event data cannot be larger than {MAX_EVENT_SIZE} bytes, is {len(data)}')
self.__events__.append(NCEvent(nc_id=self.__nc_id__, data=data))

def __log__(self, level: NCLogLevel, message: str, **kwargs: Any) -> None:
Expand Down
18 changes: 6 additions & 12 deletions hathor/nanocontracts/runner/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -631,17 +631,11 @@ def _execute_public_method_call(
rules.nc_callee_execution_rule(changes_tracker)
self._handle_index_update(action)

try:
# Although the context is immutable, we're passing a copy to the blueprint method as an added precaution.
# This ensures that, even if the blueprint method attempts to exploit or alter the context, it cannot
# impact the original context. Since the runner relies on the context for other critical checks, any
# unauthorized modification would pose a serious security risk.
ret = self._metered_executor.call(method, args=(ctx.copy(), *args))
except NCFail:
raise
except Exception as e:
# Convert any other exception to NCFail.
raise NCFail from e
# Although the context is immutable, we're passing a copy to the blueprint method as an added precaution.
# This ensures that, even if the blueprint method attempts to exploit or alter the context, it cannot
# impact the original context. Since the runner relies on the context for other critical checks, any
# unauthorized modification would pose a serious security risk.
ret = self._metered_executor.call(method, args=(ctx.copy(), *args))

if method_name == NC_INITIALIZE_METHOD:
self._check_all_field_initialized(blueprint)
Expand Down Expand Up @@ -909,7 +903,7 @@ def syscall_create_another_contract(
) -> tuple[ContractId, Any]:
"""Create a contract from another contract."""
if not salt:
raise Exception('invalid salt')
raise NCFail('invalid salt')

assert self._call_info is not None
last_call_record = self.get_current_call_record()
Expand Down
3 changes: 2 additions & 1 deletion hathor/nanocontracts/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
)
from hathor.nanocontracts.exception import BlueprintSyntaxError, NCSerializationError
from hathor.nanocontracts.faux_immutable import FauxImmutableMeta
from hathor.serialization import SerializationError
from hathor.transaction.util import bytes_to_int, get_deposit_token_withdraw_amount, int_to_bytes
from hathor.utils.typing import InnerTypeMixin

Expand Down Expand Up @@ -506,7 +507,7 @@ def try_parse_as(self, arg_types: tuple[type, ...]) -> tuple[Any, ...] | None:
try:
args_parser = ArgsOnly.from_arg_types(arg_types)
return args_parser.deserialize_args_bytes(self.args_bytes)
except (NCSerializationError, TypeError):
except (NCSerializationError, SerializationError, TypeError, ValueError):
return None


Expand Down
3 changes: 1 addition & 2 deletions hathor/verification/nano_header_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
from hathor.conf.settings import HATHOR_TOKEN_UID, HathorSettings
from hathor.nanocontracts.exception import (
NanoContractDoesNotExist,
NCError,
NCFail,
NCForbiddenAction,
NCInvalidAction,
Expand Down Expand Up @@ -171,7 +170,7 @@ def verify_method_call(self, tx: BaseTransaction, params: VerificationParams) ->

try:
blueprint_class = self._tx_storage.get_blueprint_class(blueprint_id)
except NCError as e:
except NCFail as e:
raise NCTxValidationError from e

method_name = nano_header.nc_method
Expand Down
30 changes: 30 additions & 0 deletions tests/nanocontracts/test_exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Copyright 2025 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.

import unittest


class TestExceptions(unittest.TestCase):
def test_inherit_from_nc_fail(self) -> None:
from hathor.nanocontracts import exception as nano_exceptions

skip = {
nano_exceptions.HathorError,
nano_exceptions.NCFail,
nano_exceptions.TxValidationError,
}

for name, obj in nano_exceptions.__dict__.items():
if isinstance(obj, type) and obj not in skip:
assert issubclass(obj, nano_exceptions.NCFail), f'all nano exceptions must inherit from NCFail: {name}'
4 changes: 2 additions & 2 deletions tests/nanocontracts/test_fallback_method.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import pytest

from hathor.nanocontracts import HATHOR_TOKEN_UID, NC_EXECUTION_FAIL_ID, Blueprint, Context, NCFail, public
from hathor.nanocontracts.exception import NCError, NCInvalidMethodCall
from hathor.nanocontracts.exception import NCInvalidMethodCall
from hathor.nanocontracts.method import ArgsOnly
from hathor.nanocontracts.nc_exec_logs import NCCallBeginEntry, NCCallEndEntry
from hathor.nanocontracts.runner.types import CallType
Expand Down Expand Up @@ -140,7 +140,7 @@ def test_fallback_args_kwargs_success(self) -> None:
]

def test_cannot_call_fallback_directly(self) -> None:
with pytest.raises(NCError, match='method `fallback` is not a public method'):
with pytest.raises(NCFail, match='method `fallback` is not a public method'):
self.runner.call_public_method(self.contract_id, 'fallback', self.ctx)

def test_cannot_call_another_fallback_directly(self) -> None:
Expand Down
Loading