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
2 changes: 1 addition & 1 deletion hathor/_openapi/openapi_base.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
],
"info": {
"title": "Hathor API",
"version": "0.68.0"
"version": "0.68.1"
},
"consumes": [
"application/json"
Expand Down
5 changes: 5 additions & 0 deletions hathor/nanocontracts/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,11 @@ class NCForbiddenReentrancy(NCFail):
pass


class NCTypeError(NCFail):
"""Raised when a wrong type is used."""
pass


class UnknownFieldType(NCFail):
"""Raised when there is no field available for a given type."""
pass
Expand Down
3 changes: 3 additions & 0 deletions hathor/nanocontracts/resources/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@ def render_GET(self, request: 'Request') -> bytes:
except KeyError:
fields[field] = NCValueErrorResponse(errmsg='field not found')
continue
except TypeError:
fields[field] = NCValueErrorResponse(errmsg='field cannot be rendered')
continue

json_value = field_nc_type.value_to_json(value)
fields[field] = NCValueSuccessResponse(value=json_value)
Expand Down
4 changes: 4 additions & 0 deletions hathor/nanocontracts/runner/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
NCInvalidPublicMethodCallFromView,
NCInvalidSyscall,
NCMethodNotFound,
NCTypeError,
NCUninitializedContractError,
NCViewMethodError,
)
Expand Down Expand Up @@ -1241,6 +1242,9 @@ def syscall_create_child_fee_token(
@_forbid_syscall_from_view('emit_event')
def syscall_emit_event(self, data: bytes) -> None:
"""Emit a custom event from a Nano Contract."""
if not isinstance(data, bytes):
raise NCTypeError(f'got {type(data)} instead of bytes')
data = bytes(data) # force actual bytes because isinstance could be True for "compatible" types
assert self._call_info is not None
self._call_info.nc_logger.__emit_event__(data)

Expand Down
2 changes: 1 addition & 1 deletion hathor/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

from structlog import get_logger

BASE_VERSION = '0.68.0'
BASE_VERSION = '0.68.1'

DEFAULT_VERSION_SUFFIX = "local"
BUILD_VERSION_FILE_PATH = "./BUILD_VERSION"
Expand Down
66 changes: 66 additions & 0 deletions hathor_tests/nanocontracts/test_emit_event_payload.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# 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.

from hathor.manager import HathorManager
from hathor.nanocontracts import NC_EXECUTION_FAIL_ID, Blueprint, Context, public
from hathor.transaction import Block, Transaction
from hathor.transaction.nc_execution_state import NCExecutionState
from hathor_tests.dag_builder.builder import TestDAGBuilder
from hathor_tests.nanocontracts.blueprints.unittest import BlueprintTestCase


class EmitEventWithDictBlueprint(Blueprint):
@public
def initialize(self, ctx: Context) -> None:
# Intentionally emit a non-bytes payload; runtime type checks must reject this.
self.syscall.emit_event({'should': 'fail'}) # type: ignore[arg-type]


class EmitEventPayloadTestCase(BlueprintTestCase):
def build_manager(self) -> HathorManager:
# Lower reward spend requirement to avoid reward-lock interference in this focused test.
settings = self._settings._replace(REWARD_SPEND_MIN_BLOCKS=1)
return self.create_peer(
'unittests',
nc_indexes=True,
wallet_index=True,
settings=settings,
)

def setUp(self) -> None:
super().setUp()
self.blueprint_id = self._register_blueprint_class(EmitEventWithDictBlueprint)
self.dag_builder = TestDAGBuilder.from_manager(self.manager)

def test_emit_event_requires_bytes_payload(self) -> None:
artifacts = self.dag_builder.build_from_str(f'''
blockchain genesis b[1..5]
b3 < dummy # ensure enough confirmations to unlock rewards

tx1.nc_id = "{self.blueprint_id.hex()}"
tx1.nc_method = initialize()

tx1 < b4 < b5
tx1 <-- b4
''')

b4 = artifacts.get_typed_vertex('b4', Block)
tx1 = artifacts.get_typed_vertex('tx1', Transaction)

# Executing the contract must fail because emit_event payload is not bytes.
artifacts.propagate_with(self.manager, up_to='b4')
meta = tx1.get_metadata()
assert meta.first_block == b4.hash
assert meta.nc_execution == NCExecutionState.FAILURE
assert meta.voided_by == {NC_EXECUTION_FAIL_ID, tx1.hash}
50 changes: 50 additions & 0 deletions hathor_tests/resources/nanocontracts/test_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ class MyBlueprint(Blueprint):
address_details: dict[Address, dict[str, Amount]]
bytes_field: bytes
dict_with_bytes: dict[bytes, str]
list_field: list[int]
set_field: set[int]
last_caller_id: CallerId
last_bet_address: Address
last_vertex_id: VertexId
Expand All @@ -67,6 +69,8 @@ def initialize(self, ctx: Context, token_uid: TokenUid, date_last_bet: Timestamp
self.address_details = {}
self.bytes_field = b''
self.dict_with_bytes = {}
self.list_field = []
self.set_field = set()
self.last_caller_id = ctx.caller_id
self.last_bet_address = Address(b'\00' * 25)
self.last_vertex_id = VertexId(ctx.vertex.hash)
Expand Down Expand Up @@ -214,6 +218,52 @@ def _fill_nc(
sign_openssl(nano_header, private_key)
self.manager.cpu_mining_service.resolve(nc)

@inlineCallbacks
def test_container_field_returns_field_error(self):
parents = [tx.hash for tx in self.genesis_txs]
timestamp = 1 + max(tx.timestamp for tx in self.genesis)

nc = Transaction(
weight=1,
inputs=[],
outputs=[],
parents=parents,
storage=self.tx_storage,
timestamp=timestamp
)
self._fill_nc(
nc,
self.bet_id,
'initialize',
[settings.HATHOR_TOKEN_UID, timestamp],
self.genesis_private_key,
)
self.assertTrue(self.manager.on_new_tx(nc))
add_new_block(self.manager)

response = yield self.web.get(
'state', [
(b'id', nc.hash.hex().encode('ascii')),
(b'fields[]', b'token_uid'),
(b'fields[]', b'address_details'),
(b'fields[]', b'list_field'),
(b'fields[]', b'set_field'),
]
)

data = response.json_value()
self.assertTrue(data['success'])
fields = data['fields']
self.assertEqual(fields['token_uid'], {'value': settings.HATHOR_TOKEN_UID.hex()})
self.assertEqual(fields['address_details'], {'errmsg': 'field cannot be rendered'})
# XXX: these are ideally the errors that make sense to return:
# self.assertEqual(fields['list_field'], {'errmsg': 'field cannot be rendered'})
# self.assertEqual(fields['set_field'], {'errmsg': 'field cannot be rendered'})
# XXX: however the current implementation quirks field these:
self.assertEqual(fields['list_field'], {'errmsg': 'field not found'})
self.assertEqual(fields['set_field'], {'errmsg': 'field not found'})
# XXX: it will change in the future along with a complete state-api implementation for container fields

@inlineCallbacks
def test_success(self):
parents = [tx.hash for tx in self.genesis_txs]
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

[tool.poetry]
name = "hathor"
version = "0.68.0"
version = "0.68.1"
description = "Hathor Network full-node"
authors = ["Hathor Team <contact@hathor.network>"]
license = "Apache-2.0"
Expand Down
Loading