Example #1
0
    def setUp(self):
        if self.network_definition is None:
            self.skipTest("This class doesn't represent a real test case.")
            raise NotImplementedError(
                "A network definition needs to be given.")

        self.testnet = RegtestNetwork(
            binary_folder=bin_dir,
            network_definition_location=self.network_definition,
            nodedata_folder=test_data_dir,
            node_limit='H',
            from_scratch=True)
        self.testnet.run_nocleanup()

        master_node_data_dir = self.testnet.master_node.lnd_data_dir
        master_node_port = self.testnet.master_node.grpc_port
        self.master_node_networkinfo = self.testnet.master_node.getnetworkinfo(
        )

        self.lndnode = LndNode(lnd_home=master_node_data_dir,
                               lnd_host='localhost:' + str(master_node_port),
                               regtest=True)

        self.rebalancer = Rebalancer(self.lndnode,
                                     max_effective_fee_rate=50,
                                     budget_sat=20)
        self.graph_test()
Example #2
0
def main():
    parser = Parser()

    # config.ini is expected to be in home/.lndmanage directory
    config_file = os.path.join(settings.home_dir, 'config.ini')

    # if lndmanage is run with arguments, run once
    if len(sys.argv) > 1:
        # take arguments from sys.argv
        args = parser.parse_arguments()
        node = LndNode(config_file=config_file)
        parser.run_commands(node, args)

    # otherwise enter an interactive mode
    else:
        history_file = os.path.join(settings.home_dir, "command_history")
        try:
            readline.read_history_file(history_file)
        except FileNotFoundError:
            # history will be written later
            pass

        logger.info("Running in interactive mode. "
                    "You can type 'help' or 'exit'.")
        node = LndNode(config_file=config_file)

        while True:
            try:
                user_input = input("$ lndmanage ")
            except (EOFError, KeyboardInterrupt):
                readline.write_history_file(history_file)
                logger.info("exit")
                return 0

            if user_input == 'help':
                parser.parser.print_help()
                continue
            elif user_input == 'exit':
                readline.write_history_file(history_file)
                return 0

            args_list = user_input.split(" ")
            try:
                # need to run with parse_known_args to get an exception
                args = parser.parser.parse_args(args_list)
                parser.run_commands(node, args)
            except:
                logger.exception("Exception encountered.")
                continue
Example #3
0
    def setUp(self):
        if self.network_definition is None:
            self.skipTest("This class doesn't represent a real test case.")
            raise NotImplementedError("A network definition path needs to be "
                                      "given.")

        self.testnet = Network(
            binary_folder=bin_dir,
            network_definition_location=self.network_definition,
            nodedata_folder=test_data_dir,
            node_limit='H',
            from_scratch=True
        )
        self.testnet.run_nocleanup()
        # to run the lightning network in the background and do some testing
        # here, run:
        # $ lnregtest --nodedata_folder /path/to/lndmanage/test/test_data/
        # self.testnet.run_from_background()

        # logger.info("Generated network information:")
        # logger.info(format_dict(self.testnet.node_mapping))
        # logger.info(format_dict(self.testnet.channel_mapping))
        # logger.info(format_dict(self.testnet.assemble_graph()))

        master_node_data_dir = self.testnet.master_node.data_dir
        master_node_port = self.testnet.master_node._grpc_port
        self.master_node_networkinfo = self.testnet.master_node.getnetworkinfo()

        self.lndnode = LndNode(
            lnd_home=master_node_data_dir,
            lnd_host='localhost:' + str(master_node_port),
            regtest=True
        )
        self.graph_test()
Example #4
0
            c['fees_total'] = 0
            c['fees_total_per_week'] = 0
            c['flow_direction'] = float('nan')
            c['median_forwarding_out'] = float('nan')
            c['median_forwarding_in'] = float('nan')
            c['mean_forwarding_out'] = float('nan')
            c['mean_forwarding_in'] = float('nan')
            c['number_forwardings'] = 0
            c['largest_forwarding_amount_in'] = float('nan')
            c['largest_forwarding_amount_out'] = float('nan')
            c['total_forwarding_in'] = float('nan')
            c['total_forwarding_out'] = float('nan')
            if abs(c['unbalancedness']) > settings.UNBALANCED_CHANNEL:
                c['action_required'] = True
            else:
                c['action_required'] = False
            if c['bandwidth_demand'] > 0.5:
                c['action_required'] = True
    return channels


if __name__ == '__main__':
    import time
    import logging.config
    logging.config.dictConfig(settings.logger_config)
    logger = logging.getLogger()

    nd = LndNode()
    fa = ForwardingAnalyzer(nd)
    fa.initialize_forwarding_data(time_start=0, time_end=time.time())
    print(fa.simple_flow_analysis())
Example #5
0
        channels_in[f['chan_id_in']] += f['amt_in']

        total_amount += f['amt_in']
        total_fees += f['fee_msat']
        transactions += 1
        fee_rate += f['effective_fee']

    fee_rate /= transactions
    print("-------- Forwarding statistics --------")
    print("Number of forwardings: {}".format(transactions))
    print("Total forwardings [sat]: {}".format(total_amount))
    print("Total fees earned [sat]: {:.3f}".format(total_fees / 1000.))
    print("Average fee rate: {:.6f}".format(fee_rate))

    print("-------- Popular channels --------")
    print("Popular channels out:")
    for w in sorted(channels_out, key=channels_out.get, reverse=True)[:10]:
        print(w, channels_out[w])
    print("Popular channels in:")
    for w in sorted(channels_in, key=channels_in.get, reverse=True)[:10]:
        print(w, channels_in[w])


if __name__ == '__main__':
    node = LndNode()
    forwardings = node.get_forwarding_events()

    # plot_forwardings(forwardings)
    # plot_fees(forwardings)
    statistics_forwardings(forwardings)
Example #6
0
    def append_to_history(self, stats: List[dict]):
        """append_history adds the fee setting statistics to a pickle file.

        :param stats: fee statistics"""
        logger.debug("Saving fee setting stats to fee history.")
        with open(self.history_path, "a") as f:
            json.dump(stats, f)
            f.write("\n")

    def read_history(self) -> List[dict]:
        """read_history is a function for unpickling the fee setting history.

        :return: list of fee setting statistics
        :rtype: list[dict]"""
        with open(self.history_path, "r") as f:
            history = []
            for i, line in enumerate(f):
                history.append(json.loads(line))
        return history


if __name__ == "__main__":
    from lndmanage.lib.node import LndNode
    import logging.config

    logging.config.dictConfig(settings.logger_config)

    nd = LndNode("/home/user/.lndmanage/config.ini")
    fee_setter = FeeSetter(nd)
    fee_setter.set_fees()
Example #7
0
        """
        Takes a list of nodes and finds the neighbors with most connections to the nodes.

        :param nodes: list
        :param blacklist_nodes: list of node_pub_keys to be excluded from counting
        :param nnodes: int, limit for the number of nodes returned
        :return: list of tuples, (str pub_key, int number of neighbors)
        """
        nodes = set(nodes)
        # eliminate blacklisted nodes
        nodes = nodes.difference(blacklist_nodes)
        neighboring_nodes = []
        for general_node in self.graph.nodes:
            neighbors_general_node = set(self.neighbors(general_node))
            intersection_with_nodes = nodes.intersection(neighbors_general_node)
            number_of_connection_with_nodes = len(intersection_with_nodes)
            neighboring_nodes.append((general_node, number_of_connection_with_nodes))

        sorted_neighboring_nodes = sorted(neighboring_nodes, key=lambda x: x[1], reverse=True)
        return sorted_neighboring_nodes[:nnodes]


if __name__ == '__main__':
    import logging.config
    logging.config.dictConfig(settings.logger_config)

    from lndmanage.lib.node import LndNode
    nd = LndNode('')
    print(f"Graph size: {nd.network.graph.size()}")
    print(f"Number of channels: {len(nd.network.edges.keys())}")
Example #8
0
    plt.show()


def plot_cltv(time_locks):
    exponent_min = 0
    exponent_max = 3

    bin_factor = 10

    bins_log = 10**np.linspace(
        exponent_min, exponent_max, (exponent_max - exponent_min) * bin_factor + 1)

    fig, ax = plt.subplots(figsize=standard_figsize, dpi=300)

    ax.hist(time_locks, bins=bins_log)
    plt.loglog()
    ax.set_xlabel("CLTV bins [blocks]")
    ax.set_ylabel("Number of channels")
    plt.tight_layout()
    plt.show()


if __name__ == "__main__":
    nd = LndNode('/home/user/.lndmanage/config.ini')
    base_fees, fee_rates, time_locks = extract_fee_settings(nd)

    plot_fee_rates(fee_rates)
    plot_base_fees(base_fees)
    plot_cltv(time_locks)

Example #9
0
def main():
    parser = Parser()

    # config.ini is expected to be in home/.lndmanage directory
    config_file = os.path.join(settings.home_dir, 'config.ini')

    # if lndmanage is run with arguments, run once
    if len(sys.argv) > 1:
        # take arguments from sys.argv
        args = parser.parse_arguments()
        node = LndNode(config_file=config_file)
        parser.run_commands(node, args)

    # otherwise enter an interactive mode
    else:
        history_file = os.path.join(settings.home_dir, "command_history")
        try:
            readline.read_history_file(history_file)
        except FileNotFoundError:
            # history will be written later
            pass

        logger.info("Running in interactive mode. "
                    "You can type 'help' or 'exit'.")
        node = LndNode(config_file=config_file)

        if parser.lncli_path:
            logger.info("Enabled lncli: using " + parser.lncli_path)

        while True:
            try:
                user_input = input("$ lndmanage ")
            except KeyboardInterrupt:
                logger.info("")
                continue
            except EOFError:
                readline.write_history_file(history_file)
                logger.info("exit")
                return 0

            if not user_input or user_input in ['help', '-h', '--help']:
                parser.parser.print_help()
                continue
            elif user_input == 'exit':
                readline.write_history_file(history_file)
                return 0

            args_list = user_input.split(" ")

            # lncli execution
            if args_list[0] == 'lncli':
                if parser.lncli_path:
                    lncli = Lncli(parser.lncli_path, config_file)
                    lncli.lncli(args_list[1:])
                    continue
                else:
                    logger.info(
                        "lncli not enabled, put lncli in PATH or in ~/.lndmanage"
                    )
                    continue
            try:
                # need to run with parse_known_args to get an exception
                args = parser.parser.parse_args(args_list)
                parser.run_commands(node, args)
            except SystemExit:
                # argparse may raise SystemExit on incorrect user input,
                # which is a graceful exit. The user gets the standard output
                # from argparse of what went wrong.
                continue
Example #10
0
def get_channel_properties(node: LndNode, time_interval_start: float,
                           time_interval_end: float) -> Dict:
    """Joins data from listchannels and fwdinghistory to have extended
    information about channels.

    :return: dict of channel information with channel_id as keys
    """
    forwarding_analyzer = ForwardingAnalyzer(node)
    forwarding_analyzer.initialize_forwarding_stats(time_interval_start,
                                                    time_interval_end)

    # dict with channel_id keys
    statistics = forwarding_analyzer.get_forwarding_statistics_channels()
    logger.debug(f"Time interval (between first and last forwarding) is "
                 f"{forwarding_analyzer.max_time_interval_days:6.2f} days.")
    # join the two data sets:
    channels = node.get_unbalanced_channels(unbalancedness_greater_than=0.0)

    for k, c in channels.items():
        # we may not have forwarding data for every channel
        chan_stats = statistics.get(c["chan_id"], {})
        c["forwardings_per_channel_age"] = (
            chan_stats.get("number_forwardings", 0.01) / c["age"])
        c["bandwidth_demand"] = (max(
            nan_to_zero(chan_stats.get("mean_forwarding_in", 0)),
            nan_to_zero(chan_stats.get("mean_forwarding_out", 0)),
        ) / c["capacity"])
        c["fees_out"] = chan_stats.get("fees_out", 0)
        c["fees_in"] = chan_stats.get("fees_in", 0)
        c["fees_in_out"] = chan_stats.get("fees_in_out", 0)
        try:
            c["fees_out_per_week"] = chan_stats.get("fees_out", 0) / (
                forwarding_analyzer.max_time_interval_days / 7)
        except ZeroDivisionError:
            c["fees_out_per_week"] = float("nan")
        try:
            c["fees_in_per_week"] = chan_stats.get("fees_in", 0) / (
                forwarding_analyzer.max_time_interval_days / 7)
        except ZeroDivisionError:
            c["fees_in_per_week"] = float("nan")
        c["flow_direction"] = chan_stats.get("flow_direction", float("nan"))
        c["median_forwarding_in"] = chan_stats.get("median_forwarding_in",
                                                   float("nan"))
        c["median_forwarding_out"] = chan_stats.get("median_forwarding_out",
                                                    float("nan"))
        c["mean_forwarding_in"] = chan_stats.get("mean_forwarding_in",
                                                 float("nan"))
        c["mean_forwarding_out"] = chan_stats.get("mean_forwarding_out",
                                                  float("nan"))
        c["number_forwardings"] = chan_stats.get("number_forwardings", 0)
        c["largest_forwarding_amount_in"] = chan_stats.get(
            "largest_forwarding_amount_in", float("nan"))
        c["largest_forwarding_amount_out"] = chan_stats.get(
            "largest_forwarding_amount_out", float("nan"))
        c["total_forwarding_in"] = chan_stats.get("total_forwarding_in", 0)
        c["total_forwarding_out"] = chan_stats.get("total_forwarding_out", 0)

        # action required if flow same direction as unbalancedness
        # or bandwidth demand too high
        # TODO: refine 'action_required' by better metric
        if (c["unbalancedness"] * c["flow_direction"] > 0
                and abs(c["unbalancedness"]) > settings.UNBALANCED_CHANNEL):
            c["action_required"] = True
        else:
            c["action_required"] = False
        if c["bandwidth_demand"] > 0.5:
            c["action_required"] = True

    return channels
Example #11
0
def get_node_properites(node: LndNode, time_interval_start: float,
                        time_interval_end: float) -> Dict:
    """Joins data from channels and fwdinghistory to have extended
    information about a node.

    :return: dict of node information with channel_id as keys
    """
    forwarding_analyzer = ForwardingAnalyzer(node)
    forwarding_analyzer.initialize_forwarding_stats(time_interval_start,
                                                    time_interval_end)
    node_forwarding_statistics = forwarding_analyzer.get_forwarding_statistics_nodes(
    )
    logger.debug(f"Time interval (between first and last forwarding) is "
                 f"{forwarding_analyzer.max_time_interval_days:6.2f} days.")
    channel_id_to_node_id = node.channel_id_to_node_id(open_only=True)
    node_ids_with_open_channels = {
        nid
        for nid in channel_id_to_node_id.values()
    }
    open_channels = node.get_open_channels()

    nodes_properties = {}  # type: Dict[str, NodeProperties]

    # for each channel, accumulate properties in node properties
    for k, c in open_channels.items():
        remote_pubkey = c["remote_pubkey"]
        try:
            properties = nodes_properties[remote_pubkey]
        except KeyError:
            nodes_properties[remote_pubkey] = NodeProperties(
                age=c["age"],
                local_fee_rates=[c["local_fee_rate"]],
                local_base_fees=[c["local_base_fee"]],
                local_balances=[c["local_balance"]],
                number_active_channels=1 if c["active"] else 0,
                number_channels=1,
                number_private_channels=1 if c["private"] else 0,
                remote_fee_rates=[c["peer_fee_rate"]],
                remote_base_fees=[c["peer_base_fee"]],
                remote_balances=[c["remote_balance"]],
                sent_received_per_week=c["sent_received_per_week"],
                public_capacities=[c["capacity"]] if not c["private"] else [],
                private_capacites=[c["capacity"]] if c["private"] else [],
            )
        else:
            properties.age = max(c["age"],
                                 nodes_properties[c["remote_pubkey"]].age)
            properties.local_fee_rates.append(c["local_fee_rate"])
            properties.local_base_fees.append(c["local_base_fee"])
            properties.local_balances.append(c["local_balance"])
            properties.number_active_channels += 1 if c["active"] else 0
            properties.number_channels += 1
            properties.number_private_channels += 1 if c["private"] else 0
            properties.remote_fee_rates.append(c["peer_fee_rate"])
            properties.remote_base_fees.append(c["peer_base_fee"])
            properties.remote_balances.append(c["remote_balance"])
            properties.sent_received_per_week += c["sent_received_per_week"]
            if not c["private"]:
                properties.public_capacities.append(c["capacity"])
            else:
                properties.private_capacites.append(c["capacity"])

    # unify node properties with forwarding data
    node_properties_forwardings = {}
    # we start with looping over node properties, as this info is complete
    for node_id, properties in nodes_properties.items():
        local_balance = sum(properties.local_balances)
        remote_balance = sum(properties.remote_balances)
        capacity = sum(properties.private_capacites) + sum(
            properties.public_capacities)
        # there can be old forwarding data, which we neglect
        if node_id not in node_ids_with_open_channels:
            continue

        # initial data:
        node_properties_forwardings[node_id] = {
            "age":
            properties.age,
            "alias":
            node.network.node_alias(node_id),
            "local_base_fee":
            np.median(properties.local_base_fees),
            "local_fee_rate":
            np.median(properties.local_fee_rates),
            "local_balance":
            local_balance,
            "max_local_balance":
            max(properties.local_balances),
            "max_remote_balance":
            max(properties.remote_balances),
            "number_channels":
            properties.number_channels,
            "number_active_channels":
            properties.number_active_channels,
            "number_private_channels":
            properties.number_private_channels,
            "node_id":
            node_id,
            "remote_base_fee":
            np.median(properties.remote_base_fees),
            "remote_fee_rate":
            np.median(properties.remote_fee_rates),
            "remote_balance":
            remote_balance,
            "sent_reveived_per_week":
            properties.sent_received_per_week,
            "total_capacity":
            capacity,
            "max_public_capacity":
            max(properties.public_capacities)
            if properties.public_capacities else 0,
            "unbalancedness":
            local_balance_to_unbalancedness(local_balance, capacity, 0,
                                            False)[0],
        }

        # add forwarding data if available:
        try:
            statistics = node_forwarding_statistics[node_id]
        except KeyError:  # we don't have forwarding data, populate with defaults
            node_properties_forwardings[node_id].update({
                "fees_out":
                0,
                "fees_in":
                0,
                "fees_in_out":
                0,
                "flow_direction":
                float("nan"),
                "largest_forwarding_amount_in":
                float("nan"),
                "largest_forwarding_amount_out":
                float("nan"),
                "median_forwarding_out":
                float("nan"),
                "median_forwarding_in":
                float("nan"),
                "mean_forwarding_out":
                float("nan"),
                "mean_forwarding_in":
                float("nan"),
                "number_forwardings":
                0,
                "total_forwarding_in":
                0,
                "total_forwarding_out":
                0,
                "total_forwarding":
                0,
            })
        else:
            node_properties_forwardings[node_id].update(**statistics)

        try:
            node_properties_forwardings[node_id][
                "fees_out_per_week"] = node_properties_forwardings[node_id][
                    "fees_out"] / (forwarding_analyzer.max_time_interval_days /
                                   7)
        except ZeroDivisionError:
            node_properties_forwardings[node_id]["fees_out_per_week"] = float(
                "nan")
        try:
            node_properties_forwardings[node_id][
                "fees_in_per_week"] = node_properties_forwardings[node_id][
                    "fees_in"] / (forwarding_analyzer.max_time_interval_days /
                                  7)
        except ZeroDivisionError:
            node_properties_forwardings[node_id]["fees_in_per_week"] = float(
                "nan")

    # TODO: unify with information from liquidity hints

    return node_properties_forwardings
Example #12
0
from lndmanage.lib.listchannels import ListChannels
from lndmanage.lib.node import LndNode
from lndmanage import settings

import logging.config
logging.config.dictConfig(settings.logger_config)

if __name__ == '__main__':
    node = LndNode()
    listchannels = ListChannels(node)
    node.print_status()
    listchannels.print_channels_unbalanced(
        unbalancedness=settings.UNBALANCED_CHANNEL, sort_string='alias')
Example #13
0
class CircleTest(TestCase):

    network_definition = None

    def setUp(self):
        if self.network_definition is None:
            self.skipTest("This class doesn't represent a real test case.")
            raise NotImplementedError(
                "A network definition needs to be given.")

        self.testnet = RegtestNetwork(
            binary_folder=bin_dir,
            network_definition_location=self.network_definition,
            nodedata_folder=test_data_dir,
            node_limit='H',
            from_scratch=True)
        self.testnet.run_nocleanup()

        master_node_data_dir = self.testnet.master_node.lnd_data_dir
        master_node_port = self.testnet.master_node.grpc_port
        self.master_node_networkinfo = self.testnet.master_node.getnetworkinfo(
        )

        self.lndnode = LndNode(lnd_home=master_node_data_dir,
                               lnd_host='localhost:' + str(master_node_port),
                               regtest=True)

        self.rebalancer = Rebalancer(self.lndnode,
                                     max_effective_fee_rate=50,
                                     budget_sat=20)
        self.graph_test()

    def tearDown(self):
        self.testnet.cleanup()

    def graph_test(self):
        raise NotImplementedError

    def circle_and_check(self,
                         rebalancer,
                         channel_number_send,
                         channel_number_receive,
                         amount_sat,
                         expected_fees_msat,
                         dry=False):
        """
        Helper function for testing a circular payment.

        :param rebalancer:
        :type rebalancer: lndmanage.lib.rebalance.Rebalancer

        :param channel_number_send: channel whose local balance is decreased
        :type channel_number_send: int

        :param channel_number_receive: channel whose local balance is increased
        :type channel_number_receive: int

        :param amount_sat: amount in satoshi to rebalance
        :type amount_sat: int

        :param expected_fees_msat: expected fees in millisatoshi for
            the rebalance
        :type expected_fees_msat: int

        :param dry: if it should be a dry run
        :type dry: bool
        """
        # setup
        graph_before = self.testnet.assemble_graph()
        channel_id_send = self.testnet.channel_mapping[channel_number_send][
            'channel_id']
        channel_id_receive = self.testnet.channel_mapping[
            channel_number_receive]['channel_id']
        invoice_r_hash = self.lndnode.get_invoice(amount_sat, '')

        # exercise
        try:
            fees_msat = rebalancer.rebalance_two_channels(
                channel_id_send,
                channel_id_receive,
                amount_sat,
                invoice_r_hash,
                rebalancer.budget_sat,
                dry=dry)
            time.sleep(SLEEP_SEC_AFTER_REBALANCING)
        except Exception as e:
            raise e

        # check
        graph_after = self.testnet.assemble_graph()
        channel_data_send_before = graph_before['A'][channel_number_send]
        channel_data_receive_before = graph_before['A'][channel_number_receive]
        channel_data_send_after = graph_after['A'][channel_number_send]
        channel_data_receive_after = graph_after['A'][channel_number_receive]
        listchannels = ListChannels(self.lndnode)
        listchannels.print_all_channels('rev_alias')

        # test that the fees are correct
        self.assertEqual(fees_msat, expected_fees_msat)
        # test if sending channel's remote balance has increased correctly
        self.assertEqual(
            amount_sat, channel_data_send_after['remote_balance'] -
            channel_data_send_before['remote_balance'] -
            int(expected_fees_msat // 1000), "Sending local balance is wrong")
        # test if receiving channel's local balance has increased correctly
        self.assertEqual(
            amount_sat, channel_data_receive_after['local_balance'] -
            channel_data_receive_before['local_balance'],
            "Receiving local balance is wrong")
Example #14
0
from lndmanage.lib.network_info import NetworkAnalysis
from lndmanage.lib.node import LndNode
from lndmanage import settings

import logging.config
logging.config.dictConfig(settings.logger_config)
logger = logging.getLogger(__name__)

if __name__ == '__main__':
    node = LndNode()
    network_analysis = NetworkAnalysis(node)

    network_analysis.print_node_overview(node.pub_key)

    logger.info('-------- Nodes with highest capacity: --------')
    for n in network_analysis.get_sorted_nodes_by_property():
        logger.info(n)
    logger.info('-------- Nodes with highest degree: --------')
    for n in network_analysis.get_sorted_nodes_by_property(key='degree'):
        logger.info(n)
    logger.info('-------- Nodes with highest capacity/channel: --------')
    for n in network_analysis.get_sorted_nodes_by_property(
            key='capacity_per_channel', min_degree=10):
        logger.info(n)
    logger.info('-------- Nodes with lowest capacity/channel: --------')
    for n in network_analysis.get_sorted_nodes_by_property(
            key='capacity_per_channel', min_degree=20, decrementing=False):
        logger.info(n)
    logger.info('-------- Nodes with most user nodes: --------')
    for n in network_analysis.get_sorted_nodes_by_property(key='user_nodes',
                                                           min_degree=20):