Skip to content

Commit a3ee71c

Browse files
committed
fix(nano): Fix voidness clearing during reorg
1 parent afca28d commit a3ee71c

File tree

11 files changed

+355
-13
lines changed

11 files changed

+355
-13
lines changed

hathor/consensus/block_consensus.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@
1313
# limitations under the License.
1414

1515
from itertools import chain
16-
from typing import TYPE_CHECKING, Any, Iterable, Optional, cast
16+
from typing import TYPE_CHECKING, Any, Iterable, Optional, assert_never, cast
1717

1818
from structlog import get_logger
1919

2020
from hathor.conf.get_settings import get_global_settings
2121
from hathor.transaction import BaseTransaction, Block, Transaction
22+
from hathor.transaction.nc_execution_state import NCExecutionState
2223
from hathor.util import classproperty
2324
from hathor.utils.weight import weight_to_work
2425

@@ -140,10 +141,10 @@ def _nc_execute_calls(self, block: Block, *, is_reorg: bool) -> None:
140141
tx_conflict_meta = tx_conflict.get_metadata()
141142
assert tx_conflict_meta.first_block is None
142143
assert tx_conflict_meta.voided_by
144+
self.context.transaction_algorithm.remove_voided_by(tx, tx.hash)
143145
tx_meta.voided_by = None
144146
self.context.save(tx)
145-
if tx_meta.voided_by:
146-
continue
147+
tx_meta.nc_execution = NCExecutionState.PENDING
147148
nc_calls.append(tx)
148149

149150
if not nc_calls:
@@ -160,6 +161,8 @@ def _nc_execute_calls(self, block: Block, *, is_reorg: bool) -> None:
160161
if tx_meta.voided_by:
161162
# Skip voided transactions. This might happen if a previous tx in nc_calls fails and
162163
# mark this tx as voided.
164+
tx_meta.nc_execution = NCExecutionState.SKIPPED
165+
self.context.save(tx)
163166
continue
164167

165168
from hathor.nanocontracts.runner import Runner
@@ -169,6 +172,8 @@ def _nc_execute_calls(self, block: Block, *, is_reorg: bool) -> None:
169172
runner = Runner(tx.storage, storage_factory, block_trie)
170173
try:
171174
tx.execute(runner)
175+
tx_meta.nc_execution = NCExecutionState.SUCCESS
176+
self.context.save(tx)
172177
# TODO Avoid calling multiple commits for the same contract. The best would be to call the commit
173178
# method once per contract per block, just like we do for the block_trie. This ensures we will
174179
# have a clean database with no orphan nodes.
@@ -183,11 +188,28 @@ def _nc_execute_calls(self, block: Block, *, is_reorg: bool) -> None:
183188
meta.nc_block_root_id = block_trie.root.id
184189
self.context.save(block)
185190

191+
for tx in nc_calls:
192+
tx_meta = tx.get_metadata()
193+
assert tx_meta.nc_execution is not None
194+
match tx_meta.nc_execution:
195+
case NCExecutionState.PENDING:
196+
assert False # should never happen
197+
case NCExecutionState.SUCCESS:
198+
assert tx_meta.voided_by is None
199+
case NCExecutionState.FAILURE:
200+
assert tx_meta.voided_by == {tx.hash, self._settings.NC_EXECUTION_FAIL_ID}
201+
case NCExecutionState.SKIPPED:
202+
assert tx_meta.voided_by
203+
assert self._settings.NC_EXECUTION_FAIL_ID not in tx_meta.voided_by
204+
case _:
205+
assert_never(tx_meta.nc_execution)
206+
186207
def mark_as_nc_fail_execution(self, tx: Transaction) -> None:
187208
"""Mark that a transaction failed execution. It also propagates its voidedness through the DAG of funds."""
188209
assert tx.storage is not None
189210
tx_meta = tx.get_metadata()
190211
tx_meta.add_voided_by(self._settings.NC_EXECUTION_FAIL_ID)
212+
tx_meta.nc_execution = NCExecutionState.FAILURE
191213
self.context.save(tx)
192214
self.context.transaction_algorithm.add_voided_by(tx,
193215
tx.hash,

hathor/dag_builder/vertex_exporter.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,7 @@ def create_vertex(self, node: DAGNode) -> BaseTransaction:
417417
assert node.name not in self._vertices
418418
self._vertice_per_id[vertex.hash] = vertex
419419
self._vertices[node.name] = vertex
420+
vertex.name = node.name
420421
return vertex
421422

422423
def export(self) -> Iterator[tuple[DAGNode, BaseTransaction]]:

hathor/nanocontracts/blueprint.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
from structlog import get_logger
2020

21-
from hathor.conf import HathorSettings
2221
from hathor.nanocontracts.fields import get_field_for_attr
2322
from hathor.nanocontracts.storage import NCStorage
2423
from hathor.nanocontracts.types import ContractId, NCAction, TokenUid
@@ -27,7 +26,6 @@
2726
from hathor.nanocontracts.runner import Runner
2827

2928
logger = get_logger()
30-
settings = HathorSettings()
3129

3230

3331
class _BlueprintBase(type):

hathor/nanocontracts/method_parser.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,20 @@
1818

1919
from structlog import get_logger
2020

21-
from hathor.conf import HathorSettings
21+
from hathor.conf.get_settings import get_global_settings
2222
from hathor.nanocontracts.context import Context
2323
from hathor.nanocontracts.exception import NCSerializationArgTooLong, NCSerializationError
2424
from hathor.nanocontracts.serializers import Deserializer, Serializer
2525
from hathor.transaction.util import unpack
2626

2727
logger = get_logger()
28-
settings = HathorSettings()
2928

3029

3130
class NCMethodParser:
3231
"""Utility class to serialize and deserialize method arguments."""
3332
def __init__(self, method: Callable) -> None:
3433
self.method = method
34+
self._settings = get_global_settings()
3535

3636
def get_method_args(self) -> list[tuple[str, Type[Any]]]:
3737
"""Return the list of arguments for the method, including the types."""
@@ -59,7 +59,7 @@ def serialize_args(self, args: list[Any]) -> bytes:
5959
assert len(args) == len(method_args), f'{len(args)} != {len(method_args)} ({method_args})'
6060
for (arg_name, arg_type), arg_value in zip(method_args, args):
6161
arg_bytes = serializer.from_type(arg_type, arg_value)
62-
if len(arg_bytes) > settings.NC_MAX_LENGTH_SERIALIZED_ARG:
62+
if len(arg_bytes) > self._settings.NC_MAX_LENGTH_SERIALIZED_ARG:
6363
raise NCSerializationArgTooLong
6464
ret.append(struct.pack('!H', len(arg_bytes)))
6565
ret.append(arg_bytes)

hathor/nanocontracts/on_chain_blueprint.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from __future__ import annotations
16+
1517
import ast
1618
import zlib
1719
from dataclasses import InitVar, dataclass, field
@@ -24,7 +26,6 @@
2426
from typing_extensions import Self, override
2527

2628
from hathor.conf.get_settings import get_global_settings
27-
from hathor.conf.settings import HathorSettings
2829
from hathor.crypto.util import get_public_key_bytes_compressed
2930
from hathor.nanocontracts.blueprint import Blueprint
3031
from hathor.nanocontracts.exception import OCBOutOfFuelDuringLoading, OCBOutOfMemoryDuringLoading
@@ -34,6 +35,7 @@
3435
from hathor.transaction.util import VerboseCallback, int_to_bytes, unpack, unpack_len
3536

3637
if TYPE_CHECKING:
38+
from hathor.conf.settings import HathorSettings
3739
from hathor.nanocontracts.storage import NCStorage # noqa: F401
3840
from hathor.transaction.storage import TransactionStorage # noqa: F401
3941

hathor/reward_lock/reward_lock.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,14 @@ def iter_spent_rewards(tx: 'Transaction', storage: 'VertexStorageProtocol') -> I
3434
yield spent_tx
3535

3636

37-
def is_spent_reward_locked(settings: HathorSettings, tx: 'Transaction') -> bool:
37+
def is_spent_reward_locked(settings: 'HathorSettings', tx: 'Transaction') -> bool:
3838
""" Check whether any spent reward is currently locked, considering only the block rewards spent by this tx
3939
itself, and not the inherited `min_height`"""
4040
return get_spent_reward_locked_info(settings, tx, not_none(tx.storage)) is not None
4141

4242

4343
def get_spent_reward_locked_info(
44-
settings: HathorSettings,
44+
settings: 'HathorSettings',
4545
tx: 'Transaction',
4646
storage: 'VertexStorageProtocol',
4747
) -> Optional['RewardLockedInfo']:
@@ -71,7 +71,7 @@ def get_minimum_best_height(storage: 'VertexStorageProtocol') -> int:
7171
return best_height
7272

7373

74-
def _spent_reward_needed_height(settings: HathorSettings, block: Block, best_height: int) -> int:
74+
def _spent_reward_needed_height(settings: 'HathorSettings', block: Block, best_height: int) -> int:
7575
""" Returns height still needed to unlock this `block` reward: 0 means it's unlocked."""
7676
spent_height = block.get_height()
7777
spend_blocks = best_height - spent_height

hathor/transaction/base_transaction.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,9 @@ def __init__(
198198
self._hash: VertexId | None = hash # Stored as bytes.
199199
self._static_metadata = None
200200

201+
# A name solely for debugging purposes.
202+
self.name: str | None = None
203+
201204
self.MAX_NUM_INPUTS = self._settings.MAX_NUM_INPUTS
202205
self.MAX_NUM_OUTPUTS = self._settings.MAX_NUM_OUTPUTS
203206

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Copyright 2023 Hathor Labs
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from enum import Enum, unique
16+
17+
18+
@unique
19+
class NCExecutionState(Enum):
20+
PENDING = 'pending' # aka, not even tried to execute it
21+
SUCCESS = 'success' # execution was sucessful
22+
FAILURE = 'failure' # execution failed and the transaction is voided
23+
SKIPPED = 'skipped' # execution was skipped, usually because the transaction was voided

hathor/transaction/transaction_metadata.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from hathor.conf.get_settings import get_global_settings
2121
from hathor.feature_activation.feature import Feature
2222
from hathor.feature_activation.model.feature_state import FeatureState
23+
from hathor.transaction.nc_execution_state import NCExecutionState
2324
from hathor.transaction.validation_state import ValidationState
2425
from hathor.util import json_dumpb, json_loadb, practically_equal
2526
from hathor.utils.weight import work_to_weight
@@ -48,6 +49,7 @@ class TransactionMetadata:
4849

4950
# Used to store the root node id of the contract tree related to this block.
5051
nc_block_root_id: Optional[bytes]
52+
nc_execution: Optional[NCExecutionState]
5153

5254
# A dict of features in the feature activation process and their respective state. Must only be used by Blocks,
5355
# is None otherwise. This is only used for caching, so it can be safely cleared up, as it would be recalculated
@@ -76,6 +78,7 @@ def __init__(
7678
self._tx_ref = None
7779

7880
self.nc_block_root_id = nc_block_root_id
81+
self.nc_execution = None
7982

8083
# Tx outputs that have been spent.
8184
# The key is the output index, while the value is a set of the transactions which spend the output.
@@ -238,6 +241,7 @@ def to_storage_json(self) -> dict[str, Any]:
238241
data['first_block'] = None
239242
data['validation'] = self.validation.name.lower()
240243
data['nc_block_root_id'] = self.nc_block_root_id.hex() if self.nc_block_root_id else None
244+
data['nc_execution'] = self.nc_execution.value if self.nc_execution else None
241245
return data
242246

243247
def to_json(self) -> dict[str, Any]:
@@ -305,6 +309,12 @@ def create_from_json(cls, data: dict[str, Any]) -> 'TransactionMetadata':
305309
else:
306310
meta.nc_block_root_id = None
307311

312+
nc_execution_raw = data.get('nc_execution_raw')
313+
if nc_execution_raw is not None:
314+
meta.nc_execution = NCExecutionState(nc_execution_raw)
315+
else:
316+
meta.nc_execution = None
317+
308318
return meta
309319

310320
@classmethod

0 commit comments

Comments
 (0)