Skip to content
Open
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
15 changes: 15 additions & 0 deletions hathor/consensus/consensus.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,9 @@ def _feature_activation_rules(self, tx: Transaction, new_best_block: Block) -> b
case Feature.FEE_TOKENS:
if not self._fee_tokens_activation_rule(tx, is_active):
return False
case Feature.TRANSFER_HEADER:
if not self._transfer_headers_activation_rule(tx, is_active):
return False
case Feature.COUNT_CHECKDATASIG_OP:
if not self._checkdatasig_count_rule(tx):
return False
Expand Down Expand Up @@ -522,6 +525,18 @@ def _checkdatasig_count_rule(self, tx: Transaction) -> bool:
return False
return True

def _transfer_headers_activation_rule(self, tx: Transaction, is_active: bool) -> bool:
"""
Check whether a tx became invalid because the reorg changed the transfer-headers feature activation state.
"""
if is_active:
return True

if tx.has_transfer_header():
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
Expand Down
18 changes: 18 additions & 0 deletions hathor/dag_builder/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
NC_WITHDRAWAL_KEY = 'nc_withdrawal'
TOKEN_VERSION_KEY = 'token_version'
FEE_KEY = 'fee'
NC_TRANSFER_INPUT_KEY = 'nc_transfer_input'
NC_TRANSFER_OUTPUT_KEY = 'nc_transfer_output'


class DAGBuilder:
Expand Down Expand Up @@ -240,6 +242,22 @@ def _add_nc_attribute(self, name: str, key: str, value: str) -> None:
actions.append((token, amount))
node.attrs[key] = actions

elif key == NC_TRANSFER_INPUT_KEY:
transfer_inputs = node.get_attr_list(key, default=[])
token, amount, (wallet,) = parse_amount_token(value)
if amount < 0:
raise SyntaxError(f'unexpected negative amount in `{key}`')
transfer_inputs.append((wallet, token, amount))
node.attrs[key] = transfer_inputs

elif key == NC_TRANSFER_OUTPUT_KEY:
transfer_outputs = node.get_attr_list(key, default=[])
token, amount, (wallet,) = parse_amount_token(value)
if amount < 0:
raise SyntaxError(f'unexpected negative amount in `{key}`')
transfer_outputs.append((wallet, token, amount))
node.attrs[key] = transfer_outputs

else:
node.attrs[key] = value

Expand Down
75 changes: 74 additions & 1 deletion hathor/dag_builder/vertex_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# limitations under the License.

import ast
import hashlib
import re
from collections import defaultdict
from types import ModuleType
Expand All @@ -23,7 +24,15 @@
from hathor.conf.settings import HathorSettings
from hathor.crypto.util import decode_address, get_address_from_public_key_bytes
from hathor.daa import DifficultyAdjustmentAlgorithm
from hathor.dag_builder.builder import FEE_KEY, NC_DEPOSIT_KEY, NC_WITHDRAWAL_KEY, DAGBuilder, DAGNode
from hathor.dag_builder.builder import (
FEE_KEY,
NC_DEPOSIT_KEY,
NC_TRANSFER_INPUT_KEY,
NC_TRANSFER_OUTPUT_KEY,
NC_WITHDRAWAL_KEY,
DAGBuilder,
DAGNode,
)
from hathor.dag_builder.types import DAGNodeType, VertexResolverType, WalletFactoryType
from hathor.dag_builder.utils import get_literal, is_literal
from hathor.nanocontracts import Blueprint, OnChainBlueprint
Expand All @@ -42,6 +51,7 @@
from hathor.transaction.base_transaction import TxInput, TxOutput
from hathor.transaction.headers.fee_header import FeeHeader, FeeHeaderEntry
from hathor.transaction.headers.nano_header import ADDRESS_LEN_BYTES
from hathor.transaction.headers.transfer_header import TxTransferInput, TxTransferOutput
from hathor.transaction.scripts.p2pkh import P2PKH
from hathor.transaction.token_creation_tx import TokenCreationTransaction
from hathor.wallet import BaseWallet, HDWallet, KeyPair
Expand Down Expand Up @@ -330,6 +340,69 @@ def add_headers_if_needed(self, node: DAGNode, vertex: BaseTransaction) -> None:
"""Add the configured headers."""
self.add_nano_header_if_needed(node, vertex)
self.add_fee_header_if_needed(node, vertex)
self.add_transfer_header_if_needed(node, vertex)

def _get_token_index(self, token_name: str, vertex: Transaction) -> int:
token_index = 0
if token_name != 'HTR':
token_creation_tx = self._vertices[token_name]
if token_creation_tx.hash not in vertex.tokens:
vertex.tokens.append(token_creation_tx.hash)
token_index = 1 + vertex.tokens.index(token_creation_tx.hash)
return token_index

def add_transfer_header_if_needed(self, node: DAGNode, vertex: BaseTransaction) -> None:
inputs = node.get_attr_list(NC_TRANSFER_INPUT_KEY, default=[])
outputs = node.get_attr_list(NC_TRANSFER_OUTPUT_KEY, default=[])

if not inputs and not outputs:
return

if not isinstance(vertex, Transaction):
raise TypeError('TransferHeader is only supported for transactions')

transfer_inputs: list[TxTransferInput] = []
for wallet_name, token_name, amount in inputs:
wallet = self.get_wallet(wallet_name)
assert isinstance(wallet, HDWallet)
privkey = wallet.get_key_at_index(0)
pubkey_bytes = privkey.sec()
address = get_address_from_public_key_bytes(pubkey_bytes)
token_index = self._get_token_index(token_name, vertex)

sighash_data = vertex.get_sighash_all_data()
sighash_data_hash = hashlib.sha256(sighash_data).digest()
signature = privkey.sign(sighash_data_hash)
script = P2PKH.create_input_data(public_key_bytes=pubkey_bytes, signature=signature)

transfer_inputs.append(TxTransferInput(
address=address,
amount=amount,
token_index=token_index,
script=script,
))

transfer_outputs: list[TxTransferOutput] = []
for wallet_name, token_name, amount in outputs:
wallet = self.get_wallet(wallet_name)
assert isinstance(wallet, HDWallet)
privkey = wallet.get_key_at_index(0)
pubkey_bytes = privkey.sec()
address = get_address_from_public_key_bytes(pubkey_bytes)
token_index = self._get_token_index(token_name, vertex)
transfer_outputs.append(TxTransferOutput(
address=address,
amount=amount,
token_index=token_index,
))

from hathor.transaction.headers import TransferHeader
transfer_header = TransferHeader(
tx=vertex,
inputs=transfer_inputs,
outputs=transfer_outputs,
)
vertex.headers.append(transfer_header)

def add_nano_header_if_needed(self, node: DAGNode, vertex: BaseTransaction) -> None:
if 'nc_id' not in node.attrs:
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,4 +32,5 @@ class Feature(StrEnum):
COUNT_CHECKDATASIG_OP = 'COUNT_CHECKDATASIG_OP'
NANO_CONTRACTS = 'NANO_CONTRACTS'
FEE_TOKENS = 'FEE_TOKENS'
TRANSFER_HEADER = 'TRANSFER_HEADER'
OPCODES_V2 = 'OPCODES_V2'
5 changes: 4 additions & 1 deletion hathor/feature_activation/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,18 @@ class Features:
nanocontracts: bool
fee_tokens: bool
opcodes_version: OpcodesVersion
transfer_headers: bool = False

@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."""
"""Return active/inactive state for every runtime feature according to the provided settings and vertex."""
from hathorlib.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.TRANSFER_HEADER: settings.ENABLE_TRANSFER_HEADER,
Feature.OPCODES_V2: settings.ENABLE_OPCODES_V2,
}

Expand All @@ -61,6 +63,7 @@ def from_vertex(*, settings: HathorSettings, feature_service: FeatureService, ve
nanocontracts=feature_is_active[Feature.NANO_CONTRACTS],
fee_tokens=feature_is_active[Feature.FEE_TOKENS],
opcodes_version=opcodes_version,
transfer_headers=feature_is_active[Feature.TRANSFER_HEADER],
)


Expand Down
5 changes: 5 additions & 0 deletions hathor/nanocontracts/blueprint_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from hathor.nanocontracts.rng import NanoRNG
from hathor.nanocontracts.runner import Runner
from hathor.nanocontracts.storage import NCContractStorage
from hathor.nanocontracts.types import Address


NCAttrCache: TypeAlias = dict[bytes, Any] | None
Expand Down Expand Up @@ -266,3 +267,7 @@ def setup_new_contract(
actions=actions,
fees=fees or (),
)

def transfer_to_address(self, address: Address, amount: Amount, token: TokenUid) -> None:
"""Transfer a given amount of token to an address balance."""
self.__runner.syscall_transfer_to_address(address, amount, token)
67 changes: 66 additions & 1 deletion hathor/nanocontracts/execution/block_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@

import hashlib
import traceback
from collections import defaultdict
from dataclasses import dataclass
from typing import TYPE_CHECKING, Iterator

from hathor.nanocontracts.exception import NCFail
from hathor.nanocontracts.exception import NCFail, NCInsufficientFunds
from hathor.transaction import Block, Transaction
from hathor.transaction.exceptions import TokenNotFound
from hathor.transaction.nc_execution_state import NCExecutionState
Expand All @@ -32,6 +33,7 @@
from hathor.nanocontracts.runner.runner import RunnerFactory
from hathor.nanocontracts.sorter.types import NCSorterCallable
from hathor.nanocontracts.storage import NCBlockStorage, NCStorageFactory
from hathor.nanocontracts.types import Address, TokenUid


# Transaction execution result types (also used as block execution effects)
Expand Down Expand Up @@ -233,19 +235,23 @@ def execute_transaction(
block_storage.set_address_seqnum(nc_address, nc_header.nc_seqnum)
return NCTxExecutionSkipped(tx=tx)

transfer_header_diffs = self._get_transfer_header_diffs(tx)
runner = self._runner_factory.create(
block_storage=block_storage,
seed=rng_seed,
)

try:
self._verify_transfer_header_balances(block_storage, transfer_header_diffs)
runner.execute_from_tx(tx)

# after the execution we have the latest state in the storage
# and at this point no tokens pending creation
self._verify_sum_after_execution(tx, block_storage)
self._apply_transfer_header_diffs(block_storage, transfer_header_diffs)

except NCFail as e:
self._ensure_runner_has_last_call_info(tx, runner)
return NCTxExecutionFailure(
tx=tx,
runner=runner,
Expand All @@ -255,6 +261,65 @@ def execute_transaction(

return NCTxExecutionSuccess(tx=tx, runner=runner)

def _get_transfer_header_diffs(self, tx: Transaction) -> dict[tuple['Address', 'TokenUid'], int]:
from hathor.nanocontracts.types import Address, TokenUid

diffs: defaultdict[tuple[Address, TokenUid], int] = defaultdict(int)
if not tx.has_transfer_header():
return dict(diffs)

transfer_header = tx.get_transfer_header()
for txin in transfer_header.inputs:
token_uid = TokenUid(tx.get_token_uid(txin.token_index))
diffs[(Address(txin.address), token_uid)] -= txin.amount

for txout in transfer_header.outputs:
token_uid = TokenUid(tx.get_token_uid(txout.token_index))
diffs[(Address(txout.address), token_uid)] += txout.amount

return dict(diffs)

def _verify_transfer_header_balances(
self,
block_storage: 'NCBlockStorage',
transfer_header_diffs: dict[tuple['Address', 'TokenUid'], int],
) -> None:
for (address, token_uid), diff in transfer_header_diffs.items():
if diff >= 0:
continue

balance = block_storage.get_address_balance(address, token_uid)
if balance + diff < 0:
raise NCInsufficientFunds(
f'insufficient transfer-header balance for address={address.hex()} '
f'token={token_uid.hex()}: available={balance} required={-diff}'
)

def _apply_transfer_header_diffs(
self,
block_storage: 'NCBlockStorage',
transfer_header_diffs: dict[tuple['Address', 'TokenUid'], int],
) -> None:
from hathor.nanocontracts.types import Amount

for (address, token_uid), diff in transfer_header_diffs.items():
if diff == 0:
continue
block_storage.add_address_balance(address, Amount(diff), token_uid)

def _ensure_runner_has_last_call_info(self, tx: Transaction, runner: 'Runner') -> None:
from hathor.nanocontracts.types import ContractId, VertexId

if runner._last_call_info is not None:
return

nano_header = tx.get_nano_header()
if nano_header.is_creating_a_new_contract():
contract_id = ContractId(VertexId(tx.hash))
else:
contract_id = ContractId(VertexId(nano_header.nc_id))
runner._last_call_info = runner._build_call_info(contract_id)

def _verify_sum_after_execution(self, tx: Transaction, block_storage: 'NCBlockStorage') -> None:
"""Verify token sums after execution for dynamically created tokens."""
from hathor.verification.transaction_verifier import TransactionVerifier
Expand Down
21 changes: 21 additions & 0 deletions hathor/nanocontracts/runner/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
NC_FALLBACK_METHOD,
NC_INITIALIZE_METHOD,
Address,
Amount,
BaseTokenAction,
BlueprintId,
ContractId,
Expand All @@ -91,6 +92,7 @@
from hathor.reactor import ReactorProtocol
from hathor.transaction import Transaction
from hathor.transaction.exceptions import InvalidFeeAmount
from hathor.transaction.headers.nano_header import ADDRESS_LEN_BYTES
from hathor.transaction.storage import TransactionStorage
from hathor.transaction.storage.exceptions import TransactionDoesNotExist
from hathor.transaction.token_info import TokenDescription, TokenVersion
Expand Down Expand Up @@ -585,6 +587,7 @@ def _validate_balances(self, ctx: Context) -> None:
def _commit_all_changes_to_storage(self) -> None:
"""Commit all change trackers."""
assert self._call_info is not None

for nc_id, change_trackers in self._call_info.change_trackers.items():
assert len(change_trackers) == 1
change_tracker = change_trackers[0]
Expand Down Expand Up @@ -1434,6 +1437,24 @@ def forbid_call_on_view(self, name: str) -> None:
if current_call_record.type == CallType.VIEW:
raise NCViewMethodError(f'@view method cannot call `syscall.{name}`')

@_forbid_syscall_from_view('transfer_to_address')
def syscall_transfer_to_address(self, address: Address, amount: Amount, token: TokenUid) -> None:
if amount < 0:
raise NCInvalidSyscall('amount cannot be negative')

if amount == 0:
# XXX Should we fail?
return

if not isinstance(address, Address) or len(address) != ADDRESS_LEN_BYTES:
raise NCInvalidSyscall(f'only addresses with {ADDRESS_LEN_BYTES} bytes are allowed')

# XXX: this makes sure the token exists
self._get_token(token)

changes_tracker = self.get_current_changes_tracker()
changes_tracker.add_address_balance(address, amount, token)


class RunnerFactory:
__slots__ = ('reactor', 'settings', 'tx_storage', 'nc_storage_factory')
Expand Down
Loading
Loading