From 45f592016f48a39f4847b76322ba0e9073e71e74 Mon Sep 17 00:00:00 2001 From: Gabriel Levcovitz Date: Fri, 15 Aug 2025 17:37:00 -0300 Subject: [PATCH 1/6] fix(nano): allow import CallerId on OCBs (#1364) --- hathor/nanocontracts/allowed_imports.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hathor/nanocontracts/allowed_imports.py b/hathor/nanocontracts/allowed_imports.py index 7d82d49bc..f124acfae 100644 --- a/hathor/nanocontracts/allowed_imports.py +++ b/hathor/nanocontracts/allowed_imports.py @@ -52,6 +52,7 @@ BlueprintId=nc.types.BlueprintId, ContractId=nc.types.ContractId, VertexId=nc.types.VertexId, + CallerId=nc.types.CallerId, NCDepositAction=nc.types.NCDepositAction, NCWithdrawalAction=nc.types.NCWithdrawalAction, NCGrantAuthorityAction=nc.types.NCGrantAuthorityAction, From 93ded98bdd30eafaff222a87238f76b814113e4c Mon Sep 17 00:00:00 2001 From: Marcelo Salhab Brogliato Date: Wed, 23 Jul 2025 13:22:54 -0500 Subject: [PATCH 2/6] feat(nano): Add reentrancy protection by default --- hathor/nanocontracts/exception.py | 5 ++ hathor/nanocontracts/runner/runner.py | 31 ++++++++-- .../nanocontracts/storage/changes_tracker.py | 5 -- hathor/nanocontracts/types.py | 7 +++ .../test_authorities_call_another.py | 4 +- .../nanocontracts/test_call_other_contract.py | 2 +- tests/nanocontracts/test_contract_upgrade.py | 2 +- tests/nanocontracts/test_execution_order.py | 4 +- tests/nanocontracts/test_reentrancy.py | 61 ++++++++++++++----- 9 files changed, 89 insertions(+), 32 deletions(-) diff --git a/hathor/nanocontracts/exception.py b/hathor/nanocontracts/exception.py index ac23763a4..5cb45cb01 100644 --- a/hathor/nanocontracts/exception.py +++ b/hathor/nanocontracts/exception.py @@ -137,6 +137,11 @@ class NCForbiddenAction(NCFail): pass +class NCForbiddenReentrancy(NCFail): + """Raised when a reentrancy is forbidden on a method.""" + pass + + class UnknownFieldType(NCError): """Raised when there is no field available for a given type.""" pass diff --git a/hathor/nanocontracts/runner/runner.py b/hathor/nanocontracts/runner/runner.py index ebccae7a5..fc8699be4 100644 --- a/hathor/nanocontracts/runner/runner.py +++ b/hathor/nanocontracts/runner/runner.py @@ -28,6 +28,7 @@ NCAlreadyInitializedContractError, NCFail, NCForbiddenAction, + NCForbiddenReentrancy, NCInvalidContext, NCInvalidContractId, NCInvalidInitializeMethodCall, @@ -55,6 +56,7 @@ from hathor.nanocontracts.storage import NCBlockStorage, NCChangesTracker, NCContractStorage, NCStorageFactory from hathor.nanocontracts.storage.contract_storage import Balance from hathor.nanocontracts.types import ( + NC_ALLOW_REENTRANCY, NC_ALLOWED_ACTIONS_ATTR, NC_FALLBACK_METHOD, NC_INITIALIZE_METHOD, @@ -371,15 +373,18 @@ def syscall_proxy_call_public_method_nc_args( method_name=method_name, actions=actions, nc_args=nc_args, + skip_reentrancy_validation=True, ) def _unsafe_call_another_contract_public_method( self, + *, contract_id: ContractId, blueprint_id: BlueprintId, method_name: str, actions: Sequence[NCAction], nc_args: NCArgs, + skip_reentrancy_validation: bool = False, ) -> Any: """Invoke another contract's public method without running the usual guard‑safety checks. @@ -419,6 +424,7 @@ def _unsafe_call_another_contract_public_method( method_name=method_name, ctx=ctx, nc_args=nc_args, + skip_reentrancy_validation=skip_reentrancy_validation, ) def _reset_all_change_trackers(self) -> None: @@ -527,6 +533,7 @@ def _execute_public_method_call( method_name: str, ctx: Context, nc_args: NCArgs, + skip_reentrancy_validation: bool = False, ) -> Any: """An internal method that actually execute the public method call. It is also used when a contract calls another contract. @@ -558,6 +565,9 @@ def _execute_public_method_call( parser = Method.from_callable(method) args = self._validate_nc_args_for_method(parser, nc_args) + if not skip_reentrancy_validation: + self._validate_reentrancy(contract_id, called_method_name, method) + call_record = CallRecord( type=CallType.PUBLIC, depth=self._call_info.depth, @@ -855,11 +865,11 @@ def syscall_create_another_contract( self._internal_create_contract(child_id, blueprint_id) nc_args = NCParsedArgs(args, kwargs) ret = self._unsafe_call_another_contract_public_method( - child_id, - blueprint_id, - NC_INITIALIZE_METHOD, - actions, - nc_args, + contract_id=child_id, + blueprint_id=blueprint_id, + method_name=NC_INITIALIZE_METHOD, + actions=actions, + nc_args=nc_args, ) assert last_call_record.index_updates is not None @@ -973,6 +983,17 @@ def _validate_context(self, ctx: Context) -> None: if isinstance(action, BaseTokenAction) and action.amount < 0: raise NCInvalidContext('amount must be positive') + def _validate_reentrancy(self, contract_id: ContractId, method_name: str, method: Any) -> None: + """Check whether a reentrancy is happening and whether it is allowed.""" + assert self._call_info is not None + allow_reentrancy = getattr(method, NC_ALLOW_REENTRANCY, False) + if allow_reentrancy: + return + + for call_record in self._call_info.stack: + if call_record.contract_id == contract_id: + raise NCForbiddenReentrancy(f'reentrancy is forbidden on method `{method_name}`') + def _validate_actions(self, method: Any, method_name: str, ctx: Context) -> None: """Check whether actions are allowed.""" allowed_actions: set[NCActionType] = getattr(method, NC_ALLOWED_ACTIONS_ATTR, set()) diff --git a/hathor/nanocontracts/storage/changes_tracker.py b/hathor/nanocontracts/storage/changes_tracker.py index f4353a35e..f902b6026 100644 --- a/hathor/nanocontracts/storage/changes_tracker.py +++ b/hathor/nanocontracts/storage/changes_tracker.py @@ -190,11 +190,6 @@ def commit(self) -> None: self.has_been_commited = True - def reset(self) -> None: - """Discard all local changes without persisting.""" - self.data = {} - self._balance_diff = {} - @override def _get_mutable_balance(self, token_uid: bytes) -> MutableBalance: internal_key = BalanceKey(self.nc_id, token_uid) diff --git a/hathor/nanocontracts/types.py b/hathor/nanocontracts/types.py index 115c68c5f..16a64b399 100644 --- a/hathor/nanocontracts/types.py +++ b/hathor/nanocontracts/types.py @@ -58,6 +58,7 @@ class ContractId(VertexId): NC_FALLBACK_METHOD: str = 'fallback' NC_ALLOWED_ACTIONS_ATTR = '__nc_allowed_actions' +NC_ALLOW_REENTRANCY = '__nc_allow_reentrancy' NC_METHOD_TYPE_ATTR: str = '__nc_method_type' @@ -163,6 +164,7 @@ def _create_decorator_with_allowed_actions( allow_grant_authority: bool | None, allow_acquire_authority: bool | None, allow_actions: list[NCActionType] | None, + allow_reentrancy: bool, ) -> Callable: """Internal utility to create a decorator that sets allowed actions.""" flags = { @@ -179,6 +181,7 @@ def decorator(fn: Callable) -> Callable: allowed_actions = set(allow_actions) if allow_actions else set() allowed_actions.update(action for action, flag in flags.items() if flag) setattr(fn, NC_ALLOWED_ACTIONS_ATTR, allowed_actions) + setattr(fn, NC_ALLOW_REENTRANCY, allow_reentrancy) decorator_body(fn) return fn @@ -197,6 +200,7 @@ def public( allow_grant_authority: bool | None = None, allow_acquire_authority: bool | None = None, allow_actions: list[NCActionType] | None = None, + allow_reentrancy: bool = False, ) -> Callable: """Decorator to mark a blueprint method as public.""" def decorator(fn: Callable) -> None: @@ -219,6 +223,7 @@ def decorator(fn: Callable) -> None: allow_grant_authority=allow_grant_authority, allow_acquire_authority=allow_acquire_authority, allow_actions=allow_actions, + allow_reentrancy=allow_reentrancy, ) @@ -246,6 +251,7 @@ def fallback( allow_grant_authority: bool | None = None, allow_acquire_authority: bool | None = None, allow_actions: list[NCActionType] | None = None, + allow_reentrancy: bool = False, ) -> Callable: """Decorator to mark a blueprint method as fallback. The method must also be called `fallback`.""" def decorator(fn: Callable) -> None: @@ -279,6 +285,7 @@ def decorator(fn: Callable) -> None: allow_grant_authority=allow_grant_authority, allow_acquire_authority=allow_acquire_authority, allow_actions=allow_actions, + allow_reentrancy=allow_reentrancy, ) diff --git a/tests/nanocontracts/test_authorities_call_another.py b/tests/nanocontracts/test_authorities_call_another.py index 27525993a..7b4053f1a 100644 --- a/tests/nanocontracts/test_authorities_call_another.py +++ b/tests/nanocontracts/test_authorities_call_another.py @@ -51,7 +51,7 @@ class CallerBlueprint(Blueprint): def initialize(self, ctx: Context, other_id: ContractId) -> None: self.other_id = other_id - @public(allow_grant_authority=True) + @public(allow_grant_authority=True, allow_reentrancy=True) def nop(self, ctx: Context) -> None: pass @@ -60,7 +60,7 @@ def grant_to_other(self, ctx: Context, token_uid: TokenUid, mint: bool, melt: bo action = NCGrantAuthorityAction(token_uid=token_uid, mint=mint, melt=melt) self.syscall.call_public_method(self.other_id, 'nop', [action]) - @public(allow_grant_authority=True) + @public(allow_grant_authority=True, allow_reentrancy=True) def revoke_from_self(self, ctx: Context, token_uid: TokenUid, mint: bool, melt: bool) -> None: self.syscall.revoke_authorities(token_uid, revoke_mint=mint, revoke_melt=melt) diff --git a/tests/nanocontracts/test_call_other_contract.py b/tests/nanocontracts/test_call_other_contract.py index c01cb1b15..7aa3569f6 100644 --- a/tests/nanocontracts/test_call_other_contract.py +++ b/tests/nanocontracts/test_call_other_contract.py @@ -80,7 +80,7 @@ def get_tokens_from_another_contract(self, ctx: Context) -> None: if actions: self.syscall.call_public_method(self.contract, 'get_tokens_from_another_contract', actions) - @public + @public(allow_reentrancy=True) def dec(self, ctx: Context, fail_on_zero: bool) -> None: if self.counter == 0: if fail_on_zero: diff --git a/tests/nanocontracts/test_contract_upgrade.py b/tests/nanocontracts/test_contract_upgrade.py index c06b43f88..7747a5dfd 100644 --- a/tests/nanocontracts/test_contract_upgrade.py +++ b/tests/nanocontracts/test_contract_upgrade.py @@ -88,7 +88,7 @@ def initialize(self, ctx: Context) -> None: def inc(self, ctx: Context) -> None: self.counter += 3 - @public + @public(allow_reentrancy=True) def on_upgrade_inc(self, ctx: Context) -> None: self.counter += 100 diff --git a/tests/nanocontracts/test_execution_order.py b/tests/nanocontracts/test_execution_order.py index 0252d0429..c56a9c9fc 100644 --- a/tests/nanocontracts/test_execution_order.py +++ b/tests/nanocontracts/test_execution_order.py @@ -96,7 +96,7 @@ def accept_deposit_from_another(self, ctx: Context, contract_id: ContractId) -> self.syscall.call_public_method(contract_id, 'accept_deposit_from_another_callback', [action]) self.assert_token_balance(before=0, current=4) - @public(allow_deposit=True) + @public(allow_deposit=True, allow_reentrancy=True) def accept_deposit_from_another_callback(self, ctx: Context) -> None: self.assert_token_balance(before=3, current=6) @@ -116,7 +116,7 @@ def accept_withdrawal_from_another(self, ctx: Context, contract_id: ContractId) self.syscall.call_public_method(contract_id, 'accept_withdrawal_from_another_callback', [action]) self.assert_token_balance(before=4, current=3) - @public(allow_withdrawal=True) + @public(allow_withdrawal=True, allow_reentrancy=True) def accept_withdrawal_from_another_callback(self, ctx: Context) -> None: self.assert_token_balance(before=7, current=6) diff --git a/tests/nanocontracts/test_reentrancy.py b/tests/nanocontracts/test_reentrancy.py index 434f23b59..90194813c 100644 --- a/tests/nanocontracts/test_reentrancy.py +++ b/tests/nanocontracts/test_reentrancy.py @@ -1,5 +1,6 @@ from hathor.nanocontracts import Blueprint, Context, NCFail, public -from hathor.nanocontracts.types import Amount, ContractId, NCAction, NCDepositAction, TokenUid +from hathor.nanocontracts.exception import NCForbiddenReentrancy +from hathor.nanocontracts.types import Amount, CallerId, ContractId, NCAction, NCDepositAction, TokenUid from tests.nanocontracts.blueprints.unittest import BlueprintTestCase HTR_TOKEN_UID = TokenUid(b'\0') @@ -10,10 +11,8 @@ class InsufficientBalance(NCFail): class MyBlueprint(Blueprint): - # I used dict[bytes, int] for two reasons: - # 1. `bytes` works for both Address and ContractId - # 2. int allows negative values - balances: dict[bytes, int] + # I used dict[CallerId, int] because int allows negative values. + balances: dict[CallerId, int] @public def initialize(self, ctx: Context) -> None: @@ -31,7 +30,7 @@ def deposit(self, ctx: Context) -> None: else: self.balances[address] += amount - @public + @public(allow_reentrancy=True) def transfer_to(self, ctx: Context, amount: Amount, contract: ContractId, method: str) -> None: address = ctx.caller_id if amount > self.balances.get(address, 0): @@ -43,7 +42,7 @@ def transfer_to(self, ctx: Context, amount: Amount, contract: ContractId, method self.syscall.call_public_method(contract, method, actions=actions) self.balances[address] -= amount - @public + @public(allow_reentrancy=True) def fixed_transfer_to(self, ctx: Context, amount: Amount, contract: ContractId, method: str) -> None: address = ctx.caller_id if amount > self.balances.get(address, 0): @@ -55,6 +54,16 @@ def fixed_transfer_to(self, ctx: Context, amount: Amount, contract: ContractId, self.balances[address] -= amount self.syscall.call_public_method(contract, method, actions=actions) + @public + def protected_transfer_to(self, ctx: Context, amount: Amount, contract: ContractId, method: str) -> None: + address = ctx.caller_id + if amount > self.balances.get(address, 0): + raise InsufficientBalance('insufficient balance') + + actions: list[NCAction] = [NCDepositAction(token_uid=HTR_TOKEN_UID, amount=amount)] + self.syscall.call_public_method(contract, method, actions=actions) + self.balances[address] -= amount + class AttackerBlueprint(Blueprint): target: ContractId @@ -79,15 +88,19 @@ def initialize(self, ctx: Context, target: ContractId, n_calls: int) -> None: def nop(self, ctx: Context) -> None: pass - @public(allow_deposit=True) + @public(allow_deposit=True, allow_reentrancy=True) def attack(self, ctx: Context) -> None: - self._run_attack('transfer_to') + self._run_attack('transfer_to', 'attack') - @public(allow_deposit=True) - def attack_fail(self, ctx: Context) -> None: - self._run_attack('fixed_transfer_to') + @public(allow_deposit=True, allow_reentrancy=True) + def attack_fixed(self, ctx: Context) -> None: + self._run_attack('fixed_transfer_to', 'attack_fixed') + + @public(allow_deposit=True, allow_reentrancy=True) + def attack_protected(self, ctx: Context) -> None: + self._run_attack('protected_transfer_to', 'attack_protected') - def _run_attack(self, method: str) -> None: + def _run_attack(self, method: str, callback: str) -> None: if self.counter >= self.n_calls: return @@ -98,7 +111,7 @@ def _run_attack(self, method: str) -> None: actions=[], amount=self.amount, contract=self.syscall.get_contract_id(), - method='attack', + method=callback, ) @@ -197,7 +210,7 @@ def test_attack_succeed(self) -> None: assert self.target_storage.get_balance(HTR_TOKEN_UID).value == 10_150 - self.n_calls * 50 assert self.attacker_storage.get_balance(HTR_TOKEN_UID).value == self.n_calls * 50 - def test_attack_fail(self) -> None: + def test_attack_fail_fixed(self) -> None: tx = self.get_genesis_tx() # Attacker contract has a balance of 0.50 HTR in the target contract. @@ -206,7 +219,23 @@ def test_attack_fail(self) -> None: ctx = Context([], tx, self.address1, timestamp=0) self.runner.call_public_method( self.nc_attacker_id, - 'attack_fail', + 'attack_fixed', + ctx, + ) + + assert self.target_storage.get_balance(HTR_TOKEN_UID).value == 10_150 + assert self.attacker_storage.get_balance(HTR_TOKEN_UID).value == 0 + + def test_attack_fail_protected(self) -> None: + tx = self.get_genesis_tx() + + # Attacker contract has a balance of 0.50 HTR in the target contract. + # It tries to extract more than 0.50 HTR and fails. + with self.assertRaises(NCForbiddenReentrancy): + ctx = Context([], tx, self.address1, timestamp=0) + self.runner.call_public_method( + self.nc_attacker_id, + 'attack_protected', ctx, ) From a7ca60b4e919ea731bcee93672ad0f0e75b4ccab Mon Sep 17 00:00:00 2001 From: Gabriel Levcovitz Date: Tue, 19 Aug 2025 18:01:09 -0300 Subject: [PATCH 3/6] fix(nano): fix NC context on tx API (#1368) --- hathor/nanocontracts/context.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/hathor/nanocontracts/context.py b/hathor/nanocontracts/context.py index 6b94b3ba6..e6a81734e 100644 --- a/hathor/nanocontracts/context.py +++ b/hathor/nanocontracts/context.py @@ -143,8 +143,19 @@ def copy(self) -> Context: def to_json(self) -> dict[str, Any]: """Return a JSON representation of the context.""" + caller_id: str + match self.caller_id: + case Address(): + caller_id = get_address_b58_from_bytes(self.caller_id) + case ContractId(): + caller_id = self.caller_id.hex() + case _: + assert_never(self.caller_id) + return { 'actions': [action.to_json() for action in self.__all_actions__], - 'caller_id': get_address_b58_from_bytes(self.caller_id), + 'caller_id': caller_id, 'timestamp': self.timestamp, + # XXX: Deprecated attribute + 'address': caller_id, } From 9588a186b6e260cdbcbd76a34e1676968433426a Mon Sep 17 00:00:00 2001 From: Jan Segre Date: Mon, 18 Aug 2025 17:35:36 +0200 Subject: [PATCH 4/6] chore: bump version to v0.65.0 --- hathor/cli/openapi_files/openapi_base.json | 2 +- hathor/version.py | 4 +-- pyproject.toml | 2 +- tests/feature_activation/test_criteria.py | 38 ++++++++++++++++++---- 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/hathor/cli/openapi_files/openapi_base.json b/hathor/cli/openapi_files/openapi_base.json index 46a0daac3..167296b37 100644 --- a/hathor/cli/openapi_files/openapi_base.json +++ b/hathor/cli/openapi_files/openapi_base.json @@ -7,7 +7,7 @@ ], "info": { "title": "Hathor API", - "version": "0.64.0" + "version": "0.65.0" }, "consumes": [ "application/json" diff --git a/hathor/version.py b/hathor/version.py index cea619850..d8fa558ca 100644 --- a/hathor/version.py +++ b/hathor/version.py @@ -19,13 +19,13 @@ from structlog import get_logger -BASE_VERSION = '0.64.0' +BASE_VERSION = '0.65.0' DEFAULT_VERSION_SUFFIX = "local" BUILD_VERSION_FILE_PATH = "./BUILD_VERSION" # Valid formats: 1.2.3, 1.2.3-rc.1 and nightly-ab49c20f -BUILD_VERSION_REGEX = r"^(\d+\.\d+\.\d+(-rc\.\d+)?|nightly-[a-f0-9]{7,8})$" +BUILD_VERSION_REGEX = r"^(\d+\.\d+\.\d+(-(rc|alpha|beta)\.\d+)?|nightly-[a-f0-9]{7,8})$" logger = get_logger() diff --git a/pyproject.toml b/pyproject.toml index a4c1cf41b..6e7c24e2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ [tool.poetry] name = "hathor" -version = "0.64.0" +version = "0.65.0" description = "Hathor Network full-node" authors = ["Hathor Team "] license = "Apache-2.0" diff --git a/tests/feature_activation/test_criteria.py b/tests/feature_activation/test_criteria.py index b8ef70d2e..f84e4ca4d 100644 --- a/tests/feature_activation/test_criteria.py +++ b/tests/feature_activation/test_criteria.py @@ -143,18 +143,44 @@ def test_minimum_activation_height(minimum_activation_height: int, error: str) - assert errors[0]['msg'] == error +_invalid_version_msg = r'string does not match regex "^(\d+\.\d+\.\d+(-(rc|alpha|beta)\.\d+)?|nightly-[a-f0-9]{7,8})$"' + + @pytest.mark.parametrize( - ['version', 'error'], + ['version'], [ - ('0', 'string does not match regex "^(\\d+\\.\\d+\\.\\d+(-rc\\.\\d+)?|nightly-[a-f0-9]{7,8})$"'), - ('alpha', 'string does not match regex "^(\\d+\\.\\d+\\.\\d+(-rc\\.\\d+)?|nightly-[a-f0-9]{7,8})$"'), - ('0.0', 'string does not match regex "^(\\d+\\.\\d+\\.\\d+(-rc\\.\\d+)?|nightly-[a-f0-9]{7,8})$"') + ('0',), + ('alpha',), + ('0.0',), + ('0.0.0-',), + ('0.1.0-alpha',), + ('0.1.0-alpha.x',), + ('0.1.0-gamma.1',), + ('0.1.0-RC.1',), ] ) -def test_version(version: str, error: str) -> None: +def test_invalid_version(version: str) -> None: criteria = VALID_CRITERIA | dict(version=version) with pytest.raises(ValidationError) as e: Criteria(**criteria).to_validated(evaluation_interval=1000, max_signal_bits=2) # type: ignore[arg-type] errors = e.value.errors() - assert errors[0]['msg'] == error + assert errors[0]['msg'] == _invalid_version_msg + + +@pytest.mark.parametrize( + ['version'], + [ + ('1.0.0',), + ('1.2.3',), + ('1.22222.30000',), + ('1.2.3-alpha.1',), + ('1.2.3-alpha.2',), + ('1.2.3-rc.2',), + ('1.2.3-beta.2',), + ('1.2.3-alpha.299',), + ] +) +def test_valid_version(version: str) -> None: + criteria = VALID_CRITERIA | dict(version=version) + Criteria(**criteria).to_validated(evaluation_interval=1000, max_signal_bits=2) # type: ignore[arg-type] From e0842ff596bf643f1f53595adf3739ea949be9b1 Mon Sep 17 00:00:00 2001 From: Jan Segre Date: Tue, 19 Aug 2025 19:49:04 +0200 Subject: [PATCH 5/6] fix(nano): state API regression after ctx.address refactor --- hathor/nanocontracts/resources/state.py | 5 ++-- tests/resources/nanocontracts/test_state.py | 30 ++++++++++++++++++++- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/hathor/nanocontracts/resources/state.py b/hathor/nanocontracts/resources/state.py index d44362368..d782c62f7 100644 --- a/hathor/nanocontracts/resources/state.py +++ b/hathor/nanocontracts/resources/state.py @@ -204,9 +204,8 @@ def render_GET(self, request: 'Request') -> bytes: fields[field] = NCValueErrorResponse(errmsg='field not found') continue - if type(value) is bytes: - value = value.hex() - fields[field] = NCValueSuccessResponse(value=value) + json_value = field_nc_type.value_to_json(value) + fields[field] = NCValueSuccessResponse(value=json_value) # Call view methods. runner.disable_call_trace() # call trace is not required for calling view methods. diff --git a/tests/resources/nanocontracts/test_state.py b/tests/resources/nanocontracts/test_state.py index de5f86312..f7fabee38 100644 --- a/tests/resources/nanocontracts/test_state.py +++ b/tests/resources/nanocontracts/test_state.py @@ -12,7 +12,16 @@ from hathor.nanocontracts.catalog import NCBlueprintCatalog from hathor.nanocontracts.method import Method from hathor.nanocontracts.resources import NanoContractStateResource -from hathor.nanocontracts.types import Address, NCActionType, NCDepositAction, Timestamp, TokenUid +from hathor.nanocontracts.types import ( + Address, + CallerId, + ContractId, + NCActionType, + NCDepositAction, + Timestamp, + TokenUid, + VertexId, +) from hathor.nanocontracts.utils import sign_openssl from hathor.simulator.utils import add_new_block from hathor.transaction import Transaction, TxInput @@ -40,12 +49,19 @@ class MyBlueprint(Blueprint): address_details: dict[Address, dict[str, Amount]] bytes_field: bytes dict_with_bytes: dict[bytes, str] + last_caller_id: CallerId + last_bet_address: Address + last_vertex_id: VertexId + self_contract_id: ContractId @public def initialize(self, ctx: Context, token_uid: TokenUid, date_last_bet: Timestamp) -> None: self.token_uid = token_uid self.date_last_bet = date_last_bet self.total = 0 + self.last_caller_id = ctx.caller_id + self.self_contract_id = ContractId(ctx.vertex.hash) + self.last_vertex_id = VertexId(ctx.vertex.hash) @public(allow_deposit=True) def bet(self, ctx: Context, address: Address, score: str) -> None: @@ -58,6 +74,9 @@ def bet(self, ctx: Context, address: Address, score: str) -> None: else: partial[score] += action.amount self.address_details[address] = partial + self.last_bet_address = address + self.last_caller_id = ctx.caller_id + self.last_vertex_id = VertexId(ctx.vertex.hash) encoded_score = score.encode() self.bytes_field = encoded_score @@ -302,6 +321,10 @@ def test_success(self): (b'fields[]', b'token_uid'), (b'fields[]', b'total'), (b'fields[]', b'date_last_bet'), + (b'fields[]', b'last_caller_id'), + (b'fields[]', b'last_bet_address'), + (b'fields[]', b'last_vertex_id'), + (b'fields[]', b'self_contract_id'), (b'fields[]', address_param.encode()), (b'fields[]', b'bytes_field'), (b'fields[]', dict_with_bytes_param.encode()), @@ -312,9 +335,14 @@ def test_success(self): fields2 = data2['fields'] self.assertEqual(data2['blueprint_id'], self.bet_id.hex()) self.assertEqual(data2['blueprint_name'], 'MyBlueprint') + self.assertEqual(len(data2['fields']), 10) self.assertEqual(fields2['token_uid'], {'value': settings.HATHOR_TOKEN_UID.hex()}) self.assertEqual(fields2['total'], {'value': 10**11}) self.assertEqual(fields2['date_last_bet'], {'value': date_last_bet}) + self.assertEqual(fields2['last_caller_id'], {'value': address_b58}) + self.assertEqual(fields2['last_bet_address'], {'value': address_b58}) + self.assertEqual(fields2['last_vertex_id'], {'value': nc_bet.hash.hex()}) + self.assertEqual(fields2['self_contract_id'], {'value': nc.hash.hex()}) self.assertEqual(len(fields2[address_param]), 1) # TODO: RE-IMPLEMENT SUPPORT FOR THIS # FIXME From 5ed03c1c677d477ea9d910cf1687ee9b158e5a64 Mon Sep 17 00:00:00 2001 From: Jan Segre Date: Wed, 20 Aug 2025 18:13:52 +0200 Subject: [PATCH 6/6] chore: increase activation window for new feature --- hathor/conf/mainnet.py | 2 +- hathor/conf/mainnet.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hathor/conf/mainnet.py b/hathor/conf/mainnet.py index 937328a75..66272bab4 100644 --- a/hathor/conf/mainnet.py +++ b/hathor/conf/mainnet.py @@ -233,7 +233,7 @@ # Expected to be reached around Tuesday, 2025-08-12 17:39:56 GMT # Right now the best block is 5_748_286 at Wednesday, 2025-08-06 16:02:56 GMT start_height=5_765_760, - timeout_height=5_886_720, # N + 6 * 20160 (6 weeks after the start) + timeout_height=5_967_360, # N + 10 * 20160 (10 weeks after the start) minimum_activation_height=0, lock_in_on_timeout=False, version='0.64.0', diff --git a/hathor/conf/mainnet.yml b/hathor/conf/mainnet.yml index 1ddef948f..099f51adc 100644 --- a/hathor/conf/mainnet.yml +++ b/hathor/conf/mainnet.yml @@ -214,7 +214,7 @@ FEATURE_ACTIVATION: # Expected to be reached around Tuesday, 2025-08-12 17:39:56 GMT # Right now the best block is 5_748_286 at Wednesday, 2025-08-06 16:02:56 GMT start_height: 5_765_760 - timeout_height: 5_886_720 # N + 6 * 20160 (6 weeks after the start) + timeout_height: 5_967_360 # N + 10 * 20160 (10 weeks after the start) minimum_activation_height: 0 lock_in_on_timeout: false version: 0.64.0