electrum

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

commit d5d9270d0c3d09972ec94fa90c016449a93077ea
parent eca5545004bbe07ba7ead674bedff290b4664f33
Author: Janus <ysangkok@gmail.com>
Date:   Tue, 18 Sep 2018 18:38:57 +0200

lnhtlc: save logs and feeupdates

Diffstat:
Melectrum/gui/qt/channels_list.py | 5+++--
Melectrum/lnbase.py | 25++++++++++++++-----------
Melectrum/lnhtlc.py | 138++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Melectrum/lnutil.py | 2+-
Melectrum/tests/test_lnhtlc.py | 31++++++++++++++++++-------------
5 files changed, 131 insertions(+), 70 deletions(-)

diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py @@ -6,6 +6,7 @@ from electrum.util import inv_dict, bh2u, bfh from electrum.i18n import _ from electrum.lnhtlc import HTLCStateMachine from electrum.lnaddr import lndecode +from electrum.lnutil import LOCAL, REMOTE from .util import MyTreeWidget, SortableTreeWidgetItem, WindowModalDialog, Buttons, OkButton, CancelButton from .amountedit import BTCAmountEdit @@ -24,8 +25,8 @@ class ChannelsList(MyTreeWidget): def format_fields(self, chan): return [ bh2u(chan.node_id), - self.parent.format_amount(chan.local_state.amount_msat//1000), - self.parent.format_amount(chan.remote_state.amount_msat//1000), + self.parent.format_amount(chan.balance(LOCAL)//1000), + self.parent.format_amount(chan.balance(REMOTE)//1000), chan.get_state() ] diff --git a/electrum/lnbase.py b/electrum/lnbase.py @@ -8,6 +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 .bitcoin import COIN from ecdsa.util import sigdecode_der, sigencode_string_canonize, sigdecode_string @@ -33,7 +34,7 @@ from .util import PrintError, bh2u, print_error, bfh, aiosafe from .transaction import opcodes, Transaction, TxOutput from .lnonion import new_onion_packet, OnionHopsDataSingle, OnionPerHop, decode_onion_error, ONION_FAILURE_CODE_MAP from .lnaddr import lndecode -from .lnhtlc import UpdateAddHtlc, HTLCStateMachine, RevokeAndAck, SettleHtlc +from .lnhtlc import HTLCStateMachine, RevokeAndAck def channel_id_from_funding_tx(funding_txid, funding_index): funding_txid_bytes = bytes.fromhex(funding_txid)[::-1] @@ -496,7 +497,8 @@ class Peer(PrintError): to_self_delay=143, dust_limit_sat=546, max_htlc_value_in_flight_msat=0xffffffffffffffff, - max_accepted_htlcs=5 + max_accepted_htlcs=5, + initial_msat=funding_sat * 1000 - push_msat, ) # TODO derive this? per_commitment_secret_seed = 0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100.to_bytes(32, 'big') @@ -536,7 +538,8 @@ class Peer(PrintError): 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'), - max_accepted_htlcs=int.from_bytes(payload["max_accepted_htlcs"], 'big') + max_accepted_htlcs=int.from_bytes(payload["max_accepted_htlcs"], 'big'), + initial_msat=push_msat ) funding_txn_minimum_depth = int.from_bytes(payload['minimum_depth'], 'big') assert remote_config.dust_limit_sat < 600 @@ -844,9 +847,9 @@ class Peer(PrintError): onion = new_onion_packet([x.node_id for x in route], self.secret_key, hops_data, associated_data) amount_msat += total_fee # FIXME this below will probably break with multiple HTLCs - msat_local = chan.local_state.amount_msat - amount_msat - msat_remote = chan.remote_state.amount_msat + amount_msat - htlc = UpdateAddHtlc(amount_msat, payment_hash, final_cltv_expiry_with_deltas) + msat_local = chan.balance(LOCAL) - amount_msat + msat_remote = chan.balance(REMOTE) + amount_msat + htlc = {'amount_msat':amount_msat, 'payment_hash':payment_hash, 'cltv_expiry':final_cltv_expiry_with_deltas} # FIXME if we raise here, this channel will not get blacklisted, and the payment can never succeed, # as we will just keep retrying this same path. using the current blacklisting is not a solution as @@ -861,10 +864,10 @@ class Peer(PrintError): # FIXME what about channel_reserve_satoshis? will the remote fail the channel if we go below? test. raise PaymentFailure('not enough local balance') - self.send_message(gen_msg("update_add_htlc", channel_id=chan.channel_id, id=chan.local_state.next_htlc_id, cltv_expiry=final_cltv_expiry_with_deltas, amount_msat=amount_msat, payment_hash=payment_hash, onion_routing_packet=onion.to_bytes())) + htlc_id = chan.add_htlc(htlc) + self.send_message(gen_msg("update_add_htlc", channel_id=chan.channel_id, id=htlc_id, cltv_expiry=final_cltv_expiry_with_deltas, amount_msat=amount_msat, payment_hash=payment_hash, onion_routing_packet=onion.to_bytes())) - chan.add_htlc(htlc) - self.attempted_route[(chan.channel_id, htlc.htlc_id)] = route + self.attempted_route[(chan.channel_id, htlc_id)] = route sig_64, htlc_sigs = chan.sign_next_commitment() self.send_message(gen_msg("commitment_signed", channel_id=chan.channel_id, signature=sig_64, num_htlcs=len(htlc_sigs), htlc_signature=b"".join(htlc_sigs))) @@ -947,7 +950,7 @@ class Peer(PrintError): assert amount_msat == expected_received_msat payment_hash = htlc["payment_hash"] - htlc = UpdateAddHtlc(amount_msat, payment_hash, cltv_expiry) + htlc = {'amount_msat': amount_msat, 'payment_hash':payment_hash, 'cltv_expiry':cltv_expiry} chan.receive_htlc(htlc) @@ -967,7 +970,7 @@ class Peer(PrintError): # remote commitment transaction without htlcs # FIXME why is this not using the HTLC state machine? bare_ctx = chan.make_commitment(chan.remote_state.ctn + 1, False, chan.remote_state.next_per_commitment_point, - chan.remote_state.amount_msat - expected_received_msat, chan.local_state.amount_msat + expected_received_msat) + chan.balance(REMOTE) - expected_received_msat, chan.balance(LOCAL) + expected_received_msat) self.lnwatcher.process_new_offchain_ctx(chan, bare_ctx, ours=False) sig_64 = sign_and_get_sig_string(bare_ctx, chan.local_config, chan.remote_config) self.send_message(gen_msg("commitment_signed", channel_id=channel_id, signature=sig_64, num_htlcs=0)) diff --git a/electrum/lnhtlc.py b/electrum/lnhtlc.py @@ -16,7 +16,7 @@ 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 +from .lnutil import LOCAL, REMOTE, SENT, RECEIVED, HTLCOwner from .transaction import Transaction @@ -34,12 +34,22 @@ FUNDEE_ACKED = FeeUpdateProgress.FUNDEE_ACKED FUNDER_SIGNED = FeeUpdateProgress.FUNDER_SIGNED COMMITTED = FeeUpdateProgress.COMMITTED -class FeeUpdate: +from collections import namedtuple - def __init__(self, chan, feerate): - self.rate = feerate - self.proposed = chan.remote_state.ctn if not chan.constraints.is_initiator else chan.local_state.ctn - self.progress = {FUNDEE_SIGNED: None, FUNDEE_ACKED: None, FUNDER_SIGNED: None, COMMITTED: None} +class FeeUpdate: + def __init__(self, chan, **kwargs): + if 'rate' in kwargs: + self.rate = kwargs['rate'] + else: + assert False + if 'proposed' not in kwargs: + self.proposed = chan.remote_state.ctn if not chan.constraints.is_initiator else chan.local_state.ctn + else: + self.proposed = kwargs['proposed'] + if 'progress' not in kwargs: + self.progress = {FUNDEE_SIGNED: None, FUNDEE_ACKED: None, FUNDER_SIGNED: None, COMMITTED: None} + else: + self.progress = {FeeUpdateProgress[x.partition('.')[2]]: y for x,y in kwargs['progress'].items()} self.chan = chan @property @@ -65,30 +75,30 @@ class FeeUpdate: if subject == LOCAL and not self.chan.constraints.is_initiator: return self.rate -class UpdateAddHtlc: - def __init__(self, amount_msat, payment_hash, cltv_expiry): - self.amount_msat = amount_msat - self.payment_hash = payment_hash - self.cltv_expiry = cltv_expiry - - # the height the htlc was locked in at, or None - self.locked_in = {LOCAL: None, REMOTE: None} - - self.settled = {LOCAL: None, REMOTE: None} - - self.htlc_id = None - - def as_tuple(self): - return (self.htlc_id, self.amount_msat, self.payment_hash, self.cltv_expiry, self.locked_in[REMOTE], self.locked_in[LOCAL], self.settled) - - def __hash__(self): - return hash(self.as_tuple()) - - def __eq__(self, o): - return type(o) is UpdateAddHtlc and self.as_tuple() == o.as_tuple() - - def __repr__(self): - return "UpdateAddHtlc" + str(self.as_tuple()) + def to_save(self): + return {'rate': self.rate, 'proposed': self.proposed, 'progress': self.progress} + +class UpdateAddHtlc(namedtuple('UpdateAddHtlc', ['amount_msat', 'payment_hash', 'cltv_expiry', 'settled', 'locked_in', 'htlc_id'])): + __slots__ = () + def __new__(cls, *args, **kwargs): + if len(args) > 0: + args = list(args) + if type(args[1]) is str: + args[1] = bfh(args[1]) + args[3] = {HTLCOwner(int(x)): y for x,y in args[3].items()} + args[4] = {HTLCOwner(int(x)): y for x,y in args[4].items()} + return super().__new__(cls, *args) + if type(kwargs['payment_hash']) is str: + kwargs['payment_hash'] = bfh(kwargs['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']} + if 'settled' not in kwargs: + kwargs['settled'] = {LOCAL: None, REMOTE: None} + else: + kwargs['settled'] = {HTLCOwner(int(x)): y for x,y in kwargs['settled']} + return super().__new__(cls, **kwargs) is_key = lambda k: k.endswith("_basepoint") or k.endswith("_key") @@ -155,10 +165,22 @@ class HTLCStateMachine(PrintError): self.remote_commitment_to_be_revoked = Transaction(state["remote_commitment_to_be_revoked"]) self.log = {LOCAL: [], REMOTE: []} + for strname, subject in [('remote_log', REMOTE), ('local_log', LOCAL)]: + if strname not in state: continue + for typ,y in state[strname]: + if typ == "UpdateAddHtlc": + self.log[subject].append(UpdateAddHtlc(*decodeAll(y))) + elif typ == "SettleHtlc": + self.log[subject].append(SettleHtlc(*decodeAll(y))) + else: + assert False self.name = name self.fee_mgr = [] + if 'fee_updates' in state: + for y in state['fee_updates']: + self.fee_mgr.append(FeeUpdate(self, **y)) self.local_commitment = self.pending_local_commitment self.remote_commitment = self.pending_remote_commitment @@ -190,13 +212,12 @@ class HTLCStateMachine(PrintError): AddHTLC adds an HTLC to the state machine's local update log. This method should be called when preparing to send an outgoing HTLC. """ - assert type(htlc) is UpdateAddHtlc + assert type(htlc) is dict + htlc = UpdateAddHtlc(**htlc, htlc_id=self.local_state.next_htlc_id) self.log[LOCAL].append(htlc) self.print_error("add_htlc") - htlc_id = self.local_state.next_htlc_id - self.local_state=self.local_state._replace(next_htlc_id=htlc_id + 1) - htlc.htlc_id = htlc_id - return htlc_id + self.local_state=self.local_state._replace(next_htlc_id=htlc.htlc_id + 1) + return htlc.htlc_id def receive_htlc(self, htlc): """ @@ -204,13 +225,12 @@ class HTLCStateMachine(PrintError): method should be called in response to receiving a new HTLC from the remote party. """ - self.print_error("receive_htlc") - assert type(htlc) is UpdateAddHtlc + assert type(htlc) is dict + htlc = UpdateAddHtlc(**htlc, htlc_id = self.remote_state.next_htlc_id) self.log[REMOTE].append(htlc) - htlc_id = self.remote_state.next_htlc_id - self.remote_state=self.remote_state._replace(next_htlc_id=htlc_id + 1) - htlc.htlc_id = htlc_id - return htlc_id + self.print_error("receive_htlc") + self.remote_state=self.remote_state._replace(next_htlc_id=htlc.htlc_id + 1) + return htlc.htlc_id def sign_next_commitment(self): """ @@ -431,6 +451,9 @@ class HTLCStateMachine(PrintError): amount_msat = self.local_state.amount_msat + (received_this_batch - sent_this_batch) ) + self.balance(LOCAL) + self.balance(REMOTE) + for pending_fee in self.fee_mgr: if pending_fee.is_proposed(): if self.constraints.is_initiator: @@ -441,6 +464,26 @@ class HTLCStateMachine(PrintError): self.remote_commitment_to_be_revoked = prev_remote_commitment 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 + + for x in self.log[-subject]: + if type(x) is not SettleHtlc: continue + htlc = self.lookup_htlc(self.log[subject], x.htlc_id) + htlc_height = htlc.settled[subject] + if htlc_height is not None and htlc_height <= self.current_height[subject]: + initial -= htlc.amount_msat + + for x in self.log[subject]: + if type(x) is not SettleHtlc: continue + htlc = self.lookup_htlc(self.log[-subject], x.htlc_id) + htlc_height = htlc.settled[-subject] + if htlc_height is not None and htlc_height <= self.current_height[-subject]: + initial += htlc.amount_msat + + assert initial == (self.local_state.amount_msat if subject == LOCAL else self.remote_state.amount_msat) + return initial + @staticmethod def htlcsum(htlcs): amount_unsettled = 0 @@ -611,13 +654,13 @@ class HTLCStateMachine(PrintError): def update_fee(self, feerate): if not self.constraints.is_initiator: raise Exception("only initiator can update_fee, this counterparty is not initiator") - pending_fee = FeeUpdate(self, feerate) + pending_fee = FeeUpdate(self, rate=feerate) self.fee_mgr.append(pending_fee) def receive_update_fee(self, feerate): if self.constraints.is_initiator: raise Exception("only the non-initiator can receive_update_fee, this counterparty is initiator") - pending_fee = FeeUpdate(self, feerate) + pending_fee = FeeUpdate(self, rate=feerate) self.fee_mgr.append(pending_fee) def to_save(self): @@ -632,6 +675,9 @@ class HTLCStateMachine(PrintError): "funding_outpoint": self.funding_outpoint, "node_id": self.node_id, "remote_commitment_to_be_revoked": str(self.remote_commitment_to_be_revoked), + "remote_log": [(type(x).__name__, x) for x in self.log[REMOTE]], + "local_log": [(type(x).__name__, x) for x in self.log[LOCAL]], + "fee_updates": [x.to_save() for x in self.fee_mgr], } def serialize(self): @@ -643,7 +689,13 @@ class HTLCStateMachine(PrintError): return binascii.hexlify(o).decode("ascii") if isinstance(o, RevocationStore): return o.serialize() + if isinstance(o, SettleHtlc): + return json.dumps(('SettleHtlc', namedtuples_to_dict(o))) + if isinstance(o, UpdateAddHtlc): + return json.dumps(('UpdateAddHtlc', namedtuples_to_dict(o))) return super(MyJsonEncoder, self) + for fee_upd in serialized_channel['fee_updates']: + fee_upd['progress'] = {str(k): v for k,v in fee_upd['progress'].items()} dumped = MyJsonEncoder().encode(serialized_channel) roundtripped = json.loads(dumped) reconstructed = HTLCStateMachine(roundtripped) diff --git a/electrum/lnutil.py b/electrum/lnutil.py @@ -18,7 +18,7 @@ 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"]) + "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"]) diff --git a/electrum/tests/test_lnhtlc.py b/electrum/tests/test_lnhtlc.py @@ -25,7 +25,8 @@ def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate to_self_delay=l_csv, dust_limit_sat=l_dust, max_htlc_value_in_flight_msat=500000 * 1000, - max_accepted_htlcs=5 + max_accepted_htlcs=5, + initial_msat=local_amount, ) remote_config=lnbase.ChannelConfig( payment_basepoint=other_pubkeys[0], @@ -36,7 +37,8 @@ def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate to_self_delay=r_csv, dust_limit_sat=r_dust, max_htlc_value_in_flight_msat=500000 * 1000, - max_accepted_htlcs=5 + max_accepted_htlcs=5, + initial_msat=remote_amount, ) return { @@ -132,11 +134,11 @@ class TestLNBaseHTLCStateMachine(unittest.TestCase): self.paymentPreimage = b"\x01" * 32 paymentHash = bitcoin.sha256(self.paymentPreimage) - self.htlc = lnhtlc.UpdateAddHtlc( - payment_hash = paymentHash, - amount_msat = one_bitcoin_in_msat, - cltv_expiry = 5, - ) + self.htlc = { + 'payment_hash' : paymentHash, + 'amount_msat' : one_bitcoin_in_msat, + 'cltv_expiry' : 5, + } # First Alice adds the outgoing HTLC to her local channel's state # update log. Then Alice sends this wire message over to Bob who adds @@ -144,6 +146,7 @@ class TestLNBaseHTLCStateMachine(unittest.TestCase): self.aliceHtlcIndex = self.alice_channel.add_htlc(self.htlc) self.bobHtlcIndex = self.bob_channel.receive_htlc(self.htlc) + self.htlc = self.bob_channel.log[lnutil.REMOTE][0] def test_SimpleAddSettleWorkflow(self): alice_channel, bob_channel = self.alice_channel, self.bob_channel @@ -250,6 +253,8 @@ class TestLNBaseHTLCStateMachine(unittest.TestCase): # revocation. #self.assertEqual(alice_channel.local_update_log, [], "alice's local not updated, should be empty, has %s entries instead"% len(alice_channel.local_update_log)) #self.assertEqual(alice_channel.remote_update_log, [], "alice's remote not updated, should be empty, has %s entries instead"% len(alice_channel.remote_update_log)) + alice_channel.update_fee(100000) + alice_channel.serialize() def alice_to_bob_fee_update(self): fee = 111 @@ -325,11 +330,11 @@ class TestLNHTLCDust(unittest.TestCase): self.assertEqual(fee_per_kw, 6000) htlcAmt = 500 + lnutil.HTLC_TIMEOUT_WEIGHT * (fee_per_kw // 1000) self.assertEqual(htlcAmt, 4478) - htlc = lnhtlc.UpdateAddHtlc( - payment_hash = paymentHash, - amount_msat = 1000 * htlcAmt, - cltv_expiry = 5, # also in create_test_channels - ) + htlc = { + 'payment_hash' : paymentHash, + 'amount_msat' : 1000 * htlcAmt, + 'cltv_expiry' : 5, # also in create_test_channels + } aliceHtlcIndex = alice_channel.add_htlc(htlc) bobHtlcIndex = bob_channel.receive_htlc(htlc) @@ -338,7 +343,7 @@ class TestLNHTLCDust(unittest.TestCase): self.assertEqual(len(bob_channel.local_commitment.outputs()), 2) default_fee = calc_static_fee(0) self.assertEqual(bob_channel.pending_local_fee, default_fee + htlcAmt) - bob_channel.settle_htlc(paymentPreimage, htlc.htlc_id) + bob_channel.settle_htlc(paymentPreimage, bobHtlcIndex) alice_channel.receive_htlc_settle(paymentPreimage, aliceHtlcIndex) force_state_transition(bob_channel, alice_channel) self.assertEqual(len(alice_channel.local_commitment.outputs()), 2)