commit b3dad9480cb0af5d2d79ad8d3cb977f143215778
parent 8fe70fc0eb6549ad019f1ab865554d672288a329
Author: Janus <ysangkok@gmail.com>
Date: Tue, 26 Jun 2018 19:18:56 +0200
ln: trim dust htlc outputs
Diffstat:
3 files changed, 105 insertions(+), 23 deletions(-)
diff --git a/lib/lnbase.py b/lib/lnbase.py
@@ -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,
@@ -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
@@ -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
@@ -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)
@@ -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,
@@ -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]
@@ -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)
diff --git a/lib/lnhtlc.py b/lib/lnhtlc.py
@@ -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"])
@@ -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)
@@ -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):
@@ -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
@@ -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
@@ -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):
@@ -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())
diff --git a/lib/tests/test_lnhtlc.py b/lib/tests/test_lnhtlc.py
@@ -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)
@@ -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
)
@@ -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
)
@@ -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
@@ -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