diff --git a/hathor/consensus/block_consensus.py b/hathor/consensus/block_consensus.py index 3e78105b46..020897a137 100644 --- a/hathor/consensus/block_consensus.py +++ b/hathor/consensus/block_consensus.py @@ -24,6 +24,7 @@ from hathor.consensus.context import ReorgInfo from hathor.execution_manager import non_critical_code +from hathor.feature_activation.utils import Features from hathor.transaction import BaseTransaction, Block, Transaction from hathor.transaction.exceptions import TokenNotFound from hathor.transaction.nc_execution_state import NCExecutionState @@ -147,9 +148,9 @@ def _should_execute_nano(self, block: Block) -> bool: """ assert not block.is_genesis - 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) + features = Features.from_vertex(settings=self._settings, feature_service=self.feature_service, vertex=parent) + return features.nanocontracts def _nc_execute_calls(self, block: Block, *, is_reorg: bool) -> None: """Internal method to execute the method calls for transactions confirmed by this block. diff --git a/hathor/consensus/consensus.py b/hathor/consensus/consensus.py index 1851982220..2519cb9b5d 100644 --- a/hathor/consensus/consensus.py +++ b/hathor/consensus/consensus.py @@ -23,7 +23,7 @@ from hathor.consensus.context import ConsensusAlgorithmContext from hathor.consensus.transaction_consensus import TransactionConsensusAlgorithmFactory from hathor.execution_manager import non_critical_code -from hathor.feature_activation.utils import is_fee_active +from hathor.feature_activation.utils import Features from hathor.profiler import get_cpu_profiler from hathor.pubsub import HathorEvents, PubSubManager from hathor.transaction import BaseTransaction, Transaction @@ -418,11 +418,13 @@ 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 best_block = storage.get_best_block() - if is_nano_active(settings=self._settings, block=best_block, feature_service=self.feature_service): + features = Features.from_vertex( + settings=self._settings, vertex=best_block, feature_service=self.feature_service + ) + if features.nanocontracts: # When nano is active, this rule has no effect. return True @@ -443,7 +445,10 @@ def _fee_tokens_activation_rule(self, storage: TransactionStorage, tx: Transacti 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): + features = Features.from_vertex( + settings=self._settings, vertex=best_block, feature_service=self.feature_service + ) + if features.fee_tokens: # When fee-based tokens feature is active, this rule has no effect. return True diff --git a/hathor/feature_activation/feature.py b/hathor/feature_activation/feature.py index ad7d62e458..b023565cae 100644 --- a/hathor/feature_activation/feature.py +++ b/hathor/feature_activation/feature.py @@ -12,11 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from enum import Enum, unique +from enum import StrEnum, unique @unique -class Feature(str, Enum): +class Feature(StrEnum): """ An enum containing all features that participate in the feature activation process, past or future, activated or not, for all networks. Features should NOT be removed from this enum, to preserve history. Their values @@ -30,7 +30,5 @@ class Feature(str, Enum): INCREASE_MAX_MERKLE_PATH_LENGTH = 'INCREASE_MAX_MERKLE_PATH_LENGTH' 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 index 37578381de..6a9fcb46d7 100644 --- a/hathor/feature_activation/utils.py +++ b/hathor/feature_activation/utils.py @@ -14,53 +14,58 @@ from __future__ import annotations +from dataclasses import dataclass from typing import TYPE_CHECKING, assert_never +from hathor.feature_activation.feature import Feature +from hathor.feature_activation.model.feature_state import FeatureState + 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.transaction import Vertex -from hathor.feature_activation.feature import Feature +@dataclass(slots=True, frozen=True, kw_only=True) +class Features: + """A dataclass holding state information about features from the Feature Activation process.""" -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 + count_checkdatasig_op: bool + nanocontracts: bool + fee_tokens: bool + + @staticmethod + def from_vertex(*, settings: HathorSettings, feature_service: FeatureService, vertex: Vertex) -> Features: + """Return whether the Nano Contracts feature is active according to the provided settings and vertex.""" + from hathor.conf.settings import FeatureSetting + feature_states = feature_service.get_feature_states(vertex=vertex) + feature_settings = { + Feature.COUNT_CHECKDATASIG_OP: FeatureSetting.FEATURE_ACTIVATION, + Feature.NANO_CONTRACTS: settings.ENABLE_NANO_CONTRACTS, + Feature.FEE_TOKENS: settings.ENABLE_FEE_BASED_TOKENS, + } + + feature_is_active: dict[Feature, bool] = { + feature: _is_feature_active(setting, feature_states.get(feature, FeatureState.DEFINED)) + for feature, setting in feature_settings.items() + } + + return Features( + count_checkdatasig_op=feature_is_active[Feature.COUNT_CHECKDATASIG_OP], + nanocontracts=feature_is_active[Feature.NANO_CONTRACTS], + fee_tokens=feature_is_active[Feature.FEE_TOKENS], + ) + +def _is_feature_active(setting: FeatureSetting, state: FeatureState) -> bool: + """Return whether a feature is active based on the setting and state.""" + 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) + return state.is_active() 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/p2p/sync_v2/transaction_streaming_client.py b/hathor/p2p/sync_v2/transaction_streaming_client.py index e4a2f79253..f91c105013 100644 --- a/hathor/p2p/sync_v2/transaction_streaming_client.py +++ b/hathor/p2p/sync_v2/transaction_streaming_client.py @@ -18,6 +18,7 @@ from structlog import get_logger from twisted.internet.defer import Deferred, inlineCallbacks +from hathor.feature_activation.utils import Features from hathor.p2p.sync_v2.exception import ( InvalidVertexError, StreamingError, @@ -53,8 +54,12 @@ def __init__(self, # We can also set the `nc_block_root_id` to `None` because we only call `verify_basic`, # which doesn't need it. self.verification_params = VerificationParams( - enable_checkdatasig_count=False, nc_block_root_id=None, + features=Features( + count_checkdatasig_op=False, + nanocontracts=False, + fee_tokens=False, + ) ) self.reactor = sync_agent.reactor diff --git a/hathor/verification/token_creation_transaction_verifier.py b/hathor/verification/token_creation_transaction_verifier.py index 01b4dba5f1..45e56f2d5c 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_fee, + tx.token_version == TokenVersion.FEE and not params.features.fee_tokens, ] if any(version_validations): diff --git a/hathor/verification/transaction_verifier.py b/hathor/verification/transaction_verifier.py index 39faa07958..ae97a90fae 100644 --- a/hathor/verification/transaction_verifier.py +++ b/hathor/verification/transaction_verifier.py @@ -338,7 +338,7 @@ def verify_version(self, tx: Transaction, params: VerificationParams) -> None: TxVersion.TOKEN_CREATION_TRANSACTION, } - if params.enable_nano: + if params.features.nanocontracts: allowed_tx_versions.add(TxVersion.ON_CHAIN_BLUEPRINT) if tx.version not in allowed_tx_versions: diff --git a/hathor/verification/verification_params.py b/hathor/verification/verification_params.py index d71f73ca05..23b9969e83 100644 --- a/hathor/verification/verification_params.py +++ b/hathor/verification/verification_params.py @@ -16,6 +16,7 @@ from dataclasses import dataclass +from hathor.feature_activation.utils import Features from hathor.transaction import Block @@ -24,11 +25,9 @@ class VerificationParams: """Contains every parameter/setting to run a single verification.""" nc_block_root_id: bytes | None - enable_checkdatasig_count: bool reject_locked_reward: bool = True skip_block_weight_verification: bool = False - enable_nano: bool = False - enable_fee: bool = False + features: Features reject_too_old_vertices: bool = False harden_token_restrictions: bool = False @@ -36,13 +35,7 @@ class VerificationParams: reject_conflicts_with_confirmed_txs: bool = False @classmethod - def default_for_mempool( - cls, - *, - best_block: Block, - enable_nano: bool = False, - enable_fee: bool = False, - ) -> VerificationParams: + def default_for_mempool(cls, *, best_block: Block, features: Features | None = None) -> VerificationParams: """This is the appropriate parameters for verifying mempool transactions, realtime blocks and API pushes. Other cases should instantiate `VerificationParams` manually with the appropriate parameter values. @@ -50,11 +43,17 @@ def default_for_mempool( best_block_meta = best_block.get_metadata() if best_block_meta.nc_block_root_id is None: assert best_block.is_genesis + + if features is None: + features = Features( + count_checkdatasig_op=True, + nanocontracts=True, + fee_tokens=False + ) + return cls( nc_block_root_id=best_block_meta.nc_block_root_id, - enable_checkdatasig_count=True, - enable_nano=enable_nano, - enable_fee=enable_fee, + features=features, reject_too_old_vertices=True, harden_token_restrictions=True, harden_nano_restrictions=True, diff --git a/hathor/verification/verification_service.py b/hathor/verification/verification_service.py index 4e41dcba47..e077aee323 100644 --- a/hathor/verification/verification_service.py +++ b/hathor/verification/verification_service.py @@ -260,7 +260,7 @@ def _verify_tx( # TODO do genesis validation return self.verify_without_storage(tx, params) - self.verifiers.tx.verify_sigops_input(tx, params.enable_checkdatasig_count) + self.verifiers.tx.verify_sigops_input(tx, params.features.count_checkdatasig_op) self.verifiers.tx.verify_inputs(tx) # need to run verify_inputs first to check if all inputs exist self.verifiers.tx.verify_version(tx, params) @@ -327,7 +327,7 @@ def _verify_without_storage_base_block(self, block: Block, params: VerificationP self.verifiers.vertex.verify_outputs(block) self.verifiers.block.verify_output_token_indexes(block) self.verifiers.block.verify_data(block) - self.verifiers.vertex.verify_sigops_output(block, params.enable_checkdatasig_count) + self.verifiers.vertex.verify_sigops_output(block, params.features.count_checkdatasig_op) def _verify_without_storage_block(self, block: Block, params: VerificationParams) -> None: """ Run all verifications that do not need a storage. @@ -349,7 +349,7 @@ def _verify_without_storage_tx(self, tx: Transaction, params: VerificationParams self.verifiers.tx.verify_number_of_inputs(tx) self.verifiers.vertex.verify_outputs(tx) self.verifiers.tx.verify_output_token_indexes(tx) - self.verifiers.vertex.verify_sigops_output(tx, params.enable_checkdatasig_count) + self.verifiers.vertex.verify_sigops_output(tx, params.features.count_checkdatasig_op) self.verifiers.tx.verify_tokens(tx, params) def _verify_without_storage_token_creation_tx( diff --git a/hathor/verification/vertex_verifier.py b/hathor/verification/vertex_verifier.py index 537cbca124..e8045d9140 100644 --- a/hathor/verification/vertex_verifier.py +++ b/hathor/verification/vertex_verifier.py @@ -226,9 +226,9 @@ def get_allowed_headers(self, vertex: BaseTransaction, params: VerificationParam case TxVersion.ON_CHAIN_BLUEPRINT: pass case TxVersion.REGULAR_TRANSACTION | TxVersion.TOKEN_CREATION_TRANSACTION: - if params.enable_nano: + if params.features.nanocontracts: allowed_headers.add(NanoHeader) - if params.enable_fee: + if params.features.fee_tokens: 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 2e2a30db82..52a6062486 100644 --- a/hathor/version_resource.py +++ b/hathor/version_resource.py @@ -17,7 +17,7 @@ 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.feature_activation.utils import Features from hathor.manager import HathorManager from hathor.util import json_dumpb @@ -46,9 +46,10 @@ def render_GET(self, request): set_cors(request, 'GET') best_block = self.manager.tx_storage.get_best_block() - nano_contracts_enabled = is_nano_active( - settings=self._settings, block=best_block, feature_service=self.feature_service + features = Features.from_vertex( + settings=self._settings, vertex=best_block, feature_service=self.feature_service ) + nano_contracts_enabled = features.nanocontracts data = { 'version': hathor.__version__, diff --git a/hathor/vertex_handler/vertex_handler.py b/hathor/vertex_handler/vertex_handler.py index 3d06a76334..a6a71a133f 100644 --- a/hathor/vertex_handler/vertex_handler.py +++ b/hathor/vertex_handler/vertex_handler.py @@ -24,9 +24,8 @@ from hathor.consensus import ConsensusAlgorithm from hathor.exception import HathorError, InvalidNewTransaction from hathor.execution_manager import ExecutionManager, non_critical_code -from hathor.feature_activation.feature import Feature from hathor.feature_activation.feature_service import FeatureService -from hathor.feature_activation.utils import is_fee_active, is_nano_active +from hathor.feature_activation.utils import Features from hathor.profiler import get_cpu_profiler from hathor.pubsub import HathorEvents, PubSubManager from hathor.reactor import ReactorProtocol @@ -90,28 +89,17 @@ def on_new_block(self, block: Block, *, deps: list[Transaction]) -> Generator[An parent_block = self._tx_storage.get_block(parent_block_hash) parent_meta = parent_block.get_metadata() - enable_checkdatasig_count = self._feature_service.is_feature_active( - vertex=parent_block, - feature=Feature.COUNT_CHECKDATASIG_OP, - ) - - enable_nano = is_nano_active( - 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 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, + features=Features.from_vertex( + settings=self._settings, + feature_service=self._feature_service, + vertex=parent_block, + ), ) for tx in deps: @@ -130,12 +118,13 @@ def on_new_block(self, block: Block, *, deps: list[Transaction]) -> Generator[An 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, + features=Features.from_vertex( + settings=self._settings, + feature_service=self._feature_service, + vertex=best_block, + ), ) return self._old_on_new_vertex(tx, params) @@ -150,17 +139,17 @@ def on_new_relayed_vertex( """Called for unsolicited vertex received, usually due to real time relay.""" 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 + params = VerificationParams( - 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, + features=Features.from_vertex( + settings=self._settings, + feature_service=self._feature_service, + vertex=best_block, + ), ) return self._old_on_new_vertex(vertex, params, quiet=quiet) diff --git a/hathor_cli/mining.py b/hathor_cli/mining.py index 2ee1f5ce81..e34bbe1f31 100644 --- a/hathor_cli/mining.py +++ b/hathor_cli/mining.py @@ -140,9 +140,14 @@ def execute(args: Namespace) -> None: from hathor.verification.verification_params import VerificationParams from hathor.verification.verification_service import VerificationService from hathor.verification.vertex_verifiers import VertexVerifiers + from hathor.feature_activation.utils import Features settings = get_global_settings() daa = DifficultyAdjustmentAlgorithm(settings=settings) - verification_params = VerificationParams(nc_block_root_id=None, enable_checkdatasig_count=True) + verification_params = VerificationParams(nc_block_root_id=None, features=Features( + count_checkdatasig_op=True, + nanocontracts=False, + fee_tokens=False, + )) verifiers = VertexVerifiers.create_defaults( reactor=Mock(), settings=settings, diff --git a/hathor_tests/nanocontracts/test_actions.py b/hathor_tests/nanocontracts/test_actions.py index 77a602d99a..8cc68e4370 100644 --- a/hathor_tests/nanocontracts/test_actions.py +++ b/hathor_tests/nanocontracts/test_actions.py @@ -18,6 +18,7 @@ import pytest +from hathor.feature_activation.utils import Features from hathor.indexes.tokens_index import TokensIndex from hathor.nanocontracts import HATHOR_TOKEN_UID, NC_EXECUTION_FAIL_ID, Blueprint, Context, public from hathor.nanocontracts.catalog import NCBlueprintCatalog @@ -118,8 +119,12 @@ def setUp(self) -> None: ) best_block = self.manager.tx_storage.get_best_block() self.verification_params = VerificationParams.default_for_mempool( - enable_nano=True, best_block=best_block, + features=Features( + count_checkdatasig_op=False, + nanocontracts=True, + fee_tokens=False, + ) ) # We finish a manual setup of tx1, so it can be used directly in verification methods. diff --git a/hathor_tests/nanocontracts/test_nano_feature_activation.py b/hathor_tests/nanocontracts/test_feature_activations.py similarity index 100% rename from hathor_tests/nanocontracts/test_nano_feature_activation.py rename to hathor_tests/nanocontracts/test_feature_activations.py