tMerge pull request #6615 from bitromortac/dumpgraph - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit 84dc181b6e7bb20e88ef6b98fb8925c5f645a765 DIR parent 2a1699c0e5ecfeb79b4395f8a52df5eee3822f3b HTML Author: ThomasV <thomasv@electrum.org> Date: Fri, 2 Oct 2020 09:59:36 +0200 Merge pull request #6615 from bitromortac/dumpgraph Fix dumpgraph command: give out json encoded nodes and channels Diffstat: M electrum/channel_db.py | 62 +++++++++++++++++++++++++------ M electrum/commands.py | 13 +++---------- M electrum/util.py | 7 +++++++ 3 files changed, 61 insertions(+), 21 deletions(-) --- DIR diff --git a/electrum/channel_db.py b/electrum/channel_db.py t@@ -37,7 +37,7 @@ from enum import IntEnum from .sql_db import SqlDB, sql from . import constants, util -from .util import bh2u, profiler, get_headers_dir, bfh, is_ip_address, list_enabled_bits +from .util import bh2u, profiler, get_headers_dir, is_ip_address, json_normalize from .logging import Logger from .lnutil import (LNPeerAddr, format_short_channel_id, ShortChannelID, validate_features, IncompatibleOrInsaneFeatures) t@@ -52,6 +52,15 @@ if TYPE_CHECKING: FLAG_DISABLE = 1 << 1 FLAG_DIRECTION = 1 << 0 + +class NodeAddress(NamedTuple): + """Holds address information of Lightning nodes + and how up to date this info is.""" + host: str + port: int + timestamp: int + + class ChannelInfo(NamedTuple): short_channel_id: ShortChannelID node1_id: bytes t@@ -123,7 +132,6 @@ class Policy(NamedTuple): return self.key[8:] - class NodeInfo(NamedTuple): node_id: bytes features: int t@@ -262,7 +270,7 @@ class ChannelDB(SqlDB): self._policies = {} # type: Dict[Tuple[bytes, ShortChannelID], Policy] # (node_id, scid) -> Policy self._nodes = {} # type: Dict[bytes, NodeInfo] # node_id -> NodeInfo # node_id -> (host, port, ts) - self._addresses = defaultdict(set) # type: Dict[bytes, Set[Tuple[str, int, int]]] + self._addresses = defaultdict(set) # type: Dict[bytes, Set[NodeAddress]] self._channels_for_node = defaultdict(set) # type: Dict[bytes, Set[ShortChannelID]] self._recent_peers = [] # type: List[bytes] # list of node_ids self._chans_with_0_policies = set() # type: Set[ShortChannelID] t@@ -287,7 +295,7 @@ class ChannelDB(SqlDB): now = int(time.time()) node_id = peer.pubkey with self.lock: - self._addresses[node_id].add((peer.host, peer.port, now)) + self._addresses[node_id].add(NodeAddress(peer.host, peer.port, now)) # list is ordered if node_id in self._recent_peers: self._recent_peers.remove(node_id) t@@ -304,10 +312,9 @@ class ChannelDB(SqlDB): r = self._addresses.get(node_id) if not r: return None - addr = sorted(list(r), key=lambda x: x[2])[0] - host, port, timestamp = addr + addr = sorted(list(r), key=lambda x: x.timestamp)[0] try: - return LNPeerAddr(host, port, node_id) + return LNPeerAddr(addr.host, addr.port, node_id) except ValueError: return None t@@ -549,7 +556,7 @@ class ChannelDB(SqlDB): self._db_save_node_info(node_id, msg_payload['raw']) with self.lock: for addr in node_addresses: - self._addresses[node_id].add((addr.host, addr.port, 0)) + self._addresses[node_id].add(NodeAddress(addr.host, addr.port, 0)) self._db_save_node_addresses(node_addresses) self.logger.debug("on_node_announcement: %d/%d"%(len(new_nodes), len(msg_payloads))) t@@ -613,11 +620,11 @@ class ChannelDB(SqlDB): c.execute("""SELECT * FROM address""") for x in c: node_id, host, port, timestamp = x - self._addresses[node_id].add((str(host), int(port), int(timestamp or 0))) + self._addresses[node_id].add(NodeAddress(str(host), int(port), int(timestamp or 0))) def newest_ts_for_node_id(node_id): newest_ts = 0 - for host, port, ts in self._addresses[node_id]: - newest_ts = max(newest_ts, ts) + for addr in self._addresses[node_id]: + newest_ts = max(newest_ts, addr.timestamp) return newest_ts sorted_node_ids = sorted(self._addresses.keys(), key=newest_ts_for_node_id, reverse=True) self._recent_peers = sorted_node_ids[:self.NUM_MAX_RECENT_PEERS] t@@ -750,3 +757,36 @@ class ChannelDB(SqlDB): def get_node_info_for_node_id(self, node_id: bytes) -> Optional['NodeInfo']: return self._nodes.get(node_id) + + def to_dict(self) -> dict: + """ Generates a graph representation in terms of a dictionary. + + The dictionary contains only native python types and can be encoded + to json. + """ + with self.lock: + graph = {'nodes': [], 'channels': []} + + # gather nodes + for pk, nodeinfo in self._nodes.items(): + # use _asdict() to convert NamedTuples to json encodable dicts + graph['nodes'].append( + nodeinfo._asdict(), + ) + graph['nodes'][-1]['addresses'] = [addr._asdict() for addr in self._addresses[pk]] + + # gather channels + for cid, channelinfo in self._channels.items(): + graph['channels'].append( + channelinfo._asdict(), + ) + policy1 = self._policies.get( + (channelinfo.node1_id, channelinfo.short_channel_id)) + policy2 = self._policies.get( + (channelinfo.node2_id, channelinfo.short_channel_id)) + graph['channels'][-1]['policy1'] = policy1._asdict() if policy1 else None + graph['channels'][-1]['policy2'] = policy2._asdict() if policy2 else None + + # need to use json_normalize otherwise json encoding in rpc server fails + graph = json_normalize(graph) + return graph DIR diff --git a/electrum/commands.py b/electrum/commands.py t@@ -39,8 +39,8 @@ from decimal import Decimal from typing import Optional, TYPE_CHECKING, Dict, List from .import util, ecc -from .util import bfh, bh2u, format_satoshis, json_decode, json_encode, is_hash256_str, is_hex_str, to_bytes, timestamp_to_datetime -from .util import standardize_path +from .util import (bfh, bh2u, format_satoshis, json_decode, json_normalize, + is_hash256_str, is_hex_str, to_bytes) from . import bitcoin from .bitcoin import is_address, hash_160, COIN from .bip32 import BIP32Node t@@ -82,13 +82,6 @@ def satoshis(amount): def format_satoshis(x): return str(Decimal(x)/COIN) if x is not None else None -def json_normalize(x): - # note: The return value of commands, when going through the JSON-RPC interface, - # is json-encoded. The encoder used there cannot handle some types, e.g. electrum.util.Satoshis. - # note: We should not simply do "json_encode(x)" here, as then later x would get doubly json-encoded. - # see #5868 - return json_decode(json_encode(x)) - class Command: def __init__(self, func, s): t@@ -1050,7 +1043,7 @@ class Commands: @command('wn') async def dumpgraph(self, wallet: Abstract_Wallet = None): - return list(map(bh2u, wallet.lnworker.channel_db.nodes.keys())) + return wallet.lnworker.channel_db.to_dict() @command('n') async def inject_fees(self, fees): DIR diff --git a/electrum/util.py b/electrum/util.py t@@ -379,6 +379,13 @@ def json_decode(x): except: return x +def json_normalize(x): + # note: The return value of commands, when going through the JSON-RPC interface, + # is json-encoded. The encoder used there cannot handle some types, e.g. electrum.util.Satoshis. + # note: We should not simply do "json_encode(x)" here, as then later x would get doubly json-encoded. + # see #5868 + return json_decode(json_encode(x)) + # taken from Django Source Code def constant_time_compare(val1, val2):