Files
plugins/noise/noise.py

195 lines
6.1 KiB
Python
Executable File

#!/usr/bin/env python3
from pyln.client import Plugin, RpcError
from pyln.proto.primitives import varint_decode, varint_encode
from onion import TlvPayload
from binascii import hexlify, unhexlify
import struct
import string
import random
from io import BytesIO
import logging
from collections import namedtuple
import shelve
from pyln.proto.onion import OnionPayload
import zbase32
import hashlib
import os
plugin = Plugin()
TLV_KEYSEND_PREIMAGE = 34349341
TLV_NOISE_MESSAGE = 34349334
TLV_NOISE_SIGNATURE = 34349335
class Message(object):
def __init__(self, sender, body, signature, payment=None, id=None):
self.id = id
self.sender = sender
self.body = body
self.signature = signature
self.payment = payment
self.verified = None
def to_dict(self):
return {
"id": self.id,
"sender": self.sender,
"body": self.body,
"signature": hexlify(self.signature).decode('ASCII'),
"payment": self.payment,
"verified": self.verified,
}
def serialize_payload(n, blockheight):
block, tx, out = n['channel'].split('x')
payload = hexlify(struct.pack(
"!cQQL", b'\x00',
int(block) << 40 | int(tx) << 16 | int(out),
int(n['amount_msat']),
blockheight + n['delay'])).decode('ASCII')
payload += "00" * 12
return payload
def buildpath(plugin, node_id, payload, amt, exclusions):
blockheight = plugin.rpc.getinfo()['blockheight']
route = plugin.rpc.getroute(node_id, amt, 10, exclude=exclusions)['route']
first_hop = route[0]
# Need to shift the parameters by one hop
hops = []
for h, n in zip(route[:-1], route[1:]):
# We tell the node h about the parameters to use for n (a.k.a. h + 1)
hops.append({
"type": "legacy",
"pubkey": h['id'],
"payload": serialize_payload(n, blockheight)
})
# The last hop has a special payload:
hops.append({
"type": "tlv",
"pubkey": route[-1]['id'],
"payload": hexlify(payload).decode('ASCII'),
})
return first_hop, hops, route
def deliver(node_id, payload, amt, payment_hash, max_attempts=5):
"""Do your best to deliver `payload` to `node_id`.
"""
exclusions = []
payment_hash = hexlify(payment_hash).decode('ASCII')
for attempt in range(max_attempts):
plugin.log("Starting attempt {} to deliver message to {}".format(attempt, node_id))
first_hop, hops, route = buildpath(plugin, node_id, payload, amt, exclusions)
onion = plugin.rpc.createonion(hops=hops, assocdata=payment_hash)
plugin.rpc.sendonion(onion=onion['onion'],
first_hop=first_hop,
payment_hash=payment_hash,
shared_secrets=onion['shared_secrets']
)
try:
plugin.rpc.waitsendpay(payment_hash=payment_hash)
return {'route': route, 'payment_hash': payment_hash, 'attempt': attempt}
except RpcError as e:
failcode = e.error['data']['failcode']
failingidx = e.error['data']['erring_index']
if failcode == 16399 or failingidx == len(hops):
return {'route': route, 'payment_hash': payment_hash, 'attempt': attempt+1}
plugin.log("Retrying delivery.")
# TODO Store the failing channel in the exclusions
raise ValueError('Could not reach destination {node_id}'.format(node_id=node_id))
@plugin.async_method('sendmsg')
def sendmsg(node_id, msg, plugin, request, pay=None, **kwargs):
payload = TlvPayload()
payload.add_field(TLV_NOISE_MESSAGE, msg.encode('UTF-8'))
payment_key = os.urandom(32)
payment_hash = hashlib.sha256(payment_key).digest()
# If we don't want to tell the recipient how to claim the funds unset the
# payment_key
if pay is not None:
payload.add_field(TLV_KEYSEND_PREIMAGE, payment_key)
# Signature generation always has to be last, otherwise we won't agree on
# the TLV payload and verification ends up with a bogus sender node_id.
sigmsg = hexlify(payload.to_bytes()).decode('ASCII')
sig = plugin.rpc.signmessage(sigmsg)
sigcheck = plugin.rpc.checkmessage(sigmsg, sig['zbase'])
sig = zbase32.decode(sig['zbase'])
payload.add_field(TLV_NOISE_SIGNATURE, sig)
res = deliver(
node_id,
payload.to_bytes(),
amt=pay if pay is not None else 10,
payment_hash=payment_hash
)
request.set_result(res)
@plugin.async_method('recvmsg')
def recvmsg(plugin, request, last_id=None, **kwargs):
next_id = int(last_id) + 1 if last_id is not None else len(plugin.messages)
if next_id < len(plugin.messages):
request.set_result(plugin.messages[int(last_id)].to_dict())
else:
plugin.receive_waiters.append(request)
@plugin.hook('htlc_accepted')
def on_htlc_accepted(onion, htlc, plugin, **kwargs):
payload = OnionPayload.from_hex(onion['payload'])
msg = Message(
id=len(plugin.messages),
sender=None,
body=payload.get(34349334).value,
signature=payload.get(34349335).value,
payment=None)
# Filter out the signature so we can check it against the rest of the payload
sigpayload = TlvPayload()
sigpayload.fields = filter(lambda x: x.typenum != TLV_NOISE_SIGNATURE, payload.fields)
sigmsg = hexlify(sigpayload.to_bytes()).decode('ASCII')
zsig = zbase32.encode(msg.signature).decode('ASCII')
sigcheck = plugin.rpc.checkmessage(sigmsg, zsig)
msg.sender = sigcheck['pubkey']
msg.verified = sigcheck['verified']
preimage = payload.get(TLV_KEYSEND_PREIMAGE)
if preimage is not None:
res = {
'result': 'resolve',
'payment_key': hexlify(preimage.value).decode('ASCII')
}
else:
res = {'result': 'continue'}
plugin.messages.append(msg)
for r in plugin.receive_waiters:
r.set_result(msg.to_dict())
plugin.receive_waiters = []
return res
@plugin.init()
def init(configuration, options, plugin, **kwargs):
print("Starting noise chat plugin")
plugin.messages = []
plugin.receive_waiters = []
plugin.run()