electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 224226f427dd26594d30f9db5c252b38ecd497ec
parent ff902a55eea5c02677042b34db9a0282e634235d
Author: Janus <ysangkok@gmail.com>
Date:   Fri, 21 Sep 2018 19:18:34 +0200

ln: cooperative close with remote peer initiating

Diffstat:
Melectrum/lnbase.py | 40++++++++++++++++++++++++++++++++++++----
Melectrum/lnhtlc.py | 29++++++++++++++++++++++++++---
Melectrum/lnutil.py | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Melectrum/lnworker.py | 3++-
4 files changed, 120 insertions(+), 35 deletions(-)

diff --git a/electrum/lnbase.py b/electrum/lnbase.py @@ -291,6 +291,7 @@ class Peer(PrintError): self.commitment_signed = defaultdict(asyncio.Queue) self.announcement_signatures = 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.invoices = lnworker.invoices self.attempted_route = {} @@ -393,7 +394,7 @@ class Peer(PrintError): self._sn = 0 return o - def process_message(self, message): + async def process_message(self, message): message_type, payload = decode_msg(message) #self.print_error("Received '%s'" % message_type.upper()) try: @@ -404,7 +405,10 @@ class Peer(PrintError): # raw message is needed to check signature if message_type=='node_announcement': payload['raw'] = message - f(payload) + if asyncio.iscoroutinefunction(f): + await f(payload) + else: + f(payload) def on_error(self, payload): self.print_error("error", payload["data"].decode("ascii")) @@ -451,7 +455,7 @@ class Peer(PrintError): self.send_message(gen_msg("init", gflen=0, lflen=1, localfeatures=self.localfeatures)) # read init msg = await self.read_message() - self.process_message(msg) + await self.process_message(msg) self.initialized.set_result(True) @aiosafe @@ -462,7 +466,7 @@ class Peer(PrintError): while True: self.ping_if_required() msg = await self.read_message() - self.process_message(msg) + await self.process_message(msg) def close_and_cleanup(self): try: @@ -1076,3 +1080,31 @@ class Peer(PrintError): if feerate_per_kvbyte is None: feerate_per_kvbyte = FEERATE_FALLBACK_STATIC_FEE 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') diff --git a/electrum/lnhtlc.py b/electrum/lnhtlc.py @@ -3,9 +3,10 @@ from collections import namedtuple import binascii import json from enum import Enum, auto +from typing import Optional from .util import bfh, PrintError, bh2u -from .bitcoin import Hash +from .bitcoin import Hash, TYPE_SCRIPT from .bitcoin import redeem_script_to_address from .crypto import sha256 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 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 funding_output_script, extract_ctn_from_tx_and_chan -from .lnutil import LOCAL, REMOTE, SENT, RECEIVED, HTLCOwner +from .lnutil import funding_output_script, LOCAL, REMOTE, HTLCOwner, make_closing_tx, make_outputs from .transaction import Transaction @@ -730,3 +730,26 @@ class HTLCStateMachine(PrintError): for_us, chan.constraints.is_initiator, 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 diff --git a/electrum/lnutil.py b/electrum/lnutil.py @@ -1,7 +1,7 @@ from enum import IntFlag import json from collections import namedtuple -from typing import NamedTuple +from typing import NamedTuple, List, Tuple from .util import bfh, bh2u, inv_dict 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) return htlc_tx - -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): - +def make_funding_input(local_funding_pubkey: bytes, remote_funding_pubkey: bytes, + payment_basepoint: bytes, remote_payment_basepoint: bytes, we_are_initiator: bool, + funding_pos: int, funding_txid: bytes, funding_sat: int): pubkeys = sorted([bh2u(local_funding_pubkey), bh2u(remote_funding_pubkey)]) payments = [payment_basepoint, remote_payment_basepoint] if not we_are_initiator: payments.reverse() - obs = get_obscured_ctn(ctn, *payments) - locktime = (0x20 << 24) + (obs & 0xffffff) - sequence = (0x80 << 24) + (obs >> 24) # commitment tx input - c_inputs = [{ + c_input = { 'type': 'p2wsh', 'x_pubkeys': pubkeys, 'signatures': [None, None], @@ -296,19 +287,15 @@ def make_commitment(ctn, local_funding_pubkey, remote_funding_pubkey, 'prevout_hash': funding_txid, 'value': funding_sat, 'coinbase': False, - 'sequence': sequence - }] - # 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 - 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) + } + return c_input, payments + +def make_outputs(fee_msat: int, we_pay_fee: bool, local_amount: int, remote_amount: int, + local_tupl, remote_tupl, htlcs: List[Tuple[bytes, int]], dust_limit_sat: int) -> Tuple[List[TxOutput], List[TxOutput]]: + to_local_amt = local_amount - (fee_msat if we_pay_fee else 0) + to_local = TxOutput(*local_tupl, to_local_amt // 1000) + to_remote_amt = remote_amount - (fee_msat if not we_pay_fee else 0) + to_remote = TxOutput(*remote_tupl, to_remote_amt // 1000) c_outputs = [to_local, to_remote] for script, msat_amount in htlcs: c_outputs += [TxOutput(bitcoin.TYPE_ADDRESS, @@ -317,6 +304,37 @@ def make_commitment(ctn, local_funding_pubkey, remote_funding_pubkey, # trim 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 # create commitment tx @@ -445,3 +463,14 @@ SENT = HTLCOwner.SENT RECEIVED = HTLCOwner.RECEIVED LOCAL = HTLCOwner.LOCAL 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 diff --git a/electrum/lnworker.py b/electrum/lnworker.py @@ -218,7 +218,8 @@ class LNWorker(PrintError): def list_channels(self): 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): chan = self.channels[chan_id]