Skip to content

Commit 0806806

Browse files
committed
refactor(nano): implement BlueprintService
1 parent 2d949c7 commit 0806806

Some content is hidden

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

59 files changed

+308
-272
lines changed

hathor/builder/builder.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +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.catalog import NCBlueprintCatalog
38+
from hathor.nanocontracts.blueprint_service import BlueprintService
3939
from hathor.nanocontracts.nc_exec_logs import NCLogConfig, NCLogStorage
4040
from hathor.nanocontracts.runner.runner import RunnerFactory
4141
from hathor.nanocontracts.sorter.types import NCSorterCallable
@@ -110,7 +110,7 @@ class BuildArtifacts(NamedTuple):
110110

111111

112112
_VertexVerifiersBuilder: TypeAlias = Callable[
113-
[Reactor, HathorSettingsType, DifficultyAdjustmentAlgorithm, FeatureService, TransactionStorage],
113+
[Reactor, HathorSettingsType, DifficultyAdjustmentAlgorithm, FeatureService, TransactionStorage, BlueprintService],
114114
VertexVerifiers
115115
]
116116

@@ -199,6 +199,7 @@ def __init__(self) -> None:
199199
self._nc_log_config: NCLogConfig = NCLogConfig.NONE
200200

201201
self._vertex_json_serializer: VertexJsonSerializer | None = None
202+
self._blueprint_service: BlueprintService | None = None
202203

203204
def build(self) -> BuildArtifacts:
204205
if self.artifacts is not None:
@@ -233,9 +234,7 @@ def build(self) -> BuildArtifacts:
233234
poa_block_producer = self._get_or_create_poa_block_producer()
234235
runner_factory = self._get_or_create_runner_factory()
235236
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()
237+
blueprint_service = self._get_or_create_blueprint_service()
239238

240239
if self._enable_address_index:
241240
indexes.enable_address_index(pubsub)
@@ -279,6 +278,7 @@ def build(self) -> BuildArtifacts:
279278
runner_factory=runner_factory,
280279
feature_service=feature_service,
281280
vertex_json_serializer=vertex_json_serializer,
281+
blueprint_service=blueprint_service,
282282
**kwargs
283283
)
284284

@@ -426,18 +426,14 @@ def _get_or_create_consensus(self) -> ConsensusAlgorithm:
426426

427427
return self._consensus
428428

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-
434429
def _get_or_create_runner_factory(self) -> RunnerFactory:
435430
if self._runner_factory is None:
436431
self._runner_factory = RunnerFactory(
437432
reactor=self._get_reactor(),
438433
settings=self._get_or_create_settings(),
439434
tx_storage=self._get_or_create_tx_storage(),
440435
nc_storage_factory=self._get_or_create_nc_storage_factory(),
436+
blueprint_service=self._get_or_create_blueprint_service(),
441437
)
442438
return self._runner_factory
443439

@@ -611,14 +607,16 @@ def _get_or_create_vertex_verifiers(self) -> VertexVerifiers:
611607
feature_service = self._get_or_create_feature_service()
612608
daa = self._get_or_create_daa()
613609
tx_storage = self._get_or_create_tx_storage()
610+
blueprint_service = self._get_or_create_blueprint_service()
614611

615612
if self._vertex_verifiers_builder:
616613
self._vertex_verifiers = self._vertex_verifiers_builder(
617614
reactor,
618615
settings,
619616
daa,
620617
feature_service,
621-
tx_storage
618+
tx_storage,
619+
blueprint_service,
622620
)
623621
else:
624622
self._vertex_verifiers = VertexVerifiers.create_defaults(
@@ -627,6 +625,7 @@ def _get_or_create_vertex_verifiers(self) -> VertexVerifiers:
627625
daa=daa,
628626
feature_service=feature_service,
629627
tx_storage=tx_storage,
628+
blueprint_service=blueprint_service,
630629
)
631630

632631
return self._vertex_verifiers
@@ -684,13 +683,25 @@ def _get_or_create_vertex_json_serializer(self) -> VertexJsonSerializer:
684683
if self._vertex_json_serializer is None:
685684
tx_storage = self._get_or_create_tx_storage()
686685
nc_log_storage = self._get_or_create_nc_log_storage()
686+
blueprint_service = self._get_or_create_blueprint_service()
687687
self._vertex_json_serializer = VertexJsonSerializer(
688688
storage=tx_storage,
689689
nc_log_storage=nc_log_storage,
690+
blueprint_service=blueprint_service,
690691
)
691692

692693
return self._vertex_json_serializer
693694

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

0 commit comments

Comments
 (0)