From 9151f4af029a02691e06e20a33579ef632d7c029 Mon Sep 17 00:00:00 2001 From: Alex Ruzenhack Date: Thu, 29 Jun 2023 00:42:05 +0100 Subject: [PATCH 1/4] feat: add get-best-blockchain capability --- hathor/manager.py | 30 +++++++++++++++++++++++++++++- hathor/transaction/block.py | 4 ++++ hathor/types.py | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/hathor/manager.py b/hathor/manager.py index 2d0b14577..d945c3de3 100644 --- a/hathor/manager.py +++ b/hathor/manager.py @@ -52,7 +52,7 @@ from hathor.transaction.storage import TransactionStorage from hathor.transaction.storage.exceptions import TransactionDoesNotExist from hathor.transaction.storage.tx_allow_scope import TxAllowScope -from hathor.types import Address, VertexId +from hathor.types import Address, BlockInfo, VertexId from hathor.util import EnvironmentInfo, LogDuration, Random, Reactor, calculate_min_significant_weight, not_none from hathor.wallet import BaseWallet @@ -217,6 +217,9 @@ def __init__(self, # This is included in some logs to provide more context self.environment_info = environment_info + # Memoize latest best blockchain + self._latest_best_blockchain: list[BlockInfo] = [] + # Task that will count the total sync time self.lc_check_sync_state = LoopingCall(self.check_sync_state) self.lc_check_sync_state.clock = self.reactor @@ -712,6 +715,31 @@ def generate_parent_txs(self, timestamp: Optional[float]) -> 'ParentTxs': can_include = [i.data for i in can_include_intervals] return ParentTxs(max_timestamp, can_include, must_include) + def get_best_blockchain(self, n_blocks: int) -> list['BlockInfo']: + """ Get a list with N blocks from the best blockchain + in descending order. + """ + if not (n_blocks > 0): + raise ValueError( + 'Invalid N. N must be greater than 0.' + ) + + block = self.tx_storage.get_best_block() + if self._latest_best_blockchain: + best_block = self._latest_best_blockchain[0] + if block.hash_hex == best_block.hash_hex and n_blocks <= len(self._latest_best_blockchain): + return self._latest_best_blockchain[:n_blocks] + + best_blockchain: list[BlockInfo] = [] + while len(best_blockchain) < n_blocks: + best_blockchain.append(block.to_blockinfo()) + if block.is_genesis: + break + block = block.get_block_parent() + + self._latest_best_blockchain = best_blockchain + return best_blockchain + def allow_mining_without_peers(self) -> None: """Allow mining without being synced to at least one peer. It should be used only for debugging purposes. diff --git a/hathor/transaction/block.py b/hathor/transaction/block.py index 699c6e763..b57fa0601 100644 --- a/hathor/transaction/block.py +++ b/hathor/transaction/block.py @@ -35,6 +35,7 @@ WeightError, ) from hathor.transaction.util import VerboseCallback, int_to_bytes, unpack, unpack_len +from hathor.types import BlockInfo from hathor.utils.int import get_bit_list if TYPE_CHECKING: @@ -306,6 +307,9 @@ def to_json_extended(self) -> dict[str, Any]: return json + def to_blockinfo(self) -> BlockInfo: + return BlockInfo(self.hash_hex, self.get_height(), self.weight) + def has_basic_block_parent(self) -> bool: """Whether all block parent is in storage and is at least basic-valid.""" assert self.storage is not None diff --git a/hathor/types.py b/hathor/types.py index 07c42194a..1a1fb9da7 100644 --- a/hathor/types.py +++ b/hathor/types.py @@ -12,6 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +import re +from typing import NamedTuple + # XXX There is a lot of refactor to be done before we can use `NewType`. # So, let's skip using NewType until everything is refactored. @@ -21,3 +24,34 @@ Timestamp = int # NewType('Timestamp', int) TokenUid = VertexId # NewType('TokenUid', VertexId) Amount = int # NewType('Amount', int) + + +class BlockInfo(NamedTuple): + hash_hex: str + height: int + weight: float + + @staticmethod + def from_raw(block_info_raw: tuple[str, int, float]) -> 'BlockInfo': + """ Instantiate BlockInfo from a literal tuple. + """ + if not (isinstance(block_info_raw, list) and len(block_info_raw) == 3): + raise ValueError(f"block_info_raw must be a tuple with length 3. We got {block_info_raw}.") + + hash_hex, height, weight = block_info_raw + + if not isinstance(hash_hex, str): + raise ValueError(f"hash_hex must be a string. We got {hash_hex}.") + hash_pattern = r'[a-fA-F\d]{64}' + if not re.match(hash_pattern, hash_hex): + raise ValueError(f"hash_hex must be valid. We got {hash_hex}.") + if not isinstance(height, int): + raise ValueError(f"height must be an integer. We got {height}.") + if height < 0: + raise ValueError(f"height must greater than or equal to 0. We got {height}.") + if not isinstance(weight, (float, int)): + raise ValueError(f"weight must be a float. We got {weight}.") + if not weight > 0: + raise ValueError(f"weight must be greater than 0. We got {weight}.") + + return BlockInfo(hash_hex, height, weight) From 9704bd45c6d7ac92547ff50c312dab5f37150cd6 Mon Sep 17 00:00:00 2001 From: Alex Ruzenhack Date: Thu, 29 Jun 2023 00:42:05 +0100 Subject: [PATCH 2/4] feat: add best-blockchain to status payload --- hathor/p2p/resources/status.py | 49 +++++++++--- tests/p2p/test_get_best_blockchain.py | 104 ++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 12 deletions(-) diff --git a/hathor/p2p/resources/status.py b/hathor/p2p/resources/status.py index bda60cc1c..a335ec64c 100644 --- a/hathor/p2p/resources/status.py +++ b/hathor/p2p/resources/status.py @@ -17,8 +17,11 @@ 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.util import json_dumpb +settings = HathorSettings() + @register_resource class StatusResource(Resource): @@ -70,6 +73,7 @@ def render_GET(self, request): 'plugins': status, 'warning_flags': [flag.value for flag in conn.warning_flags], 'protocol_version': str(conn.sync_version), + 'best_blockchain': conn.state.best_blockchain, }) known_peers = [] @@ -114,11 +118,36 @@ def render_GET(self, request): 'hash': best_block.hash_hex, 'height': best_block.get_metadata().height, }, + 'best_blockchain': self.manager.get_best_blockchain(settings.DEFAULT_BEST_BLOCKCHAIN_BLOCKS), } } return json_dumpb(data) +_openapi_best_blockchain_block = ['0000045de9ac8365c43ccc96222873cb80c340c6c9c8949b56d2e2e51b6a3dbe', 59, 21.0] +_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', + 'best_blockchain': [_openapi_best_blockchain_block] +} +_openapi_connecting_peer = { + 'deferred': '>', # noqa + 'address': '192.168.1.1:54321' +} + StatusResource.openapi = { '/status': { 'x-visibility': 'public', @@ -164,7 +193,7 @@ def render_GET(self, request): }, 'known_peers': [], 'connections': { - 'connected_peers': [], + 'connected_peers': [_openapi_connected_peer], 'handshaking_peers': [ { 'address': '192.168.1.1:54321', @@ -173,28 +202,24 @@ def render_GET(self, request): 'app_version': 'Unknown' } ], - 'connecting_peers': [ - { - 'deferred': ('>'), - '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_best_blockchain_block] } } } diff --git a/tests/p2p/test_get_best_blockchain.py b/tests/p2p/test_get_best_blockchain.py index 367fdc70e..de6e96d49 100644 --- a/tests/p2p/test_get_best_blockchain.py +++ b/tests/p2p/test_get_best_blockchain.py @@ -1,12 +1,16 @@ +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.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() @@ -339,6 +343,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_within_connected_peers(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 + best_blockchain = connections['connected_peers'][0]['best_blockchain'] + self.assertEqual(len(best_blockchain), 1) + # assert the block_info height is from genesis + raw_block_info_height = best_blockchain[0][1] + self.assertEqual(raw_block_info_height, 0) + + # dag + # assert there is the genesis block + best_blockchain = dag['best_blockchain'] + self.assertEqual(len(best_blockchain), 1) + # assert the block_info height is from genesis + raw_block_info_height = best_blockchain[0][1] + self.assertEqual(raw_block_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 best_blockchain length + best_blockchain = connections['connected_peers'][0]['best_blockchain'] + self.assertEqual(len(best_blockchain), settings.DEFAULT_BEST_BLOCKCHAIN_BLOCKS) + + # assert a raw_block_info can be converted to BlockInfo + try: + raw_block_info = best_blockchain[0] + block_info = BlockInfo.from_raw(raw_block_info) + # assert the first element height is from the lastest block mined + self.assertEqual(block_info.height, 20) + except ValueError: + self.fail('Block info not valid') + + # assert decreasing order for a sequence of heights + height_sequence = [b[1] for b in best_blockchain] + try: + self.assertTrue(check_decreasing_monotonicity(height_sequence)) + except ValueError as e: + self.fail(str(e)) + + # dag + # assert default best_blockchain length + best_blockchain = dag['best_blockchain'] + self.assertEqual(len(best_blockchain), settings.DEFAULT_BEST_BLOCKCHAIN_BLOCKS) + + # assert a raw_block_info can be converted to BlockInfo + try: + raw_block_info = best_blockchain[0] + block_info = BlockInfo.from_raw(raw_block_info) + # assert the first element height is from the lastest block mined + self.assertEqual(block_info.height, 20) + except ValueError: + self.fail('Block info not valid') + + # assert decreasing order for a sequence of heights + height_sequence = [b[1] for b in 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 From a3318d41d592cbc567caf78d94a38addda286f20 Mon Sep 17 00:00:00 2001 From: Alex Ruzenhack Date: Fri, 28 Jul 2023 14:08:29 +0100 Subject: [PATCH 3/4] chore: tailor the ready state, the status resource and tests --- hathor/manager.py | 30 +-------------- hathor/p2p/resources/status.py | 13 ++++--- hathor/p2p/states/ready.py | 5 +-- hathor/p2p/states/utils.py | 24 ------------ hathor/p2p/utils.py | 27 +++++++++++++ hathor/transaction/block.py | 4 -- hathor/types.py | 34 ----------------- tests/p2p/test_get_best_blockchain.py | 55 ++++++++++++++------------- 8 files changed, 66 insertions(+), 126 deletions(-) delete mode 100644 hathor/p2p/states/utils.py diff --git a/hathor/manager.py b/hathor/manager.py index d945c3de3..2d0b14577 100644 --- a/hathor/manager.py +++ b/hathor/manager.py @@ -52,7 +52,7 @@ from hathor.transaction.storage import TransactionStorage from hathor.transaction.storage.exceptions import TransactionDoesNotExist from hathor.transaction.storage.tx_allow_scope import TxAllowScope -from hathor.types import Address, BlockInfo, VertexId +from hathor.types import Address, VertexId from hathor.util import EnvironmentInfo, LogDuration, Random, Reactor, calculate_min_significant_weight, not_none from hathor.wallet import BaseWallet @@ -217,9 +217,6 @@ def __init__(self, # This is included in some logs to provide more context self.environment_info = environment_info - # Memoize latest best blockchain - self._latest_best_blockchain: list[BlockInfo] = [] - # Task that will count the total sync time self.lc_check_sync_state = LoopingCall(self.check_sync_state) self.lc_check_sync_state.clock = self.reactor @@ -715,31 +712,6 @@ def generate_parent_txs(self, timestamp: Optional[float]) -> 'ParentTxs': can_include = [i.data for i in can_include_intervals] return ParentTxs(max_timestamp, can_include, must_include) - def get_best_blockchain(self, n_blocks: int) -> list['BlockInfo']: - """ Get a list with N blocks from the best blockchain - in descending order. - """ - if not (n_blocks > 0): - raise ValueError( - 'Invalid N. N must be greater than 0.' - ) - - block = self.tx_storage.get_best_block() - if self._latest_best_blockchain: - best_block = self._latest_best_blockchain[0] - if block.hash_hex == best_block.hash_hex and n_blocks <= len(self._latest_best_blockchain): - return self._latest_best_blockchain[:n_blocks] - - best_blockchain: list[BlockInfo] = [] - while len(best_blockchain) < n_blocks: - best_blockchain.append(block.to_blockinfo()) - if block.is_genesis: - break - block = block.get_block_parent() - - self._latest_best_blockchain = best_blockchain - return best_blockchain - def allow_mining_without_peers(self) -> None: """Allow mining without being synced to at least one peer. It should be used only for debugging purposes. diff --git a/hathor/p2p/resources/status.py b/hathor/p2p/resources/status.py index a335ec64c..940ea6ac8 100644 --- a/hathor/p2p/resources/status.py +++ b/hathor/p2p/resources/status.py @@ -18,6 +18,7 @@ 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() @@ -73,7 +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), - 'best_blockchain': conn.state.best_blockchain, + 'peer_best_blockchain': to_serializable_best_blockchain(conn.state.peer_best_blockchain), }) known_peers = [] @@ -93,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() + best_blockchain = to_serializable_best_blockchain( + self.manager.tx_storage.get_n_height_tips(settings.DEFAULT_BEST_BLOCKCHAIN_BLOCKS)) data = { 'server': { @@ -118,13 +121,13 @@ def render_GET(self, request): 'hash': best_block.hash_hex, 'height': best_block.get_metadata().height, }, - 'best_blockchain': self.manager.get_best_blockchain(settings.DEFAULT_BEST_BLOCKCHAIN_BLOCKS), + 'best_blockchain': best_blockchain, } } return json_dumpb(data) -_openapi_best_blockchain_block = ['0000045de9ac8365c43ccc96222873cb80c340c6c9c8949b56d2e2e51b6a3dbe', 59, 21.0] +_openapi_height_info = [59, '0000045de9ac8365c43ccc96222873cb80c340c6c9c8949b56d2e2e51b6a3dbe'] _openapi_connected_peer = { 'id': '5578ab3bcaa861fb9d07135b8b167dd230d4487b147be8fd2c94a79bd349d123', 'app_version': 'Hathor v0.14.0-beta', @@ -141,7 +144,7 @@ def render_GET(self, request): }, 'warning_flags': ['no_entrypoints'], 'protocol_version': 'sync-v1.1', - 'best_blockchain': [_openapi_best_blockchain_block] + 'peer_best_blockchain': [_openapi_height_info] } _openapi_connecting_peer = { 'deferred': '>', # noqa @@ -219,7 +222,7 @@ def render_GET(self, request): '000007eb968a6cdf0499e2d033faf1e163e0dc9cf41876acad4d421836972038', # noqa 'height': 0 }, - 'best_blockchain': [_openapi_best_blockchain_block] + 'best_blockchain': [_openapi_height_info] } } } diff --git a/hathor/p2p/states/ready.py b/hathor/p2p/states/ready.py index b6813c9c4..19d5bddc0 100644 --- a/hathor/p2p/states/ready.py +++ b/hathor/p2p/states/ready.py @@ -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 @@ -233,7 +233,6 @@ 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) @@ -241,7 +240,7 @@ def handle_get_best_blockchain(self, payload: str) -> None: 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: diff --git a/hathor/p2p/states/utils.py b/hathor/p2p/states/utils.py deleted file mode 100644 index 317077f3b..000000000 --- a/hathor/p2p/states/utils.py +++ /dev/null @@ -1,24 +0,0 @@ -import re - -from hathor.indexes.height_index import HeightInfo - - -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"block_info_raw must be a tuple with length 3. We got {raw}.") - - height, id = raw - - if not isinstance(id, str): - raise ValueError(f"hash_hex 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"hash_hex 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 greater than or equal to 0. We got {height}.") - - return HeightInfo(height, bytes.fromhex(id)) diff --git a/hathor/p2p/utils.py b/hathor/p2p/utils.py index e9c778807..55d872ad3 100644 --- a/hathor/p2p/utils.py +++ b/hathor/p2p/utils.py @@ -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 @@ -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 @@ -200,3 +202,28 @@ 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]]: + return [(hi.height, hi.id.hex()) for hi in best_blockchain] diff --git a/hathor/transaction/block.py b/hathor/transaction/block.py index b57fa0601..699c6e763 100644 --- a/hathor/transaction/block.py +++ b/hathor/transaction/block.py @@ -35,7 +35,6 @@ WeightError, ) from hathor.transaction.util import VerboseCallback, int_to_bytes, unpack, unpack_len -from hathor.types import BlockInfo from hathor.utils.int import get_bit_list if TYPE_CHECKING: @@ -307,9 +306,6 @@ def to_json_extended(self) -> dict[str, Any]: return json - def to_blockinfo(self) -> BlockInfo: - return BlockInfo(self.hash_hex, self.get_height(), self.weight) - def has_basic_block_parent(self) -> bool: """Whether all block parent is in storage and is at least basic-valid.""" assert self.storage is not None diff --git a/hathor/types.py b/hathor/types.py index 1a1fb9da7..07c42194a 100644 --- a/hathor/types.py +++ b/hathor/types.py @@ -12,9 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import re -from typing import NamedTuple - # XXX There is a lot of refactor to be done before we can use `NewType`. # So, let's skip using NewType until everything is refactored. @@ -24,34 +21,3 @@ Timestamp = int # NewType('Timestamp', int) TokenUid = VertexId # NewType('TokenUid', VertexId) Amount = int # NewType('Amount', int) - - -class BlockInfo(NamedTuple): - hash_hex: str - height: int - weight: float - - @staticmethod - def from_raw(block_info_raw: tuple[str, int, float]) -> 'BlockInfo': - """ Instantiate BlockInfo from a literal tuple. - """ - if not (isinstance(block_info_raw, list) and len(block_info_raw) == 3): - raise ValueError(f"block_info_raw must be a tuple with length 3. We got {block_info_raw}.") - - hash_hex, height, weight = block_info_raw - - if not isinstance(hash_hex, str): - raise ValueError(f"hash_hex must be a string. We got {hash_hex}.") - hash_pattern = r'[a-fA-F\d]{64}' - if not re.match(hash_pattern, hash_hex): - raise ValueError(f"hash_hex must be valid. We got {hash_hex}.") - if not isinstance(height, int): - raise ValueError(f"height must be an integer. We got {height}.") - if height < 0: - raise ValueError(f"height must greater than or equal to 0. We got {height}.") - if not isinstance(weight, (float, int)): - raise ValueError(f"weight must be a float. We got {weight}.") - if not weight > 0: - raise ValueError(f"weight must be greater than 0. We got {weight}.") - - return BlockInfo(hash_hex, height, weight) diff --git a/tests/p2p/test_get_best_blockchain.py b/tests/p2p/test_get_best_blockchain.py index de6e96d49..a37e4a742 100644 --- a/tests/p2p/test_get_best_blockchain.py +++ b/tests/p2p/test_get_best_blockchain.py @@ -6,6 +6,7 @@ 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 @@ -344,7 +345,7 @@ def test_stop_looping_on_exit(self): self.assertFalse(state2.lc_get_best_blockchain.running) @inlineCallbacks - def test_best_blockchain_within_connected_peers(self): + def test_best_blockchain_from_status_resource(self): manager1 = self.create_peer() manager2 = self.create_peer() conn12 = FakeConnection(manager1, manager2, latency=0.05) @@ -361,19 +362,19 @@ def test_best_blockchain_within_connected_peers(self): # connected_peers # assert there is the genesis block - best_blockchain = connections['connected_peers'][0]['best_blockchain'] - self.assertEqual(len(best_blockchain), 1) - # assert the block_info height is from genesis - raw_block_info_height = best_blockchain[0][1] - self.assertEqual(raw_block_info_height, 0) + 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 - best_blockchain = dag['best_blockchain'] - self.assertEqual(len(best_blockchain), 1) - # assert the block_info height is from genesis - raw_block_info_height = best_blockchain[0][1] - self.assertEqual(raw_block_info_height, 0) + 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) @@ -392,42 +393,42 @@ def test_best_blockchain_within_connected_peers(self): dag = data.get('dag') # connected_peers - # assert default best_blockchain length - best_blockchain = connections['connected_peers'][0]['best_blockchain'] - self.assertEqual(len(best_blockchain), settings.DEFAULT_BEST_BLOCKCHAIN_BLOCKS) + # 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_block_info can be converted to BlockInfo + # assert a raw_height_info can be converted to HeightInfo try: - raw_block_info = best_blockchain[0] - block_info = BlockInfo.from_raw(raw_block_info) + 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(block_info.height, 20) + self.assertEqual(height_info.height, 20) except ValueError: self.fail('Block info not valid') # assert decreasing order for a sequence of heights - height_sequence = [b[1] for b in best_blockchain] + 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 best_blockchain length - best_blockchain = dag['best_blockchain'] - self.assertEqual(len(best_blockchain), settings.DEFAULT_BEST_BLOCKCHAIN_BLOCKS) + # 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_block_info can be converted to BlockInfo + # assert a raw_height_info can be converted to HeightInfo try: - raw_block_info = best_blockchain[0] - block_info = BlockInfo.from_raw(raw_block_info) + 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(block_info.height, 20) + self.assertEqual(height_info.height, 20) except ValueError: self.fail('Block info not valid') # assert decreasing order for a sequence of heights - height_sequence = [b[1] for b in best_blockchain] + height_sequence = [hi[0] for hi in peer_best_blockchain] try: self.assertTrue(check_decreasing_monotonicity(height_sequence)) except ValueError as e: From 8ab76c4bab09afae3cb7ae342ba822ecbd69d46f Mon Sep 17 00:00:00 2001 From: Alex Ruzenhack Date: Thu, 3 Aug 2023 17:34:08 +0100 Subject: [PATCH 4/4] chore: add docstring and fix style --- hathor/p2p/resources/status.py | 4 ++-- hathor/p2p/utils.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/hathor/p2p/resources/status.py b/hathor/p2p/resources/status.py index 940ea6ac8..7287c6c1a 100644 --- a/hathor/p2p/resources/status.py +++ b/hathor/p2p/resources/status.py @@ -94,8 +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() - best_blockchain = to_serializable_best_blockchain( - self.manager.tx_storage.get_n_height_tips(settings.DEFAULT_BEST_BLOCKCHAIN_BLOCKS)) + 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': { diff --git a/hathor/p2p/utils.py b/hathor/p2p/utils.py index 55d872ad3..745ab05ee 100644 --- a/hathor/p2p/utils.py +++ b/hathor/p2p/utils.py @@ -226,4 +226,6 @@ def to_height_info(raw: tuple[int, str]) -> HeightInfo: 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]