Browse Source

ln: cooperative close with remote peer initiating

regtest_lnd
Janus 6 years ago
committed by SomberNight
parent
commit
6fa4f2fefd
No known key found for this signature in database GPG Key ID: B33B5F232C6271E9
  1. 38
      electrum/lnbase.py
  2. 29
      electrum/lnhtlc.py
  3. 83
      electrum/lnutil.py
  4. 3
      electrum/lnworker.py

38
electrum/lnbase.py

@ -291,6 +291,7 @@ class Peer(PrintError):
self.commitment_signed = defaultdict(asyncio.Queue) self.commitment_signed = defaultdict(asyncio.Queue)
self.announcement_signatures = defaultdict(asyncio.Queue) self.announcement_signatures = defaultdict(asyncio.Queue)
self.update_fail_htlc = defaultdict(asyncio.Queue) self.update_fail_htlc = defaultdict(asyncio.Queue)
self.closing_signed = defaultdict(asyncio.Queue)
self.localfeatures = (0x08 if request_initial_sync else 0) self.localfeatures = (0x08 if request_initial_sync else 0)
self.invoices = lnworker.invoices self.invoices = lnworker.invoices
self.attempted_route = {} self.attempted_route = {}
@ -393,7 +394,7 @@ class Peer(PrintError):
self._sn = 0 self._sn = 0
return o return o
def process_message(self, message): async def process_message(self, message):
message_type, payload = decode_msg(message) message_type, payload = decode_msg(message)
#self.print_error("Received '%s'" % message_type.upper()) #self.print_error("Received '%s'" % message_type.upper())
try: try:
@ -404,6 +405,9 @@ class Peer(PrintError):
# raw message is needed to check signature # raw message is needed to check signature
if message_type=='node_announcement': if message_type=='node_announcement':
payload['raw'] = message payload['raw'] = message
if asyncio.iscoroutinefunction(f):
await f(payload)
else:
f(payload) f(payload)
def on_error(self, payload): def on_error(self, payload):
@ -451,7 +455,7 @@ class Peer(PrintError):
self.send_message(gen_msg("init", gflen=0, lflen=1, localfeatures=self.localfeatures)) self.send_message(gen_msg("init", gflen=0, lflen=1, localfeatures=self.localfeatures))
# read init # read init
msg = await self.read_message() msg = await self.read_message()
self.process_message(msg) await self.process_message(msg)
self.initialized.set_result(True) self.initialized.set_result(True)
@aiosafe @aiosafe
@ -462,7 +466,7 @@ class Peer(PrintError):
while True: while True:
self.ping_if_required() self.ping_if_required()
msg = await self.read_message() msg = await self.read_message()
self.process_message(msg) await self.process_message(msg)
def close_and_cleanup(self): def close_and_cleanup(self):
try: try:
@ -1076,3 +1080,31 @@ class Peer(PrintError):
if feerate_per_kvbyte is None: if feerate_per_kvbyte is None:
feerate_per_kvbyte = FEERATE_FALLBACK_STATIC_FEE feerate_per_kvbyte = FEERATE_FALLBACK_STATIC_FEE
return max(253, feerate_per_kvbyte // 4) return max(253, feerate_per_kvbyte // 4)
def on_closing_signed(self, payload):
chan_id = payload["channel_id"]
if chan_id not in self.closing_signed: raise Exception("Got unknown closing_signed")
self.closing_signed[chan_id].put_nowait(payload)
async def on_shutdown(self, payload):
# length of scripts allowed in BOLT-02
if int.from_bytes(payload['len'], 'big') not in (3+20+2, 2+20+1, 2+20, 2+32):
raise Exception('scriptpubkey length in received shutdown message invalid: ' + str(payload['len']))
chan = self.channels[payload['channel_id']]
scriptpubkey = bfh(bitcoin.address_to_script(chan.sweep_address))
self.send_message(gen_msg('shutdown', channel_id=chan.channel_id, len=len(scriptpubkey), scriptpubkey=scriptpubkey))
signature, fee = chan.make_closing_tx(scriptpubkey, payload['scriptpubkey'])
self.send_message(gen_msg('closing_signed', channel_id=chan.channel_id, fee_satoshis=fee, signature=signature))
while chan.get_state() != 'CLOSED':
try:
closing_signed = await asyncio.wait_for(self.closing_signed[chan.channel_id].get(), 1)
except asyncio.TimeoutError:
pass
else:
fee = closing_signed['fee_satoshis']
signature, _ = chan.make_closing_tx(scriptpubkey, payload['scriptpubkey'], fee_sat=fee)
self.send_message(gen_msg('closing_signed', channel_id=chan.channel_id, fee_satoshis=fee, signature=signature))
self.print_error('REMOTE PEER CLOSED CHANNEL')

29
electrum/lnhtlc.py

@ -3,9 +3,10 @@ from collections import namedtuple
import binascii import binascii
import json import json
from enum import Enum, auto from enum import Enum, auto
from typing import Optional
from .util import bfh, PrintError, bh2u from .util import bfh, PrintError, bh2u
from .bitcoin import Hash from .bitcoin import Hash, TYPE_SCRIPT
from .bitcoin import redeem_script_to_address from .bitcoin import redeem_script_to_address
from .crypto import sha256 from .crypto import sha256
from . import ecc from . import ecc
@ -15,8 +16,7 @@ from .lnutil import secret_to_pubkey, derive_privkey, derive_pubkey, derive_blin
from .lnutil import sign_and_get_sig_string from .lnutil import sign_and_get_sig_string
from .lnutil import make_htlc_tx_with_open_channel, make_commitment, make_received_htlc, make_offered_htlc from .lnutil import make_htlc_tx_with_open_channel, make_commitment, make_received_htlc, make_offered_htlc
from .lnutil import HTLC_TIMEOUT_WEIGHT, HTLC_SUCCESS_WEIGHT from .lnutil import HTLC_TIMEOUT_WEIGHT, HTLC_SUCCESS_WEIGHT
from .lnutil import funding_output_script, extract_ctn_from_tx_and_chan from .lnutil import funding_output_script, LOCAL, REMOTE, HTLCOwner, make_closing_tx, make_outputs
from .lnutil import LOCAL, REMOTE, SENT, RECEIVED, HTLCOwner
from .transaction import Transaction from .transaction import Transaction
@ -730,3 +730,26 @@ class HTLCStateMachine(PrintError):
for_us, for_us,
chan.constraints.is_initiator, chan.constraints.is_initiator,
htlcs=htlcs) htlcs=htlcs)
def make_closing_tx(self, local_script: bytes, remote_script: bytes, fee_sat: Optional[int] = None) -> (bytes, int):
if fee_sat is None:
fee_sat = self.pending_local_fee
_, outputs = make_outputs(fee_sat * 1000, True,
self.local_state.amount_msat,
self.remote_state.amount_msat,
(TYPE_SCRIPT, bh2u(local_script)),
(TYPE_SCRIPT, bh2u(remote_script)),
[], self.local_config.dust_limit_sat)
closing_tx = make_closing_tx(self.local_config.multisig_key.pubkey,
self.remote_config.multisig_key.pubkey,
self.local_config.payment_basepoint.pubkey,
self.remote_config.payment_basepoint.pubkey,
# TODO hardcoded we_are_initiator:
True, *self.funding_outpoint, self.constraints.capacity,
outputs)
der_sig = bfh(closing_tx.sign_txin(0, self.local_config.multisig_key.privkey))
sig = ecc.sig_string_from_der_sig(der_sig[:-1])
return sig, fee_sat

83
electrum/lnutil.py

@ -1,7 +1,7 @@
from enum import IntFlag from enum import IntFlag
import json import json
from collections import namedtuple from collections import namedtuple
from typing import NamedTuple from typing import NamedTuple, List, Tuple
from .util import bfh, bh2u, inv_dict from .util import bfh, bh2u, inv_dict
from .crypto import sha256 from .crypto import sha256
@ -270,24 +270,15 @@ def make_htlc_tx_with_open_channel(chan, pcp, for_us, we_receive, amount_msat, c
htlc_tx = make_htlc_tx(cltv_expiry, inputs=htlc_tx_inputs, output=htlc_tx_output) htlc_tx = make_htlc_tx(cltv_expiry, inputs=htlc_tx_inputs, output=htlc_tx_output)
return htlc_tx return htlc_tx
def make_funding_input(local_funding_pubkey: bytes, remote_funding_pubkey: bytes,
def make_commitment(ctn, local_funding_pubkey, remote_funding_pubkey, payment_basepoint: bytes, remote_payment_basepoint: bytes, we_are_initiator: bool,
remote_payment_pubkey, payment_basepoint, funding_pos: int, funding_txid: bytes, funding_sat: int):
remote_payment_basepoint, revocation_pubkey,
delayed_pubkey, to_self_delay, funding_txid,
funding_pos, funding_sat, local_amount, remote_amount,
dust_limit_sat, local_feerate, for_us, we_are_initiator,
htlcs):
pubkeys = sorted([bh2u(local_funding_pubkey), bh2u(remote_funding_pubkey)]) pubkeys = sorted([bh2u(local_funding_pubkey), bh2u(remote_funding_pubkey)])
payments = [payment_basepoint, remote_payment_basepoint] payments = [payment_basepoint, remote_payment_basepoint]
if not we_are_initiator: if not we_are_initiator:
payments.reverse() payments.reverse()
obs = get_obscured_ctn(ctn, *payments)
locktime = (0x20 << 24) + (obs & 0xffffff)
sequence = (0x80 << 24) + (obs >> 24)
# commitment tx input # commitment tx input
c_inputs = [{ c_input = {
'type': 'p2wsh', 'type': 'p2wsh',
'x_pubkeys': pubkeys, 'x_pubkeys': pubkeys,
'signatures': [None, None], 'signatures': [None, None],
@ -296,19 +287,15 @@ def make_commitment(ctn, local_funding_pubkey, remote_funding_pubkey,
'prevout_hash': funding_txid, 'prevout_hash': funding_txid,
'value': funding_sat, 'value': funding_sat,
'coinbase': False, 'coinbase': False,
'sequence': sequence }
}] return c_input, payments
# commitment tx outputs
local_address = make_commitment_output_to_local_address(revocation_pubkey, to_self_delay, delayed_pubkey) def make_outputs(fee_msat: int, we_pay_fee: bool, local_amount: int, remote_amount: int,
remote_address = make_commitment_output_to_remote_address(remote_payment_pubkey) local_tupl, remote_tupl, htlcs: List[Tuple[bytes, int]], dust_limit_sat: int) -> Tuple[List[TxOutput], List[TxOutput]]:
# TODO trim htlc outputs here while also considering 2nd stage htlc transactions to_local_amt = local_amount - (fee_msat if we_pay_fee else 0)
fee = local_feerate * overall_weight(len(htlcs)) to_local = TxOutput(*local_tupl, to_local_amt // 1000)
fee = fee // 1000 * 1000 to_remote_amt = remote_amount - (fee_msat if not we_pay_fee else 0)
we_pay_fee = for_us == we_are_initiator to_remote = TxOutput(*remote_tupl, to_remote_amt // 1000)
to_local_amt = local_amount - (fee if we_pay_fee else 0)
to_local = TxOutput(bitcoin.TYPE_ADDRESS, local_address, to_local_amt // 1000)
to_remote_amt = remote_amount - (fee if not we_pay_fee else 0)
to_remote = TxOutput(bitcoin.TYPE_ADDRESS, remote_address, to_remote_amt // 1000)
c_outputs = [to_local, to_remote] c_outputs = [to_local, to_remote]
for script, msat_amount in htlcs: for script, msat_amount in htlcs:
c_outputs += [TxOutput(bitcoin.TYPE_ADDRESS, c_outputs += [TxOutput(bitcoin.TYPE_ADDRESS,
@ -317,6 +304,37 @@ def make_commitment(ctn, local_funding_pubkey, remote_funding_pubkey,
# trim outputs # trim outputs
c_outputs_filtered = list(filter(lambda x:x[2]>= dust_limit_sat, c_outputs)) c_outputs_filtered = list(filter(lambda x:x[2]>= dust_limit_sat, c_outputs))
return c_outputs, c_outputs_filtered
def make_commitment(ctn, local_funding_pubkey, remote_funding_pubkey,
remote_payment_pubkey, payment_basepoint,
remote_payment_basepoint, revocation_pubkey,
delayed_pubkey, to_self_delay, funding_txid,
funding_pos, funding_sat, local_amount, remote_amount,
dust_limit_sat, local_feerate, for_us, we_are_initiator,
htlcs):
c_input, payments = make_funding_input(local_funding_pubkey, remote_funding_pubkey,
payment_basepoint, remote_payment_basepoint, we_are_initiator, funding_pos,
funding_txid, funding_sat)
obs = get_obscured_ctn(ctn, *payments)
locktime = (0x20 << 24) + (obs & 0xffffff)
sequence = (0x80 << 24) + (obs >> 24)
c_input['sequence'] = sequence
c_inputs = [c_input]
# commitment tx outputs
local_address = make_commitment_output_to_local_address(revocation_pubkey, to_self_delay, delayed_pubkey)
remote_address = make_commitment_output_to_remote_address(remote_payment_pubkey)
# TODO trim htlc outputs here while also considering 2nd stage htlc transactions
fee = local_feerate * overall_weight(len(htlcs))
fee = fee // 1000 * 1000
we_pay_fee = for_us == we_are_initiator
c_outputs, c_outputs_filtered = make_outputs(fee, we_pay_fee, local_amount, remote_amount,
(bitcoin.TYPE_ADDRESS, local_address), (bitcoin.TYPE_ADDRESS, remote_address), htlcs, dust_limit_sat)
assert sum(x[2] for x in c_outputs) <= funding_sat assert sum(x[2] for x in c_outputs) <= funding_sat
# create commitment tx # create commitment tx
@ -445,3 +463,14 @@ SENT = HTLCOwner.SENT
RECEIVED = HTLCOwner.RECEIVED RECEIVED = HTLCOwner.RECEIVED
LOCAL = HTLCOwner.LOCAL LOCAL = HTLCOwner.LOCAL
REMOTE = HTLCOwner.REMOTE REMOTE = HTLCOwner.REMOTE
def make_closing_tx(local_funding_pubkey: bytes, remote_funding_pubkey: bytes,
payment_basepoint: bytes, remote_payment_basepoint: bytes, we_are_initiator: bool,
funding_txid: bytes, funding_pos: int, funding_sat: int, outputs: List[TxOutput]):
c_input, payments = make_funding_input(local_funding_pubkey, remote_funding_pubkey,
payment_basepoint, remote_payment_basepoint, we_are_initiator, funding_pos,
funding_txid, funding_sat)
c_input['sequence'] = 0xFFFF_FFFF
tx = Transaction.from_io([c_input], outputs, locktime=0, version=2)
tx.BIP_LI01_sort()
return tx

3
electrum/lnworker.py

@ -218,7 +218,8 @@ class LNWorker(PrintError):
def list_channels(self): def list_channels(self):
with self.lock: with self.lock:
return [str(x) for x in self.channels] # we output the funding_outpoint instead of the channel_id because lnd uses channel_point (funding outpoint) to identify channels
return [(chan.funding_outpoint.to_str(), chan.get_state()) for channel_id, chan in self.channels.items()]
def close_channel(self, chan_id): def close_channel(self, chan_id):
chan = self.channels[chan_id] chan = self.channels[chan_id]

Loading…
Cancel
Save