Protocol Development Using c-lightning

Christian Decker

dev-sendcustommsg and custommsg 📨 (PR #3315)



The sendcustommsg RPC method can be used to inject custom messages into the communication with a direct peer. The custommsg plugin hook is the counterpart of sendcustommsg and allows plugins to receive custom messages


Examples:
  • Advanced gossip mechanisms
  • Share blockchain information with peers
  • Peer coordination for rebalancings
Currently limited to odd-numbered messages and developer mode (DEVELOPER=1).

createonion and sendonion 🧭 (PR #3260)


The createonion and sendonion RPC methods can be used to create a fully customized onion, and initiate a payment using the custom onion respectively.


Examples:
  • Atomic cross-chain swaps
  • Chat messages routed over the Lightning Network
  • Rendez-vous routing prototypes
  • Trampoline payments
  • ...

featurebits 🌟 (WIP)

Let's build a chat 📣

Overview

Sender

  1. Computes route to destination
  2. Computes payloads for hops on route
  3. Encodes payloads in onion
  4. Sends onion to receiver
  5. Optionally waits for delivery

Receiver

  1. Receives an incoming HTLC
  2. Extracts message from its payload
  3. Optionally accepts the payment

Skaffolding


#!/usr/bin/env python3
from pyln.client import Plugin
from onion import TlvPayload, LegacyOnionPayload

plugin = Plugin()

@plugin.init()
def on_init(plugin, **kwargs):
    pass

@plugin.method('recvmsg')
def recvmsg(plugin):
    pass

@plugin.hook('htlc_accepted')
def on_htlc_accepted(onion, plugin, **kwargs):
    pass

@plugin.method('sendmsg')
def sendmsg(destination, message, plugin):
    pass

plugin.run()
	    

Initializing state


@plugin.init()
def on_init(plugin, **kwargs):
    # Just a place to stash incoming messages
    plugin.messages = []
	    

Retrieving a message


@plugin.method('recvmsg')
def recvmsg(plugin):
    if len(plugin.messages) > 0:
        return plugin.messages.pop(0)
    else:
	return None
	    

Receiving a message


@plugin.hook('htlc_accepted')
def on_htlc_accepted(onion, plugin, **kwargs):
    payload = TlvPayload.from_hex(onion['payload'])
    plugin.messages.append(payload.get(34349334).value)
    return {'result': 'continue'}
	    

Aside: TLV-Messages

Sending a message


@plugin.method('sendmsg')
def sendmsg(destination, message, plugin):
    # Compute route
    route = plugin.rpc.getroute(destination, 1000, 10)['route']
    first_hop = route[0]

    # Compute payloads
    payloads = compute_payloads(route, plugin)

    # Replace last payload
    last = TlvPayload()
    last.add_field(34349334, message.encode('UTF-8'))
    payloads[-1]['payload'] = last.to_hex()

    # Create the onion to take us from here to the destination
    paymenthash = "AA" * 32
    onion = plugin.rpc.createonion(hops=payloads, assocdata=paymenthash)

    # Initiate the payment
    delivermsg(plugin, first_hop, onion, payment_hash)
    return {'result': 'delivered'}
	    

Compute payloads


def compute_payloads(route, plugin):
    blockheight = plugin.rpc.getinfo()['blockheight']
    # Need to tell each hop how to get to the next one
    hops = []
    for i, h in enumerate(route[1:]):
        hops.append({
            "pubkey": route[i]['id'],
            "payload": LegacyOnionPayload(
                amt_to_forward=h['msatoshi'],
                outgoing_cltv_value=blockheight + h['delay'],
                short_channel_id=h['channel']
            ).to_hex(),
        })

    # The last hop is a copy of the previous one:
    hops.append({
        "pubkey": route[-1]['id'],
        "payload": hops[-1]['payload']
    })
    return hops
	    
We will replace the last hop anyway, this is just a demonstration.

Deliver the message


def delivermsg(plugin, first_hop, onion, 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)
    except:
        return True
	    
Some more error handling might be necessary:
  • Error 16399 means we reached the destination.
  • Otherwise we might need to retry.

Putting everything together 📦

#!/usr/bin/env python3
from pyln.client import Plugin
from onion import TlvPayload, LegacyOnionPayload

plugin = Plugin()

@plugin.hook('htlc_accepted')
def on_htlc_accepted(onion, plugin, **kwargs):
    payload = TlvPayload.from_hex(onion['payload'])
    plugin.messages.append(payload.get(34349334).value)
    return {'result': 'continue'}

def compute_payloads(route, plugin):
    blockheight = plugin.rpc.getinfo()['blockheight']
    # Need to tell each hop how to get to the next one
    hops = []
    for i, h in enumerate(route[1:]):
        hops.append({
            "pubkey": route[i]['id'],
            "payload": LegacyOnionPayload(
                amt_to_forward=h['msatoshi'],
                outgoing_cltv_value=blockheight + h['delay'],
                short_channel_id=h['channel']
            ).to_hex(),
        })

    # The last hop is a copy of the previous one (just checks consistency):
    hops.append({
        "pubkey": route[-1]['id'],
        "payload": hops[-1]['payload']
    })
    return hops

def delivermsg(plugin, first_hop, onion, 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)
    except:
        return True

@plugin.method('sendmsg')
def sendmsg(destination, message, plugin):
    # Compute route
    route = plugin.rpc.getroute(destination, 1000, 10)['route']
    first_hop = route[0]

    # Compute payloads
    payloads = compute_payloads(route, plugin)

    # Replace last payload
    last = TlvPayload()
    last.add_field(34349334, message.encode('UTF-8'))
    payloads[-1]['payload'] = last.to_hex()

    # Create the onion to take us from here to the destination
    payment_hash = "AA" * 32
    onion = plugin.rpc.createonion(hops=payloads, assocdata=payment_hash)

    # Initiate the payment
    delivermsg(plugin, first_hop, onion, payment_hash)
    return {'result': 'delivered'}

@plugin.method('recvmsg')
def recvmsg(plugin):
    if len(plugin.messages) > 0:
        return plugin.messages.pop(0)
    else:
        return None

@plugin.init()
def on_init(plugin, **kwargs):
    # Just a place to stash incoming messages
    plugin.messages = []

plugin.run()

Testing your plugin 🛠


from pyln.testing.fixtures import *
from pyln.testing.utils import wait_for

ppath = os.path.join(os.path.dirname(__file__), "chat.py")
opts = {'plugin': ppath}

def test_sendmsg(node_factory):
    l1, l2, l3 = node_factory.line_graph(
        3,
        opts=[opts, {}, opts],
        wait_for_announce=True
    )

    # Wait for l1 to learn about all channels in the network
    wait_for(lambda: len(l1.rpc.listchannels()['channels']) == 4)

    l1.rpc.sendmsg(l3.info['id'], "Hello world")

    l3.daemon.wait_for_log(r'Failing HTLC because')
    assert(l3.rpc.recvmsg() == "Hello world")
    assert(l3.rpc.recvmsg() is None)

	  

Resources

Thanks!

Questions?