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
1 change: 0 additions & 1 deletion hathor/conf/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ class HathorSettings(NamedTuple):
GENESIS_TOKENS: int = GENESIS_TOKENS

# Fee rate settings
ENABLE_FEE_TOKEN: bool = False
FEE_PER_OUTPUT: int = 1

# To disable reward halving, just set this to `None` and make sure that INITIAL_TOKEN_UNITS_PER_BLOCK is equal to
Expand Down
1 change: 0 additions & 1 deletion hathor/conf/unittests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ GENESIS_TX2_HASH: 33e14cb555a96967841dcbe0f95e9eab5810481d01de8f4f73afb8cce365e8
REWARD_SPEND_MIN_BLOCKS: 10
SLOW_ASSERTS: true
MAX_TX_WEIGHT_DIFF_ACTIVATION: 0.0
ENABLE_FEE_TOKEN: true

FEATURE_ACTIVATION:
evaluation_interval: 4
Expand Down
35 changes: 28 additions & 7 deletions hathor/dag_builder/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@

NC_DEPOSIT_KEY = 'nc_deposit'
NC_WITHDRAWAL_KEY = 'nc_withdrawal'
TOKEN_VERSION_KEY = 'token_version'
FEE_KEY = 'fee'


class DAGBuilder:
Expand Down Expand Up @@ -150,17 +152,15 @@ def add_deps(self, _from: str, _to: str) -> Self:
from_node.deps.add(_to)
return self

def set_balance(self, name: str, token: str, value: int) -> Self:
"""Set the expected balance for a given token, where balance = sum(outputs) - sum(inputs).
def update_balance(self, name: str, token: str, value: int) -> Self:
"""Update the expected balance for a given token, where balance = sum(outputs) - sum(inputs).

=0 means sum(txouts) = sum(txins)
>0 means sum(txouts) > sum(txins), e.g., withdrawal
<0 means sum(txouts) < sum(txins), e.g., deposit
"""
node = self._get_or_create_node(name)
if token in node.balances:
raise SyntaxError(f'{name}: balance set more than once for {token}')
node.balances[token] = value
node.balances[token] = node.balances.get(token, 0) + value
if token != 'HTR':
self._get_or_create_node(token, default_type=DAGNodeType.Token)
self.add_deps(name, token)
Expand Down Expand Up @@ -235,7 +235,7 @@ def _add_nc_attribute(self, name: str, key: str, value: str) -> None:
if amount < 0:
raise SyntaxError(f'unexpected negative action in `{value}`')
multiplier = 1 if key == NC_WITHDRAWAL_KEY else -1
self.set_balance(name, token, amount * multiplier)
self.update_balance(name, token, amount * multiplier)
actions = node.get_attr_list(key, default=[])
actions.append((token, amount))
node.attrs[key] = actions
Expand Down Expand Up @@ -263,6 +263,20 @@ def _add_ocb_attribute(self, name: str, key: str, value: str) -> None:
else:
node.attrs[key] = value

def _append_fee(self, name: str, key: str, value: str) -> None:
"""Add a fee payment."""
assert key == FEE_KEY
node = self._get_or_create_node(name)
fees = node.get_attr_list(key, default=[])
token, amount, args = parse_amount_token(value)
if args:
raise SyntaxError(f'unexpected args in `{value}`')
if amount < 0:
raise SyntaxError(f'unexpected negative fee in `{value}`')
self.update_balance(name, token, -amount)
fees.append((token, amount))
node.attrs[key] = fees

def add_attribute(self, name: str, key: str, value: str) -> Self:
"""Add an attribute to a node."""
if key.startswith('nc_'):
Expand All @@ -273,9 +287,16 @@ def add_attribute(self, name: str, key: str, value: str) -> Self:
self._add_ocb_attribute(name, key, value)
return self

if key == FEE_KEY:
self._append_fee(name, key, value)
return self

if key.startswith('balance_'):
node = self._get_or_create_node(name)
token = key[len('balance_'):]
self.set_balance(name, token, int(value))
if token in node.balances:
raise SyntaxError(f'{name}: balance set more than once for {token}')
self.update_balance(name, token, int(value))
return self

node = self._get_or_create_node(name)
Expand Down
13 changes: 12 additions & 1 deletion hathor/dag_builder/default_filler.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from hathor.conf.settings import HathorSettings
from hathor.daa import DifficultyAdjustmentAlgorithm
from hathor.dag_builder.builder import DAGBuilder, DAGInput, DAGNode, DAGNodeType, DAGOutput
from hathor.transaction.token_info import TokenVersion
from hathor.transaction.util import get_deposit_token_deposit_amount


Expand Down Expand Up @@ -240,7 +241,17 @@ def run(self) -> None:
balance = self.calculate_balance(node)
assert set(balance.keys()).issubset({'HTR', token})

htr_deposit = get_deposit_token_deposit_amount(self._settings, balance[token])
token_version = node.get_attr_token_version()
htr_deposit: int

match token_version:
case TokenVersion.NATIVE:
raise AssertionError
case TokenVersion.DEPOSIT:
htr_deposit = get_deposit_token_deposit_amount(self._settings, balance[token])
case TokenVersion.FEE:
htr_deposit = 0

htr_balance = balance.get('HTR', 0)

# target = sum(outputs) - sum(inputs)
Expand Down
6 changes: 6 additions & 0 deletions hathor/dag_builder/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

from hathor.dag_builder.utils import get_literal
from hathor.transaction import BaseTransaction
from hathor.transaction.token_info import TokenVersion
from hathor.wallet import BaseWallet

AttributeType: TypeAlias = dict[str, str | int]
Expand Down Expand Up @@ -83,6 +84,11 @@ def get_attr_list(self, attr: str, *, default: list[Any] | None = None) -> list[
return default
raise SyntaxError(f'missing required attribute: {self.name}.{attr}')

def get_attr_token_version(self) -> TokenVersion:
"""Return the token version for this node."""
from hathor.dag_builder.builder import TOKEN_VERSION_KEY
return TokenVersion[self.attrs.get(TOKEN_VERSION_KEY, TokenVersion.DEPOSIT.name).upper()]

def get_required_literal(self, attr: str) -> str:
"""Return the value of a required attribute as a literal or raise a SyntaxError if it doesn't exist."""
value = self.get_attr_str(attr)
Expand Down
48 changes: 43 additions & 5 deletions hathor/dag_builder/vertex_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
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 NC_DEPOSIT_KEY, NC_WITHDRAWAL_KEY, DAGBuilder, DAGNode
from hathor.dag_builder.builder import FEE_KEY, NC_DEPOSIT_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 @@ -33,6 +33,7 @@
from hathor.nanocontracts.utils import derive_child_contract_id, load_builtin_blueprint_for_ocb, sign_pycoin
from hathor.transaction import BaseTransaction, Block, Transaction
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.scripts.p2pkh import P2PKH
from hathor.transaction.token_creation_tx import TokenCreationTransaction
Expand Down Expand Up @@ -227,8 +228,9 @@ def create_vertex_token(self, node: DAGNode) -> TokenCreationTransaction:
vertex = TokenCreationTransaction(parents=txs_parents, inputs=inputs, outputs=outputs)
vertex.token_name = node.name
vertex.token_symbol = node.name
vertex.token_version = node.get_attr_token_version()
vertex.timestamp = self.get_min_timestamp(node)
self.add_nano_header_if_needed(node, vertex)
self.add_headers_if_needed(node, vertex)
self.sign_all_inputs(vertex, node=node)
if 'weight' in node.attrs:
vertex.weight = float(node.attrs['weight'])
Expand All @@ -252,7 +254,7 @@ def create_vertex_block(self, node: DAGNode) -> Block:
parents = block_parents + txs_parents

blk = Block(parents=parents, outputs=outputs)
self.add_nano_header_if_needed(node, blk)
self.add_headers_if_needed(node, blk)
blk.timestamp = self.get_min_timestamp(node) + self._settings.AVG_TIME_BETWEEN_BLOCKS
blk.get_height = lambda: height # type: ignore[method-assign]
blk.update_hash() # the next call fails is blk.hash is None
Expand Down Expand Up @@ -302,6 +304,11 @@ def _get_next_nc_seqnum(self, nc_pubkey: bytes) -> int:
self._next_nc_seqnum[address] = cur + 1
return cur

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)

def add_nano_header_if_needed(self, node: DAGNode, vertex: BaseTransaction) -> None:
if 'nc_id' not in node.attrs:
return
Expand Down Expand Up @@ -396,6 +403,37 @@ def append_actions(action: NCActionType, key: str) -> None:
else:
nano_header.nc_seqnum = self._get_next_nc_seqnum(nano_header.nc_address)

def add_fee_header_if_needed(self, node: DAGNode, vertex: BaseTransaction) -> None:
"""Add a FeeHeader if one is configured."""
if FEE_KEY not in node.attrs:
return
assert isinstance(vertex, Transaction)

fees = node.get_attr_list(FEE_KEY)

entries = []
for token_name, fee_amount in fees:
assert isinstance(token_name, str)
assert isinstance(fee_amount, int)
token_index = 0
if token_name != 'HTR':
token_creation_tx = self._vertices[token_name]
if token_creation_tx.hash not in vertex.tokens:
# when paying fees, the token uid must be added to the tokens list
# because it's possible that there are no outputs with this token.
vertex.tokens.append(token_creation_tx.hash)
token_index = 1 + vertex.tokens.index(token_creation_tx.hash)

entry = FeeHeaderEntry(token_index=token_index, amount=fee_amount)
entries.append(entry)

fee_header = FeeHeader(
settings=vertex._settings,
tx=vertex,
fees=entries,
)
vertex.headers.append(fee_header)

def create_vertex_on_chain_blueprint(self, node: DAGNode) -> OnChainBlueprint:
"""Create an OnChainBlueprint given a node."""
block_parents, txs_parents = self._create_vertex_parents(node)
Expand All @@ -404,7 +442,7 @@ def create_vertex_on_chain_blueprint(self, node: DAGNode) -> OnChainBlueprint:

assert len(block_parents) == 0
ocb = OnChainBlueprint(parents=txs_parents, inputs=inputs, outputs=outputs, tokens=tokens)
self.add_nano_header_if_needed(node, ocb)
self.add_headers_if_needed(node, ocb)
code_attr = node.get_attr_str('ocb_code')

if is_literal(code_attr):
Expand Down Expand Up @@ -452,7 +490,7 @@ def create_vertex_transaction(self, node: DAGNode, *, cls: type[Transaction] = T
assert len(block_parents) == 0
tx = cls(parents=txs_parents, inputs=inputs, outputs=outputs, tokens=tokens)
tx.timestamp = self.get_min_timestamp(node)
self.add_nano_header_if_needed(node, tx)
self.add_headers_if_needed(node, tx)
self.sign_all_inputs(tx, node=node)
if 'weight' in node.attrs:
tx.weight = float(node.attrs['weight'])
Expand Down
2 changes: 1 addition & 1 deletion hathor/transaction/base_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ def get_header_from_bytes(self, buf: bytes, *, verbose: VerboseCallback = None)

def get_maximum_number_of_headers(self) -> int:
"""Return the maximum number of headers for this vertex."""
return 1
return 2

@classmethod
@abstractmethod
Expand Down
5 changes: 3 additions & 2 deletions hathor/verification/token_creation_transaction_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from hathor.transaction.token_info import TokenInfo, TokenVersion
from hathor.transaction.util import validate_token_name_and_symbol
from hathor.types import TokenUid
from hathor.verification.verification_params import VerificationParams


class TokenCreationTransactionVerifier:
Expand All @@ -39,15 +40,15 @@ def verify_minted_tokens(self, tx: TokenCreationTransaction, token_dict: dict[To
if token_info.amount <= 0:
raise InvalidToken('Token creation transaction must mint new tokens')

def verify_token_info(self, tx: TokenCreationTransaction) -> None:
def verify_token_info(self, tx: TokenCreationTransaction, params: VerificationParams) -> None:
""" Validates token info
"""
validate_token_name_and_symbol(self._settings, tx.token_name, tx.token_symbol)

# 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 self._settings.ENABLE_FEE_TOKEN
tx.token_version == TokenVersion.FEE and not params.enable_nano,
]

if any(version_validations):
Expand Down
2 changes: 1 addition & 1 deletion hathor/verification/verification_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ def _verify_token_creation_tx(self, tx: TokenCreationTransaction, params: Verifi
We also overload verify_sum to make some different checks
"""
# we should validate the token info before verifying the tx
self.verifiers.token_creation_tx.verify_token_info(tx)
self.verifiers.token_creation_tx.verify_token_info(tx, params)
token_dict = tx.get_complete_token_info()
self._verify_tx(tx, params, token_dict=token_dict)
self.verifiers.token_creation_tx.verify_minted_tokens(tx, token_dict)
Expand Down
9 changes: 8 additions & 1 deletion hathor/verification/vertex_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
TooManyOutputs,
TooManySigOps,
)
from hathor.transaction.headers import NanoHeader, VertexBaseHeader
from hathor.transaction.headers import FeeHeader, NanoHeader, VertexBaseHeader
from hathor.verification.verification_params import VerificationParams

# tx should have 2 parents, both other transactions
Expand Down Expand Up @@ -214,6 +214,7 @@ def get_allowed_headers(self, vertex: BaseTransaction, params: VerificationParam
case TxVersion.REGULAR_TRANSACTION | TxVersion.TOKEN_CREATION_TRANSACTION:
if params.enable_nano:
allowed_headers.add(NanoHeader)
allowed_headers.add(FeeHeader)
case _: # pragma: no cover
assert_never(vertex.version)
return allowed_headers
Expand All @@ -223,12 +224,18 @@ def verify_headers(self, vertex: BaseTransaction, params: VerificationParams) ->
if len(vertex.headers) > vertex.get_maximum_number_of_headers():
raise TooManyHeaders('Maximum number of headers exceeded')

seen_header_types: set[type] = set()
allowed_headers = self.get_allowed_headers(vertex, params)
for header in vertex.headers:
if type(header) in seen_header_types:
raise HeaderNotSupported(
f'only one instance of `{type(header).__name__}` is allowed'
)
if type(header) not in allowed_headers:
raise HeaderNotSupported(
f'Header `{type(header).__name__}` not supported by `{type(vertex).__name__}`'
)
seen_header_types.add(type(header))

def verify_old_timestamp(self, vertex: BaseTransaction, params: VerificationParams) -> None:
"""Verify that the timestamp is not too old. Mempool only."""
Expand Down
Loading