Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions hathor/builder/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from hathor.reactor import ReactorProtocol as Reactor
from hathor.storage import RocksDBStorage
from hathor.stratum import StratumFactory
from hathor.transaction.json_serializer import VertexJsonSerializer
from hathor.transaction.storage import TransactionCacheStorage, TransactionRocksDBStorage, TransactionStorage
from hathor.transaction.vertex_children import RocksDBVertexChildrenService
from hathor.transaction.vertex_parser import VertexParser
Expand Down Expand Up @@ -196,6 +197,8 @@ def __init__(self) -> None:
self._runner_factory: RunnerFactory | None = None
self._nc_log_config: NCLogConfig = NCLogConfig.NONE

self._vertex_json_serializer: VertexJsonSerializer | None = None

def build(self) -> BuildArtifacts:
if self.artifacts is not None:
raise ValueError('cannot call build twice')
Expand Down Expand Up @@ -228,6 +231,7 @@ def build(self) -> BuildArtifacts:
vertex_parser = self._get_or_create_vertex_parser()
poa_block_producer = self._get_or_create_poa_block_producer()
runner_factory = self._get_or_create_runner_factory()
vertex_json_serializer = self._get_or_create_vertex_json_serializer()

if settings.ENABLE_NANO_CONTRACTS:
tx_storage.nc_catalog = self._get_nc_catalog()
Expand Down Expand Up @@ -273,6 +277,7 @@ def build(self) -> BuildArtifacts:
poa_block_producer=poa_block_producer,
runner_factory=runner_factory,
feature_service=feature_service,
vertex_json_serializer=vertex_json_serializer,
**kwargs
)

Expand Down Expand Up @@ -687,6 +692,17 @@ def _get_or_create_poa_block_producer(self) -> PoaBlockProducer | None:

return self._poa_block_producer

def _get_or_create_vertex_json_serializer(self) -> VertexJsonSerializer:
if self._vertex_json_serializer is None:
tx_storage = self._get_or_create_tx_storage()
nc_log_storage = self._get_or_create_nc_log_storage()
self._vertex_json_serializer = VertexJsonSerializer(
storage=tx_storage,
nc_log_storage=nc_log_storage,
)

return self._vertex_json_serializer

def set_rocksdb_path(self, path: str | tempfile.TemporaryDirectory) -> 'Builder':
if self._tx_storage:
raise ValueError('cannot set rocksdb path after tx storage is set')
Expand Down
8 changes: 7 additions & 1 deletion hathor/consensus/block_consensus.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,13 @@ def _nc_execute_calls(self, block: Block, *, is_reorg: bool) -> None:
# We only emit events when the nc is successfully executed.
assert self.context.nc_events is not None
last_call_info = runner.get_last_call_info()
self.context.nc_events.append((tx, last_call_info.nc_logger.__events__))
events_list = last_call_info.nc_logger.__events__
self.context.nc_events.append((tx, events_list))

# Store events in transaction metadata
if events_list:
tx_meta.nc_events = [(event.nc_id, event.data) for event in events_list]
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a limit to the number of stored events?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this should be stored in metadata since it's completely useless for most full nodes, we could either store it in files just like the nc logs, or for quicker solution, just ignore events and require blueprint devs to write logs when they write events — that is, events would not be available though the APIs, only logs

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can refactor this later.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a limit to the number of stored events?

This is also not problematic while OCB is restricted, so we should review for unreasonably large event generation. We can also improve it after removing it from metadata.

self.context.save(tx)
finally:
# We save logs regardless of whether the nc successfully executed.
self._nc_log_storage.save_logs(tx, runner.get_last_call_info(), exception_and_tb)
Expand Down
3 changes: 3 additions & 0 deletions hathor/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
from hathor.reward_lock import is_spent_reward_locked
from hathor.stratum import StratumFactory
from hathor.transaction import BaseTransaction, Block, MergeMinedBlock, Transaction, TxVersion
from hathor.transaction.json_serializer import VertexJsonSerializer
from hathor.transaction.storage.exceptions import TransactionDoesNotExist
from hathor.transaction.storage.transaction_storage import TransactionStorage
from hathor.transaction.storage.tx_allow_scope import TxAllowScope
Expand Down Expand Up @@ -114,6 +115,7 @@ def __init__(
vertex_parser: VertexParser,
runner_factory: RunnerFactory,
feature_service: FeatureService,
vertex_json_serializer: VertexJsonSerializer,
hostname: Optional[str] = None,
wallet: Optional[BaseWallet] = None,
capabilities: Optional[list[str]] = None,
Expand Down Expand Up @@ -203,6 +205,7 @@ def __init__(
self.vertex_parser = vertex_parser
self.runner_factory = runner_factory
self.feature_service = feature_service
self.vertex_json_serializer = vertex_json_serializer

self.websocket_factory = websocket_factory

Expand Down
29 changes: 27 additions & 2 deletions hathor/nanocontracts/resources/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,15 @@ def render_GET(self, request: 'Request') -> bytes:

count = params.count
has_more = False
history_list = []
history_list: list[dict[str, Any]] = []
for idx, tx_id in enumerate(iter_history):
history_list.append(tx_storage.get_transaction(tx_id).to_json_extended())
tx = tx_storage.get_transaction(tx_id)
tx_json = self.manager.vertex_json_serializer.to_json_extended(
tx,
include_nc_logs=params.include_nc_logs,
include_nc_events=params.include_nc_events,
)
history_list.append(tx_json)
if idx >= count - 1:
# Check if iterator still has more elements
try:
Expand All @@ -129,6 +135,8 @@ class NCHistoryParams(QueryParams):
after: Optional[str]
before: Optional[str]
count: int = Field(default=100, lt=500)
include_nc_logs: bool = Field(default=False)
include_nc_events: bool = Field(default=False)


class NCHistoryResponse(Response):
Expand Down Expand Up @@ -231,6 +239,23 @@ class NCHistoryResponse(Response):
'schema': {
'type': 'string',
}
}, {
'name': 'include_nc_logs',
'in': 'query',
'description': 'Include nano contract execution logs in the response. Default is false.',
'required': False,
'schema': {
'type': 'boolean',
}
}, {
'name': 'include_nc_events',
'in': 'query',
'description': 'Include nano contract events emitted during execution in the response. '
'Default is false.',
'required': False,
'schema': {
'type': 'boolean',
}
}
],
'responses': {
Expand Down
152 changes: 152 additions & 0 deletions hathor/transaction/json_serializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# Copyright 2021 Hathor Labs
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import TYPE_CHECKING, Any, Optional

from hathor.transaction import BaseTransaction, Transaction

if TYPE_CHECKING:
from hathor.nanocontracts.nc_exec_logs import NCLogStorage
from hathor.transaction.storage import TransactionStorage


class VertexJsonSerializer:
"""Helper class for vertex/transaction serialization."""

def __init__(
self,
storage: 'TransactionStorage',
nc_log_storage: Optional['NCLogStorage'] = None,
) -> None:
self.tx_storage = storage
self.nc_log_storage = nc_log_storage

def to_json(
self,
tx: BaseTransaction,
decode_script: bool = False,
include_metadata: bool = False,
include_nc_logs: bool = False,
include_nc_events: bool = False,
) -> dict[str, Any]:
"""Serialize transaction to JSON."""
# Get base JSON from transaction
data = tx.to_json(decode_script=decode_script, include_metadata=include_metadata)

# Add nano contract logs if requested
if include_nc_logs:
self._add_nc_logs_to_dict(tx, data)

# Add nano contract events if requested
if include_nc_events:
self._add_nc_events_to_dict(tx, data)

return data

def to_json_extended(
self,
tx: BaseTransaction,
include_nc_logs: bool = False,
include_nc_events: bool = False,
) -> dict[str, Any]:
"""Serialize transaction to extended JSON format."""
data = tx.to_json_extended()

# Add nano contract logs if requested
if include_nc_logs:
self._add_nc_logs_to_dict(tx, data)

# Add nano contract events if requested
if include_nc_events:
self._add_nc_events_to_dict(tx, data)

# Add decoded arguments if applicable
self._add_nc_args_decoded(tx, data)

return data

def _add_nc_logs_to_dict(self, tx: BaseTransaction, data: dict[str, Any]) -> None:
"""Add nano contract execution logs to the data dictionary."""
if not tx.is_nano_contract():
return

nc_logs: dict[str, Any] | None
if self.nc_log_storage is None:
nc_logs = {}
else:
nc_logs = self.nc_log_storage.get_json_logs(tx.hash)

data['nc_logs'] = nc_logs

def _add_nc_events_to_dict(self, tx: BaseTransaction, data: dict[str, Any]) -> None:
"""Add nano contract events to the data dictionary."""
if not tx.is_nano_contract():
return

meta = tx.get_metadata()
if meta.nc_events is None:
nc_events = []
else:
nc_events = [
{'nc_id': nc_id.hex(), 'data': event_data.hex()}
for nc_id, event_data in meta.nc_events
]

data['nc_events'] = nc_events

def _add_nc_args_decoded(self, tx: BaseTransaction, data: dict[str, Any]) -> None:
if not tx.is_nano_contract():
return

assert isinstance(tx, Transaction)
nc_args_decoded = self.decode_nc_args(tx)
if nc_args_decoded is not None:
data['nc_args_decoded'] = nc_args_decoded

def decode_nc_args(self, tx: 'Transaction') -> Any:
"""Decode nano contract arguments.

Returns a list of JSON-serialized argument strings, or None if decoding is not applicable.
"""
from hathor.nanocontracts.exception import NCFail
from hathor.nanocontracts.method import Method
from hathor.nanocontracts.types import BlueprintId

meta = tx.get_metadata()
nano_header = tx.get_nano_header()

if meta.nc_calls and len(meta.nc_calls) > 0:
# Get blueprint_id from the first nc_calls record
blueprint_id = BlueprintId(meta.nc_calls[0].blueprint_id)
else:
# Get blueprint_id from NanoHeader
blueprint_id = nano_header.get_blueprint_id(accept_failed_execution=True)

try:
blueprint_class = self.tx_storage.get_blueprint_class(blueprint_id)
except NCFail:
return None

method_callable = getattr(blueprint_class, nano_header.nc_method, None)
if method_callable is None:
return None

method = Method.from_callable(method_callable)

try:
args_tuple = method.deserialize_args_bytes(nano_header.nc_args_bytes)
except NCFail:
return None

return method.args._value_to_json(args_tuple)
38 changes: 37 additions & 1 deletion hathor/transaction/resources/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,15 @@ def get_tx_extra_data(

serialized['tokens'] = detailed_tokens

return {
result = {
'success': True,
'tx': serialized,
'meta': meta.to_json_extended(tx.storage),
'spent_outputs': spent_outputs,
}

return result


@register_resource
class TransactionResource(Resource):
Expand Down Expand Up @@ -202,8 +204,22 @@ def get_one_tx(self, request: Request) -> bytes:
hash_bytes = bytes.fromhex(requested_hash)
tx = self.manager.tx_storage.get_transaction(hash_bytes)
tx.storage = self.manager.tx_storage

data = get_tx_extra_data(tx)

# Check for optional log/event parameters and add them if requested
include_nc_logs = raw_args.get(b'include_nc_logs', [b'false'])[0].decode('utf-8').lower() == 'true'
include_nc_events = raw_args.get(b'include_nc_events', [b'false'])[0].decode('utf-8').lower() == 'true'

if include_nc_logs or include_nc_events:
if include_nc_logs:
self.manager.vertex_json_serializer._add_nc_logs_to_dict(tx, data)
if include_nc_events:
self.manager.vertex_json_serializer._add_nc_events_to_dict(tx, data)

# Add decoded nano contract arguments if applicable
self.manager.vertex_json_serializer._add_nc_args_decoded(tx, data)

return json_dumpb(data)

def _validate_index(self, request: Request) -> bytes | None:
Expand Down Expand Up @@ -374,6 +390,26 @@ def get_list_tx(self, request):
'schema': {
'type': 'string'
}
},
{
'name': 'include_nc_logs',
'in': 'query',
'description': 'Include nano contract execution logs for nano contract transactions. '
'Default is false.',
'required': False,
'schema': {
'type': 'boolean'
}
},
{
'name': 'include_nc_events',
'in': 'query',
'description': 'Include nano contract events emitted during execution for nano contract '
'transactions. Default is false.',
'required': False,
'schema': {
'type': 'boolean'
}
}
],
'responses': {
Expand Down
Loading
Loading