taccept channel opening requests initiated by remote - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit 0405f0d9ad2dfb356172d7454a45cf4de2008678 DIR parent b18a17ef79ef0272eae0c66d3e4af039037e8d91 HTML Author: Janus <ysangkok@gmail.com> Date: Thu, 4 Oct 2018 14:03:29 +0200 accept channel opening requests initiated by remote Diffstat: M electrum/commands.py | 4 ++++ M electrum/lnbase.py | 154 +++++++++++++++++++++++++++++-- M electrum/lnwatcher.py | 9 +++++---- M electrum/lnworker.py | 20 +++++++++++--------- 4 files changed, 164 insertions(+), 23 deletions(-) --- DIR diff --git a/electrum/commands.py b/electrum/commands.py t@@ -784,6 +784,10 @@ class Commands: return self.wallet.lnworker.add_invoice(satoshis(requested_amount), message) @command('wn') + def nodeid(self): + return bh2u(self.wallet.lnworker.pubkey) + + @command('wn') def listchannels(self): return self.wallet.lnworker.list_channels() DIR diff --git a/electrum/lnbase.py b/electrum/lnbase.py t@@ -8,7 +8,7 @@ from collections import namedtuple, defaultdict, OrderedDict, defaultdict from .lnutil import Outpoint, ChannelConfig, LocalState, RemoteState, Keypair, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore from .lnutil import sign_and_get_sig_string, funding_output_script, get_ecdh, get_per_commitment_secret_from_seed from .lnutil import secret_to_pubkey, LNPeerAddr, PaymentFailure -from .lnutil import LOCAL, REMOTE +from .lnutil import LOCAL, REMOTE, HTLCOwner from .bitcoin import COIN from ecdsa.util import sigdecode_der, sigencode_string_canonize, sigdecode_string t@@ -22,6 +22,8 @@ import binascii import hashlib import hmac import cryptography.hazmat.primitives.ciphers.aead as AEAD +import aiorpcx +from functools import partial from . import bitcoin from . import ecc t@@ -285,6 +287,7 @@ class Peer(PrintError): self.channel_accepted = defaultdict(asyncio.Queue) self.channel_reestablished = defaultdict(asyncio.Future) self.funding_signed = defaultdict(asyncio.Queue) + self.funding_created = defaultdict(asyncio.Queue) self.revoke_and_ack = defaultdict(asyncio.Queue) self.commitment_signed = defaultdict(asyncio.Queue) self.announcement_signatures = defaultdict(asyncio.Queue) t@@ -426,6 +429,11 @@ class Peer(PrintError): if channel_id not in self.funding_signed: raise Exception("Got unknown funding_signed") self.funding_signed[channel_id].put_nowait(payload) + def on_funding_created(self, payload): + channel_id = payload['temporary_channel_id'] + if channel_id not in self.funding_created: raise Exception("Got unknown funding_created") + self.funding_created[channel_id].put_nowait(payload) + def on_node_announcement(self, payload): self.channel_db.on_node_announcement(payload) self.network.trigger_callback('ln_status') t@@ -476,9 +484,7 @@ class Peer(PrintError): chan.set_state('DISCONNECTED') self.network.trigger_callback('channel', chan) - @aiosafe - async def channel_establishment_flow(self, wallet, config, password, funding_sat, push_msat, temp_channel_id, sweep_address): - await self.initialized + def make_local_config(self, funding_msat, push_msat, initiator: HTLCOwner, password): # see lnd/keychain/derivation.go keyfamilymultisig = 0 keyfamilyrevocationbase = 1 t@@ -487,10 +493,12 @@ class Peer(PrintError): keyfamilydelaybase = 4 keyfamilyrevocationroot = 5 keyfamilynodekey = 6 # TODO currently unused - # amounts - local_feerate = self.current_feerate_per_kw() # key derivation - keypair_generator = lambda family, i: Keypair(*wallet.keystore.get_keypair([family, i], password)) + keypair_generator = lambda family, i: Keypair(*self.lnworker.wallet.keystore.get_keypair([family, i], password)) + if initiator == LOCAL: + initial_msat = funding_sat * 1000 - push_msat + else: + initial_msat = push_msat local_config=ChannelConfig( payment_basepoint=keypair_generator(keyfamilypaymentbase, 0), multisig_key=keypair_generator(keyfamilymultisig, 0), t@@ -501,10 +509,22 @@ class Peer(PrintError): dust_limit_sat=546, max_htlc_value_in_flight_msat=0xffffffffffffffff, max_accepted_htlcs=5, - initial_msat=funding_sat * 1000 - push_msat, + initial_msat=initial_msat, ) + return local_config + + def make_per_commitment_secret_seed(self): + # TODO + return 0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100.to_bytes(32, 'big') + + @aiosafe + async def channel_establishment_flow(self, password, funding_sat, push_msat, temp_channel_id, sweep_address): + await self.initialized + local_config = self.make_local_config(funding_msat, push_msat, LOCAL, password) + # amounts + local_feerate = self.current_feerate_per_kw() # TODO derive this? - per_commitment_secret_seed = 0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100.to_bytes(32, 'big') + per_commitment_secret_seed = self.make_per_commitment_secret_seed() per_commitment_secret_index = RevocationStore.START_INDEX # for the first commitment transaction per_commitment_secret_first = get_per_commitment_secret_from_seed(per_commitment_secret_seed, per_commitment_secret_index) t@@ -554,7 +574,7 @@ class Peer(PrintError): redeem_script = funding_output_script(local_config, remote_config) funding_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script) funding_output = TxOutput(bitcoin.TYPE_ADDRESS, funding_address, funding_sat) - funding_tx = wallet.mktx([funding_output], password, config, 1000) + funding_tx = self.lnworker.wallet.mktx([funding_output], password, self.lnworker.config, 1000) funding_txid = funding_tx.txid() funding_index = funding_tx.outputs().index(funding_output) # compute amounts t@@ -615,6 +635,120 @@ class Peer(PrintError): m.set_state('OPENING') return m + async def on_open_channel(self, payload): + # payload['channel_flags'] + # payload['channel_reserve_satoshis'] + if payload['chain_hash'] != constants.net.rev_genesis_bytes(): + raise Exception('wrong chain_hash') + funding_sat = int.from_bytes(payload['funding_satoshis'], 'big') + push_msat = int.from_bytes(payload['push_msat'], 'big') + + remote_config = ChannelConfig( + payment_basepoint=OnlyPubkeyKeypair(payload['payment_basepoint']), + multisig_key=OnlyPubkeyKeypair(payload['funding_pubkey']), + htlc_basepoint=OnlyPubkeyKeypair(payload['htlc_basepoint']), + delayed_basepoint=OnlyPubkeyKeypair(payload['delayed_payment_basepoint']), + revocation_basepoint=OnlyPubkeyKeypair(payload['revocation_basepoint']), + to_self_delay=int.from_bytes(payload['to_self_delay'], 'big'), + dust_limit_sat=int.from_bytes(payload['dust_limit_satoshis'], 'big'), + max_htlc_value_in_flight_msat=int.from_bytes(payload['max_htlc_value_in_flight_msat'], 'big'), + max_accepted_htlcs=int.from_bytes(payload['max_accepted_htlcs'], 'big'), + initial_msat=funding_sat * 1000 - push_msat, + ) + temp_chan_id = payload['temporary_channel_id'] + password = None # TODO + local_config = self.make_local_config(funding_sat * 1000, push_msat, REMOTE, password) + + per_commitment_secret_seed = self.make_per_commitment_secret_seed() + per_commitment_secret_index = RevocationStore.START_INDEX + # for the first commitment transaction + per_commitment_secret_first = get_per_commitment_secret_from_seed(per_commitment_secret_seed, per_commitment_secret_index) + per_commitment_point_first = secret_to_pubkey(int.from_bytes(per_commitment_secret_first, 'big')) + + min_depth = 3 + self.send_message(gen_msg('accept_channel', + temporary_channel_id=temp_chan_id, + dust_limit_satoshis=local_config.dust_limit_sat, + max_htlc_value_in_flight_msat=local_config.max_htlc_value_in_flight_msat, + channel_reserve_satoshis=546, + htlc_minimum_msat=1000, + minimum_depth=min_depth, + to_self_delay=local_config.to_self_delay, + max_accepted_htlcs=local_config.max_accepted_htlcs, + funding_pubkey=local_config.multisig_key.pubkey, + revocation_basepoint=local_config.revocation_basepoint.pubkey, + payment_basepoint=local_config.payment_basepoint.pubkey, + delayed_payment_basepoint=local_config.delayed_basepoint.pubkey, + htlc_basepoint=local_config.htlc_basepoint.pubkey, + first_per_commitment_point=per_commitment_point_first, + )) + funding_created = await self.funding_created[temp_chan_id].get() + funding_idx = int.from_bytes(funding_created['funding_output_index'], 'big') + funding_txid = bh2u(funding_created['funding_txid'][::-1]) + channel_id, funding_txid_bytes = channel_id_from_funding_tx(funding_txid, funding_idx) + their_revocation_store = RevocationStore() + local_feerate = int.from_bytes(payload['feerate_per_kw'], 'big') + chan = { + "node_id": self.pubkey, + "channel_id": channel_id, + "short_channel_id": None, + "funding_outpoint": Outpoint(funding_txid, funding_idx), + "local_config": local_config, + "remote_config": remote_config, + "remote_state": RemoteState( + ctn = -1, + next_per_commitment_point=payload['first_per_commitment_point'], + current_per_commitment_point=None, + amount_msat=remote_config.initial_msat, + revocation_store=their_revocation_store, + next_htlc_id = 0, + feerate=local_feerate + ), + "local_state": LocalState( + ctn = -1, + per_commitment_secret_seed=per_commitment_secret_seed, + amount_msat=local_config.initial_msat, + next_htlc_id = 0, + funding_locked_received = False, + was_announced = False, + current_commitment_signature = None, + current_htlc_signatures = None, + feerate=local_feerate + ), + "constraints": ChannelConstraints(capacity=funding_sat, is_initiator=False, funding_txn_minimum_depth=min_depth), + "remote_commitment_to_be_revoked": None, + } + m = HTLCStateMachine(chan) + m.lnwatcher = self.lnwatcher + m.sweep_address = self.lnworker.wallet.get_unused_address() + remote_sig = funding_created['signature'] + m.receive_new_commitment(remote_sig, []) + sig_64, _ = m.sign_next_commitment() + self.send_message(gen_msg('funding_signed', + channel_id=channel_id, + signature=sig_64, + )) + m.set_state('OPENING') + m.remote_commitment_to_be_revoked = m.pending_remote_commitment + m.remote_state = m.remote_state._replace(ctn=0) + m.local_state = m.local_state._replace(ctn=0, current_commitment_signature=remote_sig) + self.lnworker.save_channel(m) + self.lnwatcher.watch_channel(m, m.sweep_address, partial(self.lnworker.on_channel_utxos, m)) + while True: + try: + funding_tx = Transaction(await self.network.get_transaction(funding_txid)) + except aiorpcx.jsonrpc.RPCError as e: + print("sleeping", str(e)) + await asyncio.sleep(1) + else: + break + outp = funding_tx.outputs()[funding_idx] + redeem_script = funding_output_script(remote_config, local_config) + funding_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script) + if outp != TxOutput(bitcoin.TYPE_ADDRESS, funding_address, funding_sat): + m.set_state('DISCONNECTED') + raise Exception('funding outpoint mismatch') + @aiosafe async def reestablish_channel(self, chan): await self.initialized DIR diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py t@@ -265,9 +265,10 @@ class LNWatcher(PrintError): ctn = extract_ctn_from_tx_and_chan(ctx, chan) latest_ctn_on_channel = chan.local_state.ctn if ours else chan.remote_state.ctn last_ctn_watcher_saw = self._get_last_ctn_for_processed_ctx(funding_address, ours) - if latest_ctn_on_channel + 1 != ctn: + # TODO make it work when we are not initiator + if chan.constraints.is_initiator and latest_ctn_on_channel + 1 != ctn: raise Exception('unexpected ctn {}. latest is {}. our ctx: {}'.format(ctn, latest_ctn_on_channel, ours)) - if last_ctn_watcher_saw + 1 != ctn: + if chan.constraints.is_initiator and last_ctn_watcher_saw + 1 != ctn: raise Exception('watcher skipping ctns!! ctn {}. last seen {}. our ctx: {}'.format(ctn, last_ctn_watcher_saw, ours)) #self.print_error("process_new_offchain_ctx. funding {}, ours {}, ctn {}, ctx {}" # .format(chan.funding_outpoint.to_str(), ours, ctn, ctx.txid())) t@@ -290,9 +291,9 @@ class LNWatcher(PrintError): ctn = extract_ctn_from_tx_and_chan(ctx, chan) latest_ctn_on_channel = chan.remote_state.ctn last_ctn_watcher_saw = self._get_last_ctn_for_revoked_secret(funding_address) - if latest_ctn_on_channel != ctn: + if chan.constraints.is_initiator and latest_ctn_on_channel != ctn: raise Exception('unexpected ctn {}. latest is {}'.format(ctn, latest_ctn_on_channel)) - if last_ctn_watcher_saw + 1 != ctn: + if chan.constraints.is_initiator and last_ctn_watcher_saw + 1 != ctn: raise Exception('watcher skipping ctns!! ctn {}. last seen {}'.format(ctn, last_ctn_watcher_saw)) sweep_address = self._get_sweep_address_for_chan(chan) encumbered_sweeptx = maybe_create_sweeptx_for_their_ctx_to_local(chan, ctx, per_commitment_secret, sweep_address) DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py t@@ -112,17 +112,18 @@ class LNWorker(PrintError): """ assert chan.get_state() in ["OPEN", "OPENING"] peer = self.peers[chan.node_id] - conf = self.wallet.get_tx_height(chan.funding_outpoint.txid).conf + addr_sync = self.network.lnwatcher.addr_sync + conf = addr_sync.get_tx_height(chan.funding_outpoint.txid).conf if conf >= chan.constraints.funding_txn_minimum_depth: - block_height, tx_pos = self.wallet.get_txpos(chan.funding_outpoint.txid) + block_height, tx_pos = addr_sync.get_txpos(chan.funding_outpoint.txid) if tx_pos == -1: self.print_error('funding tx is not yet SPV verified.. but there are ' 'already enough confirmations (currently {})'.format(conf)) - return False + return False, conf chan.short_channel_id = calc_short_channel_id(block_height, tx_pos, chan.funding_outpoint.output_index) self.save_channel(chan) - return True - return False + return True, conf + return False, conf def on_channel_utxos(self, chan, is_funding_txo_spent: bool): chan.set_funding_txo_spentness(is_funding_txo_spent) t@@ -138,11 +139,12 @@ class LNWorker(PrintError): # since short_channel_id could be changed while saving. with self.lock: channels = list(self.channels.values()) + addr_sync = self.network.lnwatcher.addr_sync for chan in channels: if chan.get_state() == "OPENING": - res = self.save_short_chan_id(chan) + res, depth = self.save_short_chan_id(chan) if not res: - self.print_error("network update but funding tx is still not at sufficient depth") + self.print_error("network update but funding tx is still not at sufficient depth. actual depth: " + str(depth)) continue # this results in the channel being marked OPEN peer = self.peers[chan.node_id] t@@ -154,14 +156,14 @@ class LNWorker(PrintError): return if event == 'fee': await peer.bitcoin_fee_update(chan) - conf = self.wallet.get_tx_height(chan.funding_outpoint.txid).conf + conf = addr_sync.get_tx_height(chan.funding_outpoint.txid).conf peer.on_network_update(chan, conf) async def _open_channel_coroutine(self, peer, local_amount_sat, push_sat, password): # peer might just have been connected to await asyncio.wait_for(peer.initialized, 5) - openingchannel = await peer.channel_establishment_flow(self.wallet, self.config, password, + openingchannel = await peer.channel_establishment_flow(password, funding_sat=local_amount_sat + push_sat, push_msat=push_sat * 1000, temp_channel_id=os.urandom(32),