From a5c6ab2ad3bca8e5f0b3337dbc250336dd2d0dee Mon Sep 17 00:00:00 2001 From: Gabriel Levcovitz Date: Mon, 26 Jan 2026 11:27:53 -0300 Subject: [PATCH] feat(script): deprecate unnecessary opcodes --- .yamllint.yml | 3 + hathor/conf/settings.py | 3 + hathor/consensus/consensus.py | 42 ++++++- hathor/feature_activation/feature.py | 1 + hathor/feature_activation/utils.py | 6 + hathor/nanocontracts/types.py | 3 +- .../sync_v2/transaction_streaming_client.py | 2 + hathor/transaction/resources/create_tx.py | 6 +- hathor/transaction/scripts/execute.py | 14 ++- hathor/transaction/scripts/opcode.py | 46 ++++--- hathor/verification/nano_header_verifier.py | 10 +- hathor/verification/transaction_verifier.py | 30 ++++- hathor/verification/verification_params.py | 4 +- hathor/verification/verification_service.py | 8 +- hathor/vertex_handler/vertex_handler.py | 24 ++-- hathor_cli/mining.py | 2 + hathor_tests/nanocontracts/test_actions.py | 2 + .../nanocontracts/test_feature_activations.py | 118 ++++++++++++++---- .../nanocontracts/test_nanocontract.py | 38 +++--- .../resources/wallet/test_nano_contract.py | 2 + hathor_tests/tx/test_multisig.py | 3 +- hathor_tests/tx/test_nano_contracts.py | 3 +- hathor_tests/tx/test_scripts.py | 17 +-- hathor_tests/tx/test_tx.py | 13 +- hathor_tests/wallet/test_wallet_hd.py | 8 +- 25 files changed, 301 insertions(+), 107 deletions(-) diff --git a/.yamllint.yml b/.yamllint.yml index 077d1e328..880ef028f 100644 --- a/.yamllint.yml +++ b/.yamllint.yml @@ -1,5 +1,8 @@ extends: default +ignore: + - .venv/ + rules: document-start: disable line-length: diff --git a/hathor/conf/settings.py b/hathor/conf/settings.py index e79d0e154..d135e0881 100644 --- a/hathor/conf/settings.py +++ b/hathor/conf/settings.py @@ -485,6 +485,9 @@ def GENESIS_TX2_TIMESTAMP(self) -> int: # Used to enable fee-based tokens. ENABLE_FEE_BASED_TOKENS: FeatureSetting = FeatureSetting.DISABLED + # Used to enable opcodes V2. + ENABLE_OPCODES_V2: FeatureSetting = FeatureSetting.DISABLED + # List of enabled blueprints. BLUEPRINTS: dict[bytes, str] = {} diff --git a/hathor/consensus/consensus.py b/hathor/consensus/consensus.py index eb35438f5..eaf1cda3b 100644 --- a/hathor/consensus/consensus.py +++ b/hathor/consensus/consensus.py @@ -24,12 +24,14 @@ from hathor.consensus.transaction_consensus import TransactionConsensusAlgorithmFactory from hathor.execution_manager import non_critical_code from hathor.feature_activation.feature import Feature +from hathor.nanocontracts.exception import NCInvalidSignature from hathor.nanocontracts.execution import NCBlockExecutor from hathor.profiler import get_cpu_profiler from hathor.pubsub import HathorEvents, PubSubManager from hathor.transaction import BaseTransaction, Block, Transaction -from hathor.transaction.exceptions import RewardLocked +from hathor.transaction.exceptions import InvalidInputData, RewardLocked, TooManySigOps from hathor.util import not_none +from hathor.verification.verification_params import VerificationParams if TYPE_CHECKING: from hathor.conf.settings import HathorSettings @@ -431,6 +433,9 @@ def _feature_activation_rules(self, tx: Transaction, new_best_block: Block) -> b case Feature.COUNT_CHECKDATASIG_OP: if not self._checkdatasig_count_rule(tx): return False + case Feature.OPCODES_V2: + if not self._opcodes_v2_activation_rule(tx, new_best_block): + return False case ( Feature.INCREASE_MAX_MERKLE_PATH_LENGTH | Feature.NOP_FEATURE_1 @@ -491,8 +496,41 @@ def _checkdatasig_count_rule(self, tx: Transaction) -> bool: # a fail and the tx will be removed from the mempool. try: VertexVerifier._verify_sigops_output(settings=self._settings, vertex=tx, enable_checkdatasig_count=True) - except Exception: + except Exception as e: + if not isinstance(e, TooManySigOps): + self.log.exception('unexpected exception in mempool-reverification') + return False + return True + + def _opcodes_v2_activation_rule(self, tx: Transaction, new_best_block: Block) -> bool: + """Check whether a tx became invalid because of the opcodes V2 feature.""" + from hathor.verification.nano_header_verifier import NanoHeaderVerifier + from hathor.verification.transaction_verifier import TransactionVerifier + + # We check all txs regardless of the feature state, because this rule + # already prohibited mempool txs before the block feature activation. + + params = VerificationParams.default_for_mempool(best_block=new_best_block) + + # Any exception in the inputs verification will be considered + # a fail and the tx will be removed from the mempool. + try: + TransactionVerifier._verify_inputs(self._settings, tx, params, skip_script=False) + except Exception as e: + if not isinstance(e, InvalidInputData): + self.log.exception('unexpected exception in mempool-reverification') return False + + # Any exception in the nc_signature verification will be considered + # a fail and the tx will be removed from the mempool. + if tx.is_nano_contract(): + try: + NanoHeaderVerifier._verify_nc_signature(self._settings, tx, params) + except Exception as e: + if not isinstance(e, NCInvalidSignature): + self.log.exception('unexpected exception in mempool-reverification') + return False + return True diff --git a/hathor/feature_activation/feature.py b/hathor/feature_activation/feature.py index b023565ca..480ef5685 100644 --- a/hathor/feature_activation/feature.py +++ b/hathor/feature_activation/feature.py @@ -32,3 +32,4 @@ class Feature(StrEnum): COUNT_CHECKDATASIG_OP = 'COUNT_CHECKDATASIG_OP' NANO_CONTRACTS = 'NANO_CONTRACTS' FEE_TOKENS = 'FEE_TOKENS' + OPCODES_V2 = 'OPCODES_V2' diff --git a/hathor/feature_activation/utils.py b/hathor/feature_activation/utils.py index 6a9fcb46d..774707bac 100644 --- a/hathor/feature_activation/utils.py +++ b/hathor/feature_activation/utils.py @@ -19,6 +19,7 @@ from hathor.feature_activation.feature import Feature from hathor.feature_activation.model.feature_state import FeatureState +from hathor.transaction.scripts.opcode import OpcodesVersion if TYPE_CHECKING: from hathor.conf.settings import FeatureSetting, HathorSettings @@ -33,6 +34,7 @@ class Features: count_checkdatasig_op: bool nanocontracts: bool fee_tokens: bool + opcodes_version: OpcodesVersion @staticmethod def from_vertex(*, settings: HathorSettings, feature_service: FeatureService, vertex: Vertex) -> Features: @@ -43,6 +45,7 @@ def from_vertex(*, settings: HathorSettings, feature_service: FeatureService, ve Feature.COUNT_CHECKDATASIG_OP: FeatureSetting.FEATURE_ACTIVATION, Feature.NANO_CONTRACTS: settings.ENABLE_NANO_CONTRACTS, Feature.FEE_TOKENS: settings.ENABLE_FEE_BASED_TOKENS, + Feature.OPCODES_V2: settings.ENABLE_OPCODES_V2, } feature_is_active: dict[Feature, bool] = { @@ -50,10 +53,13 @@ def from_vertex(*, settings: HathorSettings, feature_service: FeatureService, ve for feature, setting in feature_settings.items() } + opcodes_version = OpcodesVersion.V2 if feature_is_active[Feature.OPCODES_V2] else OpcodesVersion.V1 + 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], + opcodes_version=opcodes_version, ) diff --git a/hathor/nanocontracts/types.py b/hathor/nanocontracts/types.py index 8635f48c5..3d1835988 100644 --- a/hathor/nanocontracts/types.py +++ b/hathor/nanocontracts/types.py @@ -32,6 +32,7 @@ from hathor.nanocontracts.exception import BlueprintSyntaxError, NCSerializationError from hathor.nanocontracts.faux_immutable import FauxImmutableMeta from hathor.serialization import SerializationError +from hathor.transaction.scripts.opcode import OpcodesVersion from hathor.transaction.util import bytes_to_int, get_deposit_token_withdraw_amount, int_to_bytes from hathor.utils.typing import InnerTypeMixin @@ -162,7 +163,7 @@ def checksig(self, script: bytes) -> bool: from hathor.transaction.exceptions import ScriptError from hathor.transaction.scripts import ScriptExtras from hathor.transaction.scripts.execute import raw_script_eval - extras = ScriptExtras(tx=self) # type: ignore[arg-type] + extras = ScriptExtras(tx=self, version=OpcodesVersion.V2) # type: ignore[arg-type] try: raw_script_eval(input_data=self.script_input, output_script=script, extras=extras) except ScriptError: diff --git a/hathor/p2p/sync_v2/transaction_streaming_client.py b/hathor/p2p/sync_v2/transaction_streaming_client.py index f91c10501..92402cd2d 100644 --- a/hathor/p2p/sync_v2/transaction_streaming_client.py +++ b/hathor/p2p/sync_v2/transaction_streaming_client.py @@ -28,6 +28,7 @@ from hathor.p2p.sync_v2.streamers import StreamEnd from hathor.transaction import BaseTransaction, Transaction from hathor.transaction.exceptions import HathorError, TxValidationError +from hathor.transaction.scripts.opcode import OpcodesVersion from hathor.types import VertexId from hathor.verification.verification_params import VerificationParams @@ -59,6 +60,7 @@ def __init__(self, count_checkdatasig_op=False, nanocontracts=False, fee_tokens=False, + opcodes_version=OpcodesVersion.V1, ) ) diff --git a/hathor/transaction/resources/create_tx.py b/hathor/transaction/resources/create_tx.py index 03d4cbfc9..dbc58af52 100644 --- a/hathor/transaction/resources/create_tx.py +++ b/hathor/transaction/resources/create_tx.py @@ -22,6 +22,7 @@ from hathor.transaction import Transaction, TxInput, TxOutput from hathor.transaction.scripts import create_output_script from hathor.util import api_catch_exceptions, json_dumpb, json_loadb +from hathor.verification.verification_params import VerificationParams def from_raw_output(raw_output: dict, tokens: list[bytes]) -> TxOutput: @@ -116,11 +117,12 @@ def _verify_unsigned_skip_pow(self, tx: Transaction) -> None: verifiers.tx.verify_output_token_indexes(tx) verifiers.vertex.verify_sigops_output(tx, enable_checkdatasig_count=True) verifiers.tx.verify_sigops_input(tx, enable_checkdatasig_count=True) + best_block = self.manager.tx_storage.get_best_block() + params = VerificationParams.default_for_mempool(best_block=best_block) # need to run verify_inputs first to check if all inputs exist - verifiers.tx.verify_inputs(tx, skip_script=True) + verifiers.tx.verify_inputs(tx, params, skip_script=True) verifiers.vertex.verify_parents(tx) - best_block = self.manager.tx_storage.get_best_block() block_storage = self.manager.get_nc_block_storage(best_block) verifiers.tx.verify_sum(self.manager._settings, tx, tx.get_complete_token_info(block_storage)) diff --git a/hathor/transaction/scripts/execute.py b/hathor/transaction/scripts/execute.py index b19ab6c0a..1b393712d 100644 --- a/hathor/transaction/scripts/execute.py +++ b/hathor/transaction/scripts/execute.py @@ -12,17 +12,23 @@ # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations + import struct from dataclasses import dataclass -from typing import NamedTuple, Optional, Union +from typing import TYPE_CHECKING, NamedTuple, Optional, Union from hathor.transaction import BaseTransaction, Transaction, TxInput from hathor.transaction.exceptions import DataIndexError, FinalStackInvalid, InvalidScriptError, OutOfData +if TYPE_CHECKING: + from hathor.transaction.scripts.opcode import OpcodesVersion + @dataclass(slots=True, frozen=True, kw_only=True) class ScriptExtras: tx: Transaction + version: OpcodesVersion @dataclass(slots=True, frozen=True, kw_only=True) @@ -72,7 +78,7 @@ def execute_eval(data: bytes, log: list[str], extras: ScriptExtras) -> None: continue # this is an opcode manipulating the stack - execute_op_code(Opcode(opcode), context) + execute_op_code(Opcode(opcode), context, extras.version) evaluate_final_stack(stack, log) @@ -94,7 +100,7 @@ def evaluate_final_stack(stack: Stack, log: list[str]) -> None: raise FinalStackInvalid('\n'.join(log)) -def script_eval(tx: Transaction, txin: TxInput, spent_tx: BaseTransaction) -> None: +def script_eval(tx: Transaction, txin: TxInput, spent_tx: BaseTransaction, version: OpcodesVersion) -> None: """Evaluates the output script and input data according to a very limited subset of Bitcoin's scripting language. @@ -112,7 +118,7 @@ def script_eval(tx: Transaction, txin: TxInput, spent_tx: BaseTransaction) -> No raw_script_eval( input_data=txin.data, output_script=spent_tx.outputs[txin.index].script, - extras=UtxoScriptExtras(tx=tx, txin=txin, spent_tx=spent_tx), + extras=UtxoScriptExtras(tx=tx, txin=txin, spent_tx=spent_tx, version=version), ) diff --git a/hathor/transaction/scripts/opcode.py b/hathor/transaction/scripts/opcode.py index eddaecfbb..0f39424fb 100644 --- a/hathor/transaction/scripts/opcode.py +++ b/hathor/transaction/scripts/opcode.py @@ -15,6 +15,7 @@ import datetime import struct from enum import IntEnum +from typing import Callable from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives import hashes @@ -48,6 +49,11 @@ from hathor.transaction.scripts.script_context import ScriptContext +class OpcodesVersion(IntEnum): + V1 = 1 + V2 = 2 + + class Opcode(IntEnum): OP_0 = 0x50 OP_1 = 0x51 @@ -626,7 +632,7 @@ def op_integer(opcode: int, stack: Stack) -> None: raise ScriptError(e) from e -def execute_op_code(opcode: Opcode, context: ScriptContext) -> None: +def execute_op_code(opcode: Opcode, context: ScriptContext, version: OpcodesVersion) -> None: """ Execute a function opcode. @@ -635,17 +641,27 @@ def execute_op_code(opcode: Opcode, context: ScriptContext) -> None: context: the script context to be manipulated. """ context.logs.append(f'Executing function opcode {opcode.name} ({hex(opcode.value)})') - match opcode: - case Opcode.OP_DUP: op_dup(context) - case Opcode.OP_EQUAL: op_equal(context) - case Opcode.OP_EQUALVERIFY: op_equalverify(context) - case Opcode.OP_CHECKSIG: op_checksig(context) - case Opcode.OP_HASH160: op_hash160(context) - case Opcode.OP_GREATERTHAN_TIMESTAMP: op_greaterthan_timestamp(context) - case Opcode.OP_CHECKMULTISIG: op_checkmultisig(context) - case Opcode.OP_DATA_STREQUAL: op_data_strequal(context) - case Opcode.OP_DATA_GREATERTHAN: op_data_greaterthan(context) - case Opcode.OP_DATA_MATCH_VALUE: op_data_match_value(context) - case Opcode.OP_CHECKDATASIG: op_checkdatasig(context) - case Opcode.OP_FIND_P2PKH: op_find_p2pkh(context) - case _: raise ScriptError(f'unknown opcode: {opcode}') + opcode_fns: dict[Opcode, Callable[[ScriptContext], None]] = { + Opcode.OP_DUP: op_dup, + Opcode.OP_EQUAL: op_equal, + Opcode.OP_EQUALVERIFY: op_equalverify, + Opcode.OP_CHECKSIG: op_checksig, + Opcode.OP_HASH160: op_hash160, + Opcode.OP_GREATERTHAN_TIMESTAMP: op_greaterthan_timestamp, + Opcode.OP_CHECKMULTISIG: op_checkmultisig, + } + + if version == OpcodesVersion.V1: + opcode_fns.update({ + Opcode.OP_DATA_STREQUAL: op_data_strequal, + Opcode.OP_DATA_GREATERTHAN: op_data_greaterthan, + Opcode.OP_DATA_MATCH_VALUE: op_data_match_value, + Opcode.OP_CHECKDATASIG: op_checkdatasig, + Opcode.OP_FIND_P2PKH: op_find_p2pkh, + }) + + opcode_fn = opcode_fns.get(opcode) + if opcode_fn is None: + raise ScriptError(f'unknown opcode: {opcode}') + + opcode_fn(context) diff --git a/hathor/verification/nano_header_verifier.py b/hathor/verification/nano_header_verifier.py index 0db78d7bb..6da773a3d 100644 --- a/hathor/verification/nano_header_verifier.py +++ b/hathor/verification/nano_header_verifier.py @@ -76,8 +76,12 @@ def __init__(self, *, settings: HathorSettings, tx_storage: TransactionStorage) self._settings = settings self._tx_storage = tx_storage - def verify_nc_signature(self, tx: BaseTransaction) -> None: + def verify_nc_signature(self, tx: BaseTransaction, params: VerificationParams) -> None: """Verify if the caller's signature is valid.""" + self._verify_nc_signature(self._settings, tx, params) + + @staticmethod + def _verify_nc_signature(settings: HathorSettings, tx: BaseTransaction, params: VerificationParams) -> None: assert tx.is_nano_contract() assert isinstance(tx, Transaction) @@ -91,7 +95,7 @@ def verify_nc_signature(self, tx: BaseTransaction) -> None: ) counter = SigopCounter( - max_multisig_pubkeys=self._settings.MAX_MULTISIG_PUBKEYS, + max_multisig_pubkeys=settings.MAX_MULTISIG_PUBKEYS, enable_checkdatasig_count=True, ) output_script = create_output_script(nano_header.nc_address) @@ -103,7 +107,7 @@ def verify_nc_signature(self, tx: BaseTransaction) -> None: raw_script_eval( input_data=nano_header.nc_script, output_script=output_script, - extras=ScriptExtras(tx=tx) + extras=ScriptExtras(tx=tx, version=params.features.opcodes_version) ) except ScriptError as e: raise NCInvalidSignature from e diff --git a/hathor/verification/transaction_verifier.py b/hathor/verification/transaction_verifier.py index ae97a90fa..562207192 100644 --- a/hathor/verification/transaction_verifier.py +++ b/hathor/verification/transaction_verifier.py @@ -128,13 +128,24 @@ def verify_sigops_input(self, tx: Transaction, enable_checkdatasig_count: bool = raise TooManySigOps( 'TX[{}]: Max number of sigops for inputs exceeded ({})'.format(tx.hash_hex, n_txops)) - def verify_inputs(self, tx: Transaction, *, skip_script: bool = False) -> None: + def verify_inputs(self, tx: Transaction, params: VerificationParams, *, skip_script: bool = False) -> None: """Verify inputs signatures and ownership and all inputs actually exist""" + self._verify_inputs(self._settings, tx, params, skip_script=skip_script) + + @classmethod + def _verify_inputs( + cls, + settings: HathorSettings, + tx: Transaction, + params: VerificationParams, + *, + skip_script: bool, + ) -> None: spent_outputs: set[tuple[VertexId, int]] = set() for input_tx in tx.inputs: - if len(input_tx.data) > self._settings.MAX_INPUT_DATA_SIZE: + if len(input_tx.data) > settings.MAX_INPUT_DATA_SIZE: raise InvalidInputDataSize('size: {} and max-size: {}'.format( - len(input_tx.data), self._settings.MAX_INPUT_DATA_SIZE + len(input_tx.data), settings.MAX_INPUT_DATA_SIZE )) spent_tx = tx.get_spent_tx(input_tx) @@ -149,7 +160,7 @@ def verify_inputs(self, tx: Transaction, *, skip_script: bool = False) -> None: )) if not skip_script: - self.verify_script(tx=tx, input_tx=input_tx, spent_tx=spent_tx) + cls.verify_script(tx=tx, input_tx=input_tx, spent_tx=spent_tx, params=params) # check if any other input in this tx is spending the same output key = (input_tx.tx_id, input_tx.index) @@ -158,7 +169,14 @@ def verify_inputs(self, tx: Transaction, *, skip_script: bool = False) -> None: tx.hash_hex, input_tx.tx_id.hex(), input_tx.index)) spent_outputs.add(key) - def verify_script(self, *, tx: Transaction, input_tx: TxInput, spent_tx: BaseTransaction) -> None: + @staticmethod + def verify_script( + *, + tx: Transaction, + input_tx: TxInput, + spent_tx: BaseTransaction, + params: VerificationParams, + ) -> None: """ :type tx: Transaction :type input_tx: TxInput @@ -166,7 +184,7 @@ def verify_script(self, *, tx: Transaction, input_tx: TxInput, spent_tx: BaseTra """ from hathor.transaction.scripts import script_eval try: - script_eval(tx, input_tx, spent_tx) + script_eval(tx, input_tx, spent_tx, params.features.opcodes_version) except ScriptError as e: raise InvalidInputData(e) from e diff --git a/hathor/verification/verification_params.py b/hathor/verification/verification_params.py index 23b9969e8..e677d09f2 100644 --- a/hathor/verification/verification_params.py +++ b/hathor/verification/verification_params.py @@ -18,6 +18,7 @@ from hathor.feature_activation.utils import Features from hathor.transaction import Block +from hathor.transaction.scripts.opcode import OpcodesVersion @dataclass(slots=True, frozen=True, kw_only=True) @@ -48,7 +49,8 @@ def default_for_mempool(cls, *, best_block: Block, features: Features | None = N features = Features( count_checkdatasig_op=True, nanocontracts=True, - fee_tokens=False + fee_tokens=False, + opcodes_version=OpcodesVersion.V2, ) return cls( diff --git a/hathor/verification/verification_service.py b/hathor/verification/verification_service.py index e077aee32..6f5ec9476 100644 --- a/hathor/verification/verification_service.py +++ b/hathor/verification/verification_service.py @@ -261,7 +261,7 @@ def _verify_tx( return self.verify_without_storage(tx, params) 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_inputs(tx, params) # need to run verify_inputs first to check if all inputs exist self.verifiers.tx.verify_version(tx, params) block_storage = self._get_block_storage(params) @@ -320,7 +320,7 @@ def verify_without_storage(self, vertex: BaseTransaction, params: VerificationPa if vertex.is_nano_contract(): assert self._settings.ENABLE_NANO_CONTRACTS - self._verify_without_storage_nano_header(vertex) + self._verify_without_storage_nano_header(vertex, params) def _verify_without_storage_base_block(self, block: Block, params: VerificationParams) -> None: self.verifiers.block.verify_no_inputs(block) @@ -359,9 +359,9 @@ def _verify_without_storage_token_creation_tx( ) -> None: self._verify_without_storage_tx(tx, params) - def _verify_without_storage_nano_header(self, tx: BaseTransaction) -> None: + def _verify_without_storage_nano_header(self, tx: BaseTransaction, params: VerificationParams) -> None: assert tx.is_nano_contract() - self.verifiers.nano_header.verify_nc_signature(tx) + self.verifiers.nano_header.verify_nc_signature(tx, params) self.verifiers.nano_header.verify_actions(tx) def _verify_without_storage_fee_header(self, tx: BaseTransaction) -> None: diff --git a/hathor/vertex_handler/vertex_handler.py b/hathor/vertex_handler/vertex_handler.py index a6a71a133..9a066bef2 100644 --- a/hathor/vertex_handler/vertex_handler.py +++ b/hathor/vertex_handler/vertex_handler.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import dataclasses import datetime from dataclasses import replace from typing import Any, Generator @@ -30,6 +31,7 @@ from hathor.pubsub import HathorEvents, PubSubManager from hathor.reactor import ReactorProtocol from hathor.transaction import BaseTransaction, Block, Transaction +from hathor.transaction.scripts.opcode import OpcodesVersion from hathor.transaction.storage import TransactionStorage from hathor.transaction.storage.exceptions import TransactionDoesNotExist from hathor.verification.verification_params import VerificationParams @@ -118,13 +120,14 @@ 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() + features = Features.from_vertex( + settings=self._settings, + feature_service=self._feature_service, + vertex=best_block, + ) params = VerificationParams.default_for_mempool( best_block=best_block, - features=Features.from_vertex( - settings=self._settings, - feature_service=self._feature_service, - vertex=best_block, - ), + features=dataclasses.replace(features, opcodes_version=OpcodesVersion.V2), ) return self._old_on_new_vertex(tx, params) @@ -142,14 +145,15 @@ def on_new_relayed_vertex( if best_block_meta.nc_block_root_id is None: assert best_block.is_genesis + features = Features.from_vertex( + settings=self._settings, + feature_service=self._feature_service, + vertex=best_block, + ) params = VerificationParams( reject_locked_reward=reject_locked_reward, 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, - ), + features=dataclasses.replace(features, opcodes_version=OpcodesVersion.V2), ) return self._old_on_new_vertex(vertex, params, quiet=quiet) diff --git a/hathor_cli/mining.py b/hathor_cli/mining.py index e34bbe1f3..2620016c6 100644 --- a/hathor_cli/mining.py +++ b/hathor_cli/mining.py @@ -141,12 +141,14 @@ def execute(args: Namespace) -> None: from hathor.verification.verification_service import VerificationService from hathor.verification.vertex_verifiers import VertexVerifiers from hathor.feature_activation.utils import Features + from hathor.transaction.scripts.opcode import OpcodesVersion settings = get_global_settings() daa = DifficultyAdjustmentAlgorithm(settings=settings) verification_params = VerificationParams(nc_block_root_id=None, features=Features( count_checkdatasig_op=True, nanocontracts=False, fee_tokens=False, + opcodes_version=OpcodesVersion.V2, )) verifiers = VertexVerifiers.create_defaults( reactor=Mock(), diff --git a/hathor_tests/nanocontracts/test_actions.py b/hathor_tests/nanocontracts/test_actions.py index 8cc68e437..934c0f0ea 100644 --- a/hathor_tests/nanocontracts/test_actions.py +++ b/hathor_tests/nanocontracts/test_actions.py @@ -29,6 +29,7 @@ from hathor.transaction import Block, Transaction, TxInput, TxOutput from hathor.transaction.exceptions import InvalidToken from hathor.transaction.headers.nano_header import NanoHeaderAction +from hathor.transaction.scripts.opcode import OpcodesVersion from hathor.util import not_none from hathor.verification.nano_header_verifier import MAX_ACTIONS_LEN from hathor.verification.verification_params import VerificationParams @@ -124,6 +125,7 @@ def setUp(self) -> None: count_checkdatasig_op=False, nanocontracts=True, fee_tokens=False, + opcodes_version=OpcodesVersion.V1, ) ) diff --git a/hathor_tests/nanocontracts/test_feature_activations.py b/hathor_tests/nanocontracts/test_feature_activations.py index 8b4a86170..49abb05f6 100644 --- a/hathor_tests/nanocontracts/test_feature_activations.py +++ b/hathor_tests/nanocontracts/test_feature_activations.py @@ -15,6 +15,7 @@ import pytest from hathor.conf.settings import FeatureSetting +from hathor.crypto.util import decode_address, get_address_from_public_key_hash from hathor.daa import DifficultyAdjustmentAlgorithm, TestMode from hathor.exception import InvalidNewTransaction from hathor.feature_activation.feature import Feature @@ -25,6 +26,7 @@ from hathor.nanocontracts.types import BlueprintId from hathor.transaction import Block, Transaction, Vertex from hathor.transaction.nc_execution_state import NCExecutionState +from hathor.transaction.scripts import P2PKH, Opcode from hathor_tests import unittest from hathor_tests.dag_builder.builder import TestDAGBuilder @@ -49,15 +51,22 @@ def setUp(self) -> None: evaluation_interval=4, default_threshold=3, features={ + Feature.OPCODES_V2: Criteria( + bit=0, + start_height=4, + timeout_height=12, + signal_support_by_default=True, + version='0.0.0' + ), Feature.NANO_CONTRACTS: Criteria( - bit=2, + bit=1, start_height=4, timeout_height=12, signal_support_by_default=True, version='0.0.0' ), Feature.FEE_TOKENS: Criteria( - bit=3, + bit=2, start_height=4, timeout_height=12, signal_support_by_default=True, @@ -69,6 +78,7 @@ def setUp(self) -> None: settings = self._settings._replace( ENABLE_NANO_CONTRACTS=FeatureSetting.FEATURE_ACTIVATION, ENABLE_FEE_BASED_TOKENS=FeatureSetting.FEATURE_ACTIVATION, + ENABLE_OPCODES_V2=FeatureSetting.FEATURE_ACTIVATION, FEATURE_ACTIVATION=feature_settings, ) daa = DifficultyAdjustmentAlgorithm(settings=self._settings, test_mode=TestMode.TEST_ALL_WEIGHT) @@ -88,7 +98,7 @@ def setUp(self) -> None: empty_block_storage.commit() self.empty_root_id = empty_block_storage.get_root_id() - def test_activation(self) -> None: + async def test_activation(self) -> None: private_key = unittest.OCB_TEST_PRIVKEY.hex() password = unittest.OCB_TEST_PASSWORD.hex() artifacts = self.dag_builder.build_from_str(f''' @@ -106,10 +116,13 @@ def test_activation(self) -> None: FBT.token_version = fee FBT.fee = 1 HTR - tx1.out[0] = 123 FBT - tx1.fee = 1 HTR + fee_tx.out[0] = 123 FBT + fee_tx.fee = 1 HTR + + op_v2_a.out[0] <<< op_v2_b + op_v2_b <-- b11 - b12 < nc1 < ocb1 < FBT < tx1 < b13 < a11 + b10 < op_v2_a < op_v2_b < b11 < b12 < nc1 < ocb1 < FBT < fee_tx < b13 < a11 nc1 <-- b13 ocb1 <-- b13 @@ -124,15 +137,40 @@ def test_activation(self) -> None: ('b3', 'b4', 'b7', 'b8', 'b11', 'b12', 'b13', 'a11', 'a12', 'a13'), Block, ) - nc1, ocb1, fbt, tx1 = artifacts.get_typed_vertices(('nc1', 'ocb1', 'FBT', 'tx1'), Transaction) + nc1, ocb1, fbt, fee_tx, op_v2_a, op_v2_b = artifacts.get_typed_vertices( + ('nc1', 'ocb1', 'FBT', 'fee_tx', 'op_v2_a', 'op_v2_b'), + Transaction, + ) + + # Setup txs for testing OPCODES_V2. + assert len(op_v2_b.outputs) == 1 + op_v2_b_out = op_v2_b.outputs[0] + p2pkh = P2PKH.parse_script(op_v2_b_out.script) + assert p2pkh is not None + op_v2_address = decode_address(p2pkh.address) + + # This is a custom script that uses one of the deprecated opcodes and will end with 1 on the stack. + assert len(op_v2_b.inputs) == 1 + op_v2_b_in = op_v2_b.inputs[0] + op_v2_b_in.data = bytes([ + 0x19, + *get_address_from_public_key_hash(op_v2_address[1:-4]), + Opcode.OP_FIND_P2PKH, + ]) + + assert op_v2_b_in.tx_id == op_v2_a.hash + op_v2_a_out = op_v2_a.outputs[op_v2_b_in.index] + op_v2_a_out.script = b'' # Empty script so op_v2_b can spend it with the custom script. 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 + assert self.feature_service.get_state(block=b3, feature=Feature.OPCODES_V2) == 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 + assert self.feature_service.get_state(block=b4, feature=Feature.OPCODES_V2) == FeatureState.STARTED signaling_blocks = ('b5', 'b6', 'b7') for block_name in signaling_blocks: @@ -144,30 +182,51 @@ def test_activation(self) -> None: 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 + assert self.feature_service.get_state(block=b7, feature=Feature.OPCODES_V2) == 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 + assert self.feature_service.get_state(block=b8, feature=Feature.OPCODES_V2) == FeatureState.LOCKED_IN + + artifacts.propagate_with(self.manager, up_to='op_v2_a') + + # At this point the OPCODES_V2 feature is not active, + # but deprecated opcodes are already rejected on the mempool + msg = 'full validation failed: unknown opcode: 208' + with pytest.raises(InvalidNewTransaction, match=msg): + self.vertex_handler.on_new_relayed_vertex(op_v2_b) + assert op_v2_b.get_metadata().validation.is_initial() + assert op_v2_b.get_metadata().voided_by is None + + # However, deprecated opcodes would be accepted if relayed inside a block. + # We have to manually propagate it. + d = self.vertex_handler.on_new_block(b11, deps=[op_v2_b]) + self.clock.advance(1) + assert d.called and d.result is True + artifacts._last_propagated = 'b11' - 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 self.feature_service.get_state(block=b11, feature=Feature.OPCODES_V2) == FeatureState.LOCKED_IN assert b11.get_metadata().nc_block_root_id == self.empty_root_id - # At this point, the feature is not active, so the nc and fee txs are rejected on the mempool. + # At this point the nano feature is not active, so nano header is rejected on the mempool msg = 'full validation failed: Header `NanoHeader` not supported by `Transaction`' with pytest.raises(InvalidNewTransaction, match=msg): self.vertex_handler.on_new_relayed_vertex(nc1) assert nc1.get_metadata().validation.is_initial() assert nc1.get_metadata().voided_by is None + # At this point the nano feature is not active, so OCB is rejected on the mempool msg = 'full validation failed: invalid vertex version: 6' with pytest.raises(InvalidNewTransaction, match=msg): self.vertex_handler.on_new_relayed_vertex(ocb1) assert ocb1.get_metadata().validation.is_initial() assert ocb1.get_metadata().voided_by is None + # At this point the fee feature is not active, so fee header is rejected on the mempool msg = 'full validation failed: Header `FeeHeader` not supported by `TokenCreationTransaction`' with pytest.raises(InvalidNewTransaction, match=msg): self.vertex_handler.on_new_relayed_vertex(fbt) @@ -177,6 +236,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 self.feature_service.get_state(block=b12, feature=Feature.OPCODES_V2) == 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 @@ -194,9 +254,9 @@ def test_activation(self) -> None: assert fbt.get_metadata().validation.is_valid() assert fbt.get_metadata().voided_by is None - artifacts.propagate_with(self.manager, up_to='tx1') - assert tx1.get_metadata().validation.is_valid() - assert tx1.get_metadata().voided_by is None + artifacts.propagate_with(self.manager, up_to='fee_tx') + assert fee_tx.get_metadata().validation.is_valid() + assert fee_tx.get_metadata().voided_by is None artifacts.propagate_with(self.manager, up_to='b13') assert nc1.get_metadata().nc_execution == NCExecutionState.SUCCESS @@ -205,37 +265,44 @@ def test_activation(self) -> None: assert b12.get_metadata().nc_block_root_id == self.empty_root_id assert b13.get_metadata().nc_block_root_id not in (self.empty_root_id, None) + # A reorg happens, decreasing the best chain. artifacts.propagate_with(self.manager, up_to='a11') assert a11.get_metadata().validation.is_valid() assert a11.get_metadata().voided_by is None - assert b11.get_metadata().voided_by == {b11.hash} - assert b12.get_metadata().voided_by == {b12.hash} + assert b11.get_metadata().validation.is_invalid() + assert b12.get_metadata().validation.is_invalid() assert b13.get_metadata().validation.is_invalid() assert nc1.get_metadata().validation.is_invalid() assert ocb1.get_metadata().validation.is_invalid() assert fbt.get_metadata().validation.is_invalid() - assert tx1.get_metadata().validation.is_invalid() + assert fee_tx.get_metadata().validation.is_invalid() + assert op_v2_b.get_metadata().validation.is_invalid() assert b11.get_metadata().nc_block_root_id == self.empty_root_id assert b12.get_metadata().nc_block_root_id == self.empty_root_id assert b13.get_metadata().nc_block_root_id not in (self.empty_root_id, None) assert a11.get_metadata().nc_block_root_id == self.empty_root_id - # The nc and fee txs are removed from the mempool. + # The nc, fee, and deprecated opcodes txs are removed from the mempool. + assert not self.manager.tx_storage.transaction_exists(b11.hash) + assert not self.manager.tx_storage.transaction_exists(b12.hash) assert not self.manager.tx_storage.transaction_exists(b13.hash) assert not self.manager.tx_storage.transaction_exists(nc1.hash) assert not self.manager.tx_storage.transaction_exists(ocb1.hash) assert not self.manager.tx_storage.transaction_exists(fbt.hash) - assert not self.manager.tx_storage.transaction_exists(tx1.hash) + assert not self.manager.tx_storage.transaction_exists(fee_tx.hash) + assert not self.manager.tx_storage.transaction_exists(op_v2_b.hash) assert nc1 not in list(self.manager.tx_storage.iter_mempool_tips()) assert ocb1 not in list(self.manager.tx_storage.iter_mempool_tips()) assert fbt not in list(self.manager.tx_storage.iter_mempool_tips()) - assert tx1 not in list(self.manager.tx_storage.iter_mempool_tips()) + assert fee_tx not in list(self.manager.tx_storage.iter_mempool_tips()) + assert op_v2_b not in list(self.manager.tx_storage.iter_mempool_tips()) - # The nc and fee txs are re-accepted on the mempool. + # The feature states re-activate. 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 self.feature_service.get_state(block=a12, feature=Feature.OPCODES_V2) == 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 @@ -243,6 +310,7 @@ def test_activation(self) -> None: assert a11.get_metadata().nc_block_root_id == self.empty_root_id assert a12.get_metadata().nc_block_root_id == self.empty_root_id + # The nc and fee txs are re-accepted on the mempool. self._reset_vertex(nc1) self.vertex_handler.on_new_relayed_vertex(nc1) assert nc1.get_metadata().validation.is_valid() @@ -264,12 +332,12 @@ def test_activation(self) -> None: assert self.manager.tx_storage.transaction_exists(fbt.hash) assert fbt in list(self.manager.tx_storage.iter_mempool_tips()) - self._reset_vertex(tx1) - self.vertex_handler.on_new_relayed_vertex(tx1) - assert tx1.get_metadata().validation.is_valid() - assert tx1.get_metadata().voided_by is None - assert self.manager.tx_storage.transaction_exists(tx1.hash) - assert tx1 in list(self.manager.tx_storage.iter_mempool_tips()) + self._reset_vertex(fee_tx) + self.vertex_handler.on_new_relayed_vertex(fee_tx) + assert fee_tx.get_metadata().validation.is_valid() + assert fee_tx.get_metadata().voided_by is None + assert self.manager.tx_storage.transaction_exists(fee_tx.hash) + assert fee_tx in list(self.manager.tx_storage.iter_mempool_tips()) artifacts.propagate_with(self.manager, up_to='a13') diff --git a/hathor_tests/nanocontracts/test_nanocontract.py b/hathor_tests/nanocontracts/test_nanocontract.py index 2ca8c3271..aab9d722e 100644 --- a/hathor_tests/nanocontracts/test_nanocontract.py +++ b/hathor_tests/nanocontracts/test_nanocontract.py @@ -1,4 +1,5 @@ from typing import Any +from unittest.mock import Mock import pytest from cryptography.hazmat.primitives import hashes @@ -39,6 +40,7 @@ from hathor.transaction.scripts import P2PKH, HathorScript, Opcode from hathor.transaction.validation_state import ValidationState from hathor.verification.nano_header_verifier import MAX_NC_SCRIPT_SIGOPS_COUNT, MAX_NC_SCRIPT_SIZE +from hathor.verification.verification_params import VerificationParams from hathor.wallet import KeyPair from hathor_tests import unittest @@ -84,6 +86,8 @@ def setUp(self) -> None: self.genesis = self.peer.tx_storage.get_all_genesis() self.genesis_txs = [tx for tx in self.genesis if not tx.is_block] + self.verification_params = VerificationParams.default_for_mempool(best_block=Mock()) + def _create_nc( self, nc_id: VertexId, @@ -172,7 +176,7 @@ def test_serialization_skip_signature(self) -> None: def test_verify_signature_success(self) -> None: nc = self._get_nc() nc.clear_sighash_cache() - self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc) + self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc, self.verification_params) def test_verify_signature_fails_nc_id(self) -> None: nc = self._get_nc() @@ -180,7 +184,7 @@ def test_verify_signature_fails_nc_id(self) -> None: nano_header.nc_id = b'a' * 32 nc.clear_sighash_cache() with self.assertRaises(NCInvalidSignature): - self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc) + self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc, self.verification_params) def test_verify_signature_fails_nc_method(self) -> None: nc = self._get_nc() @@ -188,7 +192,7 @@ def test_verify_signature_fails_nc_method(self) -> None: nano_header.nc_method = 'other_nc_method' nc.clear_sighash_cache() with self.assertRaises(NCInvalidSignature): - self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc) + self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc, self.verification_params) def test_verify_signature_fails_nc_args_bytes(self) -> None: nc = self._get_nc() @@ -196,7 +200,7 @@ def test_verify_signature_fails_nc_args_bytes(self) -> None: nano_header.nc_args_bytes = b'other_nc_args_bytes' nc.clear_sighash_cache() with self.assertRaises(NCInvalidSignature): - self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc) + self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc, self.verification_params) def test_verify_signature_fails_invalid_nc_address(self) -> None: nc = self._get_nc() @@ -204,7 +208,7 @@ def test_verify_signature_fails_invalid_nc_address(self) -> None: nano_header.nc_address = b'invalid-address' nc.clear_sighash_cache() with pytest.raises(NCInvalidSignature, match=f'invalid address: {nano_header.nc_address.hex()}'): - self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc) + self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc, self.verification_params) def test_verify_signature_fails_invalid_nc_script(self) -> None: nc = self._get_nc() @@ -212,7 +216,7 @@ def test_verify_signature_fails_invalid_nc_script(self) -> None: nano_header.nc_script = b'invalid-script' nc.clear_sighash_cache() with pytest.raises(InvalidScriptError, match='Invalid Opcode'): - self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc) + self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc, self.verification_params) def test_verify_signature_fails_wrong_nc_address(self) -> None: key = KeyPair.create(b'xyz') @@ -225,7 +229,7 @@ def test_verify_signature_fails_wrong_nc_address(self) -> None: nano_header.nc_address = get_address_from_public_key_bytes(pubkey_bytes) nc.clear_sighash_cache() with pytest.raises(NCInvalidSignature) as e: - self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc) + self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc, self.verification_params) assert isinstance(e.value.__cause__, EqualVerifyFailed) def test_verify_signature_fails_wrong_pubkey(self) -> None: @@ -244,7 +248,7 @@ def test_verify_signature_fails_wrong_pubkey(self) -> None: nano_header.nc_script = P2PKH.create_input_data(public_key_bytes=pubkey_bytes, signature=signature) # First, it's passing with the key from above - self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc) + self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc, self.verification_params) # We change the script to use a new pubkey, but with the same signature key = KeyPair.create(b'wrong') @@ -254,7 +258,7 @@ def test_verify_signature_fails_wrong_pubkey(self) -> None: nano_header.nc_script = P2PKH.create_input_data(public_key_bytes=pubkey_bytes, signature=signature) with pytest.raises(NCInvalidSignature) as e: - self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc) + self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc, self.verification_params) assert isinstance(e.value.__cause__, EqualVerifyFailed) def test_verify_signature_fails_wrong_signature(self) -> None: @@ -273,7 +277,7 @@ def test_verify_signature_fails_wrong_signature(self) -> None: nano_header.nc_script = P2PKH.create_input_data(public_key_bytes=pubkey_bytes, signature=signature) # First, it's passing with the key from above - self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc) + self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc, self.verification_params) # We change the script to use a new signature, but with the same pubkey key = KeyPair.create(b'wrong') @@ -282,7 +286,7 @@ def test_verify_signature_fails_wrong_signature(self) -> None: nano_header.nc_script = P2PKH.create_input_data(public_key_bytes=pubkey_bytes, signature=signature) with pytest.raises(NCInvalidSignature) as e: - self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc) + self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc, self.verification_params) assert isinstance(e.value.__cause__, FinalStackInvalid) assert 'Stack left with False value' in e.value.__cause__.args[0] @@ -292,7 +296,7 @@ def test_verify_signature_fails_nc_script_too_large(self) -> None: nano_header.nc_script = b'\x00' * (MAX_NC_SCRIPT_SIZE + 1) with pytest.raises(NCInvalidSignature, match='nc_script larger than max: 1025 > 1024'): - self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc) + self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc, self.verification_params) def test_verify_signature_fails_nc_script_too_many_sigops(self) -> None: nc = self._get_nc() @@ -305,7 +309,7 @@ def test_verify_signature_fails_nc_script_too_many_sigops(self) -> None: nano_header.nc_script = script.data with pytest.raises(TooManySigOps, match='sigops count greater than max: 21 > 20'): - self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc) + self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc, self.verification_params) def test_verify_signature_multisig(self) -> None: nc = self._get_nc() @@ -332,7 +336,7 @@ def test_verify_signature_multisig(self) -> None: sign_privkeys=[keys[0][0]], ) with pytest.raises(NCInvalidSignature) as e: - self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc) + self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc, self.verification_params) assert isinstance(e.value.__cause__, MissingStackItems) assert e.value.__cause__.args[0] == 'OP_CHECKMULTISIG: not enough signatures on the stack' @@ -345,7 +349,7 @@ def test_verify_signature_multisig(self) -> None: sign_privkeys=[KeyPair.create(b'invalid').get_private_key(b'invalid')], ) with pytest.raises(NCInvalidSignature) as e: - self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc) + self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc, self.verification_params) assert isinstance(e.value.__cause__, FinalStackInvalid) assert 'Stack left with False value' in e.value.__cause__.args[0] @@ -357,13 +361,13 @@ def test_verify_signature_multisig(self) -> None: redeem_pubkey_bytes=redeem_pubkey_bytes, sign_privkeys=[x[0] for x in keys[:2]], ) - self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc) + self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc, self.verification_params) # Test fails because the address was changed nc.clear_sighash_cache() nano_header.nc_address = decode_address(self.peer.wallet.get_unused_address()) with pytest.raises(NCInvalidSignature) as e: - self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc) + self.peer.verification_service.verifiers.nano_header.verify_nc_signature(nc, self.verification_params) assert isinstance(e.value.__cause__, EqualVerifyFailed) def test_get_related_addresses(self) -> None: diff --git a/hathor_tests/resources/wallet/test_nano_contract.py b/hathor_tests/resources/wallet/test_nano_contract.py index 7380871d5..938fb3087 100644 --- a/hathor_tests/resources/wallet/test_nano_contract.py +++ b/hathor_tests/resources/wallet/test_nano_contract.py @@ -1,3 +1,4 @@ +import pytest from twisted.internet.defer import inlineCallbacks from hathor.simulator.utils import add_new_blocks @@ -14,6 +15,7 @@ from hathor_tests.utils import add_blocks_unlock_reward +@pytest.mark.skip(reason='old feature, this will be removed') class NanoContractsTest(_BaseResourceTest._ResourceTest): def setUp(self): super().setUp() diff --git a/hathor_tests/tx/test_multisig.py b/hathor_tests/tx/test_multisig.py index b8015db22..48b35650b 100644 --- a/hathor_tests/tx/test_multisig.py +++ b/hathor_tests/tx/test_multisig.py @@ -6,6 +6,7 @@ from hathor.transaction import Transaction, TxInput, TxOutput from hathor.transaction.exceptions import ScriptError from hathor.transaction.scripts import P2PKH, MultiSig, create_output_script, parse_address_script, script_eval +from hathor.transaction.scripts.opcode import OpcodesVersion from hathor.wallet.base_wallet import WalletBalance, WalletOutputInfo from hathor.wallet.util import generate_multisig_address, generate_multisig_redeem_script, generate_signature from hathor_tests import unittest @@ -135,7 +136,7 @@ def test_spend_multisig(self): expected_dict = {'type': 'MultiSig', 'address': self.multisig_address_b58, 'timelock': None} self.assertEqual(cls_script.to_human_readable(), expected_dict) - script_eval(tx, tx_input, tx1) + script_eval(tx, tx_input, tx1, version=OpcodesVersion.V2) # Script error with self.assertRaises(ScriptError): diff --git a/hathor_tests/tx/test_nano_contracts.py b/hathor_tests/tx/test_nano_contracts.py index 9dc195cba..00b681aaa 100644 --- a/hathor_tests/tx/test_nano_contracts.py +++ b/hathor_tests/tx/test_nano_contracts.py @@ -4,6 +4,7 @@ from hathor.transaction import Transaction, TxInput, TxOutput from hathor.transaction.scripts import P2PKH, NanoContractMatchValues, script_eval +from hathor.transaction.scripts.opcode import OpcodesVersion from hathor.util import json_dumpb from hathor_tests import unittest @@ -38,4 +39,4 @@ def test_match_values(self): txin = TxInput(b'aa', 0, input_data) spent_tx = Transaction(outputs=[TxOutput(20, script)]) tx = Transaction(outputs=[TxOutput(20, P2PKH.create_output_script(address))]) - script_eval(tx, txin, spent_tx) + script_eval(tx, txin, spent_tx, OpcodesVersion.V1) diff --git a/hathor_tests/tx/test_scripts.py b/hathor_tests/tx/test_scripts.py index a72d8409a..768d3fffb 100644 --- a/hathor_tests/tx/test_scripts.py +++ b/hathor_tests/tx/test_scripts.py @@ -32,6 +32,7 @@ get_script_op, ) from hathor.transaction.scripts.opcode import ( + OpcodesVersion, op_checkdatasig, op_checkmultisig, op_checksig, @@ -257,7 +258,7 @@ def test_checksig(self) -> None: signature = self.genesis_private_key.sign(hashed_data, ec.ECDSA(hashes.SHA256())) pubkey_bytes = get_public_key_bytes_compressed(self.genesis_public_key) - extras = UtxoScriptExtras(tx=tx, txin=Mock(), spent_tx=Mock()) + extras = UtxoScriptExtras(tx=tx, txin=Mock(), spent_tx=Mock(), version=OpcodesVersion.V2) # wrong signature puts False (0) on stack stack: Stack = [b'aaaaaaaaa', pubkey_bytes] @@ -282,7 +283,7 @@ def test_checksig_cache(self) -> None: signature = self.genesis_private_key.sign(hashed_data, ec.ECDSA(hashes.SHA256())) pubkey_bytes = get_public_key_bytes_compressed(self.genesis_public_key) - extras = UtxoScriptExtras(tx=tx, txin=Mock(), spent_tx=Mock()) + extras = UtxoScriptExtras(tx=tx, txin=Mock(), spent_tx=Mock(), version=OpcodesVersion.V2) stack: Stack = [signature, pubkey_bytes] self.assertIsNone(tx._sighash_data_cache) @@ -512,28 +513,28 @@ def test_find_p2pkh(self) -> None: # try with just 1 output stack: Stack = [genesis_address] tx = Transaction(outputs=[TxOutput(1, out_genesis)]) - extras = UtxoScriptExtras(tx=tx, txin=txin, spent_tx=spent_tx) + extras = UtxoScriptExtras(tx=tx, txin=txin, spent_tx=spent_tx, version=OpcodesVersion.V2) op_find_p2pkh(ScriptContext(stack=stack, logs=[], extras=extras)) self.assertEqual(stack.pop(), 1) # several outputs and correct output among them stack = [genesis_address] tx = Transaction(outputs=[TxOutput(1, out1), TxOutput(1, out2), TxOutput(1, out_genesis), TxOutput(1, out3)]) - extras = UtxoScriptExtras(tx=tx, txin=txin, spent_tx=spent_tx) + extras = UtxoScriptExtras(tx=tx, txin=txin, spent_tx=spent_tx, version=OpcodesVersion.V2) op_find_p2pkh(ScriptContext(stack=stack, logs=[], extras=extras)) self.assertEqual(stack.pop(), 1) # several outputs without correct amount output stack = [genesis_address] tx = Transaction(outputs=[TxOutput(1, out1), TxOutput(1, out2), TxOutput(2, out_genesis), TxOutput(1, out3)]) - extras = UtxoScriptExtras(tx=tx, txin=txin, spent_tx=spent_tx) + extras = UtxoScriptExtras(tx=tx, txin=txin, spent_tx=spent_tx, version=OpcodesVersion.V2) with self.assertRaises(VerifyFailed): op_find_p2pkh(ScriptContext(stack=stack, logs=[], extras=extras)) # several outputs without correct address output stack = [genesis_address] tx = Transaction(outputs=[TxOutput(1, out1), TxOutput(1, out2), TxOutput(1, out3)]) - extras = UtxoScriptExtras(tx=tx, txin=txin, spent_tx=spent_tx) + extras = UtxoScriptExtras(tx=tx, txin=txin, spent_tx=spent_tx, version=OpcodesVersion.V2) with self.assertRaises(VerifyFailed): op_find_p2pkh(ScriptContext(stack=stack, logs=[], extras=extras)) @@ -547,7 +548,7 @@ def test_greaterthan_timestamp(self) -> None: tx = Transaction() stack: Stack = [struct.pack('!I', timestamp)] - extras = UtxoScriptExtras(tx=tx, txin=Mock(), spent_tx=Mock()) + extras = UtxoScriptExtras(tx=tx, txin=Mock(), spent_tx=Mock(), version=OpcodesVersion.V2) with self.assertRaises(TimeLocked): tx.timestamp = timestamp - 1 @@ -573,7 +574,7 @@ def test_checkmultisig(self) -> None: tx = Transaction(inputs=[txin], outputs=[txout]) data_to_sign = tx.get_sighash_all() - extras = UtxoScriptExtras(tx=tx, txin=Mock(), spent_tx=Mock()) + extras = UtxoScriptExtras(tx=tx, txin=Mock(), spent_tx=Mock(), version=OpcodesVersion.V2) wallet = HDWallet() wallet._manually_initialize() diff --git a/hathor_tests/tx/test_tx.py b/hathor_tests/tx/test_tx.py index a9a156a82..1e537ee9a 100644 --- a/hathor_tests/tx/test_tx.py +++ b/hathor_tests/tx/test_tx.py @@ -1,7 +1,7 @@ import base64 import hashlib from math import isinf, isnan -from unittest.mock import patch +from unittest.mock import Mock, patch import pytest @@ -36,6 +36,7 @@ from hathor.transaction.scripts import P2PKH, parse_address_script from hathor.transaction.util import int_to_bytes from hathor.transaction.validation_state import ValidationState +from hathor.verification.verification_params import VerificationParams from hathor.wallet import Wallet from hathor_tests import unittest from hathor_tests.utils import ( @@ -67,6 +68,8 @@ def setUp(self): blocks = add_blocks_unlock_reward(self.manager) self.last_block = blocks[-1] + self.verification_params = VerificationParams.default_for_mempool(best_block=Mock()) + def test_input_output_match_less_htr(self): genesis_block = self.genesis_blocks[0] @@ -147,7 +150,7 @@ def test_script(self): _input.data = data_wrong with self.assertRaises(InvalidInputData): - self._verifiers.tx.verify_inputs(tx) + self._verifiers.tx.verify_inputs(tx, params=self.verification_params) def test_too_many_inputs(self): random_bytes = bytes.fromhex('0000184e64683b966b4268f387c269915cc61f6af5329823a93e3696cb0fe902') @@ -776,10 +779,10 @@ def test_tx_methods(self): tx2.timestamp = tx2_timestamp # Verify inputs timestamps - self._verifiers.tx.verify_inputs(tx2) + self._verifiers.tx.verify_inputs(tx2, params=self.verification_params) tx2.timestamp = 2 with self.assertRaises(TimestampError): - self._verifiers.tx.verify_inputs(tx2) + self._verifiers.tx.verify_inputs(tx2, params=self.verification_params) tx2.timestamp = tx2_timestamp # Validate maximum distance between blocks @@ -993,7 +996,7 @@ def _test_txin_data_limit(self, offset): outputs=[_output], storage=self.tx_storage ) - self._verifiers.tx.verify_inputs(tx, skip_script=True) + self._verifiers.tx.verify_inputs(tx, skip_script=True, params=self.verification_params) def test_txin_data_limit_exceeded(self): with self.assertRaises(InvalidInputDataSize): diff --git a/hathor_tests/wallet/test_wallet_hd.py b/hathor_tests/wallet/test_wallet_hd.py index 0b06205ed..60dc0d104 100644 --- a/hathor_tests/wallet/test_wallet_hd.py +++ b/hathor_tests/wallet/test_wallet_hd.py @@ -1,6 +1,9 @@ +from unittest.mock import Mock + from hathor.crypto.util import decode_address from hathor.simulator.utils import add_new_block from hathor.transaction import Transaction +from hathor.verification.verification_params import VerificationParams from hathor.wallet import HDWallet from hathor.wallet.base_wallet import WalletBalance, WalletInputInfo, WalletOutputInfo from hathor.wallet.exceptions import InsufficientFunds @@ -39,7 +42,8 @@ def test_transaction_and_balance(self): tx1 = self.wallet.prepare_transaction_compute_inputs(Transaction, [out], self.tx_storage) tx1.update_hash() verifier = self.manager.verification_service.verifiers.tx - verifier.verify_script(tx=tx1, input_tx=tx1.inputs[0], spent_tx=block) + params = VerificationParams.default_for_mempool(best_block=Mock()) + verifier.verify_script(tx=tx1, input_tx=tx1.inputs[0], spent_tx=block, params=params) tx1.storage = self.tx_storage tx1.get_metadata().validation = ValidationState.FULL self.wallet.on_new_tx(tx1) @@ -60,7 +64,7 @@ def test_transaction_and_balance(self): tx2.storage = self.tx_storage tx2.update_hash() tx2.storage = self.tx_storage - verifier.verify_script(tx=tx2, input_tx=tx2.inputs[0], spent_tx=tx1) + verifier.verify_script(tx=tx2, input_tx=tx2.inputs[0], spent_tx=tx1, params=params) tx2.get_metadata().validation = ValidationState.FULL tx2.init_static_metadata_from_storage(self._settings, self.tx_storage) self.tx_storage.save_transaction(tx2)