electrum

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

commit e8471e483b303e6b65f9a2979b9eb9ea530b0490
parent 1d4c113a3524cf012d5a7c40acdc7a61e16b19ed
Author: Janus <ysangkok@gmail.com>
Date:   Wed, 10 Oct 2018 22:54:30 +0200

lnhtlc: merge config and state, remove unnecessary properties

Diffstat:
Melectrum/lnbase.py | 188+++++++++++++++++++++++++++++++++++++------------------------------------------
Melectrum/lnhtlc.py | 258++++++++++++++++++++++++++++++++++++-------------------------------------------
Melectrum/lnutil.py | 48++++++++++++++++++++++++++++++++++++++----------
Melectrum/lnworker.py | 12++++++------
Melectrum/tests/test_lnhtlc.py | 90++++++++++++++++++++++++++++++++++++++-----------------------------------------
5 files changed, 292 insertions(+), 304 deletions(-)

diff --git a/electrum/lnbase.py b/electrum/lnbase.py @@ -27,9 +27,9 @@ from .util import PrintError, bh2u, print_error, bfh, log_exceptions from .transaction import Transaction, TxOutput from .lnonion import new_onion_packet, OnionHopsDataSingle, OnionPerHop, decode_onion_error, OnionFailureCode from .lnaddr import lndecode -from .lnhtlc import HTLCStateMachine, RevokeAndAck -from .lnutil import (Outpoint, ChannelConfig, LocalState, - RemoteState, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore, +from .lnhtlc import HTLCStateMachine, RevokeAndAck, htlcsum +from .lnutil import (Outpoint, LocalConfig, ChannelConfig, + RemoteConfig, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore, funding_output_script, get_ecdh, get_per_commitment_secret_from_seed, secret_to_pubkey, LNPeerAddr, PaymentFailure, LnLocalFeatures, LOCAL, REMOTE, HTLCOwner, generate_keypair, LnKeyFamily, @@ -480,7 +480,7 @@ class Peer(PrintError): def on_announcement_signatures(self, payload): channel_id = payload['channel_id'] chan = self.channels[payload['channel_id']] - if chan.local_state.was_announced: + if chan.config[LOCAL].was_announced: h, local_node_sig, local_bitcoin_sig = self.send_announcement_signatures(chan) else: self.announcement_signatures[channel_id].put_nowait(payload) @@ -530,7 +530,7 @@ class Peer(PrintError): chan.set_state('DISCONNECTED') self.network.trigger_callback('channel', chan) - def make_local_config(self, funding_sat, push_msat, initiator: HTLCOwner): + def make_local_config(self, funding_sat, push_msat, initiator: HTLCOwner, feerate): # key derivation channel_counter = self.lnworker.get_and_inc_counter_for_channel_keys() keypair_generator = lambda family: generate_keypair(self.lnworker.ln_keystore, family, channel_counter) @@ -549,6 +549,10 @@ class Peer(PrintError): max_htlc_value_in_flight_msat=0xffffffffffffffff, max_accepted_htlcs=5, initial_msat=initial_msat, + ctn=-1, + next_htlc_id=0, + amount_msat=initial_msat, + feerate=feerate, ) per_commitment_secret_seed = keypair_generator(LnKeyFamily.REVOCATION_ROOT).privkey return local_config, per_commitment_secret_seed @@ -556,9 +560,8 @@ class Peer(PrintError): @log_exceptions async def channel_establishment_flow(self, password, funding_sat, push_msat, temp_channel_id): await self.initialized - local_config, per_commitment_secret_seed = self.make_local_config(funding_sat, push_msat, LOCAL) - # amounts - local_feerate = self.current_feerate_per_kw() + feerate = self.current_feerate_per_kw() + local_config, per_commitment_secret_seed = self.make_local_config(funding_sat, push_msat, LOCAL, feerate) # for the first commitment transaction per_commitment_secret_first = get_per_commitment_secret_from_seed(per_commitment_secret_seed, RevocationStore.START_INDEX) per_commitment_point_first = secret_to_pubkey(int.from_bytes(per_commitment_secret_first, 'big')) @@ -569,7 +572,7 @@ class Peer(PrintError): funding_satoshis=funding_sat, push_msat=push_msat, dust_limit_satoshis=local_config.dust_limit_sat, - feerate_per_kw=local_feerate, + feerate_per_kw=feerate, max_accepted_htlcs=local_config.max_accepted_htlcs, funding_pubkey=local_config.multisig_key.pubkey, revocation_basepoint=local_config.revocation_basepoint.pubkey, @@ -587,24 +590,33 @@ class Peer(PrintError): if payload.get('error'): raise Exception(payload.get('error')) remote_per_commitment_point = payload['first_per_commitment_point'] - remote_config=ChannelConfig( + funding_txn_minimum_depth = int.from_bytes(payload['minimum_depth'], 'big') + remote_dust_limit_sat = int.from_bytes(payload['dust_limit_satoshis'], byteorder='big') + assert remote_dust_limit_sat < 600, remote_dust_limit_sat + assert int.from_bytes(payload['htlc_minimum_msat'], 'big') < 600 * 1000 + remote_max = int.from_bytes(payload['max_htlc_value_in_flight_msat'], 'big') + assert remote_max >= 198 * 1000 * 1000, remote_max + their_revocation_store = RevocationStore() + remote_config = RemoteConfig( 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'], byteorder='big'), - dust_limit_sat=int.from_bytes(payload['dust_limit_satoshis'], byteorder='big'), - max_htlc_value_in_flight_msat=int.from_bytes(payload['max_htlc_value_in_flight_msat'], 'big'), + dust_limit_sat=remote_dust_limit_sat, + max_htlc_value_in_flight_msat=remote_max, max_accepted_htlcs=int.from_bytes(payload["max_accepted_htlcs"], 'big'), - initial_msat=push_msat + initial_msat=push_msat, + ctn = -1, + amount_msat=push_msat, + next_htlc_id = 0, + feerate=feerate, + + next_per_commitment_point=remote_per_commitment_point, + current_per_commitment_point=None, + revocation_store=their_revocation_store, ) - funding_txn_minimum_depth = int.from_bytes(payload['minimum_depth'], 'big') - assert remote_config.dust_limit_sat < 600 - assert int.from_bytes(payload['htlc_minimum_msat'], 'big') < 600 * 1000 - assert remote_config.max_htlc_value_in_flight_msat >= 198 * 1000 * 1000, remote_config.max_htlc_value_in_flight_msat - self.print_error('remote delay', remote_config.to_self_delay) - self.print_error('funding_txn_minimum_depth', funding_txn_minimum_depth) # create funding tx redeem_script = funding_output_script(local_config, remote_config) funding_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script) @@ -612,38 +624,21 @@ class Peer(PrintError): 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 - local_amount = funding_sat*1000 - push_msat - remote_amount = push_msat # remote commitment transaction channel_id, funding_txid_bytes = channel_id_from_funding_tx(funding_txid, funding_index) - their_revocation_store = RevocationStore() chan = { "node_id": self.pubkey, "channel_id": channel_id, "short_channel_id": None, "funding_outpoint": Outpoint(funding_txid, funding_index), - "local_config": local_config, "remote_config": remote_config, - "remote_state": RemoteState( - ctn = -1, - next_per_commitment_point=remote_per_commitment_point, - current_per_commitment_point=None, - amount_msat=remote_amount, - revocation_store=their_revocation_store, - next_htlc_id = 0, - feerate=local_feerate - ), - "local_state": LocalState( - ctn = -1, + "local_config": LocalConfig( + **local_config._asdict(), per_commitment_secret_seed=per_commitment_secret_seed, - amount_msat=local_amount, - 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=True, funding_txn_minimum_depth=funding_txn_minimum_depth), "remote_commitment_to_be_revoked": None, @@ -665,8 +660,8 @@ class Peer(PrintError): success, _txid = await self.network.broadcast_transaction(funding_tx) assert success, success 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) + m.config[REMOTE] = m.config[REMOTE]._replace(ctn=0) + m.config[LOCAL] = m.config[LOCAL]._replace(ctn=0, current_commitment_signature=remote_sig) m.set_state('OPENING') return m @@ -677,21 +672,10 @@ class Peer(PrintError): raise Exception('wrong chain_hash') funding_sat = int.from_bytes(payload['funding_satoshis'], 'big') push_msat = int.from_bytes(payload['push_msat'], 'big') + feerate = int.from_bytes(payload['feerate_per_kw'], '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'] - local_config, per_commitment_secret_seed = self.make_local_config(funding_sat * 1000, push_msat, REMOTE) + local_config, per_commitment_secret_seed = self.make_local_config(funding_sat * 1000, push_msat, REMOTE, feerate) # for the first commitment transaction per_commitment_secret_first = get_per_commitment_secret_from_seed(per_commitment_secret_seed, RevocationStore.START_INDEX) @@ -719,33 +703,39 @@ class Peer(PrintError): 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') + remote_balance_sat = funding_sat * 1000 - push_msat 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( + "remote_config": RemoteConfig( + 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=remote_balance_sat, ctn = -1, + amount_msat=remote_balance_sat, + next_htlc_id = 0, + feerate=feerate, + 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, + "local_config": LocalConfig( + **local_config._asdict(), 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, @@ -762,8 +752,8 @@ class Peer(PrintError): )) 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) + m.config[REMOTE] = m.config[REMOTE]._replace(ctn=0) + m.config[LOCAL] = m.config[LOCAL]._replace(ctn=0, current_commitment_signature=remote_sig) self.lnworker.save_channel(m) self.lnwatcher.watch_channel(m.get_funding_address(), m.funding_outpoint.to_str()) self.lnworker.on_channels_updated() @@ -776,7 +766,7 @@ class Peer(PrintError): else: break outp = funding_tx.outputs()[funding_idx] - redeem_script = funding_output_script(remote_config, local_config) + redeem_script = funding_output_script(m.config[REMOTE], m.config[LOCAL]) funding_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script) if outp != TxOutput(bitcoin.TYPE_ADDRESS, funding_address, funding_sat): m.set_state('DISCONNECTED') @@ -794,12 +784,12 @@ class Peer(PrintError): self.network.trigger_callback('channel', chan) self.send_message(gen_msg("channel_reestablish", channel_id=chan_id, - next_local_commitment_number=chan.local_state.ctn+1, - next_remote_revocation_number=chan.remote_state.ctn + next_local_commitment_number=chan.config[LOCAL].ctn+1, + next_remote_revocation_number=chan.config[REMOTE].ctn )) await self.channel_reestablished[chan_id] chan.set_state('OPENING') - if chan.local_state.funding_locked_received and chan.short_channel_id: + if chan.config[LOCAL].funding_locked_received and chan.short_channel_id: self.mark_open(chan) self.network.trigger_callback('channel', chan) @@ -822,24 +812,24 @@ class Peer(PrintError): channel_reestablish_msg = payload # compare remote ctns remote_ctn = int.from_bytes(channel_reestablish_msg["next_local_commitment_number"], 'big') - if remote_ctn != chan.remote_state.ctn + 1: - self.print_error("expected remote ctn {}, got {}".format(chan.remote_state.ctn + 1, remote_ctn)) + if remote_ctn != chan.config[REMOTE].ctn + 1: + self.print_error("expected remote ctn {}, got {}".format(chan.config[REMOTE].ctn + 1, remote_ctn)) # TODO iff their ctn is lower than ours, we should force close instead try_to_get_remote_to_force_close_with_their_latest() return # compare local ctns local_ctn = int.from_bytes(channel_reestablish_msg["next_remote_revocation_number"], 'big') - if local_ctn != chan.local_state.ctn: - self.print_error("expected local ctn {}, got {}".format(chan.local_state.ctn, local_ctn)) + if local_ctn != chan.config[LOCAL].ctn: + self.print_error("expected local ctn {}, got {}".format(chan.config[LOCAL].ctn, local_ctn)) # TODO iff their ctn is lower than ours, we should force close instead try_to_get_remote_to_force_close_with_their_latest() return # compare per commitment points (needs data_protect option) their_pcp = channel_reestablish_msg.get("my_current_per_commitment_point", None) if their_pcp is not None: - our_pcp = chan.remote_state.current_per_commitment_point + our_pcp = chan.config[REMOTE].current_per_commitment_point if our_pcp is None: - our_pcp = chan.remote_state.next_per_commitment_point + our_pcp = chan.config[REMOTE].next_per_commitment_point if our_pcp != their_pcp: self.print_error("Remote PCP mismatch: {} {}".format(bh2u(our_pcp), bh2u(their_pcp))) # FIXME ...what now? @@ -852,10 +842,10 @@ class Peer(PrintError): channel_id = chan.channel_id per_commitment_secret_index = RevocationStore.START_INDEX - 1 per_commitment_point_second = secret_to_pubkey(int.from_bytes( - get_per_commitment_secret_from_seed(chan.local_state.per_commitment_secret_seed, per_commitment_secret_index), 'big')) + get_per_commitment_secret_from_seed(chan.config[LOCAL].per_commitment_secret_seed, per_commitment_secret_index), 'big')) # note: if funding_locked was not yet received, we might send it multiple times self.send_message(gen_msg("funding_locked", channel_id=channel_id, next_per_commitment_point=per_commitment_point_second)) - if chan.local_state.funding_locked_received: + if chan.config[LOCAL].funding_locked_received: self.mark_open(chan) def on_funding_locked(self, payload): @@ -864,13 +854,13 @@ class Peer(PrintError): if not chan: print(self.channels) raise Exception("Got unknown funding_locked", channel_id) - if not chan.local_state.funding_locked_received: - our_next_point = chan.remote_state.next_per_commitment_point + if not chan.config[LOCAL].funding_locked_received: + our_next_point = chan.config[REMOTE].next_per_commitment_point their_next_point = payload["next_per_commitment_point"] - new_remote_state = chan.remote_state._replace(next_per_commitment_point=their_next_point, current_per_commitment_point=our_next_point) - new_local_state = chan.local_state._replace(funding_locked_received = True) - chan.remote_state=new_remote_state - chan.local_state=new_local_state + new_remote_state = chan.config[REMOTE]._replace(next_per_commitment_point=their_next_point, current_per_commitment_point=our_next_point) + new_local_state = chan.config[LOCAL]._replace(funding_locked_received = True) + chan.config[REMOTE]=new_remote_state + chan.config[LOCAL]=new_local_state self.lnworker.save_channel(chan) if chan.short_channel_id: self.mark_open(chan) @@ -881,11 +871,11 @@ class Peer(PrintError): Runs on the Network thread. """ - if not chan.local_state.was_announced and funding_tx_depth >= 6: + if not chan.config[LOCAL].was_announced and funding_tx_depth >= 6: # don't announce our channels # FIXME should this be a field in chan.local_state maybe? return - chan.local_state=chan.local_state._replace(was_announced=True) + chan.config[LOCAL]=chan.config[LOCAL]._replace(was_announced=True) coro = self.handle_announcements(chan) self.lnworker.save_channel(chan) asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop) @@ -896,7 +886,7 @@ class Peer(PrintError): announcement_signatures_msg = await self.announcement_signatures[chan.channel_id].get() remote_node_sig = announcement_signatures_msg["node_signature"] remote_bitcoin_sig = announcement_signatures_msg["bitcoin_signature"] - if not ecc.verify_signature(chan.remote_config.multisig_key.pubkey, remote_bitcoin_sig, h): + if not ecc.verify_signature(chan.config[REMOTE].multisig_key.pubkey, remote_bitcoin_sig, h): raise Exception("bitcoin_sig invalid in announcement_signatures") if not ecc.verify_signature(self.pubkey, remote_node_sig, h): raise Exception("node_sig invalid in announcement_signatures") @@ -904,7 +894,7 @@ class Peer(PrintError): node_sigs = [local_node_sig, remote_node_sig] bitcoin_sigs = [local_bitcoin_sig, remote_bitcoin_sig] node_ids = [privkey_to_pubkey(self.privkey), self.pubkey] - bitcoin_keys = [chan.local_config.multisig_key.pubkey, chan.remote_config.multisig_key.pubkey] + bitcoin_keys = [chan.config[LOCAL].multisig_key.pubkey, chan.config[REMOTE].multisig_key.pubkey] if node_ids[0] > node_ids[1]: node_sigs.reverse() @@ -935,14 +925,14 @@ class Peer(PrintError): if chan.get_state() == "OPEN": return # NOTE: even closed channels will be temporarily marked "OPEN" - assert chan.local_state.funding_locked_received + assert chan.config[LOCAL].funding_locked_received chan.set_state("OPEN") self.network.trigger_callback('channel', chan) # add channel to database pubkey_ours = self.lnworker.node_keypair.pubkey pubkey_theirs = self.pubkey node_ids = [pubkey_theirs, pubkey_ours] - bitcoin_keys = [chan.local_config.multisig_key.pubkey, chan.remote_config.multisig_key.pubkey] + bitcoin_keys = [chan.config[LOCAL].multisig_key.pubkey, chan.config[REMOTE].multisig_key.pubkey] sorted_node_ids = list(sorted(node_ids)) if sorted_node_ids != node_ids: node_ids = sorted_node_ids @@ -977,8 +967,8 @@ class Peer(PrintError): def send_announcement_signatures(self, chan): - bitcoin_keys = [chan.local_config.multisig_key.pubkey, - chan.remote_config.multisig_key.pubkey] + bitcoin_keys = [chan.config[LOCAL].multisig_key.pubkey, + chan.config[REMOTE].multisig_key.pubkey] node_ids = [privkey_to_pubkey(self.privkey), self.pubkey] @@ -1000,7 +990,7 @@ class Peer(PrintError): ) to_hash = chan_ann[256+2:] h = bitcoin.Hash(to_hash) - bitcoin_signature = ecc.ECPrivkey(chan.local_config.multisig_key.privkey).sign(h, sig_string_from_r_and_s, get_r_and_s_from_sig_string) + bitcoin_signature = ecc.ECPrivkey(chan.config[LOCAL].multisig_key.privkey).sign(h, sig_string_from_r_and_s, get_r_and_s_from_sig_string) node_signature = ecc.ECPrivkey(self.privkey).sign(h, sig_string_from_r_and_s, get_r_and_s_from_sig_string) self.send_message(gen_msg("announcement_signatures", channel_id=chan.channel_id, @@ -1105,10 +1095,10 @@ class Peer(PrintError): # then no other payment can use this channel either. # we need finer blacklisting -- e.g. a blacklist for just this "payment session"? # or blacklist entries could store an msat value and also expire - if len(chan.htlcs_in_local) + 1 > chan.remote_config.max_accepted_htlcs: + if len(chan.htlcs(LOCAL, only_pending=True)) + 1 > chan.config[REMOTE].max_accepted_htlcs: raise PaymentFailure('too many HTLCs already in channel') - if chan.htlcsum(chan.htlcs_in_local) + amount_msat > chan.remote_config.max_htlc_value_in_flight_msat: - raise PaymentFailure('HTLC value sum would exceed max allowed: {} msat'.format(chan.remote_config.max_htlc_value_in_flight_msat)) + if htlcsum(chan.htlcs(LOCAL, only_pending=True)) + amount_msat > chan.config[REMOTE].max_htlc_value_in_flight_msat: + raise PaymentFailure('HTLC value sum would exceed max allowed: {} msat'.format(chan.config[REMOTE].max_htlc_value_in_flight_msat)) if msat_local < 0: # FIXME what about channel_reserve_satoshis? will the remote fail the channel if we go below? test. raise PaymentFailure('not enough local balance') @@ -1144,7 +1134,7 @@ class Peer(PrintError): channel_id = chan.channel_id expected_received_msat = int(decoded.amount * bitcoin.COIN * 1000) htlc_id = int.from_bytes(htlc["id"], 'big') - assert htlc_id == chan.remote_state.next_htlc_id, (htlc_id, chan.remote_state.next_htlc_id) + assert htlc_id == chan.config[REMOTE].next_htlc_id, (htlc_id, chan.config[REMOTE].next_htlc_id) assert chan.get_state() == "OPEN" cltv_expiry = int.from_bytes(htlc["cltv_expiry"], 'big') # TODO verify sanity of their cltv expiry @@ -1166,7 +1156,7 @@ class Peer(PrintError): self.print_error("commitment_signed", payload) channel_id = payload['channel_id'] chan = self.channels[channel_id] - chan.local_state=chan.local_state._replace( + chan.config[LOCAL]=chan.config[LOCAL]._replace( current_commitment_signature=payload['signature'], current_htlc_signatures=payload['htlc_signature']) self.lnworker.save_channel(chan) diff --git a/electrum/lnhtlc.py b/electrum/lnhtlc.py @@ -10,7 +10,7 @@ from .bitcoin import Hash, TYPE_SCRIPT, TYPE_ADDRESS from .bitcoin import redeem_script_to_address from .crypto import sha256 from . import ecc -from .lnutil import Outpoint, ChannelConfig, LocalState, RemoteState, Keypair, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore, EncumberedTransaction +from .lnutil import Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore, EncumberedTransaction from .lnutil import get_per_commitment_secret_from_seed from .lnutil import make_commitment_output_to_remote_address, make_commitment_output_to_local_witness_script from .lnutil import secret_to_pubkey, derive_privkey, derive_pubkey, derive_blinded_pubkey, derive_blinded_privkey @@ -75,59 +75,50 @@ class UpdateAddHtlc(namedtuple('UpdateAddHtlc', ['amount_msat', 'payment_hash', if 'locked_in' not in kwargs: kwargs['locked_in'] = {LOCAL: None, REMOTE: None} else: - kwargs['locked_in'] = {HTLCOwner(int(x)): y for x,y in kwargs['locked_in']} + kwargs['locked_in'] = {HTLCOwner(int(x)): y for x,y in kwargs['locked_in'].items()} return super().__new__(cls, **kwargs) -is_key = lambda k: k.endswith("_basepoint") or k.endswith("_key") - -def maybeDecode(k, v): - assert type(v) is not list - if k in ["node_id", "channel_id", "short_channel_id", "pubkey", "privkey", "current_per_commitment_point", "next_per_commitment_point", "per_commitment_secret_seed", "current_commitment_signature", "current_htlc_signatures"] and v is not None: - return binascii.unhexlify(v) - return v - -def decodeAll(v): - return {i: maybeDecode(i, j) for i, j in v.items()} if isinstance(v, dict) else v - -def typeWrap(k, v, local): - if is_key(k): - if local: - return Keypair(**v) +def decodeAll(d, local): + for k, v in d.items(): + if k == 'revocation_store': + yield (k, RevocationStore.from_json_obj(v)) + elif k.endswith("_basepoint") or k.endswith("_key"): + if local: + yield (k, Keypair(**dict(decodeAll(v, local)))) + else: + yield (k, OnlyPubkeyKeypair(**dict(decodeAll(v, local)))) + elif k in ["node_id", "channel_id", "short_channel_id", "pubkey", "privkey", "current_per_commitment_point", "next_per_commitment_point", "per_commitment_secret_seed", "current_commitment_signature", "current_htlc_signatures"] and v is not None: + yield (k, binascii.unhexlify(v)) else: - return OnlyPubkeyKeypair(**v) - return v + yield (k, v) + +def htlcsum(htlcs): + return sum([x.amount_msat for x in htlcs]) class HTLCStateMachine(PrintError): def diagnostic_name(self): return str(self.name) def __init__(self, state, name = None): - self.local_config = state["local_config"] - if type(self.local_config) is not ChannelConfig: - new_local_config = {k: typeWrap(k, decodeAll(v), True) for k, v in self.local_config.items()} - self.local_config = ChannelConfig(**new_local_config) - - self.remote_config = state["remote_config"] - if type(self.remote_config) is not ChannelConfig: - new_remote_config = {k: typeWrap(k, decodeAll(v), False) for k, v in self.remote_config.items()} - self.remote_config = ChannelConfig(**new_remote_config) - - self.local_state = state["local_state"] - if type(self.local_state) is not LocalState: - self.local_state = LocalState(**decodeAll(self.local_state)) - - self.remote_state = state["remote_state"] - if type(self.remote_state) is not RemoteState: - self.remote_state = RemoteState(**decodeAll(self.remote_state)) - - if type(self.remote_state.revocation_store) is not RevocationStore: - self.remote_state = self.remote_state._replace(revocation_store = RevocationStore.from_json_obj(self.remote_state.revocation_store)) - - self.channel_id = maybeDecode("channel_id", state["channel_id"]) if type(state["channel_id"]) is not bytes else state["channel_id"] - self.constraints = ChannelConstraints(**decodeAll(state["constraints"])) if type(state["constraints"]) is not ChannelConstraints else state["constraints"] - self.funding_outpoint = Outpoint(**decodeAll(state["funding_outpoint"])) if type(state["funding_outpoint"]) is not Outpoint else state["funding_outpoint"] - self.node_id = maybeDecode("node_id", state["node_id"]) if type(state["node_id"]) is not bytes else state["node_id"] - self.short_channel_id = maybeDecode("short_channel_id", state["short_channel_id"]) if type(state["short_channel_id"]) is not bytes else state["short_channel_id"] + assert 'local_state' not in state + self.config = {} + self.config[LOCAL] = state["local_config"] + if type(self.config[LOCAL]) is not LocalConfig: + conf = dict(decodeAll(self.config[LOCAL], True)) + self.config[LOCAL] = LocalConfig(**conf) + assert type(self.config[LOCAL].htlc_basepoint.privkey) is bytes + + self.config[REMOTE] = state["remote_config"] + if type(self.config[REMOTE]) is not RemoteConfig: + conf = dict(decodeAll(self.config[REMOTE], False)) + self.config[REMOTE] = RemoteConfig(**conf) + assert type(self.config[REMOTE].htlc_basepoint.pubkey) is bytes + + self.channel_id = bfh(state["channel_id"]) if type(state["channel_id"]) not in (bytes, type(None)) else state["channel_id"] + self.constraints = ChannelConstraints(**state["constraints"]) if type(state["constraints"]) is not ChannelConstraints else state["constraints"] + self.funding_outpoint = Outpoint(**dict(decodeAll(state["funding_outpoint"], False))) if type(state["funding_outpoint"]) is not Outpoint else state["funding_outpoint"] + self.node_id = bfh(state["node_id"]) if type(state["node_id"]) not in (bytes, type(None)) else state["node_id"] + self.short_channel_id = bfh(state["short_channel_id"]) if type(state["short_channel_id"]) not in (bytes, type(None)) else state["short_channel_id"] self.short_channel_id_predicted = self.short_channel_id self.onion_keys = {int(k): bfh(v) for k,v in state['onion_keys'].items()} if 'onion_keys' in state else {} @@ -141,7 +132,7 @@ class HTLCStateMachine(PrintError): for strname, subject in [('remote_log', REMOTE), ('local_log', LOCAL)]: if strname not in state: continue for y in state[strname]: - htlc = UpdateAddHtlc(*decodeAll(y)) + htlc = UpdateAddHtlc(**y) self.log[subject]['adds'][htlc.htlc_id] = htlc self.name = name @@ -172,7 +163,7 @@ class HTLCStateMachine(PrintError): return self._is_funding_txo_spent is False and self._state == 'DISCONNECTED' def get_funding_address(self): - script = funding_output_script(self.local_config, self.remote_config) + script = funding_output_script(self.config[LOCAL], self.config[REMOTE]) return redeem_script_to_address('p2wsh', script) def add_htlc(self, htlc): @@ -181,10 +172,10 @@ class HTLCStateMachine(PrintError): should be called when preparing to send an outgoing HTLC. """ assert type(htlc) is dict - htlc = UpdateAddHtlc(**htlc, htlc_id=self.local_state.next_htlc_id) + htlc = UpdateAddHtlc(**htlc, htlc_id=self.config[LOCAL].next_htlc_id) self.log[LOCAL]['adds'][htlc.htlc_id] = htlc self.print_error("add_htlc") - self.local_state=self.local_state._replace(next_htlc_id=htlc.htlc_id + 1) + self.config[LOCAL]=self.config[LOCAL]._replace(next_htlc_id=htlc.htlc_id + 1) return htlc.htlc_id def receive_htlc(self, htlc): @@ -194,10 +185,10 @@ class HTLCStateMachine(PrintError): party. """ assert type(htlc) is dict - htlc = UpdateAddHtlc(**htlc, htlc_id = self.remote_state.next_htlc_id) + htlc = UpdateAddHtlc(**htlc, htlc_id = self.config[REMOTE].next_htlc_id) self.log[REMOTE]['adds'][htlc.htlc_id] = htlc self.print_error("receive_htlc") - self.remote_state=self.remote_state._replace(next_htlc_id=htlc.htlc_id + 1) + self.config[REMOTE]=self.config[REMOTE]._replace(next_htlc_id=htlc.htlc_id + 1) return htlc.htlc_id def sign_next_commitment(self): @@ -215,15 +206,15 @@ class HTLCStateMachine(PrintError): """ for htlc in self.log[LOCAL]['adds'].values(): if htlc.locked_in[LOCAL] is None: - htlc.locked_in[LOCAL] = self.local_state.ctn + htlc.locked_in[LOCAL] = self.config[LOCAL].ctn self.print_error("sign_next_commitment") pending_remote_commitment = self.pending_remote_commitment - sig_64 = sign_and_get_sig_string(pending_remote_commitment, self.local_config, self.remote_config) + sig_64 = sign_and_get_sig_string(pending_remote_commitment, self.config[LOCAL], self.config[REMOTE]) their_remote_htlc_privkey_number = derive_privkey( - int.from_bytes(self.local_config.htlc_basepoint.privkey, 'big'), - self.remote_state.next_per_commitment_point) + int.from_bytes(self.config[LOCAL].htlc_basepoint.privkey, 'big'), + self.config[REMOTE].next_per_commitment_point) their_remote_htlc_privkey = their_remote_htlc_privkey_number.to_bytes(32, 'big') for_us = False @@ -231,7 +222,7 @@ class HTLCStateMachine(PrintError): htlcsigs = [] for we_receive, htlcs in zip([True, False], [self.included_htlcs(REMOTE, REMOTE), self.included_htlcs(REMOTE, LOCAL)]): for htlc in htlcs: - args = [self.remote_state.next_per_commitment_point, for_us, we_receive, pending_remote_commitment, htlc] + args = [self.config[REMOTE].next_per_commitment_point, for_us, we_receive, pending_remote_commitment, htlc] htlc_tx = make_htlc_tx_with_open_channel(self, *args) sig = bfh(htlc_tx.sign_txin(0, their_remote_htlc_privkey)) htlc_sig = ecc.sig_string_from_der_sig(sig[:-1]) @@ -265,13 +256,13 @@ class HTLCStateMachine(PrintError): self.print_error("receive_new_commitment") for htlc in self.log[REMOTE]['adds'].values(): if htlc.locked_in[REMOTE] is None: - htlc.locked_in[REMOTE] = self.remote_state.ctn + htlc.locked_in[REMOTE] = self.config[REMOTE].ctn assert len(htlc_sigs) == 0 or type(htlc_sigs[0]) is bytes pending_local_commitment = self.pending_local_commitment preimage_hex = pending_local_commitment.serialize_preimage(0) pre_hash = Hash(bfh(preimage_hex)) - if not ecc.verify_signature(self.remote_config.multisig_key.pubkey, sig, pre_hash): + if not ecc.verify_signature(self.config[REMOTE].multisig_key.pubkey, sig, pre_hash): raise Exception('failed verifying signature of our updated commitment transaction: ' + bh2u(sig) + ' preimage is ' + preimage_hex) _, this_point, _ = self.points @@ -280,7 +271,7 @@ class HTLCStateMachine(PrintError): for htlc in htlcs: htlc_tx = make_htlc_tx_with_open_channel(self, this_point, True, we_receive, pending_local_commitment, htlc) pre_hash = Hash(bfh(htlc_tx.serialize_preimage(0))) - remote_htlc_pubkey = derive_pubkey(self.remote_config.htlc_basepoint.pubkey, this_point) + remote_htlc_pubkey = derive_pubkey(self.config[REMOTE].htlc_basepoint.pubkey, this_point) for idx, sig in enumerate(htlc_sigs): if ecc.verify_signature(remote_htlc_pubkey, sig, pre_hash): del htlc_sigs[idx] @@ -314,8 +305,8 @@ class HTLCStateMachine(PrintError): last_secret, this_point, next_point = self.points - new_local_feerate = self.local_state.feerate - new_remote_feerate = self.remote_state.feerate + new_local_feerate = self.config[LOCAL].feerate + new_remote_feerate = self.config[REMOTE].feerate for pending_fee in self.fee_mgr[:]: if not self.constraints.is_initiator and pending_fee.had(FUNDEE_SIGNED): @@ -327,11 +318,11 @@ class HTLCStateMachine(PrintError): self.fee_mgr.remove(pending_fee) print("FEERATE CHANGE COMPLETE (initiator)") - self.local_state=self.local_state._replace( - ctn=self.local_state.ctn + 1, + self.config[LOCAL]=self.config[LOCAL]._replace( + ctn=self.config[LOCAL].ctn + 1, feerate=new_local_feerate ) - self.remote_state=self.remote_state._replace( + self.config[REMOTE]=self.config[REMOTE]._replace( feerate=new_remote_feerate ) @@ -341,13 +332,13 @@ class HTLCStateMachine(PrintError): @property def points(self): - last_small_num = self.local_state.ctn + last_small_num = self.config[LOCAL].ctn this_small_num = last_small_num + 1 next_small_num = last_small_num + 2 - last_secret = get_per_commitment_secret_from_seed(self.local_state.per_commitment_secret_seed, RevocationStore.START_INDEX - last_small_num) - this_secret = get_per_commitment_secret_from_seed(self.local_state.per_commitment_secret_seed, RevocationStore.START_INDEX - this_small_num) + last_secret = get_per_commitment_secret_from_seed(self.config[LOCAL].per_commitment_secret_seed, RevocationStore.START_INDEX - last_small_num) + this_secret = get_per_commitment_secret_from_seed(self.config[LOCAL].per_commitment_secret_seed, RevocationStore.START_INDEX - this_small_num) this_point = secret_to_pubkey(int.from_bytes(this_secret, 'big')) - next_secret = get_per_commitment_secret_from_seed(self.local_state.per_commitment_secret_seed, RevocationStore.START_INDEX - next_small_num) + next_secret = get_per_commitment_secret_from_seed(self.config[LOCAL].per_commitment_secret_seed, RevocationStore.START_INDEX - next_small_num) next_point = secret_to_pubkey(int.from_bytes(next_secret, 'big')) return last_secret, this_point, next_point @@ -358,13 +349,13 @@ class HTLCStateMachine(PrintError): return outpoint = self.funding_outpoint.to_str() if ours: - ctn = self.local_state.ctn + 1 + ctn = self.config[LOCAL].ctn + 1 our_per_commitment_secret = get_per_commitment_secret_from_seed( - self.local_state.per_commitment_secret_seed, RevocationStore.START_INDEX - ctn) + self.config[LOCAL].per_commitment_secret_seed, RevocationStore.START_INDEX - ctn) our_cur_pcp = ecc.ECPrivkey(our_per_commitment_secret).get_public_key_bytes(compressed=True) encumbered_sweeptx = maybe_create_sweeptx_for_our_ctx_to_local(self, ctx, our_cur_pcp, self.sweep_address) else: - their_cur_pcp = self.remote_state.next_per_commitment_point + their_cur_pcp = self.config[REMOTE].next_per_commitment_point encumbered_sweeptx = maybe_create_sweeptx_for_their_ctx_to_remote(self, ctx, their_cur_pcp, self.sweep_address) self.lnwatcher.add_sweep_tx(outpoint, ctx.txid(), encumbered_sweeptx) @@ -390,7 +381,7 @@ class HTLCStateMachine(PrintError): """ self.print_error("receive_revocation") - cur_point = self.remote_state.current_per_commitment_point + cur_point = self.config[REMOTE].current_per_commitment_point derived_point = ecc.ECPrivkey(revocation.per_commitment_secret).get_public_key_bytes(compressed=True) if cur_point != derived_point: raise Exception('revoked secret not for current point') @@ -400,14 +391,14 @@ class HTLCStateMachine(PrintError): # this might break prev_remote_commitment = self.pending_remote_commitment - self.remote_state.revocation_store.add_next_entry(revocation.per_commitment_secret) + self.config[REMOTE].revocation_store.add_next_entry(revocation.per_commitment_secret) self.process_new_revocation_secret(revocation.per_commitment_secret) def mark_settled(subject): """ find pending settlements for subject (LOCAL or REMOTE) and mark them settled, return value of settled htlcs """ - old_amount = self.htlcsum(self.gen_htlc_indices(subject, False)) + old_amount = htlcsum(self.htlcs(subject, False)) for htlc_id in self.log[-subject]['settles']: adds = self.log[subject]['adds'] @@ -415,23 +406,23 @@ class HTLCStateMachine(PrintError): self.settled[subject].append(htlc.amount_msat) self.log[-subject]['settles'].clear() - return old_amount - self.htlcsum(self.gen_htlc_indices(subject, False)) + return old_amount - htlcsum(self.htlcs(subject, False)) sent_this_batch = mark_settled(LOCAL) received_this_batch = mark_settled(REMOTE) - next_point = self.remote_state.next_per_commitment_point + next_point = self.config[REMOTE].next_per_commitment_point print("RECEIVED", received_this_batch) print("SENT", sent_this_batch) - self.remote_state=self.remote_state._replace( - ctn=self.remote_state.ctn + 1, + self.config[REMOTE]=self.config[REMOTE]._replace( + ctn=self.config[REMOTE].ctn + 1, current_per_commitment_point=next_point, next_per_commitment_point=revocation.next_per_commitment_point, - amount_msat=self.remote_state.amount_msat + (sent_this_batch - received_this_batch) + amount_msat=self.config[REMOTE].amount_msat + (sent_this_batch - received_this_batch) ) - self.local_state=self.local_state._replace( - amount_msat = self.local_state.amount_msat + (received_this_batch - sent_this_batch) + self.config[LOCAL]=self.config[LOCAL]._replace( + amount_msat = self.config[LOCAL].amount_msat + (received_this_batch - sent_this_batch) ) for pending_fee in self.fee_mgr: @@ -444,43 +435,39 @@ class HTLCStateMachine(PrintError): return received_this_batch, sent_this_batch def balance(self, subject): - initial = self.local_config.initial_msat if subject == LOCAL else self.remote_config.initial_msat + initial = self.config[subject].initial_msat initial -= sum(self.settled[subject]) initial += sum(self.settled[-subject]) - assert initial == (self.local_state.amount_msat if subject == LOCAL else self.remote_state.amount_msat) + assert initial == self.config[subject].amount_msat return initial - @staticmethod - def htlcsum(htlcs): - amount_unsettled = 0 - for x in htlcs: - amount_unsettled += x.amount_msat - return amount_unsettled - def amounts(self): - remote_settled= self.htlcsum(self.gen_htlc_indices(REMOTE, False)) - local_settled= self.htlcsum(self.gen_htlc_indices(LOCAL, False)) - unsettled_local = self.htlcsum(self.gen_htlc_indices(LOCAL, True)) - unsettled_remote = self.htlcsum(self.gen_htlc_indices(REMOTE, True)) - remote_msat = self.remote_state.amount_msat -\ + remote_settled= htlcsum(self.htlcs(REMOTE, False)) + local_settled= htlcsum(self.htlcs(LOCAL, False)) + unsettled_local = htlcsum(self.htlcs(LOCAL, True)) + unsettled_remote = htlcsum(self.htlcs(REMOTE, True)) + remote_msat = self.config[REMOTE].amount_msat -\ unsettled_remote + local_settled - remote_settled - local_msat = self.local_state.amount_msat -\ + local_msat = self.config[LOCAL].amount_msat -\ unsettled_local + remote_settled - local_settled return remote_msat, local_msat def included_htlcs(self, subject, htlc_initiator): + """ + return filter of non-dust htlcs for subjects commitment transaction, initiated by given party + """ feerate = self.pending_feerate(subject) - conf = self.remote_config if subject == REMOTE else self.local_config + conf = self.config[subject] weight = HTLC_SUCCESS_WEIGHT if subject != htlc_initiator else HTLC_TIMEOUT_WEIGHT - htlcs = self.htlcs_in_local if htlc_initiator == LOCAL else self.htlcs_in_remote + htlcs = self.htlcs(htlc_initiator, only_pending=True) fee_for_htlc = lambda htlc: htlc.amount_msat // 1000 - (weight * feerate // 1000) return filter(lambda htlc: fee_for_htlc(htlc) >= conf.dust_limit_sat, htlcs) @property def pending_remote_commitment(self): - this_point = self.remote_state.next_per_commitment_point + this_point = self.config[REMOTE].next_per_commitment_point return self.make_commitment(REMOTE, this_point) def pending_feerate(self, subject): @@ -495,7 +482,7 @@ class HTLCStateMachine(PrintError): @property def _committed_feerate(self): - return {LOCAL: self.local_state.feerate, REMOTE: self.remote_state.feerate} + return {LOCAL: self.config[LOCAL].feerate, REMOTE: self.config[REMOTE].feerate} @property def pending_local_commitment(self): @@ -505,10 +492,9 @@ class HTLCStateMachine(PrintError): def total_msat(self, sub): return sum(self.settled[sub]) - def gen_htlc_indices(self, subject, only_pending): + def htlcs(self, subject, only_pending): """ only_pending: require the htlc's settlement to be pending (needs additional signatures/acks) - include_settled: include settled (totally done with) htlcs """ update_log = self.log[subject] other_log = self.log[-subject] @@ -521,16 +507,6 @@ class HTLCStateMachine(PrintError): res.append(htlc) return res - @property - def htlcs_in_local(self): - """in the local log. 'offered by us'""" - return self.gen_htlc_indices(LOCAL, True) - - @property - def htlcs_in_remote(self): - """in the remote log. 'offered by them'""" - return self.gen_htlc_indices(REMOTE, True) - def settle_htlc(self, preimage, htlc_id): """ SettleHTLC attempts to settle an existing outstanding received HTLC. @@ -552,7 +528,7 @@ class HTLCStateMachine(PrintError): @property def current_height(self): - return {LOCAL: self.local_state.ctn, REMOTE: self.remote_state.ctn} + return {LOCAL: self.config[LOCAL].ctn, REMOTE: self.config[REMOTE].ctn} @property def pending_local_fee(self): @@ -581,7 +557,7 @@ class HTLCStateMachine(PrintError): for i in self.log[subject]['adds'].values(): locked_in = i.locked_in[LOCAL] is not None or i.locked_in[REMOTE] is not None if locked_in: - htlcs.append(i) + htlcs.append(i._asdict()) else: removed.append(i.htlc_id) return htlcs, removed @@ -593,10 +569,8 @@ class HTLCStateMachine(PrintError): remote_filtered, remote_removed = self.remove_uncommitted_htlcs_from_log(REMOTE) local_filtered, local_removed = self.remove_uncommitted_htlcs_from_log(LOCAL) to_save = { - "local_config": self.local_config, - "remote_config": self.remote_config, - "local_state": self.local_state, - "remote_state": self.remote_state, + "local_config": self.config[LOCAL], + "remote_config": self.config[REMOTE], "channel_id": self.channel_id, "short_channel_id": self.short_channel_id, "constraints": self.constraints, @@ -613,12 +587,12 @@ class HTLCStateMachine(PrintError): # htlcs number must be monotonically increasing, # so we have to decrease the counter if len(remote_removed) != 0: - assert min(remote_removed) < to_save['remote_state'].next_htlc_id - to_save['remote_state'] = to_save['remote_state']._replace(next_htlc_id = min(remote_removed)) + assert min(remote_removed) < to_save['remote_config'].next_htlc_id + to_save['remote_config'] = to_save['remote_config']._replace(next_htlc_id = min(remote_removed)) if len(local_removed) != 0: - assert min(local_removed) < to_save['local_state'].next_htlc_id - to_save['local_state'] = to_save['local_state']._replace(next_htlc_id = min(local_removed)) + assert min(local_removed) < to_save['local_config'].next_htlc_id + to_save['local_config'] = to_save['local_config']._replace(next_htlc_id = min(local_removed)) return to_save @@ -652,8 +626,8 @@ class HTLCStateMachine(PrintError): remote_msat, local_msat = self.amounts() assert local_msat >= 0 assert remote_msat >= 0 - this_config = self.remote_config if subject != LOCAL else self.local_config - other_config = self.remote_config if subject == LOCAL else self.local_config + this_config = self.config[subject] + other_config = self.config[-subject] other_htlc_pubkey = derive_pubkey(other_config.htlc_basepoint.pubkey, this_point) this_htlc_pubkey = derive_pubkey(this_config.htlc_basepoint.pubkey, this_point) other_revocation_pubkey = derive_blinded_pubkey(other_config.revocation_basepoint.pubkey, this_point) @@ -676,12 +650,12 @@ class HTLCStateMachine(PrintError): remote_msat, local_msat = local_msat, remote_msat payment_pubkey = derive_pubkey(other_config.payment_basepoint.pubkey, this_point) return make_commitment( - (self.local_state.ctn if subject == LOCAL else self.remote_state.ctn) + 1, + self.config[subject].ctn + 1, this_config.multisig_key.pubkey, other_config.multisig_key.pubkey, payment_pubkey, - self.local_config.payment_basepoint.pubkey, - self.remote_config.payment_basepoint.pubkey, + self.config[LOCAL].payment_basepoint.pubkey, + self.config[REMOTE].payment_basepoint.pubkey, other_revocation_pubkey, derive_pubkey(this_config.delayed_basepoint.pubkey, this_point), other_config.to_self_delay, @@ -700,28 +674,28 @@ class HTLCStateMachine(PrintError): fee_sat = self.pending_local_fee _, outputs = make_outputs(fee_sat * 1000, True, - self.local_state.amount_msat, - self.remote_state.amount_msat, + self.config[LOCAL].amount_msat, + self.config[REMOTE].amount_msat, (TYPE_SCRIPT, bh2u(local_script)), (TYPE_SCRIPT, bh2u(remote_script)), - [], self.local_config.dust_limit_sat) + [], self.config[LOCAL].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, + closing_tx = make_closing_tx(self.config[LOCAL].multisig_key.pubkey, + self.config[REMOTE].multisig_key.pubkey, + self.config[LOCAL].payment_basepoint.pubkey, + self.config[REMOTE].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)) + der_sig = bfh(closing_tx.sign_txin(0, self.config[LOCAL].multisig_key.privkey)) sig = ecc.sig_string_from_der_sig(der_sig[:-1]) return sig, fee_sat def maybe_create_sweeptx_for_their_ctx_to_remote(chan, ctx, their_pcp: bytes, sweep_address) -> Optional[EncumberedTransaction]: assert isinstance(their_pcp, bytes) - payment_bp_privkey = ecc.ECPrivkey(chan.local_config.payment_basepoint.privkey) + payment_bp_privkey = ecc.ECPrivkey(chan.config[LOCAL].payment_basepoint.privkey) our_payment_privkey = derive_privkey(payment_bp_privkey.secret_scalar, their_pcp) our_payment_privkey = ecc.ECPrivkey.from_secret_scalar(our_payment_privkey) our_payment_pubkey = our_payment_privkey.get_public_key_bytes(compressed=True) @@ -742,11 +716,11 @@ def maybe_create_sweeptx_for_their_ctx_to_local(chan, ctx, per_commitment_secret sweep_address) -> Optional[EncumberedTransaction]: assert isinstance(per_commitment_secret, bytes) per_commitment_point = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True) - revocation_privkey = derive_blinded_privkey(chan.local_config.revocation_basepoint.privkey, + revocation_privkey = derive_blinded_privkey(chan.config[LOCAL].revocation_basepoint.privkey, per_commitment_secret) revocation_pubkey = ecc.ECPrivkey(revocation_privkey).get_public_key_bytes(compressed=True) - to_self_delay = chan.local_config.to_self_delay - delayed_pubkey = derive_pubkey(chan.remote_config.delayed_basepoint.pubkey, + to_self_delay = chan.config[LOCAL].to_self_delay + delayed_pubkey = derive_pubkey(chan.config[REMOTE].delayed_basepoint.pubkey, per_commitment_point) witness_script = bh2u(make_commitment_output_to_local_witness_script( revocation_pubkey, to_self_delay, delayed_pubkey)) @@ -768,13 +742,13 @@ def maybe_create_sweeptx_for_their_ctx_to_local(chan, ctx, per_commitment_secret def maybe_create_sweeptx_for_our_ctx_to_local(chan, ctx, our_pcp: bytes, sweep_address) -> Optional[EncumberedTransaction]: assert isinstance(our_pcp, bytes) - delayed_bp_privkey = ecc.ECPrivkey(chan.local_config.delayed_basepoint.privkey) + delayed_bp_privkey = ecc.ECPrivkey(chan.config[LOCAL].delayed_basepoint.privkey) our_localdelayed_privkey = derive_privkey(delayed_bp_privkey.secret_scalar, our_pcp) our_localdelayed_privkey = ecc.ECPrivkey.from_secret_scalar(our_localdelayed_privkey) our_localdelayed_pubkey = our_localdelayed_privkey.get_public_key_bytes(compressed=True) - revocation_pubkey = derive_blinded_pubkey(chan.remote_config.revocation_basepoint.pubkey, + revocation_pubkey = derive_blinded_pubkey(chan.config[REMOTE].revocation_basepoint.pubkey, our_pcp) - to_self_delay = chan.remote_config.to_self_delay + to_self_delay = chan.config[REMOTE].to_self_delay witness_script = bh2u(make_commitment_output_to_local_witness_script( revocation_pubkey, to_self_delay, our_localdelayed_pubkey)) to_local_address = redeem_script_to_address('p2wsh', witness_script) diff --git a/electrum/lnutil.py b/electrum/lnutil.py @@ -20,14 +20,36 @@ HTLC_TIMEOUT_WEIGHT = 663 HTLC_SUCCESS_WEIGHT = 703 Keypair = namedtuple("Keypair", ["pubkey", "privkey"]) -ChannelConfig = namedtuple("ChannelConfig", [ - "payment_basepoint", "multisig_key", "htlc_basepoint", "delayed_basepoint", "revocation_basepoint", - "to_self_delay", "dust_limit_sat", "max_htlc_value_in_flight_msat", "max_accepted_htlcs", "initial_msat"]) OnlyPubkeyKeypair = namedtuple("OnlyPubkeyKeypair", ["pubkey"]) -RemoteState = namedtuple("RemoteState", ["ctn", "next_per_commitment_point", "amount_msat", "revocation_store", "current_per_commitment_point", "next_htlc_id", "feerate"]) -LocalState = namedtuple("LocalState", ["ctn", "per_commitment_secret_seed", "amount_msat", "next_htlc_id", "funding_locked_received", "was_announced", "current_commitment_signature", "current_htlc_signatures", "feerate"]) + +common = [ + ('ctn' , int), + ('amount_msat' , int), + ('next_htlc_id' , int), + ('feerate' , int), + ('payment_basepoint' , Keypair), + ('multisig_key' , Keypair), + ('htlc_basepoint' , Keypair), + ('delayed_basepoint' , Keypair), + ('revocation_basepoint' , Keypair), + ('to_self_delay' , int), + ('dust_limit_sat' , int), + ('max_htlc_value_in_flight_msat' , int), + ('max_accepted_htlcs' , int), + ('initial_msat' , int), +] + +ChannelConfig = NamedTuple('ChannelConfig', common) + +LocalConfig = NamedTuple('LocalConfig', common + [ + ('per_commitment_secret_seed', bytes), + ('funding_locked_received', bool), + ('was_announced', bool), + ('current_commitment_signature', bytes), + ('current_htlc_signatures', List[bytes]), +]) + ChannelConstraints = namedtuple("ChannelConstraints", ["capacity", "is_initiator", "funding_txn_minimum_depth"]) -#OpenChannel = namedtuple("OpenChannel", ["channel_id", "short_channel_id", "funding_outpoint", "local_config", "remote_config", "remote_state", "local_state", "constraints", "node_id"]) ScriptHtlc = namedtuple('ScriptHtlc', ['redeem_script', 'htlc']) @@ -88,6 +110,12 @@ class RevocationStore: def __hash__(self): return hash(json.dumps(self.serialize(), sort_keys=True)) +RemoteConfig = NamedTuple('RemoteConfig', common + [ + ('next_per_commitment_point' , bytes), + ('revocation_store' , RevocationStore), + ('current_per_commitment_point' , bytes), +]) + def count_trailing_zeros(index): """ BOLT-03 (where_to_put_secret) """ try: @@ -243,8 +271,8 @@ def make_received_htlc(revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, p def make_htlc_tx_with_open_channel(chan, pcp, for_us, we_receive, commit, htlc): amount_msat, cltv_expiry, payment_hash = htlc.amount_msat, htlc.cltv_expiry, htlc.payment_hash - conf = chan.local_config if for_us else chan.remote_config - other_conf = chan.local_config if not for_us else chan.remote_config + conf = chan.config[LOCAL] if for_us else chan.config[REMOTE] + other_conf = chan.config[LOCAL] if not for_us else chan.config[REMOTE] revocation_pubkey = derive_blinded_pubkey(other_conf.revocation_basepoint.pubkey, pcp) delayedpubkey = derive_pubkey(conf.delayed_basepoint.pubkey, pcp) @@ -412,8 +440,8 @@ def extract_ctn_from_tx(tx, txin_index: int, funder_payment_basepoint: bytes, return get_obscured_ctn(obs, funder_payment_basepoint, fundee_payment_basepoint) def extract_ctn_from_tx_and_chan(tx, chan) -> int: - funder_conf = chan.local_config if chan.constraints.is_initiator else chan.remote_config - fundee_conf = chan.local_config if not chan.constraints.is_initiator else chan.remote_config + funder_conf = chan.config[LOCAL] if chan.constraints.is_initiator else chan.config[REMOTE] + fundee_conf = chan.config[LOCAL] if not chan.constraints.is_initiator else chan.config[REMOTE] return extract_ctn_from_tx(tx, txin_index=0, funder_payment_basepoint=funder_conf.payment_basepoint.pubkey, fundee_payment_basepoint=fundee_conf.payment_basepoint.pubkey) diff --git a/electrum/lnworker.py b/electrum/lnworker.py @@ -116,7 +116,7 @@ class LNWorker(PrintError): def save_channel(self, openchannel): assert type(openchannel) is HTLCStateMachine - if openchannel.remote_state.next_per_commitment_point == openchannel.remote_state.current_per_commitment_point: + if openchannel.config[REMOTE].next_per_commitment_point == openchannel.config[REMOTE].current_per_commitment_point: raise Exception("Tried to save channel with next_point == current_point, this should not happen") with self.lock: self.channels[openchannel.channel_id] = openchannel @@ -350,12 +350,12 @@ class LNWorker(PrintError): chan = self.channels[chan_id] # local_commitment always gives back the next expected local_commitment, # but in this case, we want the current one. So substract one ctn number - old_local_state = chan.local_state - chan.local_state=chan.local_state._replace(ctn=chan.local_state.ctn - 1) + old_local_state = chan.config[LOCAL] + chan.config[LOCAL]=chan.config[LOCAL]._replace(ctn=chan.config[LOCAL].ctn - 1) tx = chan.pending_local_commitment - chan.local_state = old_local_state - tx.sign({bh2u(chan.local_config.multisig_key.pubkey): (chan.local_config.multisig_key.privkey, True)}) - remote_sig = chan.local_state.current_commitment_signature + chan.config[LOCAL] = old_local_state + tx.sign({bh2u(chan.config[LOCAL].multisig_key.pubkey): (chan.config[LOCAL].multisig_key.privkey, True)}) + remote_sig = chan.config[LOCAL].current_commitment_signature remote_sig = der_sig_from_sig_string(remote_sig) + b"\x01" none_idx = tx._inputs[0]["signatures"].index(None) tx.add_signature_to_txin(0, none_idx, bh2u(remote_sig)) diff --git a/electrum/tests/test_lnhtlc.py b/electrum/tests/test_lnhtlc.py @@ -16,56 +16,52 @@ def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate assert remote_amount > 0 channel_id, _ = lnbase.channel_id_from_funding_tx(funding_txid, funding_index) their_revocation_store = lnbase.RevocationStore() - local_config=lnbase.ChannelConfig( - payment_basepoint=privkeys[0], - multisig_key=privkeys[1], - htlc_basepoint=privkeys[2], - delayed_basepoint=privkeys[3], - revocation_basepoint=privkeys[4], - to_self_delay=l_csv, - dust_limit_sat=l_dust, - max_htlc_value_in_flight_msat=500000 * 1000, - max_accepted_htlcs=5, - initial_msat=local_amount, - ) - remote_config=lnbase.ChannelConfig( - payment_basepoint=other_pubkeys[0], - multisig_key=other_pubkeys[1], - htlc_basepoint=other_pubkeys[2], - delayed_basepoint=other_pubkeys[3], - revocation_basepoint=other_pubkeys[4], - to_self_delay=r_csv, - dust_limit_sat=r_dust, - max_htlc_value_in_flight_msat=500000 * 1000, - max_accepted_htlcs=5, - initial_msat=remote_amount, - ) return { "channel_id":channel_id, "short_channel_id":channel_id[:8], "funding_outpoint":lnbase.Outpoint(funding_txid, funding_index), - "local_config":local_config, - "remote_config":remote_config, - "remote_state":lnbase.RemoteState( + "remote_config":lnbase.RemoteConfig( + payment_basepoint=other_pubkeys[0], + multisig_key=other_pubkeys[1], + htlc_basepoint=other_pubkeys[2], + delayed_basepoint=other_pubkeys[3], + revocation_basepoint=other_pubkeys[4], + to_self_delay=r_csv, + dust_limit_sat=r_dust, + max_htlc_value_in_flight_msat=500000 * 1000, + max_accepted_htlcs=5, + initial_msat=remote_amount, ctn = 0, + next_htlc_id = 0, + feerate=local_feerate, + amount_msat=remote_amount, + next_per_commitment_point=nex, current_per_commitment_point=cur, - amount_msat=remote_amount, revocation_store=their_revocation_store, - next_htlc_id = 0, - feerate=local_feerate ), - "local_state":lnbase.LocalState( + "local_config":lnbase.LocalConfig( + payment_basepoint=privkeys[0], + multisig_key=privkeys[1], + htlc_basepoint=privkeys[2], + delayed_basepoint=privkeys[3], + revocation_basepoint=privkeys[4], + to_self_delay=l_csv, + dust_limit_sat=l_dust, + max_htlc_value_in_flight_msat=500000 * 1000, + max_accepted_htlcs=5, + initial_msat=local_amount, ctn = 0, - per_commitment_secret_seed=seed, - amount_msat=local_amount, next_htlc_id = 0, + feerate=local_feerate, + amount_msat=local_amount, + + per_commitment_secret_seed=seed, funding_locked_received=True, was_announced=False, current_commitment_signature=None, current_htlc_signatures=None, - feerate=local_feerate ), "constraints":lnbase.ChannelConstraints(capacity=funding_sat, is_initiator=is_initiator, funding_txn_minimum_depth=3), "node_id":other_node_id, @@ -205,8 +201,8 @@ class TestLNBaseHTLCStateMachine(unittest.TestCase): self.assertEqual(alice_channel.total_msat(RECEIVED), bobSent, "alice has incorrect milli-satoshis received") self.assertEqual(bob_channel.total_msat(SENT), bobSent, "bob has incorrect milli-satoshis sent") self.assertEqual(bob_channel.total_msat(RECEIVED), aliceSent, "bob has incorrect milli-satoshis received") - self.assertEqual(bob_channel.local_state.ctn, 1, "bob has incorrect commitment height") - self.assertEqual(alice_channel.local_state.ctn, 1, "alice has incorrect commitment height") + self.assertEqual(bob_channel.config[LOCAL].ctn, 1, "bob has incorrect commitment height") + self.assertEqual(alice_channel.config[LOCAL].ctn, 1, "alice has incorrect commitment height") # Both commitment transactions should have three outputs, and one of # them should be exactly the amount of the HTLC. @@ -275,20 +271,20 @@ class TestLNBaseHTLCStateMachine(unittest.TestCase): bob_channel.receive_new_commitment(alice_sig, alice_htlc_sigs) - self.assertNotEqual(fee, bob_channel.local_state.feerate) + self.assertNotEqual(fee, bob_channel.config[LOCAL].feerate) rev, _ = bob_channel.revoke_current_commitment() - self.assertEqual(fee, bob_channel.local_state.feerate) + self.assertEqual(fee, bob_channel.config[LOCAL].feerate) bob_sig, bob_htlc_sigs = bob_channel.sign_next_commitment() alice_channel.receive_revocation(rev) alice_channel.receive_new_commitment(bob_sig, bob_htlc_sigs) - self.assertNotEqual(fee, alice_channel.local_state.feerate) + self.assertNotEqual(fee, alice_channel.config[LOCAL].feerate) rev, _ = alice_channel.revoke_current_commitment() - self.assertEqual(fee, alice_channel.local_state.feerate) + self.assertEqual(fee, alice_channel.config[LOCAL].feerate) bob_channel.receive_revocation(rev) - self.assertEqual(fee, bob_channel.remote_state.feerate) + self.assertEqual(fee, bob_channel.config[REMOTE].feerate) def test_UpdateFeeReceiverCommits(self): @@ -304,20 +300,20 @@ class TestLNBaseHTLCStateMachine(unittest.TestCase): alice_sig, alice_htlc_sigs = alice_channel.sign_next_commitment() bob_channel.receive_new_commitment(alice_sig, alice_htlc_sigs) - self.assertNotEqual(fee, bob_channel.local_state.feerate) + self.assertNotEqual(fee, bob_channel.config[LOCAL].feerate) bob_revocation, _ = bob_channel.revoke_current_commitment() - self.assertEqual(fee, bob_channel.local_state.feerate) + self.assertEqual(fee, bob_channel.config[LOCAL].feerate) bob_sig, bob_htlc_sigs = bob_channel.sign_next_commitment() alice_channel.receive_revocation(bob_revocation) alice_channel.receive_new_commitment(bob_sig, bob_htlc_sigs) - self.assertNotEqual(fee, alice_channel.local_state.feerate) + self.assertNotEqual(fee, alice_channel.config[LOCAL].feerate) alice_revocation, _ = alice_channel.revoke_current_commitment() - self.assertEqual(fee, alice_channel.local_state.feerate) + self.assertEqual(fee, alice_channel.config[LOCAL].feerate) bob_channel.receive_revocation(alice_revocation) - self.assertEqual(fee, bob_channel.remote_state.feerate) + self.assertEqual(fee, bob_channel.config[REMOTE].feerate) @@ -327,7 +323,7 @@ class TestLNHTLCDust(unittest.TestCase): paymentPreimage = b"\x01" * 32 paymentHash = bitcoin.sha256(paymentPreimage) - fee_per_kw = alice_channel.local_state.feerate + fee_per_kw = alice_channel.config[LOCAL].feerate self.assertEqual(fee_per_kw, 6000) htlcAmt = 500 + lnutil.HTLC_TIMEOUT_WEIGHT * (fee_per_kw // 1000) self.assertEqual(htlcAmt, 4478)