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 hashlib
import hmac import hmac
from typing import Sequence, Union, Tuple from typing import Sequence, Union, Tuple
from collections import namedtuple, defaultdict
import cryptography.hazmat.primitives.ciphers.aead as AEAD import cryptography.hazmat.primitives.ciphers.aead as AEAD
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
from cryptography.hazmat.backends import default_backend 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 .ecc import ser_to_point, point_to_ser, string_to_number
from .bitcoin import (deserialize_privkey, rev_hex, int_to_hex, from .bitcoin import (deserialize_privkey, rev_hex, int_to_hex,
push_script, script_num_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 .lightning_payencode.lnaddr import lndecode
from .lnhtlc import UpdateAddHtlc, HTLCStateMachine, RevokeAndAck, SettleHtlc from .lnhtlc import UpdateAddHtlc, HTLCStateMachine, RevokeAndAck, SettleHtlc
from collections import namedtuple, defaultdict
def channel_id_from_funding_tx(funding_txid, funding_index): def channel_id_from_funding_tx(funding_txid, funding_index):
funding_txid_bytes = bytes.fromhex(funding_txid)[::-1] funding_txid_bytes = bytes.fromhex(funding_txid)[::-1]
i = int.from_bytes(funding_txid_bytes, 'big') ^ funding_index 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): def overall_weight(num_htlc):
return 500 + 172 * num_htlc + 224 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): def make_htlc_tx_output(amount_msat, local_feerate, revocationpubkey, local_delayedpubkey, success, to_self_delay):
assert type(amount_msat) is int assert type(amount_msat) is int
assert type(local_feerate) 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) htlc_tx = make_htlc_tx(cltv_expiry, inputs=htlc_tx_inputs, output=htlc_tx_output)
return htlc_tx 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 conf = chan.local_config if for_us else chan.remote_config
other_conf = chan.local_config if not 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) 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, chan.constraints.feerate,
for_us, for_us,
chan.constraints.is_initiator, chan.constraints.is_initiator,
htlcs=htlcs) htlcs=htlcs,
trimmed=trimmed)
def make_commitment(ctn, local_funding_pubkey, remote_funding_pubkey, def make_commitment(ctn, local_funding_pubkey, remote_funding_pubkey,
remote_payment_pubkey, payment_basepoint, 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, delayed_pubkey, to_self_delay, funding_txid,
funding_pos, funding_sat, local_amount, remote_amount, funding_pos, funding_sat, local_amount, remote_amount,
dust_limit_sat, local_feerate, for_us, we_are_initiator, dust_limit_sat, local_feerate, for_us, we_are_initiator,
htlcs): htlcs, trimmed=0):
pubkeys = sorted([bh2u(local_funding_pubkey), bh2u(remote_funding_pubkey)]) pubkeys = sorted([bh2u(local_funding_pubkey), bh2u(remote_funding_pubkey)])
payments = [payment_basepoint, remote_payment_basepoint] 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)) local_address = bitcoin.redeem_script_to_address('p2wsh', bh2u(local_script))
remote_address = bitcoin.pubkey_to_address('p2wpkh', bh2u(remote_payment_pubkey)) remote_address = bitcoin.pubkey_to_address('p2wpkh', bh2u(remote_payment_pubkey))
# TODO trim htlc outputs here while also considering 2nd stage htlc transactions # 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 assert type(fee) is int
we_pay_fee = for_us == we_are_initiator we_pay_fee = for_us == we_are_initiator
to_local_amt = local_amount - (fee if we_pay_fee else 0) 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 ecdsa.curves import SECP256k1
from .crypto import sha256 from .crypto import sha256
from . import ecc 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"]) SettleHtlc = namedtuple("SettleHtlc", ["htlc_id"])
RevokeAndAck = namedtuple("RevokeAndAck", ["per_commitment_secret", "next_per_commitment_point"]) 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]): for we_receive, htlcs in zip([True, False], [self.htlcs_in_remote, self.htlcs_in_local]):
assert len(htlcs) <= 1 assert len(htlcs) <= 1
for htlc in htlcs: 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 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] 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) 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 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(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) preimage_hex = self.local_commitment.serialize_preimage(0)
pre_hash = Hash(bfh(preimage_hex)) pre_hash = Hash(bfh(preimage_hex))
if not ecc.verify_signature(self.state.remote_config.multisig_key.pubkey, sig, pre_hash): 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 _, 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") print("CHECKING HTLC SIGS")
assert len(self.local_commitment.outputs()) == 3 # TODO
we_receive = True we_receive = True
payment_hash = self.htlcs_in_remote[0].payment_hash payment_hash = self.htlcs_in_remote[0].payment_hash
amount_msat = self.htlcs_in_remote[0].amount_msat 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_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) local_revocation_pubkey = derive_blinded_pubkey(self.state.local_config.revocation_basepoint.pubkey, this_point)
trimmed = 0
htlcs_in_local = [] htlcs_in_local = []
for htlc in self.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( 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)) ( 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 = [] htlcs_in_remote = []
for htlc in self.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( 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)) ( 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, commit = make_commitment_using_open_channel(self.state, self.state.remote_state.ctn + 1,
False, this_point, 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 return commit
@property @property
@ -341,19 +352,27 @@ class HTLCStateMachine(PrintError):
local_htlc_pubkey = derive_pubkey(self.state.local_config.htlc_basepoint.pubkey, this_point) 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) remote_revocation_pubkey = derive_blinded_pubkey(self.state.remote_config.revocation_basepoint.pubkey, this_point)
trimmed = 0
htlcs_in_local = [] htlcs_in_local = []
for htlc in self.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( 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)) ( make_offered_htlc(remote_revocation_pubkey, remote_htlc_pubkey, local_htlc_pubkey, htlc.payment_hash), htlc.amount_msat + htlc.total_fee))
htlcs_in_remote = [] htlcs_in_remote = []
for htlc in self.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( 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)) ( 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, commit = make_commitment_using_open_channel(self.state, self.state.local_state.ctn + 1,
True, this_point, 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 return commit
def gen_htlc_indices(self, subject, just_unsettled=True): def gen_htlc_indices(self, subject, just_unsettled=True):
@ -409,3 +428,7 @@ class HTLCStateMachine(PrintError):
@property @property
def r_current_height(self): def r_current_height(self):
return self.state.remote_state.ctn 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 os
import binascii 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 local_amount > 0
assert remote_amount > 0 assert remote_amount > 0
channel_id, _ = lnbase.channel_id_from_funding_tx(funding_txid, funding_index) 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], htlc_basepoint=privkeys[2],
delayed_basepoint=privkeys[3], delayed_basepoint=privkeys[3],
revocation_basepoint=privkeys[4], revocation_basepoint=privkeys[4],
to_self_delay=143, to_self_delay=l_csv,
dust_limit_sat=10, dust_limit_sat=l_dust,
max_htlc_value_in_flight_msat=500000 * 1000, max_htlc_value_in_flight_msat=500000 * 1000,
max_accepted_htlcs=5 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], htlc_basepoint=other_pubkeys[2],
delayed_basepoint=other_pubkeys[3], delayed_basepoint=other_pubkeys[3],
revocation_basepoint=other_pubkeys[4], revocation_basepoint=other_pubkeys[4],
to_self_delay=143, to_self_delay=r_csv,
dust_limit_sat=10, dust_limit_sat=r_dust,
max_htlc_value_in_flight_msat=500000 * 1000, max_htlc_value_in_flight_msat=500000 * 1000,
max_accepted_htlcs=5 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_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")) 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( return \
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( 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") 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 one_bitcoin_in_msat = bitcoin.COIN * 1000
@ -230,3 +232,59 @@ class TestLNBaseHTLCStateMachine(unittest.TestCase):
# revocation. # 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.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)) 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