diff --git a/hathor/conf/mainnet.py b/hathor/conf/mainnet.py index 301a2659e..a3f7d8c22 100644 --- a/hathor/conf/mainnet.py +++ b/hathor/conf/mainnet.py @@ -13,7 +13,7 @@ # limitations under the License. from hathor.checkpoint import Checkpoint as cp -from hathor.conf.settings import HathorSettings, NanoContractsSetting +from hathor.conf.settings import FeatureSetting, HathorSettings from hathor.feature_activation.feature import Feature from hathor.feature_activation.model.criteria import Criteria from hathor.feature_activation.settings import Settings as FeatureActivationSettings @@ -213,7 +213,8 @@ '00004305882eb3eef6b45f025ff58eb7baa5ca35f7d6f42c8b085482b00474e6', '000045ecbab77c9a8d819ff6d26893b9da2774eee5539f17d8fc2394f82b758e', ])), - ENABLE_NANO_CONTRACTS=NanoContractsSetting.FEATURE_ACTIVATION, + ENABLE_NANO_CONTRACTS=FeatureSetting.FEATURE_ACTIVATION, + ENABLE_FEE_BASED_TOKENS=FeatureSetting.DISABLED, NC_ON_CHAIN_BLUEPRINT_ALLOWED_ADDRESSES=[ 'HDkKGHwDHTuUGbhET73XdTJZkS8uU7PHf9', 'HUbxYhtqW8pdRCC2WngPxN7MB4SUMDPrrh', diff --git a/hathor/conf/settings.py b/hathor/conf/settings.py index fadcb3f52..e79d0e154 100644 --- a/hathor/conf/settings.py +++ b/hathor/conf/settings.py @@ -35,8 +35,8 @@ @unique -class NanoContractsSetting(StrEnum): - """Enum to configure the state of the Nano Contracts feature.""" +class FeatureSetting(StrEnum): + """Enum to configure the state of a feature.""" # Completely disabled. DISABLED = auto() @@ -49,14 +49,14 @@ class NanoContractsSetting(StrEnum): def __bool__(self) -> bool: """ - >>> bool(NanoContractsSetting.DISABLED) + >>> bool(FeatureSetting.DISABLED) False - >>> bool(NanoContractsSetting.ENABLED) + >>> bool(FeatureSetting.ENABLED) True - >>> bool(NanoContractsSetting.FEATURE_ACTIVATION) + >>> bool(FeatureSetting.FEATURE_ACTIVATION) True """ - return self in (NanoContractsSetting.ENABLED, NanoContractsSetting.FEATURE_ACTIVATION) + return self in (FeatureSetting.ENABLED, FeatureSetting.FEATURE_ACTIVATION) class HathorSettings(NamedTuple): @@ -480,7 +480,10 @@ def GENESIS_TX2_TIMESTAMP(self) -> int: MAX_UNVERIFIED_PEERS_PER_CONN: int = 100 # Used to enable nano contracts. - ENABLE_NANO_CONTRACTS: NanoContractsSetting = NanoContractsSetting.DISABLED + ENABLE_NANO_CONTRACTS: FeatureSetting = FeatureSetting.DISABLED + + # Used to enable fee-based tokens. + ENABLE_FEE_BASED_TOKENS: FeatureSetting = FeatureSetting.DISABLED # List of enabled blueprints. BLUEPRINTS: dict[bytes, str] = {} diff --git a/hathor/conf/unittests.yml b/hathor/conf/unittests.yml index ebc6a3e1b..ce6d870b5 100644 --- a/hathor/conf/unittests.yml +++ b/hathor/conf/unittests.yml @@ -24,6 +24,7 @@ FEATURE_ACTIVATION: default_threshold: 3 ENABLE_NANO_CONTRACTS: enabled +ENABLE_FEE_BASED_TOKENS: enabled NC_ON_CHAIN_BLUEPRINT_ALLOWED_ADDRESSES: # keypair wallet: diff --git a/hathor/consensus/block_consensus.py b/hathor/consensus/block_consensus.py index 53b3e4feb..568406103 100644 --- a/hathor/consensus/block_consensus.py +++ b/hathor/consensus/block_consensus.py @@ -23,7 +23,6 @@ from typing_extensions import assert_never from hathor.consensus.context import ReorgInfo -from hathor.feature_activation.feature import Feature from hathor.transaction import BaseTransaction, Block, Transaction from hathor.transaction.exceptions import TokenNotFound from hathor.transaction.nc_execution_state import NCExecutionState @@ -145,26 +144,11 @@ def _should_execute_nano(self, block: Block) -> bool: """ Determine whether we should proceed to execute Nano transactions while making the necessary initializations. """ - from hathor.conf.settings import NanoContractsSetting assert not block.is_genesis - match self._settings.ENABLE_NANO_CONTRACTS: - case NanoContractsSetting.ENABLED: - return True - - case NanoContractsSetting.FEATURE_ACTIVATION: - parent = block.get_block_parent() - is_active_on_parent = self.feature_service.is_feature_active( - vertex=parent, - feature=Feature.NANO_CONTRACTS, - ) - return is_active_on_parent - - case NanoContractsSetting.DISABLED: - return False - - case _: # pragma: no cover - assert_never(self._settings.ENABLE_NANO_CONTRACTS) + from hathor.feature_activation.utils import is_nano_active + parent = block.get_block_parent() + return is_nano_active(settings=self._settings, feature_service=self.feature_service, block=parent) def _nc_execute_calls(self, block: Block, *, is_reorg: bool) -> None: """Internal method to execute the method calls for transactions confirmed by this block. @@ -239,7 +223,10 @@ def _nc_execute_calls(self, block: Block, *, is_reorg: bool) -> None: block_storage.set_address_seqnum(Address(nc_header.nc_address), nc_header.nc_seqnum) continue - runner = self._runner_factory.create(block_storage=block_storage, seed=seed_hasher.digest()) + runner = self._runner_factory.create( + block_storage=block_storage, + seed=seed_hasher.digest(), + ) exception_and_tb: tuple[NCFail, str] | None = None token_dict = tx.get_complete_token_info(block_storage) should_verify_sum_after_execution = any(token_info.version is None for token_info in token_dict.values()) diff --git a/hathor/consensus/consensus.py b/hathor/consensus/consensus.py index 14569a885..e0cad9425 100644 --- a/hathor/consensus/consensus.py +++ b/hathor/consensus/consensus.py @@ -22,6 +22,7 @@ from hathor.consensus.block_consensus import BlockConsensusAlgorithmFactory from hathor.consensus.context import ConsensusAlgorithmContext from hathor.consensus.transaction_consensus import TransactionConsensusAlgorithmFactory +from hathor.feature_activation.utils import is_fee_active from hathor.profiler import get_cpu_profiler from hathor.pubsub import HathorEvents, PubSubManager from hathor.transaction import BaseTransaction, Transaction @@ -360,6 +361,7 @@ def _compute_vertices_that_became_invalid( lambda tx: self._reward_lock_mempool_rule(tx, new_best_height), lambda tx: self._unknown_contract_mempool_rule(tx), lambda tx: self._nano_activation_rule(storage, tx), + lambda tx: self._fee_tokens_activation_rule(storage, tx), self._checkdatasig_count_rule, ) @@ -428,24 +430,36 @@ def _unknown_contract_mempool_rule(self, tx: Transaction) -> bool: def _nano_activation_rule(self, storage: TransactionStorage, tx: Transaction) -> bool: """Check whether a tx became invalid because the reorg changed the nano feature activation state.""" + from hathor.feature_activation.utils import is_nano_active from hathor.nanocontracts import OnChainBlueprint - from hathor.nanocontracts.utils import is_nano_active - from hathor.transaction.token_creation_tx import TokenCreationTransaction - from hathor.transaction.token_info import TokenVersion best_block = storage.get_best_block() if is_nano_active(settings=self._settings, block=best_block, feature_service=self.feature_service): # When nano is active, this rule has no effect. return True - # The nano feature activation is actually used to enable 4 use cases: - + # The nano feature activation is actually used to enable 2 use cases: if tx.is_nano_contract(): return False if isinstance(tx, OnChainBlueprint): return False + return True + + def _fee_tokens_activation_rule(self, storage: TransactionStorage, tx: Transaction) -> bool: + """ + Check whether a tx became invalid because the reorg changed the fee-based tokens feature activation state. + """ + from hathor.transaction.token_creation_tx import TokenCreationTransaction + from hathor.transaction.token_info import TokenVersion + + best_block = storage.get_best_block() + if is_fee_active(settings=self._settings, block=best_block, feature_service=self.feature_service): + # When fee-based tokens feature is active, this rule has no effect. + return True + + # The fee-based tokens feature activation is actually used to enable 2 use cases: if isinstance(tx, TokenCreationTransaction) and tx.token_version == TokenVersion.FEE: return False diff --git a/hathor/feature_activation/feature.py b/hathor/feature_activation/feature.py index 4e5671093..ad7d62e45 100644 --- a/hathor/feature_activation/feature.py +++ b/hathor/feature_activation/feature.py @@ -32,3 +32,5 @@ class Feature(str, Enum): COUNT_CHECKDATASIG_OP = 'COUNT_CHECKDATASIG_OP' NANO_CONTRACTS = 'NANO_CONTRACTS' + + FEE_TOKENS = 'FEE_TOKENS' diff --git a/hathor/feature_activation/utils.py b/hathor/feature_activation/utils.py new file mode 100644 index 000000000..37578381d --- /dev/null +++ b/hathor/feature_activation/utils.py @@ -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 __future__ import annotations + +from typing import TYPE_CHECKING, assert_never + +if TYPE_CHECKING: + from hathor.conf.settings import FeatureSetting, HathorSettings + from hathor.feature_activation.feature_service import FeatureService + from hathor.transaction import Block + +from hathor.feature_activation.feature import Feature + + +def _is_feature_active( + *, + setting: FeatureSetting, + feature: Feature, + block: Block, + feature_service: FeatureService, +) -> bool: + """Return whether a feature is active based on the setting and block.""" + # Local import to avoid circular import with hathor.conf.settings + from hathor.conf.settings import FeatureSetting + + match setting: + case FeatureSetting.DISABLED: + return False + case FeatureSetting.ENABLED: + return True + case FeatureSetting.FEATURE_ACTIVATION: + return feature_service.is_feature_active(vertex=block, feature=feature) + case _: # pragma: no cover + assert_never(setting) + + +def is_nano_active(*, settings: HathorSettings, block: Block, feature_service: FeatureService) -> bool: + """Return whether the Nano Contracts feature is active according to the provided settings and block.""" + return _is_feature_active( + setting=settings.ENABLE_NANO_CONTRACTS, + feature=Feature.NANO_CONTRACTS, + block=block, + feature_service=feature_service, + ) + + +def is_fee_active(*, settings: HathorSettings, block: Block, feature_service: FeatureService) -> bool: + """Return whether the Fee feature is active according to the provided settings and block.""" + return _is_feature_active( + setting=settings.ENABLE_FEE_BASED_TOKENS, + feature=Feature.FEE_TOKENS, + block=block, + feature_service=feature_service, + ) diff --git a/hathor/manager.py b/hathor/manager.py index e48249c2d..fd3dbd971 100644 --- a/hathor/manager.py +++ b/hathor/manager.py @@ -401,7 +401,9 @@ def get_nc_runner(self, block: Block) -> Runner: """Return a contract runner for a given block.""" nc_storage_factory = self.consensus_algorithm.nc_storage_factory block_storage = nc_storage_factory.get_block_storage_from_block(block) - return self.runner_factory.create(block_storage=block_storage) + return self.runner_factory.create( + block_storage=block_storage, + ) def get_best_block_nc_runner(self) -> Runner: """Return a contract runner for the best block.""" diff --git a/hathor/nanocontracts/runner/runner.py b/hathor/nanocontracts/runner/runner.py index 662281fee..591a3c438 100644 --- a/hathor/nanocontracts/runner/runner.py +++ b/hathor/nanocontracts/runner/runner.py @@ -1451,7 +1451,12 @@ def __init__( self.tx_storage = tx_storage self.nc_storage_factory = nc_storage_factory - def create(self, *, block_storage: NCBlockStorage, seed: bytes | None = None) -> Runner: + def create( + self, + *, + block_storage: NCBlockStorage, + seed: bytes | None = None, + ) -> Runner: return Runner( reactor=self.reactor, settings=self.settings, diff --git a/hathor/nanocontracts/utils.py b/hathor/nanocontracts/utils.py index 9524eb6f7..cfcae561e 100644 --- a/hathor/nanocontracts/utils.py +++ b/hathor/nanocontracts/utils.py @@ -16,14 +16,13 @@ import hashlib from types import ModuleType -from typing import Callable, assert_never +from typing import Callable from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec from pycoin.key.Key import Key as PycoinKey -from hathor.conf.settings import HathorSettings, NanoContractsSetting from hathor.crypto.util import ( decode_address, get_address_from_public_key_bytes, @@ -31,10 +30,7 @@ get_public_key_from_bytes_compressed, is_pubkey_compressed, ) -from hathor.feature_activation.feature import Feature -from hathor.feature_activation.feature_service import FeatureService from hathor.nanocontracts.types import NC_METHOD_TYPE_ATTR, BlueprintId, ContractId, NCMethodType, TokenUid, VertexId -from hathor.transaction import Block from hathor.transaction.headers import NanoHeader from hathor.util import not_none @@ -152,19 +148,6 @@ def sign_openssl_multisig( nano_header.nc_script = MultiSig.create_input_data(redeem_script, signatures) -def is_nano_active(*, settings: HathorSettings, block: Block, feature_service: FeatureService) -> bool: - """Return whether the Nano Contracts feature is active according to the provided settings and block.""" - match settings.ENABLE_NANO_CONTRACTS: - case NanoContractsSetting.DISABLED: - return False - case NanoContractsSetting.ENABLED: - return True - case NanoContractsSetting.FEATURE_ACTIVATION: - return feature_service.is_feature_active(vertex=block, feature=Feature.NANO_CONTRACTS) - case _: # pragma: no cover - assert_never(settings.ENABLE_NANO_CONTRACTS) - - def sha3(data: bytes) -> bytes: """Calculate the SHA3-256 of some data.""" return hashlib.sha3_256(data).digest() diff --git a/hathor/transaction/vertex_parser.py b/hathor/transaction/vertex_parser.py index d09e3887f..85850a18a 100644 --- a/hathor/transaction/vertex_parser.py +++ b/hathor/transaction/vertex_parser.py @@ -17,7 +17,7 @@ from struct import error as StructError from typing import TYPE_CHECKING, Type -from hathor.transaction.headers import NanoHeader, VertexBaseHeader, VertexHeaderId +from hathor.transaction.headers import FeeHeader, NanoHeader, VertexBaseHeader, VertexHeaderId if TYPE_CHECKING: from hathor.conf.settings import HathorSettings @@ -37,6 +37,8 @@ def get_supported_headers(settings: HathorSettings) -> dict[VertexHeaderId, Type supported_headers: dict[VertexHeaderId, Type[VertexBaseHeader]] = {} if settings.ENABLE_NANO_CONTRACTS: supported_headers[VertexHeaderId.NANO_HEADER] = NanoHeader + if settings.ENABLE_FEE_BASED_TOKENS: + supported_headers[VertexHeaderId.FEE_HEADER] = FeeHeader return supported_headers @staticmethod diff --git a/hathor/verification/token_creation_transaction_verifier.py b/hathor/verification/token_creation_transaction_verifier.py index c8b6c051d..01b4dba5f 100644 --- a/hathor/verification/token_creation_transaction_verifier.py +++ b/hathor/verification/token_creation_transaction_verifier.py @@ -48,7 +48,7 @@ def verify_token_info(self, tx: TokenCreationTransaction, params: VerificationPa # Can't create the token with NATIVE or a non-activated version version_validations = [ tx.token_version == TokenVersion.NATIVE, - tx.token_version == TokenVersion.FEE and not params.enable_nano, + tx.token_version == TokenVersion.FEE and not params.enable_fee, ] if any(version_validations): diff --git a/hathor/verification/verification_params.py b/hathor/verification/verification_params.py index 7c24a06f2..d71f73ca0 100644 --- a/hathor/verification/verification_params.py +++ b/hathor/verification/verification_params.py @@ -28,6 +28,7 @@ class VerificationParams: reject_locked_reward: bool = True skip_block_weight_verification: bool = False enable_nano: bool = False + enable_fee: bool = False reject_too_old_vertices: bool = False harden_token_restrictions: bool = False @@ -40,6 +41,7 @@ def default_for_mempool( *, best_block: Block, enable_nano: bool = False, + enable_fee: bool = False, ) -> VerificationParams: """This is the appropriate parameters for verifying mempool transactions, realtime blocks and API pushes. @@ -52,6 +54,7 @@ def default_for_mempool( nc_block_root_id=best_block_meta.nc_block_root_id, enable_checkdatasig_count=True, enable_nano=enable_nano, + enable_fee=enable_fee, reject_too_old_vertices=True, harden_token_restrictions=True, harden_nano_restrictions=True, diff --git a/hathor/verification/vertex_verifier.py b/hathor/verification/vertex_verifier.py index 04d7d1a86..537cbca12 100644 --- a/hathor/verification/vertex_verifier.py +++ b/hathor/verification/vertex_verifier.py @@ -228,6 +228,7 @@ def get_allowed_headers(self, vertex: BaseTransaction, params: VerificationParam case TxVersion.REGULAR_TRANSACTION | TxVersion.TOKEN_CREATION_TRANSACTION: if params.enable_nano: allowed_headers.add(NanoHeader) + if params.enable_fee: allowed_headers.add(FeeHeader) case _: # pragma: no cover assert_never(vertex.version) diff --git a/hathor/version_resource.py b/hathor/version_resource.py index 8e28da181..2e2a30db8 100644 --- a/hathor/version_resource.py +++ b/hathor/version_resource.py @@ -17,8 +17,8 @@ from hathor.api_util import Resource, set_cors from hathor.conf.get_settings import get_global_settings from hathor.feature_activation.feature_service import FeatureService +from hathor.feature_activation.utils import is_nano_active from hathor.manager import HathorManager -from hathor.nanocontracts.utils import is_nano_active from hathor.util import json_dumpb diff --git a/hathor/vertex_handler/vertex_handler.py b/hathor/vertex_handler/vertex_handler.py index bd5e7cdc0..2b6839a03 100644 --- a/hathor/vertex_handler/vertex_handler.py +++ b/hathor/vertex_handler/vertex_handler.py @@ -26,7 +26,7 @@ from hathor.execution_manager import ExecutionManager from hathor.feature_activation.feature import Feature from hathor.feature_activation.feature_service import FeatureService -from hathor.nanocontracts.utils import is_nano_active +from hathor.feature_activation.utils import is_fee_active, is_nano_active from hathor.profiler import get_cpu_profiler from hathor.pubsub import HathorEvents, PubSubManager from hathor.reactor import ReactorProtocol @@ -99,6 +99,10 @@ def on_new_block(self, block: Block, *, deps: list[Transaction]) -> Generator[An settings=self._settings, block=parent_block, feature_service=self._feature_service ) + enable_fee = is_fee_active( + settings=self._settings, block=parent_block, feature_service=self._feature_service + ) + if parent_meta.nc_block_root_id is None: # This case only happens for the genesis and during sync of a voided chain. assert parent_block.is_genesis or parent_meta.voided_by @@ -106,6 +110,7 @@ def on_new_block(self, block: Block, *, deps: list[Transaction]) -> Generator[An params = VerificationParams( enable_checkdatasig_count=enable_checkdatasig_count, enable_nano=enable_nano, + enable_fee=enable_fee, nc_block_root_id=parent_meta.nc_block_root_id, ) @@ -126,8 +131,10 @@ def on_new_mempool_transaction(self, tx: Transaction) -> bool: """Called by mempool sync.""" best_block = self._tx_storage.get_best_block() enable_nano = is_nano_active(settings=self._settings, block=best_block, feature_service=self._feature_service) + enable_fee = is_fee_active(settings=self._settings, block=best_block, feature_service=self._feature_service) params = VerificationParams.default_for_mempool( enable_nano=enable_nano, + enable_fee=enable_fee, best_block=best_block, ) return self._old_on_new_vertex(tx, params) @@ -144,6 +151,7 @@ def on_new_relayed_vertex( best_block = self._tx_storage.get_best_block() best_block_meta = best_block.get_metadata() enable_nano = is_nano_active(settings=self._settings, block=best_block, feature_service=self._feature_service) + enable_fee = is_fee_active(settings=self._settings, block=best_block, feature_service=self._feature_service) if best_block_meta.nc_block_root_id is None: assert best_block.is_genesis # XXX: checkdatasig enabled for relayed vertices @@ -151,6 +159,7 @@ def on_new_relayed_vertex( enable_checkdatasig_count=True, reject_locked_reward=reject_locked_reward, enable_nano=enable_nano, + enable_fee=enable_fee, nc_block_root_id=best_block_meta.nc_block_root_id, ) return self._old_on_new_vertex(vertex, params, quiet=quiet) diff --git a/hathor_tests/nanocontracts/blueprints/unittest.py b/hathor_tests/nanocontracts/blueprints/unittest.py index 034c4d5b9..94605c6e6 100644 --- a/hathor_tests/nanocontracts/blueprints/unittest.py +++ b/hathor_tests/nanocontracts/blueprints/unittest.py @@ -123,7 +123,11 @@ def _register_blueprint_contents( def build_runner(self) -> TestRunner: """Create a Runner instance.""" - return TestRunner(tx_storage=self.manager.tx_storage, settings=self._settings, reactor=self.reactor) + return TestRunner( + tx_storage=self.manager.tx_storage, + settings=self._settings, + reactor=self.reactor, + ) def gen_random_token_uid(self) -> TokenUid: """Generate a random token UID (32 bytes).""" diff --git a/hathor_tests/nanocontracts/test_nano_feature_activation.py b/hathor_tests/nanocontracts/test_nano_feature_activation.py index e91e09547..8b4a86170 100644 --- a/hathor_tests/nanocontracts/test_nano_feature_activation.py +++ b/hathor_tests/nanocontracts/test_nano_feature_activation.py @@ -14,7 +14,7 @@ import pytest -from hathor.conf.settings import NanoContractsSetting +from hathor.conf.settings import FeatureSetting from hathor.daa import DifficultyAdjustmentAlgorithm, TestMode from hathor.exception import InvalidNewTransaction from hathor.feature_activation.feature import Feature @@ -55,12 +55,20 @@ def setUp(self) -> None: timeout_height=12, signal_support_by_default=True, version='0.0.0' + ), + Feature.FEE_TOKENS: Criteria( + bit=3, + start_height=4, + timeout_height=12, + signal_support_by_default=True, + version='0.0.0' ) } ) settings = self._settings._replace( - ENABLE_NANO_CONTRACTS=NanoContractsSetting.FEATURE_ACTIVATION, + ENABLE_NANO_CONTRACTS=FeatureSetting.FEATURE_ACTIVATION, + ENABLE_FEE_BASED_TOKENS=FeatureSetting.FEATURE_ACTIVATION, FEATURE_ACTIVATION=feature_settings, ) daa = DifficultyAdjustmentAlgorithm(settings=self._settings, test_mode=TestMode.TEST_ALL_WEIGHT) @@ -120,9 +128,11 @@ def test_activation(self) -> None: artifacts.propagate_with(self.manager, up_to='b3') assert self.feature_service.get_state(block=b3, feature=Feature.NANO_CONTRACTS) == FeatureState.DEFINED + assert self.feature_service.get_state(block=b3, feature=Feature.FEE_TOKENS) == FeatureState.DEFINED artifacts.propagate_with(self.manager, up_to='b4') assert self.feature_service.get_state(block=b4, feature=Feature.NANO_CONTRACTS) == FeatureState.STARTED + assert self.feature_service.get_state(block=b4, feature=Feature.FEE_TOKENS) == FeatureState.STARTED signaling_blocks = ('b5', 'b6', 'b7') for block_name in signaling_blocks: @@ -133,12 +143,15 @@ def test_activation(self) -> None: artifacts.propagate_with(self.manager, up_to=block_name) assert self.feature_service.get_state(block=b7, feature=Feature.NANO_CONTRACTS) == FeatureState.STARTED + assert self.feature_service.get_state(block=b7, feature=Feature.FEE_TOKENS) == FeatureState.STARTED artifacts.propagate_with(self.manager, up_to='b8') assert self.feature_service.get_state(block=b8, feature=Feature.NANO_CONTRACTS) == FeatureState.LOCKED_IN + assert self.feature_service.get_state(block=b8, feature=Feature.FEE_TOKENS) == FeatureState.LOCKED_IN artifacts.propagate_with(self.manager, up_to='b11') assert self.feature_service.get_state(block=b11, feature=Feature.NANO_CONTRACTS) == FeatureState.LOCKED_IN + assert self.feature_service.get_state(block=b11, feature=Feature.FEE_TOKENS) == FeatureState.LOCKED_IN assert b11.get_metadata().nc_block_root_id == self.empty_root_id @@ -163,6 +176,7 @@ def test_activation(self) -> None: artifacts.propagate_with(self.manager, up_to='b12') assert self.feature_service.get_state(block=b12, feature=Feature.NANO_CONTRACTS) == FeatureState.ACTIVE + assert self.feature_service.get_state(block=b12, feature=Feature.FEE_TOKENS) == FeatureState.ACTIVE assert b11.get_metadata().nc_block_root_id == self.empty_root_id assert b12.get_metadata().nc_block_root_id == self.empty_root_id @@ -221,6 +235,7 @@ def test_activation(self) -> None: # The nc and fee txs are re-accepted on the mempool. artifacts.propagate_with(self.manager, up_to='a12') assert self.feature_service.get_state(block=a12, feature=Feature.NANO_CONTRACTS) == FeatureState.ACTIVE + assert self.feature_service.get_state(block=a12, feature=Feature.FEE_TOKENS) == FeatureState.ACTIVE assert b11.get_metadata().nc_block_root_id == self.empty_root_id assert b12.get_metadata().nc_block_root_id == self.empty_root_id diff --git a/hathor_tests/resources/transaction/test_pushtx.py b/hathor_tests/resources/transaction/test_pushtx.py index 5cbf9a062..337092f32 100644 --- a/hathor_tests/resources/transaction/test_pushtx.py +++ b/hathor_tests/resources/transaction/test_pushtx.py @@ -10,7 +10,7 @@ from hathor.wallet.base_wallet import WalletInputInfo, WalletOutputInfo from hathor.wallet.resources import SendTokensResource from hathor_tests.resources.base_resource import StubSite, _BaseResourceTest -from hathor_tests.utils import add_blocks_unlock_reward, add_tx_with_data_script, create_tokens +from hathor_tests.utils import add_blocks_unlock_reward, add_tx_with_data_script, create_fee_tokens, create_tokens class BasePushTxTest(_BaseResourceTest._ResourceTest): @@ -23,8 +23,11 @@ def setUp(self): self.web = StubSite(PushTxResource(self.manager)) self.web_tokens = StubSite(SendTokensResource(self.manager, self._settings)) - def get_tx(self, inputs: Optional[list[WalletInputInfo]] = None, - outputs: Optional[list[WalletOutputInfo]] = None) -> Transaction: + def get_tx( + self, + inputs: Optional[list[WalletInputInfo]] = None, + outputs: Optional[list[WalletOutputInfo]] = None + ) -> Transaction: if not outputs: address = self.get_address(0) assert address is not None @@ -69,6 +72,21 @@ def push_tx(self, data=None): args[nk] = nv return self.web.get('push_tx', args) + @inlineCallbacks + def test_push_tx_fee_header(self): + self.manager.wallet.unlock(b'MYPASS') + add_blocks_unlock_reward(self.manager) + address = self.get_address(0) + assert address is not None + tx = create_fee_tokens(self.manager, address_b58=address, propagate=False) + + self.assertTrue(tx.has_fees()) + tx_hex = tx.get_struct().hex() + + response = yield self.push_tx({'hex_tx': tx_hex}) + data = response.json_value() + self.assertTrue(data['success']) + @inlineCallbacks def test_push_tx(self) -> Generator: self.manager.wallet.unlock(b'MYPASS') diff --git a/hathor_tests/tx/test_fee_tokens.py b/hathor_tests/tx/test_fee_tokens.py index 797870987..a58972201 100644 --- a/hathor_tests/tx/test_fee_tokens.py +++ b/hathor_tests/tx/test_fee_tokens.py @@ -14,7 +14,7 @@ import pytest -from hathor.conf.settings import NanoContractsSetting +from hathor.conf.settings import FeatureSetting from hathor.crypto.util import decode_address from hathor.exception import InvalidNewTransaction from hathor.indexes.tokens_index import TokenUtxoInfo @@ -634,7 +634,7 @@ def test_fee_token_activation(self) -> None: 'testnet', unlock_wallet=True, wallet_index=True, - settings=self._settings._replace(ENABLE_NANO_CONTRACTS=NanoContractsSetting.DISABLED), + settings=self._settings._replace(ENABLE_FEE_BASED_TOKENS=FeatureSetting.DISABLED), ) with pytest.raises(InvalidNewTransaction) as e: create_fee_tokens(custom_manager, self.address_b58) diff --git a/hathor_tests/utils.py b/hathor_tests/utils.py index 83ef4aef2..8fa2fcdcb 100644 --- a/hathor_tests/utils.py +++ b/hathor_tests/utils.py @@ -535,9 +535,15 @@ def create_tokens(manager: 'HathorManager', address_b58: Optional[str] = None, m return tx -def create_fee_tokens(manager: 'HathorManager', address_b58: Optional[str] = None, mint_amount: int = 300, - token_name: str = 'TestFeeCoin', token_symbol: str = 'TFC', - genesis_output_amount: Optional[int] = None) -> TokenCreationTransaction: +def create_fee_tokens( + manager: 'HathorManager', + address_b58: Optional[str] = None, + mint_amount: int = 300, + token_name: str = 'TestFeeCoin', + token_symbol: str = 'TFC', + genesis_output_amount: Optional[int] = None, + propagate: bool = True, +) -> TokenCreationTransaction: """Creates a new token and propagates a tx with the following UTXOs: 0. some tokens (already mint some tokens so they can be transferred); 1. mint authority; @@ -622,9 +628,11 @@ def create_fee_tokens(manager: 'HathorManager', address_b58: Optional[str] = Non input_.data = P2PKH.create_input_data(public_bytes, signature) manager.cpu_mining_service.resolve(tx) - manager.propagate_tx(tx) - assert isinstance(manager.reactor, Clock) - manager.reactor.advance(8) + + if propagate: + manager.propagate_tx(tx) + assert isinstance(manager.reactor, Clock) + manager.reactor.advance(8) return tx diff --git a/poetry.lock b/poetry.lock index f1ca3c74c..61ed0a2d5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -727,14 +727,14 @@ test = ["coverage", "mock (>=4)", "pytest (>=7)", "pytest-cov", "pytest-mock (>= [[package]] name = "hathorlib" -version = "0.12.0" +version = "0.14.0" description = "Hathor Network base objects library" optional = false python-versions = "<4,>=3.9" groups = ["main"] files = [ - {file = "hathorlib-0.12.0-py3-none-any.whl", hash = "sha256:f9868399519eac5efdec2c93e2fa122fcc1cf6b74fcd1efac918573d690caaa5"}, - {file = "hathorlib-0.12.0.tar.gz", hash = "sha256:09828665d081c57218b74427bf85c559e79b29c93c998fce4a49a3fd83d6c7a3"}, + {file = "hathorlib-0.14.0-py3-none-any.whl", hash = "sha256:45192eff389c273246f47222cc06a3dd2248830bc1ce8c7d698a3b0155a80604"}, + {file = "hathorlib-0.14.0.tar.gz", hash = "sha256:cb883f4ff2d1b197ab217c3ad21d58e448ec59e92583c45d009e2f7a8166c5c4"}, ] [package.dependencies] @@ -1905,10 +1905,10 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rocksdb" -version = "0.10.0" +version = "0.9.3" description = "Python bindings for RocksDB" optional = false -python-versions = ">=3.11" +python-versions = ">=3.10" groups = ["main"] files = [] develop = false @@ -1921,7 +1921,7 @@ test = ["pytest"] type = "git" url = "https://github.com/hathornetwork/python-rocksdb.git" reference = "HEAD" -resolved_reference = "9cb62eeae85002b3c0c9bf8d6625fb0d2b6e8a49" +resolved_reference = "1f0ce6a35472ad2e631335f159db9906ed2ebc86" [[package]] name = "sentry-sdk" @@ -2615,4 +2615,4 @@ sentry = ["sentry-sdk", "structlog-sentry"] [metadata] lock-version = "2.1" python-versions = ">=3.11,<4" -content-hash = "f0419daaa9dec4300673f492be9000913b1768c5ff56450d8d5cd3e0a8a6b04a" +content-hash = "564cbf922d736d050d4a360f0350d02cbaa38a6cbe717129700b533c2f25ebe8" diff --git a/pyproject.toml b/pyproject.toml index e79da5ab3..3cfd49943 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,7 +78,7 @@ idna = "~3.4" setproctitle = "^1.3.3" sentry-sdk = {version = "^1.5.11", optional = true} structlog-sentry = {version = "^1.4.0", optional = true} -hathorlib = "^0.12.0" +hathorlib = "^0.14.0" pydantic = "~1.10.17" pyyaml = "^6.0.1" typing-extensions = "~4.12.2"