Skip to content

Commit 9b52260

Browse files
committed
feat(nano): add Nano Runtime V2 feature
1 parent 3d034fb commit 9b52260

File tree

17 files changed

+256
-11
lines changed

17 files changed

+256
-11
lines changed

hathor/consensus/consensus.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ def __init__(
106106
runner_factory=runner_factory,
107107
nc_storage_factory=nc_storage_factory,
108108
nc_calls_sorter=nc_calls_sorter,
109+
feature_service=feature_service,
109110
)
110111

111112
# Create NCConsensusBlockExecutor (with side effects) for consensus
@@ -456,6 +457,9 @@ def _feature_activation_rules(self, tx: Transaction, new_best_block: Block) -> b
456457
case Feature.OPCODES_V2:
457458
if not self._opcodes_v2_activation_rule(tx):
458459
return False
460+
case Feature.NANO_RUNTIME_V2:
461+
# This feature does not affect verification, only the Nano runtime.
462+
pass
459463
case (
460464
Feature.INCREASE_MAX_MERKLE_PATH_LENGTH
461465
| Feature.NOP_FEATURE_1

hathor/dag_builder/vertex_exporter.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,8 @@ def create_vertex_block(self, node: DAGNode) -> Block:
283283
blk.weight = float(node.attrs['weight'])
284284
else:
285285
blk.weight = self._daa.calculate_block_difficulty(blk, self.get_parent_block)
286+
if 'signal_bits' in node.attrs:
287+
blk.signal_bits = int(node.attrs['signal_bits'])
286288
self.update_vertex_hash(blk)
287289
self._block_height[blk.hash] = height
288290
return blk

hathor/feature_activation/feature.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,4 @@ class Feature(StrEnum):
3333
NANO_CONTRACTS = 'NANO_CONTRACTS'
3434
FEE_TOKENS = 'FEE_TOKENS'
3535
OPCODES_V2 = 'OPCODES_V2'
36+
NANO_RUNTIME_V2 = 'NANO_RUNTIME_V2'

hathor/feature_activation/utils.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
from hathor.feature_activation.feature import Feature
2121
from hathor.feature_activation.model.feature_state import FeatureState
22+
from hathor.nanocontracts.nano_runtime_version import NanoRuntimeVersion
2223
from hathor.transaction.scripts.opcode import OpcodesVersion
2324

2425
if TYPE_CHECKING:
@@ -36,6 +37,7 @@ class Features:
3637
nanocontracts: bool
3738
fee_tokens: bool
3839
opcodes_version: OpcodesVersion
40+
nano_runtime_version: NanoRuntimeVersion
3941

4042
@staticmethod
4143
def from_vertex(*, settings: HathorSettings, feature_service: FeatureService, vertex: Vertex) -> Features:
@@ -47,6 +49,7 @@ def from_vertex(*, settings: HathorSettings, feature_service: FeatureService, ve
4749
Feature.NANO_CONTRACTS: settings.ENABLE_NANO_CONTRACTS,
4850
Feature.FEE_TOKENS: settings.ENABLE_FEE_BASED_TOKENS,
4951
Feature.OPCODES_V2: settings.ENABLE_OPCODES_V2,
52+
Feature.NANO_RUNTIME_V2: settings.ENABLE_NANO_RUNTIME_V2,
5053
}
5154

5255
feature_is_active: dict[Feature, bool] = {
@@ -55,12 +58,16 @@ def from_vertex(*, settings: HathorSettings, feature_service: FeatureService, ve
5558
}
5659

5760
opcodes_version = OpcodesVersion.V2 if feature_is_active[Feature.OPCODES_V2] else OpcodesVersion.V1
61+
nano_runtime_version = (
62+
NanoRuntimeVersion.V2 if feature_is_active[Feature.NANO_RUNTIME_V2] else NanoRuntimeVersion.V1
63+
)
5864

5965
return Features(
6066
count_checkdatasig_op=feature_is_active[Feature.COUNT_CHECKDATASIG_OP],
6167
nanocontracts=feature_is_active[Feature.NANO_CONTRACTS],
6268
fee_tokens=feature_is_active[Feature.FEE_TOKENS],
6369
opcodes_version=opcodes_version,
70+
nano_runtime_version=nano_runtime_version,
6471
)
6572

6673
@staticmethod
@@ -69,20 +76,23 @@ def for_mempool(*, settings: HathorSettings, feature_service: FeatureService, be
6976
Used for mempool verification.
7077
7178
Features can either be more restrictive (for example, `count_checkdatasig_op`) or more permissive
72-
(for example, `nanocontracts`).
79+
(for example, `nanocontracts`) in relation to vertex verification. When a feature doesn't affect
80+
verification, such as changes to the Nano runtime only (`nano_runtime_version`), it is indifferent.
7381
7482
Returns information about each feature where permissive features come from the state in the provided
7583
block, and restrictive features are always enabled. This means restrictive features are applied in
7684
mempool verification regardless of features states in the current best block.
7785
"""
7886
features = Features.from_vertex(settings=settings, feature_service=feature_service, vertex=best_block)
7987
return Features(
80-
# Permissive features (come from the block state):
81-
nanocontracts=features.nanocontracts,
82-
fee_tokens=features.fee_tokens,
8388
# Restrictive features (hardcoded as enabled):
8489
count_checkdatasig_op=True,
8590
opcodes_version=OpcodesVersion.V2,
91+
# Permissive features (come from the block state):
92+
nanocontracts=features.nanocontracts,
93+
fee_tokens=features.fee_tokens,
94+
# Indifferent features (come from the block state):
95+
nano_runtime_version=features.nano_runtime_version,
8696
)
8797

8898
@staticmethod
@@ -91,14 +101,19 @@ def all_enabled() -> Features:
91101
Used mostly for APIs and tests, it disregards the actual state of the blockchain
92102
and hardcodes all features as enabled.
93103
94-
For restrictive features, this means they're restricted on APIs just like in the mempool. For permissive
95-
features, they're allowed on APIs, which is fine since they may be blocked during verification anyway.
104+
- For restrictive features, this means they're restricted on APIs just like in the mempool.
105+
- For permissive features, they're allowed on APIs, which is fine since they may be blocked
106+
during verification anyway.
107+
- For indifferent features, it doesn't matter.
108+
109+
Read the `Features.for_mempool` docstring for more details on these types of features.
96110
"""
97111
return Features(
98112
count_checkdatasig_op=True,
99113
nanocontracts=True,
100114
fee_tokens=True,
101115
opcodes_version=OpcodesVersion.V2,
116+
nano_runtime_version=NanoRuntimeVersion.V2,
102117
)
103118

104119

hathor/manager.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from hathor.execution_manager import ExecutionManager
4444
from hathor.feature_activation.bit_signaling_service import BitSignalingService
4545
from hathor.feature_activation.feature_service import FeatureService
46+
from hathor.feature_activation.utils import Features
4647
from hathor.mining import BlockTemplate, BlockTemplates
4748
from hathor.mining.cpu_mining_service import CpuMiningService
4849
from hathor.nanocontracts.runner import Runner
@@ -410,7 +411,9 @@ def get_nc_runner(self, block: Block) -> Runner:
410411
"""Return a contract runner for a given block."""
411412
nc_storage_factory = self.consensus_algorithm.nc_storage_factory
412413
block_storage = get_block_storage_from_block(nc_storage_factory, block)
414+
features = Features.from_vertex(settings=self._settings, feature_service=self.feature_service, vertex=block)
413415
return self.runner_factory.create(
416+
runtime_version=features.nano_runtime_version,
414417
block_storage=block_storage,
415418
)
416419

hathor/nanocontracts/blueprint_env.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from typing import TYPE_CHECKING, Any, Collection, Sequence, TypeAlias, final
1818

1919
from hathor.conf.settings import HATHOR_TOKEN_UID
20+
from hathor.nanocontracts.nano_settings import NanoSettings
2021
from hathor.nanocontracts.types import Amount, BlueprintId, ContractId, NCAction, NCFee, TokenUid
2122

2223
if TYPE_CHECKING:
@@ -266,3 +267,10 @@ def setup_new_contract(
266267
actions=actions,
267268
fees=fees or (),
268269
)
270+
271+
def get_settings(self) -> NanoSettings:
272+
"""
273+
Return the settings for the current Nano runtime.
274+
Settings are not constant, they may be changed over time.
275+
"""
276+
return self.__runner.syscall_get_nano_settings()

hathor/nanocontracts/execution/block_executor.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@
2121
from dataclasses import dataclass
2222
from typing import TYPE_CHECKING, Iterator
2323

24+
from hathor.feature_activation.utils import Features
2425
from hathor.nanocontracts.exception import NCFail
26+
from hathor.nanocontracts.nano_runtime_version import NanoRuntimeVersion
2527
from hathor.transaction import Block, Transaction
2628
from hathor.transaction.exceptions import TokenNotFound
2729
from hathor.transaction.nc_execution_state import NCExecutionState
2830

2931
if TYPE_CHECKING:
3032
from hathor.conf.settings import HathorSettings
33+
from hathor.feature_activation.feature_service import FeatureService
3134
from hathor.nanocontracts.runner import Runner
3235
from hathor.nanocontracts.runner.runner import RunnerFactory
3336
from hathor.nanocontracts.sorter.types import NCSorterCallable
@@ -113,6 +116,7 @@ def __init__(
113116
runner_factory: 'RunnerFactory',
114117
nc_storage_factory: 'NCStorageFactory',
115118
nc_calls_sorter: 'NCSorterCallable',
119+
feature_service: FeatureService,
116120
) -> None:
117121
"""
118122
Initialize the block executor.
@@ -127,11 +131,9 @@ def __init__(
127131
self._runner_factory = runner_factory
128132
self._nc_storage_factory = nc_storage_factory
129133
self._nc_calls_sorter = nc_calls_sorter
134+
self._feature_service = feature_service
130135

131-
def execute_block(
132-
self,
133-
block: Block,
134-
) -> Iterator[NCBlockEffect]:
136+
def execute_block(self, block: Block) -> Iterator[NCBlockEffect]:
135137
"""Execute block as generator, yielding effects without applying them.
136138
137139
This is the pure execution method that yields lifecycle events as it processes
@@ -169,6 +171,7 @@ def execute_block(
169171

170172
nc_sorted_calls = self._nc_calls_sorter(block, nc_calls) if nc_calls else []
171173
block_storage = self._nc_storage_factory.get_block_storage(parent_root_id)
174+
features = Features.from_vertex(settings=self._settings, feature_service=self._feature_service, vertex=block)
172175

173176
yield NCBeginBlock(
174177
block=block,
@@ -189,6 +192,7 @@ def execute_block(
189192

190193
# Execute transaction and yield the result directly
191194
result = self.execute_transaction(
195+
runtime_version=features.nano_runtime_version,
192196
tx=tx,
193197
block_storage=block_storage,
194198
rng_seed=rng_seed,
@@ -210,6 +214,7 @@ def execute_block(
210214
def execute_transaction(
211215
self,
212216
*,
217+
runtime_version: NanoRuntimeVersion,
213218
tx: Transaction,
214219
block_storage: 'NCBlockStorage',
215220
rng_seed: bytes,
@@ -234,6 +239,7 @@ def execute_transaction(
234239
return NCTxExecutionSkipped(tx=tx)
235240

236241
runner = self._runner_factory.create(
242+
runtime_version=runtime_version,
237243
block_storage=block_storage,
238244
seed=rng_seed,
239245
)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Copyright 2026 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 IntEnum
16+
17+
18+
class NanoRuntimeVersion(IntEnum):
19+
"""
20+
The runtime version of Nano Contracts.
21+
It must be updated via Feature Activation and can be used to add new syscalls, for example.
22+
23+
V1:
24+
- Initial version
25+
26+
V2:
27+
- Added `get_settings` syscall
28+
"""
29+
V1 = 1
30+
V2 = 2
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Copyright 2026 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 dataclasses import dataclass
16+
17+
18+
@dataclass(slots=True, frozen=True, kw_only=True)
19+
class NanoSettings:
20+
"""
21+
This dataclass contains information about the settings used by the current Nano runtime.
22+
It is returned by the `get_settings` syscall. Note that settings are not constant, they may change over time.
23+
"""
24+
fee_per_output: int

hathor/nanocontracts/runner/runner.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747
from hathor.nanocontracts.faux_immutable import create_with_shell
4848
from hathor.nanocontracts.metered_exec import MeteredExecutor
4949
from hathor.nanocontracts.method import Method, ReturnOnly
50+
from hathor.nanocontracts.nano_runtime_version import NanoRuntimeVersion
51+
from hathor.nanocontracts.nano_settings import NanoSettings
5052
from hathor.nanocontracts.rng import NanoRNG
5153
from hathor.nanocontracts.runner.call_info import CallInfo, CallRecord, CallType
5254
from hathor.nanocontracts.runner.index_records import (
@@ -125,6 +127,7 @@ def __init__(
125127
*,
126128
reactor: ReactorProtocol,
127129
settings: HathorSettings,
130+
runtime_version: NanoRuntimeVersion,
128131
tx_storage: TransactionStorage,
129132
storage_factory: NCStorageFactory,
130133
block_storage: NCBlockStorage,
@@ -136,6 +139,7 @@ def __init__(
136139
self._storages: dict[ContractId, NCContractStorage] = {}
137140
self._settings = settings
138141
self.reactor = reactor
142+
self._runtime_version = runtime_version
139143

140144
# For tracking fuel and memory usage
141145
self._initial_fuel = self._settings.NC_INITIAL_FUEL_TO_CALL_METHOD
@@ -1271,6 +1275,18 @@ def syscall_change_blueprint(self, blueprint_id: BlueprintId) -> None:
12711275
nc_storage = self.get_current_changes_tracker()
12721276
nc_storage.set_blueprint_id(blueprint_id)
12731277

1278+
def syscall_get_nano_settings(self) -> NanoSettings:
1279+
"""Get nano settings according to the current runtime."""
1280+
match self._runtime_version:
1281+
case NanoRuntimeVersion.V1:
1282+
raise NCFail('syscall `get_settings` is not yet supported')
1283+
case NanoRuntimeVersion.V2:
1284+
return NanoSettings(
1285+
fee_per_output=self._settings.FEE_PER_OUTPUT,
1286+
)
1287+
case _:
1288+
assert_never(self._runtime_version)
1289+
12741290
def _get_token(self, token_uid: TokenUid) -> TokenDescription:
12751291
"""
12761292
Get a token from the current changes tracker or storage.
@@ -1456,12 +1472,14 @@ def __init__(
14561472
def create(
14571473
self,
14581474
*,
1475+
runtime_version: NanoRuntimeVersion,
14591476
block_storage: NCBlockStorage,
14601477
seed: bytes | None = None,
14611478
) -> Runner:
14621479
return Runner(
14631480
reactor=self.reactor,
14641481
settings=self.settings,
1482+
runtime_version=runtime_version,
14651483
tx_storage=self.tx_storage,
14661484
storage_factory=self.nc_storage_factory,
14671485
block_storage=block_storage,

0 commit comments

Comments
 (0)