diff --git a/README.md b/README.md index 7f0cd69d7..9eb27e464 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Community curated plugins for c-lightning. | [drain][drain] | Draining, filling and balancing channels with automatic chunks. | | [event-websocket][event-websocket] | Exposes notifications over a Websocket | | [feeadjuster][feeadjuster] | Dynamic fees to keep your channels more balanced | +| [fixroute] | Compute a route that includes a number of specified waypoints | | [graphql][graphql] | Exposes the c-lightning API over [graphql][graphql-spec] | | [invoice-queue][invoice-queue] | Listen to lightning invoices from multiple nodes and send to a redis queue for processing | | [lightning-qt][lightning-qt] | A bitcoin-qt-like GUI for lightningd | @@ -168,6 +169,7 @@ Python plugins developers must ensure their plugin to work with all Python versi [esplora]: https://github.com/Blockstream/esplora [pers-chans]: https://github.com/lightningd/plugins/tree/master/persistent-channels +[fixroute]: https://github.com/lightningd/plugins/tree/master/fixroute [probe]: https://github.com/lightningd/plugins/tree/master/probe [prometheus]: https://github.com/lightningd/plugins/tree/master/prometheus [summary]: https://github.com/lightningd/plugins/tree/master/summary diff --git a/fixroute/README.md b/fixroute/README.md new file mode 100644 index 000000000..ff83b302a --- /dev/null +++ b/fixroute/README.md @@ -0,0 +1,41 @@ +# Fixed Payment Route Plugin + +This plugin helps you to construct a route object (to be used with sendpay) +which goes over a sequence of node ids. If all these nodeids are on a path of +payment channels an onion following this path will be constructed. In the case +of missing channels `lightning-cli getroute` is invoked to find partial +routes. + +This plugin can be used to create circular onions or to send payments along +specific paths if that is necessary (as long as the paths provide enough +liquidity). + +I guess this plugin could also be used to simulate the behaviour of trampoline +payments. + +And of course I imagine you the reader of this message will find even more +creative ways of using it. + + +> :warning: This plugin is still work in progress and may not work in all edge cases. :construction: + +## Command line options + +The plugin exposes no new command line options. + +## JSON-RPC methods + +The plugin also exposes the following methods: + + - `getfixedroute`: constructs a route for an amount in `msat` and a list of + `nodeid`s over which the path should be constructed. + + - `getfixedroute_purge`: As the plugin builds an index over the gossip store + on startup to know the channel metadata the index might be become outdated + and require a rebuild once in a while. This happens in particular if + `sendpay` can't forward onions because of wrong channel parameters. + + +## Support: +If you like my work consider a donation at https://patreon.com/renepickhardt +or https://tallyco.in/s/lnbook diff --git a/fixroute/fixroute.py b/fixroute/fixroute.py new file mode 100755 index 000000000..05b0fa8b8 --- /dev/null +++ b/fixroute/fixroute.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +""" +author: Rene Pickhardt (rene.m.pickhardt@ntnu.no) +Date: 24.1.2020 +License: MIT + +This plugin helps you to construct a route object (to be used with `sendpay`) +which goes over a sequence of node ids. If all these nodeids are on a path of +payment channels an onion following this path will be constructed. In the case +of missing channels `lightning-cli getroute` is invoked to find partial routes. + +This plugin can be used to create circular onions or to send payments along +specific paths if that is necessary (as long as the paths provide enough +liquidity). + +I guess this plugin could also be used to simulate the behaviour of trampoline +payments. + +And of course I imagine you the reader of this message will find even more +creative ways of using it. + +=== Support: + +If you like my work consider a donation at https://patreon.com/renepickhardt +or https://tallyco.in/s/lnbook + +""" + +from pyln.client import Plugin +from pyln.client import RpcError +import itertools + + +plugin = Plugin(autopatch=True) + + +@plugin.method( + "getfixedroute", + long_desc=""" +Returns a route object to be used in send pay over a fixed set of nodes +""" +) +def getfixedroute(plugin, amount, nodes): + """Construct a fixed route over a list of node ids. + + If channels exist between consecutive nodes these channels will be used. + Otherwise `lightning-cli getroute` will be invoked to find partial routes. + + """ + delay = 9 + fees = 0 + result = [] + + def pairs(iterable): + x, y = itertools.tee(iterable) + next(y, None) + return zip(x, y) + + for src, dest in reversed(list(pairs(nodes))): + amount = amount + fees + key = "{}:{}".format(src, dest) + if key not in plugin.channels: + try: + route = plugin.rpc.getroute( + node_id=dest, msatoshi=amount, riskfactor=1, cltv=delay, + fromid=src + )["route"] + except RpcError as exc: + raise ValueError(( + "could not compute route between waypoints {src} and " + "{dest}: {e}" + ).format(src=src, dest=dest, e=exc)) + + for e in reversed(route): + result.append(e) + + key = "{}:{}".format(src, route[0]["id"]) + chan = plugin.channels[key] + + base = chan["base_fee_millisatoshi"] + prop = chan["fee_per_millionth"] + + fees = base + amount * prop / 10**6 + delay = result[-1]["delay"] + chan["delay"] + + else: + chan = plugin.channels[key] + # https://github.com/ElementsProject/lightning/blob/edbcb6/gossipd/routing.h#L253 + + direction = 0 + # I guess the following reverses the definition with the DER + # encoding of channels for all my tests the results where the same + # as in getroute but I am not sure if this is actually + # correct. please can someone verify and remove this message: + # https://github.com/ElementsProject/lightning/blob/edbcb6/gossipd/routing.h#L56 + if dest < src: + direction = 1 + + # https://github.com/ElementsProject/lightning/blob/edbcb6/gossipd/routing.c#L381 + style = "legacy" + + # https://github.com/ElementsProject/lightning/blob/edbcb6f/gossipd/routing.c#L2526 + # and : + # https://github.com/lightningnetwork/lightning-rfc/blob/master/09-features.md + features = int(plugin.nodes[dest]["globalfeatures"], 16) + if features & 0x01 << 8 != 0 or features & 0x01 << 9 != 0: + style = "tlv" + entry = { + "id": dest, + "channel": chan["short_channel_id"], + "direction": direction, + "msatoshi": amount, + "amount_msat": "{}msat".format(amount), + "delay": delay, + "style": style + } + result.append(entry) + + base = chan["base_fee_millisatoshi"] + prop = chan["fee_per_millionth"] + + fees = base + amount * prop / 10**6 + delay = delay + chan["delay"] + + fees = int(fees) + result = list(reversed(result)) + + # id, chanel, direction, msatoshi, amount_msat, delay, style + return {"route": result} + + +@plugin.method( + "getfixedroute_purge", + long_desc="purges the index of the gossip store" +) +def refresh_gossip_info(plugin): + """ + purges the index gossip store. + """ + channels = plugin.rpc.listchannels()["channels"] + + for chan in channels: + key = "{}:{}".format(chan["source"], chan["destination"]) + plugin.channels[key] = chan + + for u in plugin.rpc.listnodes()["nodes"]: + plugin.nodes[u["nodeid"]] = u + + return {"result": "successfully reindexed the gossip store."} + + +@plugin.init() +def init(options, configuration, plugin): + plugin.log("Plugin fixroute_pay registered") + refresh_gossip_info(plugin) + + +plugin.run() diff --git a/fixroute/requirements.txt b/fixroute/requirements.txt new file mode 100644 index 000000000..d0ceb1ac4 --- /dev/null +++ b/fixroute/requirements.txt @@ -0,0 +1 @@ +pyln-client>=0.8 diff --git a/fixroute/test_fixroute.py b/fixroute/test_fixroute.py new file mode 100644 index 000000000..aed1a9cb8 --- /dev/null +++ b/fixroute/test_fixroute.py @@ -0,0 +1,31 @@ +import os +from pyln.testing.fixtures import * # noqa: F401,F403 + + +plugin_path = os.path.join(os.path.dirname(__file__), "fixroute.py") + + +def test_fixroute_starts(node_factory): + l1 = node_factory.get_node(options={'plugin': plugin_path}) + + # Ensure the plugin is still running: + conf = l1.rpc.listconfigs() + expected = [{ + 'name': 'fixroute.py', + 'path': plugin_path + }] + assert(conf['plugins'] == expected) + + +def test_fixroute_dynamic_starts(node_factory): + l1 = node_factory.get_node() + l1.rpc.plugin_start(plugin_path) + l1.rpc.plugin_stop(plugin_path) + l1.rpc.plugin_start(plugin_path) + # Ensure the plugin is still running: + conf = l1.rpc.listconfigs() + expected = [{ + 'name': 'fixroute.py', + 'path': plugin_path + }] + assert(conf['plugins'] == expected)