Skip to content

Commit 50c5dbe

Browse files
committed
feat(nano): Add HTTP API and CLI to re-run blocks and nano transactions
1 parent e93c8bc commit 50c5dbe

File tree

13 files changed

+1871
-18
lines changed

13 files changed

+1871
-18
lines changed

hathor/builder/resources_builder.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ def create_resources(self) -> server.Site:
260260
BlueprintSourceCodeResource,
261261
NanoContractHistoryResource,
262262
NanoContractStateResource,
263+
NCDryRunResource,
263264
)
264265
nc_resource = Resource()
265266
root.putChild(b'nano_contract', nc_resource)
@@ -273,6 +274,7 @@ def create_resources(self) -> server.Site:
273274
nc_resource.putChild(b'state', NanoContractStateResource(self.manager))
274275
nc_resource.putChild(b'creation', NCCreationResource(self.manager))
275276
nc_resource.putChild(b'logs', NCExecLogsResource(self.manager))
277+
nc_resource.putChild(b'dry_run', NCDryRunResource(self.manager))
276278

277279
if self._args.enable_debug_api:
278280
debug_resource = Resource()

hathor/nanocontracts/execution/block_executor.py

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@
1919
import hashlib
2020
import traceback
2121
from dataclasses import dataclass
22-
from typing import TYPE_CHECKING, Iterator
22+
from typing import TYPE_CHECKING, Callable, Iterator
2323

2424
from hathor.nanocontracts.exception import NCFail
2525
from hathor.transaction import Block, Transaction
2626
from hathor.transaction.exceptions import TokenNotFound
27-
from hathor.transaction.nc_execution_state import NCExecutionState
27+
28+
# Type alias for the skip predicate
29+
ShouldSkipPredicate = Callable[[Transaction], bool]
2830

2931
if TYPE_CHECKING:
3032
from hathor.conf.settings import HathorSettings
@@ -132,6 +134,8 @@ def __init__(
132134
def execute_block(
133135
self,
134136
block: Block,
137+
*,
138+
should_skip: ShouldSkipPredicate,
135139
) -> Iterator[NCBlockEffect]:
136140
"""Execute block as generator, yielding effects without applying them.
137141
@@ -140,18 +144,17 @@ def execute_block(
140144
141145
Args:
142146
block: The block to execute.
147+
should_skip: A predicate function that determines if a transaction should be skipped.
148+
This allows the caller to provide context-specific voided state tracking.
143149
144150
Yields:
145151
NCBlockEffect instances representing each step of execution.
146152
"""
147-
from hathor.nanocontracts import NC_EXECUTION_FAIL_ID
148-
149153
assert self._settings.ENABLE_NANO_CONTRACTS
150154
assert not block.is_genesis, "Genesis blocks should be handled separately"
151155

152156
meta = block.get_metadata()
153157
assert not meta.voided_by
154-
assert meta.nc_block_root_id is None
155158

156159
parent = block.get_block_parent()
157160
parent_meta = parent.get_metadata()
@@ -162,10 +165,6 @@ def execute_block(
162165
for tx in block.iter_transactions_in_this_block():
163166
if not tx.is_nano_contract():
164167
continue
165-
tx_meta = tx.get_metadata()
166-
assert tx_meta.nc_execution in {None, NCExecutionState.PENDING}
167-
if tx_meta.voided_by:
168-
assert NC_EXECUTION_FAIL_ID not in tx_meta.voided_by
169168
nc_calls.append(tx)
170169

171170
nc_sorted_calls = self._nc_calls_sorter(block, nc_calls) if nc_calls else []
@@ -193,6 +192,7 @@ def execute_block(
193192
tx=tx,
194193
block_storage=block_storage,
195194
rng_seed=rng_seed,
195+
should_skip=should_skip,
196196
)
197197
yield result
198198

@@ -212,18 +212,24 @@ def execute_transaction(
212212
tx: Transaction,
213213
block_storage: 'NCBlockStorage',
214214
rng_seed: bytes,
215+
should_skip: ShouldSkipPredicate,
215216
) -> NCTxExecutionResult:
216217
"""Execute a single NC transaction.
217218
218219
This method is pure and side-effect free. It does not persist anything,
219220
does not call callbacks, and returns all information needed by the caller
220-
to handle success/failure cases."""
221+
to handle success/failure cases.
222+
223+
Args:
224+
tx: The transaction to execute.
225+
block_storage: The block storage for this execution context.
226+
rng_seed: The RNG seed for this transaction.
227+
should_skip: Predicate to determine if transaction should be skipped.
228+
"""
221229
from hathor.nanocontracts.types import Address
222230

223-
tx_meta = tx.get_metadata()
224-
if tx_meta.voided_by:
225-
# Skip voided transactions. This might happen if a previous tx in nc_calls fails and
226-
# mark this tx as voided.
231+
if should_skip(tx):
232+
# Skip transactions based on the caller-provided predicate.
227233
# Check if seqnum needs to be updated.
228234
nc_header = tx.get_nano_header()
229235
nc_address = Address(nc_header.nc_address)

hathor/nanocontracts/execution/consensus_block_executor.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,10 +176,24 @@ def execute_block_and_apply(
176176
self.initialize_empty(block, context)
177177
return
178178

179+
# Verify block hasn't been executed yet
180+
meta = block.get_metadata()
181+
assert meta.nc_block_root_id is None
182+
183+
# Create predicate that reads from database metadata
184+
def should_skip(tx: Transaction) -> bool:
185+
tx_meta = tx.get_metadata()
186+
if tx_meta.voided_by:
187+
# During normal execution, NC_EXECUTION_FAIL_ID should not be in voided_by
188+
# as that is added by the executor itself after a failure
189+
assert NC_EXECUTION_FAIL_ID not in tx_meta.voided_by
190+
return True
191+
return False
192+
179193
# Track NC transactions for final verification (populated from NCBeginBlock)
180194
nc_sorted_calls: list[Transaction] = []
181195

182-
for effect in self._block_executor.execute_block(block):
196+
for effect in self._block_executor.execute_block(block, should_skip=should_skip):
183197
match effect:
184198
case NCBeginBlock(nc_sorted_calls=calls):
185199
nc_sorted_calls = calls
@@ -228,9 +242,10 @@ def _apply_effect(
228242
# Nothing to apply at block start
229243
pass
230244

231-
case NCBeginTransaction():
232-
# Nothing to apply at transaction start
233-
pass
245+
case NCBeginTransaction(tx=tx):
246+
# Verify transaction hasn't been executed yet
247+
tx_meta = tx.get_metadata()
248+
assert tx_meta.nc_execution in {None, NCExecutionState.PENDING}
234249

235250
case NCTxExecutionSuccess(tx=tx, runner=runner):
236251
from hathor.nanocontracts.runner.call_info import CallType

0 commit comments

Comments
 (0)