Skip to content

Commit 3bac0a7

Browse files
committed
refactor(nano): implement BlueprintService
1 parent 9b52260 commit 3bac0a7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+273
-246
lines changed

hathor/builder/builder.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from hathor.manager import HathorManager
3636
from hathor.mining.cpu_mining_service import CpuMiningService
3737
from hathor.nanocontracts import NCRocksDBStorageFactory, NCStorageFactory
38+
from hathor.nanocontracts.blueprint_service import BlueprintService
3839
from hathor.nanocontracts.catalog import NCBlueprintCatalog
3940
from hathor.nanocontracts.nc_exec_logs import NCLogConfig, NCLogStorage
4041
from hathor.nanocontracts.runner.runner import RunnerFactory
@@ -199,6 +200,7 @@ def __init__(self) -> None:
199200
self._nc_log_config: NCLogConfig = NCLogConfig.NONE
200201

201202
self._vertex_json_serializer: VertexJsonSerializer | None = None
203+
self._blueprint_service: BlueprintService | None = None
202204

203205
def build(self) -> BuildArtifacts:
204206
if self.artifacts is not None:
@@ -233,9 +235,7 @@ def build(self) -> BuildArtifacts:
233235
poa_block_producer = self._get_or_create_poa_block_producer()
234236
runner_factory = self._get_or_create_runner_factory()
235237
vertex_json_serializer = self._get_or_create_vertex_json_serializer()
236-
237-
if settings.ENABLE_NANO_CONTRACTS:
238-
tx_storage.nc_catalog = self._get_nc_catalog()
238+
blueprint_service = self._get_or_create_blueprint_service()
239239

240240
if self._enable_address_index:
241241
indexes.enable_address_index(pubsub)
@@ -279,6 +279,7 @@ def build(self) -> BuildArtifacts:
279279
runner_factory=runner_factory,
280280
feature_service=feature_service,
281281
vertex_json_serializer=vertex_json_serializer,
282+
blueprint_service=blueprint_service,
282283
**kwargs
283284
)
284285

@@ -426,11 +427,6 @@ def _get_or_create_consensus(self) -> ConsensusAlgorithm:
426427

427428
return self._consensus
428429

429-
def _get_nc_catalog(self) -> NCBlueprintCatalog:
430-
from hathor.nanocontracts.catalog import generate_catalog_from_settings
431-
settings = self._get_or_create_settings()
432-
return generate_catalog_from_settings(settings)
433-
434430
def _get_or_create_runner_factory(self) -> RunnerFactory:
435431
if self._runner_factory is None:
436432
self._runner_factory = RunnerFactory(
@@ -691,6 +687,16 @@ def _get_or_create_vertex_json_serializer(self) -> VertexJsonSerializer:
691687

692688
return self._vertex_json_serializer
693689

690+
def _get_or_create_blueprint_service(self) -> BlueprintService:
691+
if self._blueprint_service is None:
692+
self._blueprint_service = BlueprintService(
693+
settings=self._get_or_create_settings(),
694+
tx_storage=self._get_or_create_tx_storage(),
695+
feature_service=self._get_or_create_feature_service(),
696+
)
697+
698+
return self._blueprint_service
699+
694700
def set_rocksdb_path(self, path: str | tempfile.TemporaryDirectory) -> 'Builder':
695701
if self._tx_storage:
696702
raise ValueError('cannot set rocksdb path after tx storage is set')

hathor/dag_builder/builder.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,13 @@ def from_manager(
8888
blueprints_module: ModuleType | None = None
8989
) -> DAGBuilder:
9090
"""Create a DAGBuilder instance from a HathorManager instance."""
91-
assert manager.tx_storage.nc_catalog
9291
return DAGBuilder(
9392
settings=manager._settings,
9493
daa=manager.daa,
9594
genesis_wallet=initialize_hd_wallet(genesis_words),
9695
wallet_factory=wallet_factory,
9796
vertex_resolver=lambda x: manager.cpu_mining_service.resolve(x),
98-
nc_catalog=manager.tx_storage.nc_catalog,
97+
nc_catalog=manager.blueprint_service.nc_catalog,
9998
blueprints_module=blueprints_module,
10099
)
101100

hathor/dag_builder/cli.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def main(filename: str, genesis_seed: str) -> None:
2323

2424
from hathor.conf.get_settings import get_global_settings
2525
from hathor.daa import DifficultyAdjustmentAlgorithm
26-
from hathor.nanocontracts.catalog import generate_catalog_from_settings
26+
from hathor.nanocontracts.catalog import NCBlueprintCatalog
2727
from hathor.wallet import HDWallet
2828
settings = get_global_settings()
2929

@@ -37,7 +37,9 @@ def wallet_factory(words=None):
3737

3838
genesis_wallet = wallet_factory(genesis_seed)
3939
daa = DifficultyAdjustmentAlgorithm(settings=settings)
40-
nc_catalog = generate_catalog_from_settings(settings)
40+
nc_catalog = NCBlueprintCatalog()
41+
blueprints = NCBlueprintCatalog.generate_blueprints_from_settings(settings)
42+
nc_catalog.register_blueprints(blueprints)
4143

4244
builder = DAGBuilder(
4345
settings=settings,

hathor/manager.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
from hathor.feature_activation.utils import Features
4747
from hathor.mining import BlockTemplate, BlockTemplates
4848
from hathor.mining.cpu_mining_service import CpuMiningService
49+
from hathor.nanocontracts.blueprint_service import BlueprintService
4950
from hathor.nanocontracts.runner import Runner
5051
from hathor.nanocontracts.runner.runner import RunnerFactory
5152
from hathor.nanocontracts.storage import NCBlockStorage, NCContractStorage, get_block_storage_from_block
@@ -117,6 +118,7 @@ def __init__(
117118
runner_factory: RunnerFactory,
118119
feature_service: FeatureService,
119120
vertex_json_serializer: VertexJsonSerializer,
121+
blueprint_service: BlueprintService,
120122
hostname: Optional[str] = None,
121123
wallet: Optional[BaseWallet] = None,
122124
capabilities: Optional[list[str]] = None,
@@ -207,6 +209,7 @@ def __init__(
207209
self.runner_factory = runner_factory
208210
self.feature_service = feature_service
209211
self.vertex_json_serializer = vertex_json_serializer
212+
self.blueprint_service = blueprint_service
210213

211214
self.websocket_factory = websocket_factory
212215

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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 structlog import get_logger
16+
17+
from hathor import Blueprint
18+
from hathor.conf.settings import HathorSettings
19+
from hathor.feature_activation.feature_service import FeatureService
20+
from hathor.nanocontracts.catalog import NCBlueprintCatalog
21+
from hathor.transaction.storage import TransactionStorage
22+
from hathor.transaction.storage.exceptions import TransactionDoesNotExist
23+
from hathorlib.nanocontracts.types import BlueprintId
24+
from hathor.nanocontracts import OnChainBlueprint
25+
from hathor.nanocontracts.exception import (
26+
BlueprintDoesNotExist,
27+
OCBBlueprintNotConfirmed,
28+
OCBInvalidBlueprintVertexType,
29+
)
30+
31+
logger = get_logger()
32+
33+
34+
class BlueprintService:
35+
__slots__ = ('log', 'settings', 'nc_catalog', 'tx_storage', 'feature_service')
36+
37+
def __init__(
38+
self,
39+
*,
40+
settings: HathorSettings,
41+
tx_storage: TransactionStorage,
42+
feature_service: FeatureService,
43+
) -> None:
44+
self.log = logger.new()
45+
self.settings = settings
46+
self.nc_catalog = NCBlueprintCatalog()
47+
self.tx_storage = tx_storage
48+
self.feature_service = feature_service
49+
50+
if settings.ENABLE_NANO_CONTRACTS:
51+
blueprints = NCBlueprintCatalog.generate_blueprints_from_settings(settings)
52+
self.register_blueprints(blueprints)
53+
54+
def get_on_chain_blueprint(self, blueprint_id: BlueprintId) -> OnChainBlueprint:
55+
"""Return an on-chain blueprint transaction."""
56+
try:
57+
blueprint_tx = self.tx_storage.get_transaction(blueprint_id)
58+
except TransactionDoesNotExist:
59+
self.log.debug('no transaction with the given id found', blueprint_id=blueprint_id.hex())
60+
raise BlueprintDoesNotExist(blueprint_id.hex())
61+
if not isinstance(blueprint_tx, OnChainBlueprint):
62+
raise OCBInvalidBlueprintVertexType(blueprint_id.hex())
63+
tx_meta = blueprint_tx.get_metadata()
64+
if tx_meta.voided_by or not tx_meta.first_block:
65+
raise OCBBlueprintNotConfirmed(blueprint_id.hex())
66+
# XXX: maybe use N blocks confirmation, like reward-locks
67+
return blueprint_tx
68+
69+
def get_blueprint_class(self, blueprint_id: BlueprintId) -> type[Blueprint]:
70+
"""Returns the blueprint class associated with the given blueprint_id.
71+
72+
The blueprint class could be in the catalog (first search), or it could be the tx_id of an on-chain blueprint.
73+
"""
74+
from hathor.nanocontracts import OnChainBlueprint
75+
blueprint = self._get_blueprint(blueprint_id)
76+
if isinstance(blueprint, OnChainBlueprint):
77+
return blueprint.get_blueprint_class()
78+
else:
79+
return blueprint
80+
81+
def get_blueprint_source(self, blueprint_id: BlueprintId) -> str:
82+
"""Returns the source code associated with the given blueprint_id.
83+
84+
The blueprint class could be in the catalog (first search), or it could be the tx_id of an on-chain blueprint.
85+
"""
86+
import inspect
87+
88+
from hathor.nanocontracts import OnChainBlueprint
89+
90+
blueprint = self._get_blueprint(blueprint_id)
91+
if isinstance(blueprint, OnChainBlueprint):
92+
return self.get_on_chain_blueprint(blueprint_id).code.text
93+
else:
94+
module = inspect.getmodule(blueprint)
95+
assert module is not None
96+
return inspect.getsource(module)
97+
98+
def _get_blueprint(self, blueprint_id: BlueprintId) -> type[Blueprint] | OnChainBlueprint:
99+
if blueprint_class := self.nc_catalog.get_blueprint_class(blueprint_id):
100+
return blueprint_class
101+
102+
self.log.debug(
103+
'blueprint_id not in the catalog, looking for on-chain blueprint',
104+
blueprint_id=blueprint_id.hex()
105+
)
106+
return self.get_on_chain_blueprint(blueprint_id)
107+
108+
def register_blueprint(self, blueprint_id: bytes, blueprint: type[Blueprint], *, strict: bool = False) -> None:
109+
self.nc_catalog.register_blueprints({blueprint_id: blueprint}, strict=strict)
110+
111+
def register_blueprints(self, blueprints: dict[bytes, type[Blueprint]], *, strict: bool = False) -> None:
112+
self.nc_catalog.register_blueprints(blueprints, strict=strict)
Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +0,0 @@
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 typing import TYPE_CHECKING, Type
16-
17-
if TYPE_CHECKING:
18-
from hathor.nanocontracts.blueprint import Blueprint
19-
20-
21-
_blueprints_mapper: dict[str, Type['Blueprint']] = {}

hathor/nanocontracts/catalog.py

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,31 +12,46 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from typing import TYPE_CHECKING, Type
15+
from __future__ import annotations
16+
17+
from typing import TYPE_CHECKING
1618

17-
from hathor.nanocontracts.blueprints import _blueprints_mapper
1819
from hathor.nanocontracts.types import BlueprintId
1920

2021
if TYPE_CHECKING:
2122
from hathor.conf.settings import HathorSettings
2223
from hathor.nanocontracts.blueprint import Blueprint
2324

2425

26+
_BLUEPRINTS_MAPPER: dict[str, type[Blueprint]] = {}
27+
28+
2529
class NCBlueprintCatalog:
2630
"""Catalog of blueprints available."""
31+
__slots__ = ('_blueprints',)
2732

28-
def __init__(self, blueprints: dict[bytes, Type['Blueprint']]) -> None:
29-
self.blueprints = blueprints
33+
def __init__(self) -> None:
34+
self._blueprints: dict[bytes, type[Blueprint]] = {}
3035

31-
def get_blueprint_class(self, blueprint_id: BlueprintId) -> Type['Blueprint'] | None:
36+
def get_blueprint_class(self, blueprint_id: BlueprintId) -> type[Blueprint] | None:
3237
"""Return the blueprint class related to the given blueprint id or None if it doesn't exist."""
33-
return self.blueprints.get(blueprint_id, None)
34-
35-
36-
def generate_catalog_from_settings(settings: 'HathorSettings') -> NCBlueprintCatalog:
37-
"""Generate a catalog of blueprints based on the provided settings."""
38-
assert settings.ENABLE_NANO_CONTRACTS
39-
blueprints: dict[bytes, Type['Blueprint']] = {}
40-
for _id, _name in settings.BLUEPRINTS.items():
41-
blueprints[_id] = _blueprints_mapper[_name]
42-
return NCBlueprintCatalog(blueprints)
38+
return self._blueprints.get(blueprint_id)
39+
40+
def register_blueprints(self, blueprints: dict[bytes, type[Blueprint]], *, strict: bool = False) -> None:
41+
if strict:
42+
for blueprint_id in blueprints:
43+
if blueprint := self._blueprints[blueprint_id]:
44+
raise ValueError(f'Blueprint {blueprint_id} is already registered: {blueprint.__name__}')
45+
self._blueprints.update(blueprints)
46+
47+
def get_all(self) -> dict[bytes, type[Blueprint]]:
48+
return dict(self._blueprints)
49+
50+
@staticmethod
51+
def generate_blueprints_from_settings(settings: HathorSettings) -> dict[bytes, type[Blueprint]]:
52+
"""Generate a map of blueprints based on the provided settings."""
53+
assert settings.ENABLE_NANO_CONTRACTS
54+
blueprints: dict[bytes, type[Blueprint]] = {}
55+
for id_, name in settings.BLUEPRINTS.items():
56+
blueprints[id_] = _BLUEPRINTS_MAPPER[name]
57+
return blueprints

hathor/nanocontracts/resources/blueprint.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def render_GET(self, request: 'Request') -> bytes:
9292
return error_response.json_dumpb()
9393

9494
try:
95-
blueprint_class = self.manager.tx_storage.get_blueprint_class(blueprint_id)
95+
blueprint_class = self.manager.blueprint_service.get_blueprint_class(blueprint_id)
9696
except BlueprintDoesNotExist:
9797
request.setResponseCode(404)
9898
error_response = ErrorResponse(success=False, error=f'Blueprint not found: {params.blueprint_id}')

hathor/nanocontracts/resources/blueprint_source_code.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,8 @@ def render_GET(self, request: 'Request') -> bytes:
5050
error_response = ErrorResponse(success=False, error=f'Invalid id: {params.blueprint_id}')
5151
return error_response.json_dumpb()
5252

53-
assert self.manager.tx_storage.nc_catalog is not None
54-
5553
try:
56-
blueprint_source = self.manager.tx_storage.get_blueprint_source(blueprint_id)
54+
blueprint_source = self.manager.blueprint_service.get_blueprint_source(blueprint_id)
5755
except OCBBlueprintNotConfirmed:
5856
request.setResponseCode(404)
5957
error_response = ErrorResponse(success=False, error=f'Blueprint not confirmed: {params.blueprint_id}')

hathor/nanocontracts/resources/builtin.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,7 @@ def render_GET(self, request: Request) -> bytes:
5050
success=False, error='Parameters after and before can\'t be used together.')
5151
return error_response.json_dumpb()
5252

53-
assert self.manager.tx_storage.nc_catalog is not None
54-
builtin_bps = list(self.manager.tx_storage.nc_catalog.blueprints.items())
53+
builtin_bps = list(self.manager.blueprint_service.nc_catalog.get_all().items())
5554

5655
filtered_bps = builtin_bps
5756
if params.search:

0 commit comments

Comments
 (0)