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