Browse Source

ln: trim dust htlc outputs

dependabot/pip/contrib/deterministic-build/ecdsa-0.13.3
Janus 7 years ago
committed by ThomasV
parent
commit
b3dad9480c
  1. 19
      lib/lnbase.py
  2. 35
      lib/lnhtlc.py
  3. 74
      lib/tests/test_lnhtlc.py

19
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)

35
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())

74
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

Loading…
Cancel
Save