diff --git a/hathor/dag_builder/default_filler.py b/hathor/dag_builder/default_filler.py index 80d7f318d..b49476a29 100644 --- a/hathor/dag_builder/default_filler.py +++ b/hathor/dag_builder/default_filler.py @@ -238,6 +238,10 @@ def run(self) -> None: for token in tokens: node = self._get_or_create_node(token) + if 'token_id' in node.attrs: + # Skip token creation when `token_id` is provided. + continue + balance = self.calculate_balance(node) assert set(balance.keys()).issubset({'HTR', token}) diff --git a/hathor/dag_builder/vertex_exporter.py b/hathor/dag_builder/vertex_exporter.py index 8b36f9327..a6fc2e961 100644 --- a/hathor/dag_builder/vertex_exporter.py +++ b/hathor/dag_builder/vertex_exporter.py @@ -29,7 +29,14 @@ from hathor.nanocontracts import Blueprint, OnChainBlueprint from hathor.nanocontracts.catalog import NCBlueprintCatalog from hathor.nanocontracts.on_chain_blueprint import Code -from hathor.nanocontracts.types import BlueprintId, ContractId, NCActionType, VertexId, blueprint_id_from_bytes +from hathor.nanocontracts.types import ( + BlueprintId, + ContractId, + NCActionType, + TokenUid, + VertexId, + blueprint_id_from_bytes, +) 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 @@ -121,6 +128,14 @@ def _create_vertex_txin(self, node: DAGNode) -> list[TxInput]: inputs.append(txin) return inputs + def _get_token_id(self, token_name: str) -> TokenUid: + """Return token uid for a token name.""" + node = self._get_node(token_name) + if 'token_id' in node.attrs: + return TokenUid(bytes.fromhex(get_literal(node.attrs['token_id']))) + else: + return TokenUid(self.get_vertex_id(token_name)) + def _create_vertex_txout( self, node: DAGNode, @@ -139,11 +154,11 @@ def _create_vertex_txout( elif token_creation: index = 1 else: - token_uid = self.get_vertex_id(token_name) + token_id = self._get_token_id(token_name) try: - index = tokens.index(token_uid) + 1 + index = tokens.index(token_id) + 1 except ValueError: - tokens.append(token_uid) + tokens.append(token_id) index = len(tokens) script = self.get_next_p2pkh_script() @@ -176,7 +191,7 @@ def get_min_timestamp(self, node: DAGNode) -> int: # update timestamp deps = list(node.get_all_dependencies()) assert deps - timestamp = 1 + max(self._vertices[name].timestamp for name in deps) + timestamp = 1 + max(self._vertices[name].timestamp for name in deps if name in self._vertices) return timestamp def update_vertex_hash(self, vertex: BaseTransaction, *, fix_conflict: bool = True) -> None: @@ -215,8 +230,14 @@ def sign_all_inputs(self, vertex: Transaction, *, node: DAGNode | None = None) - public_key_bytes, signature = wallet.get_input_aux_data(data_to_sign, private_key) txin.data = P2PKH.create_input_data(public_key_bytes, signature) - def create_vertex_token(self, node: DAGNode) -> TokenCreationTransaction: + def create_vertex_token(self, node: DAGNode) -> TokenCreationTransaction | None: """Create a token given a node.""" + if 'token_id' in node.attrs: + # Skip token creation when `token_id` is provided. + if list(node.attrs.keys()) != ['token_id']: + raise ValueError('no other attribute is allowed when `token_id` is provided') + return None + block_parents, txs_parents = self._create_vertex_parents(node) inputs = self._create_vertex_txin(node) tokens, outputs = self._create_vertex_txout(node, token_creation=True) @@ -363,13 +384,13 @@ def append_actions(action: NCActionType, key: str) -> None: token_index = 0 if token_name != 'HTR': assert isinstance(vertex, Transaction) - token_creation_tx = self._vertices[token_name] - if token_creation_tx.hash not in vertex.tokens: + token_id = self._get_token_id(token_name) + if token_id not in vertex.tokens: # when depositing, the token uid must be added to the tokens list # because it's possible that there are no outputs with this token. assert action == NCActionType.DEPOSIT - vertex.tokens.append(token_creation_tx.hash) - token_index = 1 + vertex.tokens.index(token_creation_tx.hash) + vertex.tokens.append(token_id) + token_index = 1 + vertex.tokens.index(token_id) nc_actions.append(NanoHeaderAction( type=action, @@ -417,12 +438,12 @@ def add_fee_header_if_needed(self, node: DAGNode, vertex: BaseTransaction) -> No 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: + token_id = self._get_token_id(token_name) + if token_id 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) + vertex.tokens.append(token_id) + token_index = 1 + vertex.tokens.index(token_id) entry = FeeHeaderEntry(token_index=token_index, amount=fee_amount) entries.append(entry) @@ -529,9 +550,9 @@ def create_genesis_vertex(self, node: DAGNode) -> BaseTransaction: return vertex - def create_vertex(self, node: DAGNode) -> BaseTransaction: + def create_vertex(self, node: DAGNode) -> BaseTransaction | None: """Create a vertex.""" - vertex: BaseTransaction + vertex: BaseTransaction | None match node.type: case DAGNodeType.Block: @@ -555,6 +576,10 @@ def create_vertex(self, node: DAGNode) -> BaseTransaction: case _: assert_never(node.type) + if vertex is None: + # skip it + return None + assert vertex is not None assert vertex.hash not in self._vertice_per_id assert node.name not in self._vertices @@ -571,6 +596,8 @@ def export(self) -> Iterator[tuple[DAGNode, BaseTransaction]]: for node in self._builder.topological_sorting(): vertex = self.create_vertex(node) + if vertex is None: + continue if node.type is not DAGNodeType.Genesis: yield node, vertex diff --git a/hathor/indexes/rocksdb_tokens_index.py b/hathor/indexes/rocksdb_tokens_index.py index 0052e0c26..cfc803c7b 100644 --- a/hathor/indexes/rocksdb_tokens_index.py +++ b/hathor/indexes/rocksdb_tokens_index.py @@ -12,9 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from dataclasses import dataclass +from dataclasses import asdict, dataclass from enum import Enum -from typing import TYPE_CHECKING, Iterator, NamedTuple, Optional, TypedDict, cast +from typing import TYPE_CHECKING, Iterator, NamedTuple, Optional, cast from structlog import get_logger from typing_extensions import assert_never, override @@ -57,7 +57,7 @@ class _Tag(Enum): TXS = 0x04 -@dataclass +@dataclass(slots=True) class _KeyAny: token_uid_internal: InternalUid tag: _Tag @@ -66,14 +66,24 @@ class _KeyAny: index: Optional[int] = None -class _InfoDict(TypedDict): - name: str - symbol: str +@dataclass(slots=True) +class _InfoDict: + name: str | None + symbol: str | None + version: TokenVersion | None total: int - version: TokenVersion n_contracts_can_mint: int n_contracts_can_melt: int + def is_unknown(self) -> bool: + if self.name is None: + assert self.symbol is None + assert self.version is None + return True + assert self.symbol is not None + assert self.version is not None + return False + class _TxIndex(NamedTuple): tx_hash: bytes @@ -176,7 +186,7 @@ def _from_key_any(self, key: bytes) -> _KeyAny: raise NotImplementedError('unreachable') def _to_value_info(self, info: _InfoDict) -> bytes: - return json_dumpb(info) + return json_dumpb(asdict(info)) def _from_value_info(self, value: bytes, token_uid: TokenUid) -> _InfoDict: """Deserialize token info from JSON bytes and handle backward compatibility. @@ -201,43 +211,57 @@ def _from_value_info(self, value: bytes, token_uid: TokenUid) -> _InfoDict: info['n_contracts_can_mint'] = 0 info['n_contracts_can_melt'] = 0 - if info.get('version') is None: - if token_uid == self._settings.HATHOR_TOKEN_UID: - info['version'] = TokenVersion.NATIVE - else: - info['version'] = TokenVersion.DEPOSIT + if info.get('name') is None: + assert info.get('symbol') is None + assert info.get('version') is None + else: + assert info.get('symbol') is not None + if info.get('version') is None: + if token_uid == self._settings.HATHOR_TOKEN_UID: + info['version'] = TokenVersion.NATIVE + else: + info['version'] = TokenVersion.DEPOSIT - assert info.get('name') is not None - assert info.get('symbol') is not None - assert info.get('version') is not None assert info.get('total') is not None assert info.get('n_contracts_can_mint') is not None assert info.get('n_contracts_can_melt') is not None - return cast(_InfoDict, info) + return _InfoDict(**info) def create_token_info( self, *, token_uid: bytes, - name: str, - symbol: str, - version: TokenVersion, + name: str | None, + symbol: str | None, + version: TokenVersion | None, total: int = 0, n_contracts_can_mint: int = 0, n_contracts_can_melt: int = 0, ) -> None: key = self._to_key_info(token_uid) old_value = self._db.get((self._cf, key)) - assert old_value is None - value = self._to_value_info({ - 'name': name, - 'symbol': symbol, - 'total': total, - 'version': version, - 'n_contracts_can_mint': n_contracts_can_mint, - 'n_contracts_can_melt': n_contracts_can_melt, - }) + if old_value is None: + value = self._to_value_info(_InfoDict( + name=name, + symbol=symbol, + total=total, + version=version, + n_contracts_can_mint=n_contracts_can_mint, + n_contracts_can_melt=n_contracts_can_melt, + )) + else: + info = self._from_value_info(old_value, token_uid) + assert info.name is None + assert info.symbol is None + assert info.version is None + info.name = name + info.symbol = symbol + info.version = version + info.total += total + info.n_contracts_can_mint += n_contracts_can_mint + info.n_contracts_can_melt += n_contracts_can_melt + value = self._to_value_info(info) self._db.put((self._cf, key), value) def create_token_info_from_contract( @@ -296,6 +320,15 @@ def _remove_authority_utxo(self, token_uid: bytes, tx_hash: bytes, index: int, * self.log.debug('remove authority utxo', token=token_uid.hex(), tx=tx_hash.hex(), index=index, is_mint=is_mint) self._db.delete((self._cf, self._to_key_authority(token_uid, TokenUtxoInfo(tx_hash, index), is_mint=is_mint))) + def _create_empty_info(self, token_uid: bytes) -> None: + self.create_token_info( + token_uid=token_uid, + name=None, + symbol=None, + version=None, + total=0, + ) + def _create_genesis_info(self) -> None: self.create_token_info( token_uid=self._settings.HATHOR_TOKEN_UID, @@ -305,16 +338,24 @@ def _create_genesis_info(self) -> None: total=self._settings.GENESIS_TOKENS, ) + def _get_value_info(self, token_uid: bytes, *, create_default: bool = True) -> _InfoDict: + key_info = self._to_key_info(token_uid) + value_info = self._db.get((self._cf, key_info)) + if token_uid == self._settings.HATHOR_TOKEN_UID and value_info is None: + self._create_genesis_info() + value_info = self._db.get((self._cf, key_info)) + elif create_default and value_info is None: + self._create_empty_info(token_uid) + value_info = self._db.get((self._cf, key_info)) + assert value_info is not None + dict_info = self._from_value_info(value_info, token_uid) + return dict_info + @override def add_to_total(self, token_uid: bytes, amount: int) -> None: + dict_info = self._get_value_info(token_uid, create_default=True) + dict_info.total += amount key_info = self._to_key_info(token_uid) - old_value_info = self._db.get((self._cf, key_info)) - if token_uid == self._settings.HATHOR_TOKEN_UID and old_value_info is None: - self._create_genesis_info() - old_value_info = self._db.get((self._cf, key_info)) - assert old_value_info is not None - dict_info = self._from_value_info(old_value_info, token_uid) - dict_info['total'] += amount new_value_info = self._to_value_info(dict_info) self._db.put((self._cf, key_info), new_value_info) @@ -451,6 +492,9 @@ def iter_all_tokens(self) -> Iterator[tuple[bytes, TokenIndexInfo]]: self.log.debug('seek found', token=key_any.token_uid_internal.hex()) token_uid = from_internal_token_uid(key_any.token_uid_internal) info = self._from_value_info(value, token_uid) + if info.is_unknown(): + # Skip unknown tokens. + continue token_index_info = RocksDBTokenIndexInfo(self, token_uid, info) yield token_uid, token_index_info self.log.debug('seek end') @@ -461,15 +505,14 @@ def get_token_info(self, token_uid: bytes) -> TokenIndexInfo: if value is None: raise KeyError('unknown token') info = self._from_value_info(value, token_uid) + if info.is_unknown(): + raise KeyError('unknown token') return RocksDBTokenIndexInfo(self, token_uid, info) @override def update_authorities_from_contract(self, record: UpdateAuthoritiesRecord, undo: bool = False) -> None: assert record.token_uid != self._settings.HATHOR_TOKEN_UID - key_info = self._to_key_info(record.token_uid) - old_value_info = self._db.get((self._cf, key_info)) - assert old_value_info is not None - dict_info = self._from_value_info(old_value_info, record.token_uid) + dict_info = self._get_value_info(record.token_uid) increment: int match record.sub_type: @@ -484,12 +527,14 @@ def update_authorities_from_contract(self, record: UpdateAuthoritiesRecord, undo increment *= -1 if record.mint: - dict_info['n_contracts_can_mint'] += increment + dict_info.n_contracts_can_mint += increment if record.melt: - dict_info['n_contracts_can_melt'] += increment + dict_info.n_contracts_can_melt += increment - assert dict_info['n_contracts_can_mint'] >= 0 - assert dict_info['n_contracts_can_melt'] >= 0 + assert dict_info.n_contracts_can_mint >= 0 + assert dict_info.n_contracts_can_melt >= 0 + + key_info = self._to_key_info(record.token_uid) new_value_info = self._to_value_info(dict_info) self._db.put((self._cf, key_info), new_value_info) @@ -556,16 +601,17 @@ def __init__(self, index: RocksDBTokensIndex, token_uid: bytes, info: _InfoDict) self._info = info def get_name(self) -> Optional[str]: - return self._info['name'] + return self._info.name def get_symbol(self) -> Optional[str]: - return self._info['symbol'] + return self._info.symbol def get_version(self) -> TokenVersion: - return self._info['version'] + assert self._info.version is not None + return self._info.version def get_total(self) -> int: - return self._info['total'] + return self._info.total def _iter_authority_utxos(self, *, is_mint: bool) -> Iterator[TokenUtxoInfo]: it = self._index._db.iterkeys(self._index._cf) @@ -589,8 +635,8 @@ def iter_melt_utxos(self) -> Iterator[TokenUtxoInfo]: @override def can_mint(self) -> bool: - return any(self.iter_mint_utxos()) or self._info['n_contracts_can_mint'] > 0 + return any(self.iter_mint_utxos()) or self._info.n_contracts_can_mint > 0 @override def can_melt(self) -> bool: - return any(self.iter_melt_utxos()) or self._info['n_contracts_can_melt'] > 0 + return any(self.iter_melt_utxos()) or self._info.n_contracts_can_melt > 0 diff --git a/tests/dag_builder/test_dag_builder.py b/tests/dag_builder/test_dag_builder.py index 3bdb1a8f7..a01badeb6 100644 --- a/tests/dag_builder/test_dag_builder.py +++ b/tests/dag_builder/test_dag_builder.py @@ -522,3 +522,26 @@ def test_duplicate_balance(self) -> None: tx1.balance_HTR = 1 tx1.balance_HTR = 2 ''') + + def test_token_id(self) -> None: + token_id = b'y' * 32 + blueprint_id = b'x' * 32 + self.nc_catalog.blueprints[blueprint_id] = MyBlueprint + artifacts = self.dag_builder.build_from_str(f''' + blockchain genesis b[1..11] + b10 < dummy + + TKA.token_id = "{token_id.hex()}" + + tx1.nc_id = "{blueprint_id.hex()}" + tx1.nc_method = initialize(0) + tx1.nc_withdrawal = 123 TKA + + tx1 <-- b11 + ''') + artifacts.propagate_with(self.manager) + + tx1 = artifacts.get_typed_vertex('tx1', Transaction) + assert set(tx1.tokens) == {token_id} + assert 'TKA' not in artifacts.by_name + assert tx1.get_metadata().nc_execution is NCExecutionState.FAILURE diff --git a/tests/nanocontracts/test_token_creation2.py b/tests/nanocontracts/test_token_creation2.py new file mode 100644 index 000000000..08c15e8b9 --- /dev/null +++ b/tests/nanocontracts/test_token_creation2.py @@ -0,0 +1,213 @@ +# Copyright 2025 Hathor Labs +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from hathor import Blueprint, Context, ContractId, NCActionType, public +from hathor.nanocontracts.utils import derive_child_token_id +from hathor.transaction import Block, Transaction, TxOutput +from hathor.transaction.headers.nano_header import NanoHeaderAction +from hathor.transaction.nc_execution_state import NCExecutionState +from tests.dag_builder.builder import TestDAGBuilder +from tests.nanocontracts.blueprints.unittest import BlueprintTestCase + + +class MyBlueprint(Blueprint): + @public(allow_deposit=True, allow_withdrawal=True) + def initialize(self, ctx: Context) -> None: + pass + + @public(allow_withdrawal=True) + def create_deposit_token(self, ctx: Context) -> None: + self.syscall.create_deposit_token( + token_name='deposit-based token', + token_symbol='DBT', + amount=100, + ) + + @public(allow_withdrawal=True) + def nop(self, ctx: Context) -> None: + pass + + +class TokenCreationTestCase(BlueprintTestCase): + def setUp(self) -> None: + super().setUp() + self.blueprint_id = self._register_blueprint_class(MyBlueprint) + self.dag_builder = TestDAGBuilder.from_manager(self.manager) + + def test_create_dbt_and_withdraw_on_another_tx(self) -> None: + artifacts = self.dag_builder.build_from_str(f''' + blockchain genesis b[1..13] + b10 < dummy + + tx1.nc_id = "{self.blueprint_id.hex()}" + tx1.nc_method = initialize() + tx1.nc_deposit = 1 HTR + + tx2.nc_id = tx1 + tx2.nc_method = create_deposit_token() + + tx3.nc_id = tx1 + tx3.nc_method = nop() + + tx1 < b11 < tx2 < b12 < tx3 < b13 + tx1 <-- b11 + tx2 <-- b12 + tx3 <-- b13 + ''') + + b11, b12, b13 = artifacts.get_typed_vertices(('b11', 'b12', 'b13'), Block) + tx1, tx2, tx3 = artifacts.get_typed_vertices(('tx1', 'tx2', 'tx3'), Transaction) + + dbt_id = derive_child_token_id(ContractId(tx1.hash), token_symbol='DBT') + tx3.tokens.append(dbt_id) + + dbt_output = TxOutput(value=100, script=b'', token_data=1) + tx3.outputs.append(dbt_output) + + dbt_withdraw = NanoHeaderAction(type=NCActionType.WITHDRAWAL, token_index=1, amount=100) + tx3_nano_header = tx3.get_nano_header() + tx3_nano_header.nc_actions.append(dbt_withdraw) + + artifacts.propagate_with(self.manager, up_to='b11') + assert tx1.get_metadata().first_block == b11.hash + assert tx1.get_metadata().nc_execution is NCExecutionState.SUCCESS + assert tx1.get_metadata().voided_by is None + + artifacts.propagate_with(self.manager, up_to='b12') + assert tx2.get_metadata().first_block == b12.hash + assert tx2.get_metadata().nc_execution is NCExecutionState.SUCCESS + assert tx2.get_metadata().voided_by is None + + artifacts.propagate_with(self.manager, up_to='b13') + assert tx3.get_metadata().first_block == b13.hash + assert tx3.get_metadata().nc_execution is NCExecutionState.SUCCESS + assert tx3.get_metadata().voided_by is None + + def test_create_dbt_and_withdraw_on_another_tx_before_block(self) -> None: + artifacts = self.dag_builder.build_from_str(f''' + blockchain genesis b[1..13] + b10 < dummy + + tx1.nc_id = "{self.blueprint_id.hex()}" + tx1.nc_method = initialize() + tx1.nc_deposit = 1 HTR + + tx2.nc_id = tx1 + tx2.nc_method = create_deposit_token() + + tx3.nc_id = tx1 + tx3.nc_method = nop() + + tx1 < b11 < tx2 < tx3 < b12 < b13 + tx1 <-- b11 + tx2 <-- b12 + tx3 <-- b13 + ''') + + b11, b12, b13 = artifacts.get_typed_vertices(('b11', 'b12', 'b13'), Block) + tx1, tx2, tx3 = artifacts.get_typed_vertices(('tx1', 'tx2', 'tx3'), Transaction) + + dbt_id = derive_child_token_id(ContractId(tx1.hash), token_symbol='DBT') + tx3.tokens.append(dbt_id) + + dbt_output = TxOutput(value=100, script=b'', token_data=1) + tx3.outputs.append(dbt_output) + + dbt_withdraw = NanoHeaderAction(type=NCActionType.WITHDRAWAL, token_index=1, amount=100) + tx3_nano_header = tx3.get_nano_header() + tx3_nano_header.nc_actions.append(dbt_withdraw) + + artifacts.propagate_with(self.manager, up_to='b11') + assert tx1.get_metadata().first_block == b11.hash + assert tx1.get_metadata().nc_execution is NCExecutionState.SUCCESS + assert tx1.get_metadata().voided_by is None + + artifacts.propagate_with(self.manager, up_to='b12') + assert tx2.get_metadata().first_block == b12.hash + assert tx2.get_metadata().nc_execution is NCExecutionState.SUCCESS + assert tx2.get_metadata().voided_by is None + + artifacts.propagate_with(self.manager, up_to='b13') + assert tx3.get_metadata().first_block == b13.hash + assert tx3.get_metadata().nc_execution is NCExecutionState.SUCCESS + assert tx3.get_metadata().voided_by is None + + def test_create_dbt_and_withdraw_on_same_tx(self) -> None: + artifacts = self.dag_builder.build_from_str(f''' + blockchain genesis b[1..12] + b10 < dummy + + tx1.nc_id = "{self.blueprint_id.hex()}" + tx1.nc_method = initialize() + tx1.nc_deposit = 1 HTR + + tx2.nc_id = tx1 + tx2.nc_method = create_deposit_token() + + tx1 < b11 < tx2 < b12 + tx1 <-- b11 + tx2 <-- b12 + ''') + + b11, b12 = artifacts.get_typed_vertices(('b11', 'b12'), Block) + tx1, tx2 = artifacts.get_typed_vertices(('tx1', 'tx2'), Transaction) + + dbt_id = derive_child_token_id(ContractId(tx1.hash), token_symbol='DBT') + tx2.tokens.append(dbt_id) + + dbt_output = TxOutput(value=100, script=b'', token_data=1) + tx2.outputs.append(dbt_output) + + dbt_withdraw = NanoHeaderAction(type=NCActionType.WITHDRAWAL, token_index=1, amount=100) + tx2_nano_header = tx2.get_nano_header() + tx2_nano_header.nc_actions.append(dbt_withdraw) + + artifacts.propagate_with(self.manager, up_to='b11') + assert tx1.get_metadata().first_block == b11.hash + assert tx1.get_metadata().nc_execution is NCExecutionState.SUCCESS + assert tx1.get_metadata().voided_by is None + + artifacts.propagate_with(self.manager, up_to='b12') + assert tx2.get_metadata().first_block == b12.hash + assert tx2.get_metadata().nc_execution is NCExecutionState.SUCCESS + assert tx2.get_metadata().voided_by is None + + def test_withdraw_nonexistent_token(self) -> None: + artifacts = self.dag_builder.build_from_str(f''' + blockchain genesis b[1..11] + b10 < dummy + + tx1.nc_id = "{self.blueprint_id.hex()}" + tx1.nc_method = initialize() + + tx1 <-- b11 + ''') + + b11, = artifacts.get_typed_vertices(('b11',), Block) + tx1, = artifacts.get_typed_vertices(('tx1',), Transaction) + + fake_token_id = self.gen_random_token_uid() + tx1.tokens.append(fake_token_id) + + fake_output = TxOutput(value=100, script=b'', token_data=1) + tx1.outputs.append(fake_output) + + fake_withdraw = NanoHeaderAction(type=NCActionType.WITHDRAWAL, token_index=1, amount=100) + tx1_nano_header = tx1.get_nano_header() + tx1_nano_header.nc_actions.append(fake_withdraw) + + artifacts.propagate_with(self.manager, up_to='b11') + assert tx1.get_metadata().first_block == b11.hash + assert tx1.get_metadata().nc_execution is NCExecutionState.FAILURE + assert tx1.get_metadata().voided_by is not None