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
52 changes: 40 additions & 12 deletions hathor/p2p/resources/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@
import hathor
from hathor.api_util import Resource, set_cors
from hathor.cli.openapi_files.register import register_resource
from hathor.conf import HathorSettings
from hathor.p2p.utils import to_serializable_best_blockchain
from hathor.util import json_dumpb

settings = HathorSettings()


@register_resource
class StatusResource(Resource):
Expand Down Expand Up @@ -70,6 +74,7 @@ def render_GET(self, request):
'plugins': status,
'warning_flags': [flag.value for flag in conn.warning_flags],
'protocol_version': str(conn.sync_version),
'peer_best_blockchain': to_serializable_best_blockchain(conn.state.peer_best_blockchain),
})

known_peers = []
Expand All @@ -89,6 +94,8 @@ def render_GET(self, request):
best_block_tips.append({'hash': tx.hash_hex, 'height': meta.height})

best_block = self.manager.tx_storage.get_best_block()
raw_best_blockchain = self.manager.tx_storage.get_n_height_tips(settings.DEFAULT_BEST_BLOCKCHAIN_BLOCKS)
best_blockchain = to_serializable_best_blockchain(raw_best_blockchain)

data = {
'server': {
Expand All @@ -114,11 +121,36 @@ def render_GET(self, request):
'hash': best_block.hash_hex,
'height': best_block.get_metadata().height,
},
'best_blockchain': best_blockchain,
}
}
return json_dumpb(data)


_openapi_height_info = [59, '0000045de9ac8365c43ccc96222873cb80c340c6c9c8949b56d2e2e51b6a3dbe']
_openapi_connected_peer = {
'id': '5578ab3bcaa861fb9d07135b8b167dd230d4487b147be8fd2c94a79bd349d123',
'app_version': 'Hathor v0.14.0-beta',
'uptime': 118.37029600143433,
'address': '192.168.1.1:54321',
'state': 'READY',
'last_message': 1539271481,
'plugins': {
'node-sync-timestamp': {
'is_enabled': True,
'latest_timestamp': 1685310912,
'synced_timestamp': 1685310912
}
},
'warning_flags': ['no_entrypoints'],
'protocol_version': 'sync-v1.1',
'peer_best_blockchain': [_openapi_height_info]
}
_openapi_connecting_peer = {
'deferred': '<bound method TCP4ClientEndpoint.connect of <twisted.internet.endpoints.TCP4ClientEndpoint object at 0x10b16b470>>', # noqa
'address': '192.168.1.1:54321'
}

StatusResource.openapi = {
'/status': {
'x-visibility': 'public',
Expand Down Expand Up @@ -164,7 +196,7 @@ def render_GET(self, request):
},
'known_peers': [],
'connections': {
'connected_peers': [],
'connected_peers': [_openapi_connected_peer],
'handshaking_peers': [
{
'address': '192.168.1.1:54321',
Expand All @@ -173,28 +205,24 @@ def render_GET(self, request):
'app_version': 'Unknown'
}
],
'connecting_peers': [
{
'deferred': ('<bound method TCP4ClientEndpoint.connect of <twisted'
'.internet.endpoints.TCP4ClientEndpoint object at '
'0x10b16b470>>'),
'address': '192.168.1.1:54321'
}
]
'connecting_peers': [_openapi_connecting_peer]
},
'dag': {
'first_timestamp': 1539271481,
'latest_timestamp': 1539271483,
'best_block_tips': [
{
'hash': '000007eb968a6cdf0499e2d033faf1e163e0dc9cf41876acad4d421836972038', # noqa: E501
'hash':
'000007eb968a6cdf0499e2d033faf1e163e0dc9cf41876acad4d421836972038', # noqa
'height': 0
}
],
'best_block': {
'hash': '000007eb968a6cdf0499e2d033faf1e163e0dc9cf41876acad4d421836972038', # noqa: E501
'hash':
'000007eb968a6cdf0499e2d033faf1e163e0dc9cf41876acad4d421836972038', # noqa
'height': 0
}
},
'best_blockchain': [_openapi_height_info]
}
}
}
Expand Down
5 changes: 2 additions & 3 deletions hathor/p2p/states/ready.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
from hathor.p2p.messages import ProtocolMessages
from hathor.p2p.peer_id import PeerId
from hathor.p2p.states.base import BaseState
from hathor.p2p.states.utils import to_height_info
from hathor.p2p.sync_agent import SyncAgent
from hathor.p2p.utils import to_height_info, to_serializable_best_blockchain
from hathor.transaction import BaseTransaction
from hathor.util import json_dumps, json_loads

Expand Down Expand Up @@ -233,15 +233,14 @@ def handle_get_best_blockchain(self, payload: str) -> None:
f'N out of bounds. Valid range: [1, {settings.MAX_BEST_BLOCKCHAIN_BLOCKS}].'
)
return
self.protocol.my_peer

best_blockchain = self.protocol.node.tx_storage.get_n_height_tips(n_blocks)
self.send_best_blockchain(best_blockchain)

def send_best_blockchain(self, best_blockchain: list[HeightInfo]) -> None:
""" Send a BEST-BLOCKCHAIN command with a best blockchain of N blocks.
"""
serialiable_best_blockchain = [(hi.height, hi.id.hex()) for hi in best_blockchain]
serialiable_best_blockchain = to_serializable_best_blockchain(best_blockchain)
self.send_message(ProtocolMessages.BEST_BLOCKCHAIN, json_dumps(serialiable_best_blockchain))

def handle_best_blockchain(self, payload: str) -> None:
Expand Down
24 changes: 0 additions & 24 deletions hathor/p2p/states/utils.py

This file was deleted.

29 changes: 29 additions & 0 deletions hathor/p2p/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# limitations under the License.

import datetime
import re
from typing import Any, Generator, Optional
from urllib.parse import parse_qs, urlparse

Expand All @@ -28,6 +29,7 @@
from twisted.internet.interfaces import IAddress

from hathor.conf import HathorSettings
from hathor.indexes.height_index import HeightInfo
from hathor.p2p.peer_discovery import DNSPeerDiscovery
from hathor.transaction.genesis import GENESIS_HASH

Expand Down Expand Up @@ -200,3 +202,30 @@ def format_address(addr: IAddress) -> str:
return f'{host}:{port}'
else:
return str(addr)


def to_height_info(raw: tuple[int, str]) -> HeightInfo:
""" Instantiate HeightInfo from a literal tuple.
"""
if not (isinstance(raw, list) and len(raw) == 2):
raise ValueError(f"height_info_raw must be a tuple with length 2. We got {raw}.")

height, id = raw

if not isinstance(id, str):
raise ValueError(f"id (hash) must be a string. We got {id}.")
hash_pattern = r'[a-fA-F\d]{64}'
if not re.match(hash_pattern, id):
raise ValueError(f"id (hash) must be valid. We got {id}.")
if not isinstance(height, int):
raise ValueError(f"height must be an integer. We got {height}.")
if height < 0:
raise ValueError(f"height must be greater than or equal to 0. We got {height}.")

return HeightInfo(height, bytes.fromhex(id))


def to_serializable_best_blockchain(best_blockchain: list[HeightInfo]) -> list[tuple[int, str]]:
""" Converts the list of HeightInfo to a tuple list that can be serializable to json afterwards.
"""
return [(hi.height, hi.id.hex()) for hi in best_blockchain]
105 changes: 105 additions & 0 deletions tests/p2p/test_get_best_blockchain.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
from twisted.internet.defer import inlineCallbacks

from hathor.conf import HathorSettings
from hathor.indexes.height_index import HeightInfo
from hathor.manager import DEFAULT_CAPABILITIES
from hathor.p2p.messages import ProtocolMessages
from hathor.p2p.resources import StatusResource
from hathor.p2p.states import ReadyState
from hathor.p2p.utils import to_height_info
from hathor.simulator import FakeConnection
from hathor.simulator.trigger import StopAfterNMinedBlocks
from hathor.util import json_dumps
from tests import unittest
from tests.resources.base_resource import StubSite
from tests.simulation.base import SimulatorTestCase

settings = HathorSettings()
Expand Down Expand Up @@ -339,6 +344,106 @@ def test_stop_looping_on_exit(self):
self.assertIsNotNone(state2.lc_get_best_blockchain)
self.assertFalse(state2.lc_get_best_blockchain.running)

@inlineCallbacks
def test_best_blockchain_from_status_resource(self):
manager1 = self.create_peer()
manager2 = self.create_peer()
conn12 = FakeConnection(manager1, manager2, latency=0.05)
self.simulator.add_connection(conn12)
self.simulator.run(60)

# check /status before generate blocks
self.web = StubSite(StatusResource(manager1))
response = yield self.web.get("status")
data = response.json_value()
connections = data.get('connections')
self.assertEqual(len(connections['connected_peers']), 1)
dag = data.get('dag')

# connected_peers
# assert there is the genesis block
peer_best_blockchain = connections['connected_peers'][0]['peer_best_blockchain']
self.assertEqual(len(peer_best_blockchain), 1)
# assert the height_info height is from genesis
raw_height_info_height = peer_best_blockchain[0][0]
self.assertEqual(raw_height_info_height, 0)

# dag
# assert there is the genesis block
peer_best_blockchain = dag['best_blockchain']
self.assertEqual(len(peer_best_blockchain), 1)
# assert the height_info height is from genesis
raw_height_info_height = peer_best_blockchain[0][0]
self.assertEqual(raw_height_info_height, 0)

# mine 20 blocks
miner = self.simulator.create_miner(manager1, hashpower=1e6)
miner.start()
trigger = StopAfterNMinedBlocks(miner, quantity=20)
self.assertTrue(self.simulator.run(1440, trigger=trigger))
miner.stop()
# let the blocks to propagate
self.simulator.run(60)

# check /status after mine blocks
response = yield self.web.get("status")
data = response.json_value()
connections = data.get('connections')
self.assertEqual(len(connections['connected_peers']), 1)
dag = data.get('dag')

# connected_peers
# assert default peer_best_blockchain length
peer_best_blockchain = connections['connected_peers'][0]['peer_best_blockchain']
self.assertEqual(len(peer_best_blockchain), settings.DEFAULT_BEST_BLOCKCHAIN_BLOCKS)

# assert a raw_height_info can be converted to HeightInfo
try:
raw_height_info = peer_best_blockchain[0]
height_info = to_height_info(raw_height_info)
# assert the first element height is from the lastest block mined
self.assertEqual(height_info.height, 20)
except ValueError:
self.fail('Block info not valid')

# assert decreasing order for a sequence of heights
height_sequence = [hi[0] for hi in peer_best_blockchain]
try:
self.assertTrue(check_decreasing_monotonicity(height_sequence))
except ValueError as e:
self.fail(str(e))

# dag
# assert default peer_best_blockchain length
peer_best_blockchain = dag['best_blockchain']
self.assertEqual(len(peer_best_blockchain), settings.DEFAULT_BEST_BLOCKCHAIN_BLOCKS)

# assert a raw_height_info can be converted to HeightInfo
try:
raw_height_info = peer_best_blockchain[0]
height_info = to_height_info(raw_height_info)
# assert the first element height is from the lastest block mined
self.assertEqual(height_info.height, 20)
except ValueError:
self.fail('Block info not valid')

# assert decreasing order for a sequence of heights
height_sequence = [hi[0] for hi in peer_best_blockchain]
try:
self.assertTrue(check_decreasing_monotonicity(height_sequence))
except ValueError as e:
self.fail(str(e))


def check_decreasing_monotonicity(sequence: list[int]) -> bool:
"""Check if a sequence is monotonic and is decreasing. Raise an exception otherwise.
"""
n = len(sequence)
for i in range(1, n):
if sequence[i] >= sequence[i-1]:
raise ValueError(f'Sequence not monotonic. Value {sequence[i]} >= {sequence[i-1]}. Index: {i}.')
return True


class SyncV1GetBestBlockchainTestCase(unittest.SyncV1Params, BaseGetBestBlockchainTestCase):
__test__ = True
Expand Down