tln: trim dust htlc outputs - electrum - Electrum Bitcoin wallet
 (HTM) git clone https://git.parazyd.org/electrum
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) Submodules
       ---
 (DIR) commit b3dad9480cb0af5d2d79ad8d3cb977f143215778
 (DIR) parent 8fe70fc0eb6549ad019f1ab865554d672288a329
 (HTM) Author: Janus <ysangkok@gmail.com>
       Date:   Tue, 26 Jun 2018 19:18:56 +0200
       
       ln: trim dust htlc outputs
       
       Diffstat:
         M lib/lnbase.py                       |      19 ++++++++++---------
         M lib/lnhtlc.py                       |      35 +++++++++++++++++++++++++------
         M lib/tests/test_lnhtlc.py            |      74 +++++++++++++++++++++++++++----
       
       3 files changed, 105 insertions(+), 23 deletions(-)
       ---
 (DIR) diff --git a/lib/lnbase.py b/lib/lnbase.py
       t@@ -18,10 +18,14 @@ import binascii
        import hashlib
        import hmac
        from typing import Sequence, Union, Tuple
       +from collections import namedtuple, defaultdict
        import cryptography.hazmat.primitives.ciphers.aead as AEAD
        from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
        from cryptography.hazmat.backends import default_backend
        
       +HTLC_TIMEOUT_WEIGHT = 663
       +HTLC_SUCCESS_WEIGHT = 703
       +
        from .ecc import ser_to_point, point_to_ser, string_to_number
        from .bitcoin import (deserialize_privkey, rev_hex, int_to_hex,
                              push_script, script_num_to_hex,
       t@@ -38,8 +42,6 @@ from .lnrouter import new_onion_packet, OnionHopsDataSingle, OnionPerHop, decode
        from .lightning_payencode.lnaddr import lndecode
        from .lnhtlc import UpdateAddHtlc, HTLCStateMachine, RevokeAndAck, SettleHtlc
        
       -from collections import namedtuple, defaultdict
       -
        def channel_id_from_funding_tx(funding_txid, funding_index):
            funding_txid_bytes = bytes.fromhex(funding_txid)[::-1]
            i = int.from_bytes(funding_txid_bytes, 'big') ^ funding_index
       t@@ -340,9 +342,6 @@ def get_per_commitment_secret_from_seed(seed: bytes, i: int, bits: int = 48) -> 
        def overall_weight(num_htlc):
            return 500 + 172 * num_htlc + 224
        
       -HTLC_TIMEOUT_WEIGHT = 663
       -HTLC_SUCCESS_WEIGHT = 703
       -
        def make_htlc_tx_output(amount_msat, local_feerate, revocationpubkey, local_delayedpubkey, success, to_self_delay):
            assert type(amount_msat) is int
            assert type(local_feerate) is int
       t@@ -468,7 +467,7 @@ def make_htlc_tx_with_open_channel(chan, pcp, for_us, we_receive, amount_msat, c
            htlc_tx = make_htlc_tx(cltv_expiry, inputs=htlc_tx_inputs, output=htlc_tx_output)
            return htlc_tx
        
       -def make_commitment_using_open_channel(chan, ctn, for_us, pcp, local_msat, remote_msat, htlcs=[]):
       +def make_commitment_using_open_channel(chan, ctn, for_us, pcp, local_msat, remote_msat, htlcs=[], trimmed=0):
            conf = chan.local_config if for_us else chan.remote_config
            other_conf = chan.local_config if not for_us else chan.remote_config
            payment_pubkey = derive_pubkey(other_conf.payment_basepoint.pubkey, pcp)
       t@@ -491,7 +490,8 @@ def make_commitment_using_open_channel(chan, ctn, for_us, pcp, local_msat, remot
                chan.constraints.feerate,
                for_us,
                chan.constraints.is_initiator,
       -        htlcs=htlcs)
       +        htlcs=htlcs,
       +        trimmed=trimmed)
        
        def make_commitment(ctn, local_funding_pubkey, remote_funding_pubkey,
                            remote_payment_pubkey, payment_basepoint,
       t@@ -499,7 +499,7 @@ def make_commitment(ctn, local_funding_pubkey, remote_funding_pubkey,
                            delayed_pubkey, to_self_delay, funding_txid,
                            funding_pos, funding_sat, local_amount, remote_amount,
                            dust_limit_sat, local_feerate, for_us, we_are_initiator,
       -                    htlcs):
       +                    htlcs, trimmed=0):
        
            pubkeys = sorted([bh2u(local_funding_pubkey), bh2u(remote_funding_pubkey)])
            payments = [payment_basepoint, remote_payment_basepoint]
       t@@ -527,7 +527,8 @@ def make_commitment(ctn, local_funding_pubkey, remote_funding_pubkey,
            local_address = bitcoin.redeem_script_to_address('p2wsh', bh2u(local_script))
            remote_address = bitcoin.pubkey_to_address('p2wpkh', bh2u(remote_payment_pubkey))
            # TODO trim htlc outputs here while also considering 2nd stage htlc transactions
       -    fee = local_feerate * overall_weight(len(htlcs)) # TODO incorrect if anything is trimmed
       +    fee = local_feerate * overall_weight(len(htlcs))
       +    fee -= trimmed * 1000
            assert type(fee) is int
            we_pay_fee = for_us == we_are_initiator
            to_local_amt = local_amount - (fee if we_pay_fee else 0)
 (DIR) diff --git a/lib/lnhtlc.py b/lib/lnhtlc.py
       t@@ -6,6 +6,9 @@ from collections import namedtuple
        from ecdsa.curves import SECP256k1
        from .crypto import sha256
        from . import ecc
       +from . import lnbase
       +HTLC_TIMEOUT_WEIGHT = lnbase.HTLC_TIMEOUT_WEIGHT
       +HTLC_SUCCESS_WEIGHT = lnbase.HTLC_SUCCESS_WEIGHT
        
        SettleHtlc = namedtuple("SettleHtlc", ["htlc_id"])
        RevokeAndAck = namedtuple("RevokeAndAck", ["per_commitment_secret", "next_per_commitment_point"])
       t@@ -117,6 +120,9 @@ class HTLCStateMachine(PrintError):
                for we_receive, htlcs in zip([True, False], [self.htlcs_in_remote, self.htlcs_in_local]):
                    assert len(htlcs) <= 1
                    for htlc in htlcs:
       +                weight = lnbase.HTLC_SUCCESS_WEIGHT if we_receive else lnbase.HTLC_TIMEOUT_WEIGHT
       +                if htlc.amount_msat // 1000 - weight * (self.state.constraints.feerate // 1000) < self.state.remote_config.dust_limit_sat:
       +                    continue
                        original_htlc_output_index = 0
                        args = [self.state.remote_state.next_per_commitment_point, for_us, we_receive, htlc.amount_msat + htlc.total_fee, htlc.cltv_expiry, htlc.payment_hash, self.remote_commitment, original_htlc_output_index]
                        htlc_tx = make_htlc_tx_with_open_channel(self.state, *args)
       t@@ -146,8 +152,6 @@ class HTLCStateMachine(PrintError):
                    if htlc.r_locked_in is None: htlc.r_locked_in = self.state.remote_state.ctn
                assert len(htlc_sigs) == 0 or type(htlc_sigs[0]) is bytes
        
       -        assert len(self.htlcs_in_local) + len(self.htlcs_in_remote) == len(htlc_sigs), len(htlc_sigs)
       -
                preimage_hex = self.local_commitment.serialize_preimage(0)
                pre_hash = Hash(bfh(preimage_hex))
                if not ecc.verify_signature(self.state.remote_config.multisig_key.pubkey, sig, pre_hash):
       t@@ -155,9 +159,8 @@ class HTLCStateMachine(PrintError):
        
                _, this_point, _ = self.points
        
       -        if len(self.htlcs_in_remote) > 0:
       +        if len(self.htlcs_in_remote) > 0 and len(self.local_commitment.outputs()) == 3:
                    print("CHECKING HTLC SIGS")
       -            assert len(self.local_commitment.outputs()) == 3 # TODO
                    we_receive = True
                    payment_hash = self.htlcs_in_remote[0].payment_hash
                    amount_msat = self.htlcs_in_remote[0].amount_msat
       t@@ -313,19 +316,27 @@ class HTLCStateMachine(PrintError):
                local_htlc_pubkey = derive_pubkey(self.state.local_config.htlc_basepoint.pubkey, this_point)
                local_revocation_pubkey = derive_blinded_pubkey(self.state.local_config.revocation_basepoint.pubkey, this_point)
        
       +        trimmed = 0
       +
                htlcs_in_local = []
                for htlc in self.htlcs_in_local:
       +            if htlc.amount_msat // 1000 - lnbase.HTLC_SUCCESS_WEIGHT * (self.state.constraints.feerate // 1000) < self.state.remote_config.dust_limit_sat:
       +                trimmed += htlc.amount_msat // 1000
       +                continue
                    htlcs_in_local.append(
                        ( make_received_htlc(local_revocation_pubkey, local_htlc_pubkey, remote_htlc_pubkey, htlc.payment_hash, htlc.cltv_expiry), htlc.amount_msat + htlc.total_fee))
        
                htlcs_in_remote = []
                for htlc in self.htlcs_in_remote:
       +            if htlc.amount_msat // 1000 - lnbase.HTLC_TIMEOUT_WEIGHT * (self.state.constraints.feerate // 1000) < self.state.remote_config.dust_limit_sat:
       +                trimmed += htlc.amount_msat // 1000
       +                continue
                    htlcs_in_remote.append(
                        ( make_offered_htlc(local_revocation_pubkey, local_htlc_pubkey, remote_htlc_pubkey, htlc.payment_hash), htlc.amount_msat + htlc.total_fee))
        
                commit = make_commitment_using_open_channel(self.state, self.state.remote_state.ctn + 1,
                    False, this_point,
       -            remote_msat - total_fee_remote, local_msat - total_fee_local, htlcs_in_local + htlcs_in_remote)
       +            remote_msat - total_fee_remote, local_msat - total_fee_local, htlcs_in_local + htlcs_in_remote, trimmed)
                return commit
        
            @property
       t@@ -341,19 +352,27 @@ class HTLCStateMachine(PrintError):
                local_htlc_pubkey = derive_pubkey(self.state.local_config.htlc_basepoint.pubkey, this_point)
                remote_revocation_pubkey = derive_blinded_pubkey(self.state.remote_config.revocation_basepoint.pubkey, this_point)
        
       +        trimmed = 0
       +
                htlcs_in_local = []
                for htlc in self.htlcs_in_local:
       +            if htlc.amount_msat // 1000 - lnbase.HTLC_TIMEOUT_WEIGHT * (self.state.constraints.feerate // 1000) < self.state.local_config.dust_limit_sat:
       +                trimmed += htlc.amount_msat // 1000
       +                continue
                    htlcs_in_local.append(
                        ( make_offered_htlc(remote_revocation_pubkey, remote_htlc_pubkey, local_htlc_pubkey, htlc.payment_hash), htlc.amount_msat + htlc.total_fee))
        
                htlcs_in_remote = []
                for htlc in self.htlcs_in_remote:
       +            if htlc.amount_msat // 1000 - lnbase.HTLC_SUCCESS_WEIGHT * (self.state.constraints.feerate // 1000) < self.state.local_config.dust_limit_sat:
       +                trimmed += htlc.amount_msat // 1000
       +                continue
                    htlcs_in_remote.append(
                        ( make_received_htlc(remote_revocation_pubkey, remote_htlc_pubkey, local_htlc_pubkey, htlc.payment_hash, htlc.cltv_expiry), htlc.amount_msat + htlc.total_fee))
        
                commit = make_commitment_using_open_channel(self.state, self.state.local_state.ctn + 1,
                    True, this_point,
       -            local_msat - total_fee_local, remote_msat - total_fee_remote, htlcs_in_local + htlcs_in_remote)
       +            local_msat - total_fee_local, remote_msat - total_fee_remote, htlcs_in_local + htlcs_in_remote, trimmed)
                return commit
        
            def gen_htlc_indices(self, subject, just_unsettled=True):
       t@@ -409,3 +428,7 @@ class HTLCStateMachine(PrintError):
            @property
            def r_current_height(self):
                return self.state.remote_state.ctn
       +
       +    @property
       +    def local_commit_fee(self):
       +        return self.state.constraints.capacity - sum(x[2] for x in self.local_commitment.outputs())
 (DIR) diff --git a/lib/tests/test_lnhtlc.py b/lib/tests/test_lnhtlc.py
       t@@ -8,7 +8,7 @@ import lib.util as util
        import os
        import binascii
        
       -def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate, is_initiator, local_amount, remote_amount, privkeys, other_pubkeys, seed, cur, nex, other_node_id):
       +def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate, is_initiator, local_amount, remote_amount, privkeys, other_pubkeys, seed, cur, nex, other_node_id, l_dust, r_dust, l_csv, r_csv):
            assert local_amount > 0
            assert remote_amount > 0
            channel_id, _ = lnbase.channel_id_from_funding_tx(funding_txid, funding_index)
       t@@ -19,8 +19,8 @@ def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate
                htlc_basepoint=privkeys[2],
                delayed_basepoint=privkeys[3],
                revocation_basepoint=privkeys[4],
       -        to_self_delay=143,
       -        dust_limit_sat=10,
       +        to_self_delay=l_csv,
       +        dust_limit_sat=l_dust,
                max_htlc_value_in_flight_msat=500000 * 1000,
                max_accepted_htlcs=5
            )
       t@@ -30,8 +30,8 @@ def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate
                htlc_basepoint=other_pubkeys[2],
                delayed_basepoint=other_pubkeys[3],
                revocation_basepoint=other_pubkeys[4],
       -        to_self_delay=143,
       -        dust_limit_sat=10,
       +        to_self_delay=r_csv,
       +        dust_limit_sat=r_dust,
                max_htlc_value_in_flight_msat=500000 * 1000,
                max_accepted_htlcs=5
            )
       t@@ -92,9 +92,11 @@ def create_test_channels():
            bob_cur = lnbase.secret_to_pubkey(int.from_bytes(lnbase.get_per_commitment_secret_from_seed(bob_seed, 2**48 - 1), "big"))
            bob_next = lnbase.secret_to_pubkey(int.from_bytes(lnbase.get_per_commitment_secret_from_seed(bob_seed, 2**48 - 2), "big"))
        
       -    return lnhtlc.HTLCStateMachine(
       -        create_channel_state(funding_txid, funding_index, funding_sat, 20000, True, local_amount, remote_amount, alice_privkeys, bob_pubkeys, alice_seed, bob_cur, bob_next, b"\x02"*33), "alice"), lnhtlc.HTLCStateMachine(
       -        create_channel_state(funding_txid, funding_index, funding_sat, 20000, False, remote_amount, local_amount, bob_privkeys, alice_pubkeys, bob_seed, alice_cur, alice_next, b"\x01"*33), "bob")
       +    return \
       +        lnhtlc.HTLCStateMachine(
       +            create_channel_state(funding_txid, funding_index, funding_sat, 6000, True, local_amount, remote_amount, alice_privkeys, bob_pubkeys, alice_seed, bob_cur, bob_next, b"\x02"*33, l_dust=200, r_dust=1300, l_csv=5, r_csv=4), "alice"), \
       +        lnhtlc.HTLCStateMachine(
       +            create_channel_state(funding_txid, funding_index, funding_sat, 6000, False, remote_amount, local_amount, bob_privkeys, alice_pubkeys, bob_seed, alice_cur, alice_next, b"\x01"*33, l_dust=1300, r_dust=200, l_csv=4, r_csv=5), "bob")
        
        one_bitcoin_in_msat = bitcoin.COIN * 1000
        
       t@@ -230,3 +232,59 @@ 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))
       +
       +    def test_HTLCDustLimit(self):
       +        alice_channel, bob_channel = create_test_channels()
       +
       +        paymentPreimage = b"\x01" * 32
       +        paymentHash = bitcoin.sha256(paymentPreimage)
       +        fee_per_kw = alice_channel.state.constraints.feerate
       +        self.assertEqual(fee_per_kw, 6000)
       +        htlcAmt = 500 + lnbase.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
       +            total_fee = 0
       +        )
       +
       +        aliceHtlcIndex = alice_channel.add_htlc(htlc)
       +
       +        bobHtlcIndex = bob_channel.receive_htlc(htlc)
       +
       +        force_state_transition(alice_channel, bob_channel)
       +
       +        self.assertEqual(len(alice_channel.local_commitment.outputs()), 3)
       +
       +        self.assertEqual(len(bob_channel.local_commitment.outputs()), 2)
       +
       +        default_fee = calc_static_fee(0)
       +
       +        self.assertEqual(bob_channel.local_commit_fee, default_fee)
       +
       +        bob_channel.settle_htlc(paymentPreimage, htlc.htlc_id)
       +        alice_channel.receive_htlc_settle(paymentPreimage, aliceHtlcIndex)
       +
       +        force_state_transition(bob_channel, alice_channel)
       +
       +        self.assertEqual(len(alice_channel.local_commitment.outputs()), 2)
       +
       +        self.assertEqual(alice_channel.total_msat_sent // 1000, htlcAmt)
       +
       +def force_state_transition(chanA, chanB):
       +    chanB.receive_new_commitment(*chanA.sign_next_commitment())
       +    rev, _ = chanB.revoke_current_commitment()
       +    bob_sig, bob_htlc_sigs = chanB.sign_next_commitment()
       +    chanA.receive_revocation(rev)
       +    chanA.receive_new_commitment(bob_sig, bob_htlc_sigs)
       +    chanB.receive_revocation(chanA.revoke_current_commitment()[0])
       +
       +# calcStaticFee calculates appropriate fees for commitment transactions.  This
       +# function provides a simple way to allow test balance assertions to take fee
       +# calculations into account.
       +def calc_static_fee(numHTLCs):
       +  commitWeight = 724
       +  htlcWeight   = 172
       +  feePerKw     = 24//4 * 1000
       +  return feePerKw * (commitWeight + htlcWeight*numHTLCs) // 1000