Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .yamllint.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
extends: default

ignore:
- .venv/

rules:
document-start: disable
line-length:
Expand Down
3 changes: 3 additions & 0 deletions hathor/conf/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = {}

Expand Down
42 changes: 40 additions & 2 deletions hathor/consensus/consensus.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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


Expand Down
1 change: 1 addition & 0 deletions hathor/feature_activation/feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
6 changes: 6 additions & 0 deletions hathor/feature_activation/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -43,17 +45,21 @@ 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] = {
feature: _is_feature_active(setting, feature_states.get(feature, FeatureState.DEFINED))
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,
)


Expand Down
3 changes: 2 additions & 1 deletion hathor/nanocontracts/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions hathor/p2p/sync_v2/transaction_streaming_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -59,6 +60,7 @@ def __init__(self,
count_checkdatasig_op=False,
nanocontracts=False,
fee_tokens=False,
opcodes_version=OpcodesVersion.V1,
)
)

Expand Down
6 changes: 4 additions & 2 deletions hathor/transaction/resources/create_tx.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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))

Expand Down
14 changes: 10 additions & 4 deletions hathor/transaction/scripts/execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand All @@ -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.
Expand All @@ -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),
)


Expand Down
46 changes: 31 additions & 15 deletions hathor/transaction/scripts/opcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand All @@ -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)
10 changes: 7 additions & 3 deletions hathor/verification/nano_header_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)
Expand All @@ -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
Expand Down
Loading
Loading