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
2 changes: 1 addition & 1 deletion hathor/cli/openapi_files/openapi_base.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
],
"info": {
"title": "Hathor API",
"version": "0.60.1"
"version": "0.61.0"
},
"consumes": [
"application/json"
Expand Down
21 changes: 16 additions & 5 deletions hathor/cli/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import json
import sys
import traceback
from argparse import ArgumentParser
from collections import OrderedDict
from datetime import datetime
Expand Down Expand Up @@ -230,7 +232,12 @@ def setup_logging(

def kwargs_formatter(_, __, event_dict):
if event_dict and event_dict.get('event') and isinstance(event_dict['event'], str):
event_dict['event'] = event_dict['event'].format(**event_dict)
try:
event_dict['event'] = event_dict['event'].format(**event_dict)
except KeyError:
# The event string may contain '{}'s that are not used for formatting, resulting in a KeyError in the
# event_dict. In this case, we don't format it.
pass
return event_dict

processors: list[Any] = [
Expand Down Expand Up @@ -275,10 +282,14 @@ def twisted_structlog_observer(event):
if failure is not None:
kwargs['exc_info'] = (failure.type, failure.value, failure.getTracebackObject())
twisted_logger.log(level, msg, **kwargs)
except Exception as e:
print('error when logging event', e)
for k, v in event.items():
print(k, v)
except Exception:
new_event = dict(
event='error when logging event',
original_event=event,
traceback=traceback.format_exc()
)
new_event_json = json.dumps(new_event, default=str)
print(new_event_json, file=sys.stderr)

# start logging to std logger so structlog can catch it
twisted.python.log.startLoggingWithObserver(twisted_structlog_observer, setStdout=capture_stdout)
Expand Down
13 changes: 12 additions & 1 deletion hathor/transaction/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from itertools import starmap, zip_longest
from operator import add
from struct import pack
from typing import TYPE_CHECKING, Any, Optional
from typing import TYPE_CHECKING, Any, Iterator, Optional

from hathor.checkpoint import Checkpoint
from hathor.feature_activation.feature import Feature
Expand Down Expand Up @@ -401,3 +401,14 @@ def get_feature_activation_bit_value(self, bit: int) -> int:
bit_list = self._get_feature_activation_bit_list()

return bit_list[bit]

def iter_transactions_in_this_block(self) -> Iterator[BaseTransaction]:
"""Return an iterator of the transactions that have this block as meta.first_block."""
from hathor.transaction.storage.traversal import BFSOrderWalk
bfs = BFSOrderWalk(self.storage, is_dag_verifications=True, is_dag_funds=True, is_left_to_right=False)
for tx in bfs.run(self, skip_root=True):
tx_meta = tx.get_metadata()
if tx_meta.first_block != self.hash:
bfs.skip_neighbors(tx)
continue
yield tx
68 changes: 48 additions & 20 deletions hathor/transaction/resources/block_at_height.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any

from hathor.api_util import Resource, get_args, get_missing_params_msg, parse_args, parse_int, set_cors
from hathor.api_util import Resource, set_cors
from hathor.cli.openapi_files.register import register_resource
from hathor.util import json_dumpb
from hathor.utils.api import ErrorResponse, QueryParams

if TYPE_CHECKING:
from twisted.web.http import Request
Expand Down Expand Up @@ -48,38 +49,52 @@ def render_GET(self, request: 'Request') -> bytes:
request.setHeader(b'content-type', b'application/json; charset=utf-8')
set_cors(request, 'GET')

# Height parameter is required
parsed = parse_args(get_args(request), ['height'])
if not parsed['success']:
return get_missing_params_msg(parsed['missing'])
params = BlockAtHeightParams.from_request(request)
if isinstance(params, ErrorResponse):
return params.json_dumpb()

args = parsed['args']
# Get hash of the block with the height
block_hash = self.manager.tx_storage.indexes.height.get(params.height)

# Height parameter must be an integer
try:
height = parse_int(args['height'])
except ValueError as e:
# If there is no block in the index with this height, block_hash will be None
if block_hash is None:
return json_dumpb({
'success': False,
'message': f'Failed to parse \'height\': {e}'
'message': 'No block with height {}.'.format(params.height)
})

# Get hash of the block with the height
block_hash = self.manager.tx_storage.indexes.height.get(height)
block = self.manager.tx_storage.get_block(block_hash)
data = {'success': True, 'block': block.to_json_extended()}

# If there is no block in the index with this height, block_hash will be None
if block_hash is None:
if params.include_transactions is None:
pass

elif params.include_transactions == 'txid':
tx_ids: list[str] = []
for tx in block.iter_transactions_in_this_block():
tx_ids.append(tx.hash.hex())
data['tx_ids'] = tx_ids

elif params.include_transactions == 'full':
tx_list: list[Any] = []
for tx in block.iter_transactions_in_this_block():
tx_list.append(tx.to_json_extended())
data['transactions'] = tx_list

else:
return json_dumpb({
'success': False,
'message': 'No block with height {}.'.format(height)
'message': 'Invalid include_transactions. Choices are: txid or full.'
})

block = self.manager.tx_storage.get_transaction(block_hash)

data = {'success': True, 'block': block.to_json_extended()}
return json_dumpb(data)


class BlockAtHeightParams(QueryParams):
height: int
include_transactions: str | None


BlockAtHeightResource.openapi = {
'/block_at_height': {
'x-visibility': 'public',
Expand Down Expand Up @@ -114,6 +129,19 @@ def render_GET(self, request: 'Request') -> bytes:
'type': 'int'
}
},
{
'name': 'include_transactions',
'in': 'query',
'description': 'Add transactions confirmed by this block.',
'required': False,
'schema': {
'type': 'string',
'enum': [
'txid',
'full',
],
}
},
],
'responses': {
'200': {
Expand Down
2 changes: 1 addition & 1 deletion hathor/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

from structlog import get_logger

BASE_VERSION = '0.60.1'
BASE_VERSION = '0.61.0'

DEFAULT_VERSION_SUFFIX = "local"
BUILD_VERSION_FILE_PATH = "./BUILD_VERSION"
Expand Down
42 changes: 7 additions & 35 deletions hathor/vertex_handler/vertex_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
from hathor.conf.settings import HathorSettings
from hathor.consensus import ConsensusAlgorithm
from hathor.exception import HathorError, InvalidNewTransaction
from hathor.feature_activation.feature import Feature
from hathor.feature_activation.feature_service import FeatureService
from hathor.p2p.manager import ConnectionsManager
from hathor.pubsub import HathorEvents, PubSubManager
Expand Down Expand Up @@ -209,7 +208,6 @@ def _post_consensus(
self._wallet.on_new_tx(vertex)

self._log_new_object(vertex, 'new {}', quiet=quiet)
self._log_feature_states(vertex)

if propagate_to_peers:
# Propagate to our peers.
Expand All @@ -231,43 +229,17 @@ def _log_new_object(self, tx: BaseTransaction, message_fmt: str, *, quiet: bool)
if tx.is_block:
message = message_fmt.format('block')
if isinstance(tx, Block):
kwargs['height'] = tx.get_height()
feature_descriptions = self._feature_service.get_bits_description(block=tx)
feature_states = {
feature.value: description.state.value
for feature, description in feature_descriptions.items()
}
kwargs['_height'] = tx.get_height()
kwargs['feature_states'] = feature_states
else:
message = message_fmt.format('tx')
if not quiet:
log_func = self._log.info
else:
log_func = self._log.debug
log_func(message, **kwargs)

def _log_feature_states(self, vertex: BaseTransaction) -> None:
"""Log features states for a block. Used as part of the Feature Activation Phased Testing."""
if not isinstance(vertex, Block):
return

feature_descriptions = self._feature_service.get_bits_description(block=vertex)
state_by_feature = {
feature.value: description.state.value
for feature, description in feature_descriptions.items()
}

self._log.info(
'New block accepted with feature activation states',
block_hash=vertex.hash_hex,
block_height=vertex.get_height(),
features_states=state_by_feature
)

features = [Feature.NOP_FEATURE_1, Feature.NOP_FEATURE_2]
for feature in features:
self._log_if_feature_is_active(vertex, feature)

def _log_if_feature_is_active(self, block: Block, feature: Feature) -> None:
"""Log if a feature is ACTIVE for a block. Used as part of the Feature Activation Phased Testing."""
if self._feature_service.is_feature_active(block=block, feature=feature):
self._log.info(
'Feature is ACTIVE for block',
feature=feature.value,
block_hash=block.hash_hex,
block_height=block.get_height()
)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

[tool.poetry]
name = "hathor"
version = "0.60.1"
version = "0.61.0"
description = "Hathor Network full-node"
authors = ["Hathor Team <contact@hathor.network>"]
license = "Apache-2.0"
Expand Down
59 changes: 58 additions & 1 deletion tests/resources/transaction/test_block_at_height.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from twisted.internet.defer import inlineCallbacks

from hathor.simulator.utils import add_new_blocks
from hathor.simulator.utils import add_new_block, add_new_blocks
from hathor.transaction.resources import BlockAtHeightResource
from tests import unittest
from tests.resources.base_resource import StubSite, _BaseResourceTest
from tests.utils import add_blocks_unlock_reward, add_new_tx


class BaseBlockAtHeightTest(_BaseResourceTest._ResourceTest):
Expand All @@ -14,6 +15,62 @@ def setUp(self):
self.web = StubSite(BlockAtHeightResource(self.manager))
self.manager.wallet.unlock(b'MYPASS')

@inlineCallbacks
def test_include_full(self):
add_new_block(self.manager, advance_clock=1)
add_blocks_unlock_reward(self.manager)
address = self.manager.wallet.get_unused_address()

confirmed_tx_list = []
for _ in range(15):
confirmed_tx_list.append(add_new_tx(self.manager, address, 1))

block = add_new_block(self.manager, advance_clock=1)
height = block.get_height()

# non-confirmed transactions
for _ in range(15):
add_new_tx(self.manager, address, 1)

response = yield self.web.get("block_at_height", {
b'height': str(height).encode('ascii'),
b'include_transactions': b'full',
})
data = response.json_value()

self.assertTrue(data['success'])
response_tx_ids = set(x['tx_id'] for x in data['transactions'])
expected_tx_ids = set(tx.hash.hex() for tx in confirmed_tx_list)
self.assertTrue(response_tx_ids.issubset(expected_tx_ids))

@inlineCallbacks
def test_include_txids(self):
add_new_block(self.manager, advance_clock=1)
add_blocks_unlock_reward(self.manager)
address = self.manager.wallet.get_unused_address()

confirmed_tx_list = []
for _ in range(15):
confirmed_tx_list.append(add_new_tx(self.manager, address, 1))

block = add_new_block(self.manager, advance_clock=1)
height = block.get_height()

# non-confirmed transactions
for _ in range(15):
add_new_tx(self.manager, address, 1)

response = yield self.web.get("block_at_height", {
b'height': str(height).encode('ascii'),
b'include_transactions': b'txid',
})
data = response.json_value()

self.assertTrue(data['success'])
response_tx_ids = set(data['tx_ids'])
expected_tx_ids = set(tx.hash.hex() for tx in confirmed_tx_list)
self.assertTrue(response_tx_ids.issubset(expected_tx_ids))

@inlineCallbacks
def test_get(self):
blocks = add_new_blocks(self.manager, 4, advance_clock=1)
Expand Down