tlnhtlc: don't throw away fee updates or htlcs - electrum - Electrum Bitcoin wallet (HTM) git clone https://git.parazyd.org/electrum (DIR) Log (DIR) Files (DIR) Refs (DIR) Submodules --- (DIR) commit eca5545004bbe07ba7ead674bedff290b4664f33 (DIR) parent 02eca034866caa85d3a2f9f8d23c5f1044349f79 (HTM) Author: Janus <ysangkok@gmail.com> Date: Thu, 13 Sep 2018 19:59:12 +0200 lnhtlc: don't throw away fee updates or htlcs also add inject_fees debug command Diffstat: M electrum/commands.py | 6 ++++++ M electrum/lnhtlc.py | 152 ++++++++++++++++++------------- M electrum/lnutil.py | 15 ++++++++++++++- M electrum/tests/test_lnhtlc.py | 6 +++--- 4 files changed, 111 insertions(+), 68 deletions(-) --- (DIR) diff --git a/electrum/commands.py b/electrum/commands.py t@@ -787,6 +787,12 @@ class Commands: def listchannels(self): return self.wallet.lnworker.list_channels() + @command('n') + def inject_fees(self, fees): + import ast + self.network.config.fee_estimates = ast.literal_eval(fees) + self.network.notify('fee') + def eval_bool(x: str) -> bool: if x == 'false': return False if x == 'true': return True (DIR) diff --git a/electrum/lnhtlc.py b/electrum/lnhtlc.py t@@ -2,7 +2,7 @@ from collections import namedtuple import binascii import json -from enum import IntFlag +from enum import Enum, auto from .util import bfh, PrintError, bh2u from .bitcoin import Hash t@@ -16,33 +16,54 @@ 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 .transaction import Transaction SettleHtlc = namedtuple("SettleHtlc", ["htlc_id"]) RevokeAndAck = namedtuple("RevokeAndAck", ["per_commitment_secret", "next_per_commitment_point"]) -class FeeUpdateProgress(IntFlag): - FUNDEE_SIGNED = 1 - FUNDEE_ACKED = 2 - FUNDER_SIGNED = 4 +class FeeUpdateProgress(Enum): + FUNDEE_SIGNED = auto() + FUNDEE_ACKED = auto() + FUNDER_SIGNED = auto() + COMMITTED = auto() -class HTLCOwner(IntFlag): - LOCAL = 1 - REMOTE = -LOCAL +FUNDEE_SIGNED = FeeUpdateProgress.FUNDEE_SIGNED +FUNDEE_ACKED = FeeUpdateProgress.FUNDEE_ACKED +FUNDER_SIGNED = FeeUpdateProgress.FUNDER_SIGNED +COMMITTED = FeeUpdateProgress.COMMITTED - SENT = LOCAL - RECEIVED = REMOTE +class FeeUpdate: -SENT = HTLCOwner.SENT -RECEIVED = HTLCOwner.RECEIVED -LOCAL = HTLCOwner.LOCAL -REMOTE = HTLCOwner.REMOTE + 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} + self.chan = chan -class FeeUpdate: - def __init__(self, rate): - self.rate = rate - self.progress = 0 + @property + def height(self): + return self.chan.current_height[LOCAL if self.chan.constraints.is_initiator else REMOTE] + + def set(self, field): + self.progress[field] = self.height + + def is_proposed(self): + return self.progress[COMMITTED] is None and self.proposed is not None and self.proposed <= self.height + + def had(self, field): + return self.progress[field] is not None and self.height >= self.progress[field] + + def pending_feerate(self, subject): + if not self.is_proposed(): + return + if self.had(FUNDEE_ACKED): + return self.rate + if subject == REMOTE and self.chan.constraints.is_initiator: + return self.rate + if subject == LOCAL and not self.chan.constraints.is_initiator: + return self.rate class UpdateAddHtlc: def __init__(self, amount_msat, payment_hash, cltv_expiry): t@@ -89,22 +110,6 @@ def typeWrap(k, v, local): return v class HTLCStateMachine(PrintError): - @property - def pending_remote_feerate(self): - if self.pending_fee is not None: - if self.constraints.is_initiator or (self.pending_fee.progress & FeeUpdateProgress.FUNDEE_ACKED): - return self.pending_fee.rate - return self.remote_state.feerate - - @property - def pending_local_feerate(self): - if self.pending_fee is not None: - if not self.constraints.is_initiator: - return self.pending_fee.rate - if self.constraints.is_initiator and (self.pending_fee.progress & FeeUpdateProgress.FUNDEE_ACKED): - return self.pending_fee.rate - return self.local_state.feerate - def lookup_htlc(self, log, htlc_id): assert type(htlc_id) is int for htlc in log: t@@ -153,7 +158,7 @@ class HTLCStateMachine(PrintError): self.name = name - self.pending_fee = None + self.fee_mgr = [] self.local_commitment = self.pending_local_commitment self.remote_commitment = self.pending_remote_commitment t@@ -235,7 +240,7 @@ class HTLCStateMachine(PrintError): for_us = False - feerate = self.pending_remote_feerate + feerate = self.pending_feerate(REMOTE) htlcsigs = [] for we_receive, htlcs in zip([True, False], [self.htlcs_in_remote, self.htlcs_in_local]): t@@ -253,11 +258,12 @@ class HTLCStateMachine(PrintError): htlc_sig = ecc.sig_string_from_der_sig(sig[:-1]) htlcsigs.append(htlc_sig) - if self.pending_fee: - if not self.constraints.is_initiator: - self.pending_fee.progress |= FeeUpdateProgress.FUNDEE_SIGNED - if self.constraints.is_initiator and (self.pending_fee.progress & FeeUpdateProgress.FUNDEE_ACKED): - self.pending_fee.progress |= FeeUpdateProgress.FUNDER_SIGNED + for pending_fee in self.fee_mgr: + if pending_fee.is_proposed(): + if not self.constraints.is_initiator: + pending_fee.set(FUNDEE_SIGNED) + if self.constraints.is_initiator and pending_fee.had(FUNDEE_ACKED): + pending_fee.set(FUNDER_SIGNED) if self.lnwatcher: self.lnwatcher.process_new_offchain_ctx(self, pending_remote_commitment, ours=False) t@@ -304,11 +310,12 @@ class HTLCStateMachine(PrintError): # TODO check htlc in htlcs_in_local - if self.pending_fee: - if not self.constraints.is_initiator: - self.pending_fee.progress |= FeeUpdateProgress.FUNDEE_SIGNED - if self.constraints.is_initiator and (self.pending_fee.progress & FeeUpdateProgress.FUNDEE_ACKED): - self.pending_fee.progress |= FeeUpdateProgress.FUNDER_SIGNED + for pending_fee in self.fee_mgr: + if pending_fee.is_proposed(): + if not self.constraints.is_initiator: + pending_fee.set(FUNDEE_SIGNED) + if self.constraints.is_initiator and pending_fee.had(FUNDEE_ACKED): + pending_fee.set(FUNDER_SIGNED) if self.lnwatcher: self.lnwatcher.process_new_offchain_ctx(self, pending_local_commitment, ours=True) t@@ -332,14 +339,14 @@ class HTLCStateMachine(PrintError): new_local_feerate = self.local_state.feerate new_remote_feerate = self.remote_state.feerate - if self.pending_fee is not None: - if not self.constraints.is_initiator and (self.pending_fee.progress & FeeUpdateProgress.FUNDEE_SIGNED): - new_local_feerate = new_remote_feerate = self.pending_fee.rate - self.pending_fee = None + for pending_fee in self.fee_mgr: + if not self.constraints.is_initiator and pending_fee.had(FUNDEE_SIGNED): + new_local_feerate = new_remote_feerate = pending_fee.rate + pending_fee.set(COMMITTED) print("FEERATE CHANGE COMPLETE (non-initiator)") - if self.constraints.is_initiator and (self.pending_fee.progress & FeeUpdateProgress.FUNDER_SIGNED): - new_local_feerate = new_remote_feerate = self.pending_fee.rate - self.pending_fee = None + if self.constraints.is_initiator and pending_fee.had(FUNDER_SIGNED): + new_local_feerate = new_remote_feerate = pending_fee.rate + pending_fee.set(COMMITTED) print("FEERATE CHANGE COMPLETE (initiator)") self.local_state=self.local_state._replace( t@@ -424,9 +431,10 @@ class HTLCStateMachine(PrintError): amount_msat = self.local_state.amount_msat + (received_this_batch - sent_this_batch) ) - if self.pending_fee: - if self.constraints.is_initiator: - self.pending_fee.progress |= FeeUpdateProgress.FUNDEE_ACKED + for pending_fee in self.fee_mgr: + if pending_fee.is_proposed(): + if self.constraints.is_initiator: + pending_fee.set(FUNDEE_ACKED) self.local_commitment = self.pending_local_commitment self.remote_commitment = self.pending_remote_commitment t@@ -463,7 +471,7 @@ class HTLCStateMachine(PrintError): local_htlc_pubkey = derive_pubkey(self.local_config.htlc_basepoint.pubkey, this_point) local_revocation_pubkey = derive_blinded_pubkey(self.local_config.revocation_basepoint.pubkey, this_point) - feerate = self.pending_remote_feerate + feerate = self.pending_feerate(REMOTE) htlcs_in_local = [] for htlc in self.htlcs_in_local: t@@ -484,6 +492,20 @@ class HTLCStateMachine(PrintError): remote_msat, local_msat, htlcs_in_local + htlcs_in_remote) return commit + def pending_feerate(self, subject): + candidate = None + for pending_fee in self.fee_mgr: + x = pending_fee.pending_feerate(subject) + if x is not None: + candidate = x + + feerate = candidate if candidate is not None else self._committed_feerate[subject] + return feerate + + @property + def _committed_feerate(self): + return {LOCAL: self.local_state.feerate, REMOTE: self.remote_state.feerate} + @property def pending_local_commitment(self): remote_msat, local_msat = self.amounts() t@@ -496,7 +518,7 @@ class HTLCStateMachine(PrintError): local_htlc_pubkey = derive_pubkey(self.local_config.htlc_basepoint.pubkey, this_point) remote_revocation_pubkey = derive_blinded_pubkey(self.remote_config.revocation_basepoint.pubkey, this_point) - feerate = self.pending_local_feerate + feerate = self.pending_feerate(LOCAL) htlcs_in_local = [] for htlc in self.htlcs_in_local: t@@ -569,7 +591,7 @@ class HTLCStateMachine(PrintError): self.print_error("receive_htlc_settle") htlc = self.lookup_htlc(self.log[LOCAL], htlc_index) assert htlc.payment_hash == sha256(preimage) - assert len([x.htlc_id == htlc_index for x in self.log[LOCAL]]) == 1 + assert len([x for x in self.log[LOCAL] if x.htlc_id == htlc_index and type(x) is UpdateAddHtlc]) == 1, (self.log[LOCAL], htlc_index) self.log[REMOTE].append(SettleHtlc(htlc_index)) def fail_htlc(self, htlc): t@@ -586,15 +608,17 @@ class HTLCStateMachine(PrintError): def pending_local_fee(self): return self.constraints.capacity - sum(x[2] for x in self.pending_local_commitment.outputs()) - def update_fee(self, fee): + def update_fee(self, feerate): if not self.constraints.is_initiator: raise Exception("only initiator can update_fee, this counterparty is not initiator") - self.pending_fee = FeeUpdate(rate=fee) + pending_fee = FeeUpdate(self, feerate) + self.fee_mgr.append(pending_fee) - def receive_update_fee(self, 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") - self.pending_fee = FeeUpdate(rate=fee) + pending_fee = FeeUpdate(self, feerate) + self.fee_mgr.append(pending_fee) def to_save(self): return { t@@ -650,7 +674,7 @@ class HTLCStateMachine(PrintError): local_msat, remote_msat, conf.dust_limit_sat, - chan.pending_local_feerate if for_us else chan.pending_remote_feerate, + chan.pending_feerate(LOCAL if for_us else REMOTE), for_us, chan.constraints.is_initiator, htlcs=htlcs) (DIR) diff --git a/electrum/lnutil.py b/electrum/lnutil.py t@@ -1,3 +1,4 @@ +from enum import IntFlag import json from collections import namedtuple from typing import NamedTuple t@@ -249,7 +250,7 @@ def make_htlc_tx_with_open_channel(chan, pcp, for_us, we_receive, amount_msat, c is_htlc_success = for_us == we_receive htlc_tx_output = make_htlc_tx_output( amount_msat = amount_msat, - local_feerate = chan.pending_local_feerate if for_us else chan.pending_remote_feerate, + local_feerate = chan.pending_feerate(LOCAL if for_us else REMOTE), revocationpubkey=revocation_pubkey, local_delayedpubkey=delayedpubkey, success = is_htlc_success, t@@ -432,3 +433,15 @@ def get_compressed_pubkey_from_bech32(bech32_pubkey: str) -> bytes: class PaymentFailure(Exception): pass + +class HTLCOwner(IntFlag): + LOCAL = 1 + REMOTE = -LOCAL + + SENT = LOCAL + RECEIVED = REMOTE + +SENT = HTLCOwner.SENT +RECEIVED = HTLCOwner.RECEIVED +LOCAL = HTLCOwner.LOCAL +REMOTE = HTLCOwner.REMOTE (DIR) diff --git a/electrum/tests/test_lnhtlc.py b/electrum/tests/test_lnhtlc.py t@@ -258,14 +258,14 @@ class TestLNBaseHTLCStateMachine(unittest.TestCase): return fee def test_UpdateFeeSenderCommits(self): - old_feerate = self.alice_channel.pending_local_feerate + old_feerate = self.alice_channel.pending_feerate(LOCAL) fee = self.alice_to_bob_fee_update() alice_channel, bob_channel = self.alice_channel, self.bob_channel - self.assertEqual(self.alice_channel.pending_local_feerate, old_feerate) + self.assertEqual(self.alice_channel.pending_feerate(LOCAL), old_feerate) alice_sig, alice_htlc_sigs = alice_channel.sign_next_commitment() - self.assertEqual(self.alice_channel.pending_local_feerate, old_feerate) + self.assertEqual(self.alice_channel.pending_feerate(LOCAL), old_feerate) bob_channel.receive_new_commitment(alice_sig, alice_htlc_sigs)