Skip to content

Commit 5f6634d

Browse files
committed
fix(nano): non-idempotent indexes handling
1 parent eab464a commit 5f6634d

File tree

4 files changed

+93
-15
lines changed

4 files changed

+93
-15
lines changed

hathor/consensus/block_consensus.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,11 @@ def _nc_execute_calls(self, block: Block, *, is_reorg: bool) -> None:
221221
# Update metadata.
222222
self.nc_update_metadata(tx, runner)
223223

224+
# Update indexes. This must be after metadata is updated.
225+
assert tx.storage is not None
226+
assert tx.storage.indexes is not None
227+
tx.storage.indexes.handle_contract_execution(tx)
228+
224229
# We only emit events when the nc is successfully executed.
225230
assert self.context.nc_events is not None
226231
last_call_info = runner.get_last_call_info()
@@ -685,7 +690,7 @@ def remove_first_block_markers(self, block: Block) -> None:
685690
if meta.nc_execution is NCExecutionState.SUCCESS:
686691
assert tx.storage is not None
687692
assert tx.storage.indexes is not None
688-
tx.storage.indexes.nc_update_remove(tx)
693+
tx.storage.indexes.handle_contract_unexecution(tx)
689694
meta.nc_execution = NCExecutionState.PENDING
690695
meta.nc_calls = None
691696
meta.first_block = None

hathor/indexes/manager.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -206,14 +206,16 @@ def _manually_initialize(self, tx_storage: 'TransactionStorage') -> None:
206206
def update(self, tx: BaseTransaction) -> None:
207207
""" This is the new update method that indexes should use instead of add_tx/del_tx
208208
"""
209-
self.nc_update_add(tx)
210-
211209
# XXX: this _should_ be here, but it breaks some tests, for now this is done explicitly in hathor.manager
212210
# self.mempool_tips.update(tx)
213211
if self.utxo:
214212
self.utxo.update(tx)
215213

216-
def nc_update_add(self, tx: BaseTransaction) -> None:
214+
def handle_contract_execution(self, tx: BaseTransaction) -> None:
215+
"""
216+
Update indexes according to a Nano Contract execution.
217+
Must only be called once for each time a contract is executed.
218+
"""
217219
from hathor.conf.settings import HATHOR_TOKEN_UID
218220
from hathor.nanocontracts.runner.types import (
219221
NCSyscallRecord,
@@ -223,13 +225,9 @@ def nc_update_add(self, tx: BaseTransaction) -> None:
223225
from hathor.nanocontracts.types import ContractId
224226
from hathor.transaction.nc_execution_state import NCExecutionState
225227

226-
if not tx.is_nano_contract():
227-
return
228-
229228
meta = tx.get_metadata()
230-
if meta.nc_execution != NCExecutionState.SUCCESS:
231-
return
232-
229+
assert tx.is_nano_contract()
230+
assert meta.nc_execution is NCExecutionState.SUCCESS
233231
assert meta.nc_calls
234232
first_call = meta.nc_calls[0]
235233
nc_syscalls: list[NCSyscallRecord] = []
@@ -280,7 +278,11 @@ def nc_update_add(self, tx: BaseTransaction) -> None:
280278
case _:
281279
assert_never(syscall)
282280

283-
def nc_update_remove(self, tx: BaseTransaction) -> None:
281+
def handle_contract_unexecution(self, tx: BaseTransaction) -> None:
282+
"""
283+
Update indexes according to a Nano Contract unexecution, which happens when a reorg unconfirms a nano tx.
284+
Must only be called once for each time a contract is unexecuted.
285+
"""
284286
from hathor.conf.settings import HATHOR_TOKEN_UID
285287
from hathor.nanocontracts.runner.types import (
286288
NCSyscallRecord,
@@ -289,10 +291,8 @@ def nc_update_remove(self, tx: BaseTransaction) -> None:
289291
)
290292
from hathor.nanocontracts.types import NC_INITIALIZE_METHOD, ContractId
291293

292-
if not tx.is_nano_contract():
293-
return
294-
295294
meta = tx.get_metadata()
295+
assert tx.is_nano_contract()
296296
assert meta.nc_execution is NCExecutionState.SUCCESS
297297
assert meta.nc_calls
298298
first_call = meta.nc_calls[0]

hathor/transaction/nc_execution_state.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@
1818
@unique
1919
class NCExecutionState(StrEnum):
2020
PENDING = auto() # aka, not even tried to execute it
21-
SUCCESS = auto() # execution was sucessful
21+
SUCCESS = auto() # execution was successful
2222
FAILURE = auto() # execution failed and the transaction is voided
2323
SKIPPED = auto() # execution was skipped, usually because the transaction was voided

tests/nanocontracts/test_indexes2.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Copyright 2025 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 hathor.conf.settings import HATHOR_TOKEN_UID
16+
from hathor.nanocontracts import Blueprint, Context, public
17+
from hathor.nanocontracts.types import ContractId, VertexId
18+
from hathor.nanocontracts.utils import derive_child_token_id
19+
from hathor.transaction import Transaction
20+
from hathor.transaction.nc_execution_state import NCExecutionState
21+
from hathor.transaction.util import get_deposit_amount
22+
from tests.dag_builder.builder import TestDAGBuilder
23+
from tests.nanocontracts.blueprints.unittest import BlueprintTestCase
24+
25+
26+
class MyBlueprint(Blueprint):
27+
@public(allow_deposit=True)
28+
def initialize(self, ctx: Context, amount: int) -> None:
29+
self.syscall.create_token(token_name='token a', token_symbol='TKA', amount=amount)
30+
31+
32+
class TestIndexes2(BlueprintTestCase):
33+
def setUp(self) -> None:
34+
super().setUp()
35+
36+
assert self.manager.tx_storage.indexes is not None
37+
assert self.manager.tx_storage.indexes.tokens is not None
38+
self.tokens_index = self.manager.tx_storage.indexes.tokens
39+
40+
self.blueprint_id = self.gen_random_blueprint_id()
41+
self.dag_builder = TestDAGBuilder.from_manager(self.manager)
42+
self.register_blueprint_class(self.blueprint_id, MyBlueprint)
43+
44+
def test_indexes(self) -> None:
45+
amount = 10000
46+
artifacts = self.dag_builder.build_from_str(f'''
47+
blockchain genesis b[1..11]
48+
b10 < dummy
49+
50+
tx1.nc_id = "{self.blueprint_id.hex()}"
51+
tx1.nc_method = initialize({amount})
52+
tx1.nc_deposit = 1000 HTR
53+
54+
tx1.out[0] <<< tx2
55+
56+
tx1 <-- b11
57+
b11 < tx2
58+
''')
59+
artifacts.propagate_with(self.manager)
60+
61+
tx1, = artifacts.get_typed_vertices(['tx1'], Transaction)
62+
tka = derive_child_token_id(ContractId(VertexId(tx1.hash)), 'TKA')
63+
64+
tka_token_info = self.tokens_index.get_token_info(tka)
65+
htr_token_info = self.tokens_index.get_token_info(HATHOR_TOKEN_UID)
66+
67+
assert tx1.get_metadata().nc_execution is NCExecutionState.SUCCESS
68+
assert tka_token_info.get_total() == amount
69+
assert htr_token_info.get_total() == (
70+
self._settings.GENESIS_TOKENS
71+
+ 11 * self._settings.INITIAL_TOKENS_PER_BLOCK
72+
- get_deposit_amount(self._settings, amount)
73+
)

0 commit comments

Comments
 (0)