invoiceless payment plugin

- sends payment without an invoice from the receiving node
- uses circular payments: takes the money to the target node, pays in the form of routing fee, and brings some change back
- no plugin/update required on the receiving side
This commit is contained in:
Gálli Zoltán
2019-04-24 23:17:26 +02:00
committed by Christian Decker
parent 799eb51528
commit 9815483491
2 changed files with 142 additions and 0 deletions

23
sendinvoiceless/README.md Normal file
View File

@@ -0,0 +1,23 @@
# Invoiceless payment plugin
This plugin sends some msatoshis without needing to have an invoice from the receiving node. It uses circular
payment: takes the money to the receiving node, pays in the form of routing fee, and brings some change back to
close the circle.
The plugin can be started with `lightningd` by adding the following `--plugin` option
(adjusting the path to wherever the plugins are actually stored):
```
lightningd --plugin=/path/to/plugin/sendinvoiceless.py
```
Once the plugin is active you can send payment by running:
`lightning-cli sendinvoiceless nodeid msatoshi [maxfeepercent] [retry_for] [exemptfee]`
The `nodeid` is the identifier of the receiving node. The `maxfeepercent` limits the money paid in fees and
defaults to 0.5. The maxfeepercent' is a percentage of the amount that is to be paid. The `exemptfee` option can
be used for tiny payments which would be dominated by the fee leveraged by forwarding nodes. Setting exemptfee
allows the maxfeepercent check to be skipped on fees that are smaller than exemptfee (default: 5000 millisatoshi).
The command will keep finding routes and retrying the payment until it succeeds, or the given `retry_for` seconds
pass. retry_for defaults to 60 seconds and can only be an integer.

View File

@@ -0,0 +1,119 @@
#!/usr/bin/env python3
from lightning import Plugin, Millisatoshi, RpcError
import time
import uuid
plugin = Plugin()
def setup_routing_fees(plugin, route, msatoshi, payload):
delay = int(plugin.get_option('cltv-final'))
for r in reversed(route):
r['msatoshi'] = r['amount_msat'] = msatoshi
r['delay'] = delay
channels = plugin.rpc.listchannels(r['channel'])
for ch in channels.get('channels'):
if ch['destination'] == r['id']:
fee = Millisatoshi(ch['base_fee_millisatoshi'])
fee += msatoshi * ch['fee_per_millionth'] // 1000000
if ch['source'] == payload['nodeid']:
if fee <= payload['msatoshi']:
fee = payload['msatoshi']
else:
raise RpcError("sendinvoiceless", payload, {'message': 'Insufficient sending amount'})
msatoshi += fee
delay += ch['delay']
r['direction'] = int(ch['channel_flags']) % 2
def find_worst_channel(route, nodeid):
worst = None
worst_val = Millisatoshi(0)
for i in range(1, len(route)):
if route[i - 1]['id'] == nodeid:
continue
val = route[i - 1]['msatoshi'] - route[i]['msatoshi']
if val > worst_val:
worst = route[i]['channel'] + '/' + str(route[i]['direction'])
worst_val = val
return worst
def sendinvoiceless_fail(plugin, label, payload, success_msg, error=None):
try:
plugin.rpc.delinvoice(label, 'unpaid')
except RpcError as e:
# race condition: waitsendpay timed out, but invoice get paid
if 'status is paid' in e.error.get('message', ""):
return success_msg
if error is None:
error = RpcError("sendinvoiceless", payload, {'message': 'Sending failed'})
raise error
@plugin.method("sendinvoiceless")
def sendinvoiceless(plugin, nodeid, msatoshi: Millisatoshi, maxfeepercent="0.5",
retry_for=60, exemptfee: Millisatoshi=Millisatoshi(5000)):
"""Invoiceless payment with circular routes.
This tool sends some msatoshis without needing to have an invoice from the receiving node.
"""
payload = {
"nodeid": nodeid,
"msatoshi": msatoshi,
"maxfeepercent": maxfeepercent,
"retry_for": retry_for,
"exemptfee": exemptfee
}
myid = plugin.rpc.getinfo().get('id')
label = "InvoicelessChange-" + str(uuid.uuid4())
description = "Sending %s to %s" % (msatoshi, nodeid)
change = Millisatoshi(1000)
invoice = plugin.rpc.invoice(change, label, description, int(retry_for) + 60)
payment_hash = invoice['payment_hash']
plugin.log("Invoice payment_hash: %s" % payment_hash)
success_msg = ""
try:
excludes = []
start_ts = int(time.time())
while int(time.time()) - start_ts < int(retry_for):
forth = plugin.rpc.getroute(nodeid, msatoshi + change, riskfactor=10, exclude=excludes)
back = plugin.rpc.getroute(myid, change, riskfactor=10, fromid=nodeid, exclude=excludes)
route = forth['route'] + back['route']
setup_routing_fees(plugin, route, change, payload)
fees = route[0]['msatoshi'] - route[-1]['msatoshi'] - msatoshi
if fees > exemptfee and fees > msatoshi * float(maxfeepercent) / 100:
worst_channel = find_worst_channel(route, nodeid)
if worst_channel is None:
raise RpcError("sendinvoiceless", payload, {'message': 'Insufficient fee'})
excludes.append(worst_channel)
continue
try:
plugin.log("Sending %s over %d hops to deliver %s and bring back %s" %
(route[0]['msatoshi'], len(route), msatoshi, change))
for r in route:
plugin.log("Node: %s, channel: %13s, %s" % (r['id'], r['channel'], r['msatoshi']))
success_msg = "%d msat delivered with %d msat fee over %d hops" % (msatoshi, fees, len(route))
plugin.rpc.sendpay(route, payment_hash)
plugin.rpc.waitsendpay(payment_hash, int(retry_for) + start_ts - int(time.time()))
return success_msg
except RpcError as e:
plugin.log("RpcError: " + str(e))
erring_channel = e.error.get('data', {}).get('erring_channel')
erring_direction = e.error.get('data', {}).get('erring_direction')
if erring_channel is not None and erring_direction is not None:
excludes.append(erring_channel + '/' + str(erring_direction))
except Exception as e:
plugin.log("Exception: " + str(e))
return sendinvoiceless_fail(plugin, label, payload, success_msg, e)
return sendinvoiceless_fail(plugin, label, payload, success_msg)
@plugin.init()
def init(options, configuration, plugin):
plugin.options['cltv-final']['value'] = plugin.rpc.listconfigs().get('cltv-final')
plugin.log("Plugin sendinvoiceless.py initialized")
plugin.add_option('cltv-final', 10, 'Number of blocks for final CheckLockTimeVerify expiry')
plugin.run()