mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-26 23:13:25 +00:00
ln: trim dust htlc outputs
This commit is contained in:
parent
047c543e19
commit
a3b67494a9
3 changed files with 105 additions and 23 deletions
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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…
Add table
Reference in a new issue