diff --git a/contrib/pyln-proto/pyln/proto/__init__.py b/contrib/pyln-proto/pyln/proto/__init__.py index f9001fb98..a0fd6bbd8 100644 --- a/contrib/pyln-proto/pyln/proto/__init__.py +++ b/contrib/pyln-proto/pyln/proto/__init__.py @@ -1,3 +1,4 @@ +from .bech32 import bech32_decode from .invoice import Invoice from .onion import OnionPayload, TlvPayload, LegacyOnionPayload from .wire import LightningConnection, LightningServerSocket @@ -11,4 +12,5 @@ __all__ = [ "OnionPayload", "LegacyOnionPayload", "TlvPayload", + "bech32_decode", ] diff --git a/tests/plugins/df_accepter.py b/tests/plugins/df_accepter.py new file mode 100755 index 000000000..b92cf3ae9 --- /dev/null +++ b/tests/plugins/df_accepter.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +"""Test plugin for adding inputs/outputs to a dual-funded transaction +""" + +from pyln.client import Plugin, Millisatoshi +from pyln.proto import bech32_decode +from typing import Iterable, List, Optional +from wallycore import psbt_add_output_at, psbt_from_base64, psbt_to_base64, tx_output_init + + +plugin = Plugin() + + +def convertbits(data: Iterable[int], frombits: int, tobits: int, pad: bool = True) -> Optional[List[int]]: + """General power-of-2 base conversion.""" + acc = 0 + bits = 0 + ret = [] + maxv = (1 << tobits) - 1 + max_acc = (1 << (frombits + tobits - 1)) - 1 + for value in data: + if value < 0 or (value >> frombits): + return None + acc = ((acc << frombits) | value) & max_acc + bits += frombits + while bits >= tobits: + bits -= tobits + ret.append((acc >> bits) & maxv) + if pad: + if bits: + ret.append((acc << (tobits - bits)) & maxv) + elif bits >= frombits or ((acc << (tobits - bits)) & maxv): + return None + return ret + + +def get_script(bech_addr): + hrp, data = bech32_decode(bech_addr) + # FIXME: verify hrp matches expected network + wprog = convertbits(data[1:], 5, 8, False) + wit_ver = data[0] + if wit_ver > 16: + raise ValueError("Invalid witness version {}".format(wit_ver[0])) + return bytes([wit_ver + 0x50 if wit_ver > 0 else wit_ver, len(wprog)] + wprog) + + +@plugin.hook('openchannel2') +def on_openchannel(openchannel2, plugin, **kwargs): + # We mirror what the peer does, wrt to funding amount + amount = openchannel2['their_funding'] + feerate = openchannel2['feerate_per_kw_funding'] + locktime = openchannel2['locktime'] + + funding = plugin.rpc.fundpsbt(amount, ''.join([str(feerate), 'perkw']), 0, reserve=True, + locktime=locktime) + psbt_obj = psbt_from_base64(funding['psbt']) + + excess = Millisatoshi(funding['excess_msat']) + change_cost = Millisatoshi(164 * feerate // 1000 * 1000) + dust_limit = Millisatoshi(253 * 1000) + if excess > (dust_limit + change_cost): + addr = plugin.rpc.newaddr()['bech32'] + change = excess - change_cost + output = tx_output_init(int(change.to_satoshi()), get_script(addr)) + psbt_add_output_at(psbt_obj, 0, 0, output) + + return {'result': 'continue', 'psbt': psbt_to_base64(psbt_obj, 0), + 'accepter_funding_msat': amount} + + +@plugin.hook('openchannel2_changed') +def on_tx_changed(openchannel2_changed, plugin, **kwargs): + # In this example, we have nothing to add, so we + # pass back the same psbt that was forwarded in here + return {'result': 'continue', 'psbt': openchannel2_changed['psbt']} + + +@plugin.hook('openchannel2_sign') +def on_tx_sign(openchannel2_sign, plugin, **kwargs): + psbt = openchannel2_sign['psbt'] + + # We only sign the ones with our parity of a serial_id + # FIXME: find the inputs with an odd-serial, these are ours + # the key for a serial_id :: + our_inputs = [1] + + signed_psbt = plugin.rpc.signpsbt(psbt, signonly=our_inputs)['signed_psbt'] + return {'result': 'continue', 'psbt': signed_psbt} + + +plugin.run()