1
0

lnutil+lnchannel: add anchors, adapt to_remote

* to_remote has now an additional csv lock of 1
* anchor outputs are added if to_local/remote outputs are present
* funder balance is reduced to accomodate anchors
This commit is contained in:
bitromortac
2021-09-13 13:41:01 +02:00
committed by ThomasV
parent 7907b9c05d
commit 7aa3dc1e40
3 changed files with 155 additions and 45 deletions

View File

@@ -55,7 +55,7 @@ from .lnutil import (Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKey
ScriptHtlc, PaymentFailure, calc_fees_for_commitment_tx, RemoteMisbehaving, make_htlc_output_witness_script,
ShortChannelID, map_htlcs_to_ctx_output_idxs,
fee_for_htlc_output, offered_htlc_trim_threshold_sat,
received_htlc_trim_threshold_sat, make_commitment_output_to_remote_address,
received_htlc_trim_threshold_sat, make_commitment_output_to_remote_address, FIXED_ANCHOR_SAT,
ChannelType, LNProtocolWarning)
from .lnsweep import create_sweeptxs_for_our_ctx, create_sweeptxs_for_their_ctx
from .lnsweep import create_sweeptx_for_their_revoked_htlc, SweepInfo
@@ -888,7 +888,7 @@ class Channel(AbstractChannel):
# TODO: in case of unilateral close with pending HTLCs, this address will be reused
assert self.is_static_remotekey_enabled()
our_payment_pubkey = self.config[LOCAL].payment_basepoint.pubkey
addr = make_commitment_output_to_remote_address(our_payment_pubkey)
addr = make_commitment_output_to_remote_address(our_payment_pubkey, has_anchors=self.has_anchors())
if self.lnworker:
assert self.lnworker.wallet.is_mine(addr)
return addr
@@ -900,7 +900,7 @@ class Channel(AbstractChannel):
def get_wallet_addresses_channel_might_want_reserved(self) -> Sequence[str]:
assert self.is_static_remotekey_enabled()
our_payment_pubkey = self.config[LOCAL].payment_basepoint.pubkey
to_remote_address = make_commitment_output_to_remote_address(our_payment_pubkey)
to_remote_address = make_commitment_output_to_remote_address(our_payment_pubkey, has_anchors=self.has_anchors())
return [to_remote_address]
def get_feerate(self, subject: HTLCOwner, *, ctn: int) -> int:
@@ -1341,7 +1341,7 @@ class Channel(AbstractChannel):
return len(self.hm.htlcs(LOCAL)) + len(self.hm.htlcs(REMOTE)) > 0
def available_to_spend(self, subject: HTLCOwner, *, strict: bool = True) -> int:
"""The usable balance of 'subject' in msat, after taking reserve and fees into
"""The usable balance of 'subject' in msat, after taking reserve and fees (and anchors) into
consideration. Note that fees (and hence the result) fluctuate even without user interaction.
"""
assert type(subject) is HTLCOwner
@@ -1367,25 +1367,42 @@ class Channel(AbstractChannel):
htlc_fee_msat = fee_for_htlc_output(feerate=feerate)
htlc_trim_func = received_htlc_trim_threshold_sat if ctx_owner == receiver else offered_htlc_trim_threshold_sat
htlc_trim_threshold_msat = htlc_trim_func(dust_limit_sat=self.config[ctx_owner].dust_limit_sat, feerate=feerate, has_anchors=self.has_anchors()) * 1000
if sender == initiator == LOCAL: # see https://github.com/lightningnetwork/lightning-rfc/pull/740
# the sender cannot spend below its reserve
max_send_msat = sender_balance_msat - sender_reserve_msat
# reserve a fee spike buffer
# see https://github.com/lightningnetwork/lightning-rfc/pull/740
if sender == initiator == LOCAL:
fee_spike_buffer = calc_fees_for_commitment_tx(
num_htlcs=num_htlcs_in_ctx + int(not is_htlc_dust) + 1,
feerate=2 * feerate,
is_local_initiator=self.constraints.is_initiator,
round_to_sat=False,
has_anchors=self.has_anchors()
)[sender]
max_send_msat = sender_balance_msat - sender_reserve_msat - fee_spike_buffer
else:
max_send_msat = sender_balance_msat - sender_reserve_msat - ctx_fees_msat[sender]
has_anchors=self.has_anchors())[sender]
max_send_msat -= fee_spike_buffer
# we can't enforce the fee spike buffer on the remote party
elif sender == initiator == REMOTE:
max_send_msat -= ctx_fees_msat[sender]
# initiator pays for anchor outputs
if sender == initiator and self.has_anchors():
max_send_msat -= 2 * FIXED_ANCHOR_SAT * 1000
# handle the transaction fees for the HTLC transaction
if is_htlc_dust:
# nobody pays additional HTLC transaction fees
return min(max_send_msat, htlc_trim_threshold_msat - 1)
else:
# somebody has to pay for the additonal HTLC transaction fees
if sender == initiator:
return max_send_msat - htlc_fee_msat
else:
# the receiver is the initiator, so they need to be able to pay tx fees
if receiver_balance_msat - receiver_reserve_msat - ctx_fees_msat[receiver] - htlc_fee_msat < 0:
# check if the receiver can afford to pay for the HTLC transaction fees
new_receiver_balance = receiver_balance_msat - receiver_reserve_msat - ctx_fees_msat[receiver] - htlc_fee_msat
if self.has_anchors():
new_receiver_balance -= 2 * FIXED_ANCHOR_SAT * 1000
if new_receiver_balance < 0:
return 0
return max_send_msat
@@ -1629,22 +1646,27 @@ class Channel(AbstractChannel):
dust_limit_sat=this_config.dust_limit_sat,
fees_per_participant=onchain_fees,
htlcs=htlcs,
has_anchors=self.has_anchors()
)
def make_closing_tx(self, local_script: bytes, remote_script: bytes,
fee_sat: int, *, drop_remote = False) -> Tuple[bytes, PartialTransaction]:
""" cooperative close """
_, outputs = make_commitment_outputs(
fees_per_participant={
LOCAL: fee_sat * 1000 if self.constraints.is_initiator else 0,
REMOTE: fee_sat * 1000 if not self.constraints.is_initiator else 0,
},
local_amount_msat=self.balance(LOCAL),
remote_amount_msat=self.balance(REMOTE) if not drop_remote else 0,
local_script=local_script,
remote_script=remote_script,
htlcs=[],
dust_limit_sat=self.config[LOCAL].dust_limit_sat)
fees_per_participant={
LOCAL: fee_sat * 1000 if self.constraints.is_initiator else 0,
REMOTE: fee_sat * 1000 if not self.constraints.is_initiator else 0,
},
local_amount_msat=self.balance(LOCAL),
remote_amount_msat=self.balance(REMOTE) if not drop_remote else 0,
local_script=local_script,
remote_script=remote_script,
htlcs=[],
dust_limit_sat=self.config[LOCAL].dust_limit_sat,
has_anchors=self.has_anchors(),
local_anchor_script=None,
remote_anchor_script=None,
)
closing_tx = make_closing_tx(self.config[LOCAL].multisig_key.pubkey,
self.config[REMOTE].multisig_key.pubkey,

View File

@@ -52,6 +52,7 @@ HTLC_SUCCESS_WEIGHT_ANCHORS = 706
COMMITMENT_TX_WEIGHT = 724
COMMITMENT_TX_WEIGHT_ANCHORS = 1124
HTLC_OUTPUT_WEIGHT = 172
FIXED_ANCHOR_SAT = 330
LN_MAX_FUNDING_SAT_LEGACY = pow(2, 24) - 1
DUST_LIMIT_MAX = 1000
@@ -992,26 +993,64 @@ RECEIVED = Direction.RECEIVED
LOCAL = HTLCOwner.LOCAL
REMOTE = HTLCOwner.REMOTE
def make_commitment_outputs(*, fees_per_participant: Mapping[HTLCOwner, int], local_amount_msat: int, remote_amount_msat: int,
local_script: bytes, remote_script: bytes, htlcs: List[ScriptHtlc], dust_limit_sat: int) -> Tuple[List[PartialTxOutput], List[PartialTxOutput]]:
# BOLT-03: "Base commitment transaction fees are extracted from the funder's amount;
# if that amount is insufficient, the entire amount of the funder's output is used."
# -> if funder cannot afford feerate, their output might go negative, so take max(0, x) here:
to_local_amt = max(0, local_amount_msat - fees_per_participant[LOCAL])
to_local = PartialTxOutput(scriptpubkey=local_script, value=to_local_amt // 1000)
to_remote_amt = max(0, remote_amount_msat - fees_per_participant[REMOTE])
to_remote = PartialTxOutput(scriptpubkey=remote_script, value=to_remote_amt // 1000)
non_htlc_outputs = [to_local, to_remote]
def make_commitment_outputs(
*,
fees_per_participant: Mapping[HTLCOwner, int],
local_amount_msat: int,
remote_amount_msat: int,
local_script: bytes,
remote_script: bytes,
htlcs: List[ScriptHtlc],
dust_limit_sat: int,
has_anchors: bool,
local_anchor_script: Optional[str],
remote_anchor_script: Optional[str]
) -> Tuple[List[PartialTxOutput], List[PartialTxOutput]]:
# determine HTLC outputs and trim below dust to know if anchors need to be included
htlc_outputs = []
for script, htlc in htlcs:
addr = bitcoin.redeem_script_to_address('p2wsh', script)
htlc_outputs.append(PartialTxOutput(scriptpubkey=address_to_script(addr),
value=htlc.amount_msat // 1000))
if htlc.amount_msat // 1000 > dust_limit_sat:
htlc_outputs.append(
PartialTxOutput(
scriptpubkey=address_to_script(addr),
value=htlc.amount_msat // 1000
))
# BOLT-03: "Base commitment transaction fees are extracted from the funder's amount;
# if that amount is insufficient, the entire amount of the funder's output is used."
non_htlc_outputs = []
to_local_amt_msat = local_amount_msat - fees_per_participant[LOCAL]
to_remote_amt_msat = remote_amount_msat - fees_per_participant[REMOTE]
anchor_outputs = []
# if no anchor scripts are set, we ignore anchor outputs, useful when this
# function is used to determine outputs for a collaborative close
if has_anchors and local_anchor_script and remote_anchor_script:
local_pays_anchors = bool(fees_per_participant[LOCAL])
# we always allocate for two anchor outputs even if they are not added
if local_pays_anchors:
to_local_amt_msat -= 2 * FIXED_ANCHOR_SAT * 1000
else:
to_remote_amt_msat -= 2 * FIXED_ANCHOR_SAT * 1000
# include anchors for outputs that materialize, include both if there are HTLCs present
if to_local_amt_msat // 1000 >= dust_limit_sat or htlc_outputs:
anchor_outputs.append(PartialTxOutput(scriptpubkey=local_anchor_script, value=FIXED_ANCHOR_SAT))
if to_remote_amt_msat // 1000 >= dust_limit_sat or htlc_outputs:
anchor_outputs.append(PartialTxOutput(scriptpubkey=remote_anchor_script, value=FIXED_ANCHOR_SAT))
# if funder cannot afford feerate, their output might go negative, so take max(0, x) here
to_local_amt_msat = max(0, to_local_amt_msat)
to_remote_amt_msat = max(0, to_remote_amt_msat)
non_htlc_outputs.append(PartialTxOutput(scriptpubkey=local_script, value=to_local_amt_msat // 1000))
non_htlc_outputs.append(PartialTxOutput(scriptpubkey=remote_script, value=to_remote_amt_msat // 1000))
# trim outputs
c_outputs_filtered = list(filter(lambda x: x.value >= dust_limit_sat, non_htlc_outputs + htlc_outputs))
return htlc_outputs, c_outputs_filtered
c_outputs = c_outputs_filtered + anchor_outputs
return htlc_outputs, c_outputs
def effective_htlc_tx_weight(success: bool, has_anchors: bool):
@@ -1086,7 +1125,8 @@ def make_commitment(
remote_amount: int,
dust_limit_sat: int,
fees_per_participant: Mapping[HTLCOwner, int],
htlcs: List[ScriptHtlc]
htlcs: List[ScriptHtlc],
has_anchors: bool
) -> PartialTransaction:
c_input = make_funding_input(local_funding_pubkey, remote_funding_pubkey,
funding_pos, funding_txid, funding_sat)
@@ -1099,7 +1139,12 @@ def make_commitment(
# commitment tx outputs
local_address = make_commitment_output_to_local_address(revocation_pubkey, to_self_delay, delayed_pubkey)
remote_address = make_commitment_output_to_remote_address(remote_payment_pubkey)
remote_address = make_commitment_output_to_remote_address(remote_payment_pubkey, has_anchors)
local_anchor_address = None
remote_anchor_address = None
if has_anchors:
local_anchor_address = make_commitment_output_to_anchor_address(local_funding_pubkey)
remote_anchor_address = make_commitment_output_to_anchor_address(remote_funding_pubkey)
# note: it is assumed that the given 'htlcs' are all non-dust (dust htlcs already trimmed)
# BOLT-03: "Transaction Input and Output Ordering
@@ -1116,7 +1161,11 @@ def make_commitment(
local_script=address_to_script(local_address),
remote_script=address_to_script(remote_address),
htlcs=htlcs,
dust_limit_sat=dust_limit_sat)
dust_limit_sat=dust_limit_sat,
has_anchors=has_anchors,
local_anchor_script=address_to_script(local_anchor_address) if local_anchor_address else None,
remote_anchor_script=address_to_script(remote_anchor_address) if remote_anchor_address else None
)
assert sum(x.value for x in c_outputs_filtered) <= funding_sat, (c_outputs_filtered, funding_sat)
@@ -1148,8 +1197,39 @@ def make_commitment_output_to_local_address(
local_script = make_commitment_output_to_local_witness_script(revocation_pubkey, to_self_delay, delayed_pubkey)
return bitcoin.redeem_script_to_address('p2wsh', local_script)
def make_commitment_output_to_remote_address(remote_payment_pubkey: bytes) -> str:
return bitcoin.pubkey_to_address('p2wpkh', remote_payment_pubkey.hex())
def make_commitment_output_to_remote_witness_script(remote_payment_pubkey: bytes) -> bytes:
assert isinstance(remote_payment_pubkey, bytes)
script = construct_script([
remote_payment_pubkey,
opcodes.OP_CHECKSIGVERIFY,
opcodes.OP_1,
opcodes.OP_CHECKSEQUENCEVERIFY,
])
return script
def make_commitment_output_to_remote_address(remote_payment_pubkey: bytes, has_anchors: bool) -> str:
if has_anchors:
remote_script = make_commitment_output_to_remote_witness_script(remote_payment_pubkey)
return bitcoin.redeem_script_to_address('p2wsh', remote_script.hex())
else:
return bitcoin.pubkey_to_address('p2wpkh', remote_payment_pubkey.hex())
def make_commitment_output_to_anchor_witness_script(funding_pubkey: bytes) -> bytes:
assert isinstance(funding_pubkey, bytes)
script = construct_script([
funding_pubkey,
opcodes.OP_CHECKSIG,
opcodes.OP_IFDUP,
opcodes.OP_NOTIF,
opcodes.OP_16,
opcodes.OP_CHECKSEQUENCEVERIFY,
opcodes.OP_ENDIF,
])
return script
def make_commitment_output_to_anchor_address(funding_pubkey: bytes) -> str:
script = make_commitment_output_to_anchor_witness_script(funding_pubkey)
return bitcoin.redeem_script_to_address('p2wsh', script.hex())
def sign_and_get_sig_string(tx: PartialTransaction, local_config, remote_config):
tx.sign({local_config.multisig_key.pubkey: local_config.multisig_key.privkey})

View File

@@ -539,7 +539,9 @@ class TestLNUtil(ElectrumTestCase):
remote_amount=to_remote_msat,
dust_limit_sat=local_dust_limit_satoshi,
fees_per_participant=calc_fees_for_commitment_tx(num_htlcs=len(htlcs), feerate=local_feerate_per_kw, is_local_initiator=True, has_anchors=False),
htlcs=htlcs)
htlcs=htlcs,
has_anchors=False
)
self.sign_and_insert_remote_sig(our_commit_tx, remote_funding_pubkey, remote_signature, local_funding_pubkey, local_funding_privkey)
self.assertEqual(str(our_commit_tx), output_commit_tx)
@@ -643,7 +645,9 @@ class TestLNUtil(ElectrumTestCase):
remote_amount=to_remote_msat,
dust_limit_sat=local_dust_limit_satoshi,
fees_per_participant=calc_fees_for_commitment_tx(num_htlcs=0, feerate=local_feerate_per_kw, is_local_initiator=True, has_anchors=False),
htlcs=[])
htlcs=[],
has_anchors=False
)
self.sign_and_insert_remote_sig(our_commit_tx, remote_funding_pubkey, remote_signature, local_funding_pubkey, local_funding_privkey)
self.assertEqual(str(our_commit_tx), output_commit_tx)
@@ -672,7 +676,9 @@ class TestLNUtil(ElectrumTestCase):
remote_amount=to_remote_msat,
dust_limit_sat=local_dust_limit_satoshi,
fees_per_participant=calc_fees_for_commitment_tx(num_htlcs=0, feerate=local_feerate_per_kw, is_local_initiator=True, has_anchors=False),
htlcs=[])
htlcs=[],
has_anchors=False
)
self.sign_and_insert_remote_sig(our_commit_tx, remote_funding_pubkey, remote_signature, local_funding_pubkey, local_funding_privkey)
self.assertEqual(str(our_commit_tx), output_commit_tx)
@@ -739,7 +745,9 @@ class TestLNUtil(ElectrumTestCase):
remote_amount=to_remote_msat,
dust_limit_sat=local_dust_limit_satoshi,
fees_per_participant=calc_fees_for_commitment_tx(num_htlcs=0, feerate=local_feerate_per_kw, is_local_initiator=True, has_anchors=False),
htlcs=[])
htlcs=[],
has_anchors=False
)
self.sign_and_insert_remote_sig(our_commit_tx, remote_funding_pubkey, remote_signature, local_funding_pubkey, local_funding_privkey)
ref_commit_tx_str = '02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8002c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de84311054a56a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400473044022051b75c73198c6deee1a875871c3961832909acd297c6b908d59e3319e5185a46022055c419379c5051a78d00dbbce11b5b664a0c22815fbcc6fcef6b1937c383693901483045022100f51d2e566a70ba740fc5d8c0f07b9b93d2ed741c3c0860c613173de7d39e7968022041376d520e9c0e1ad52248ddf4b22e12be8763007df977253ef45a4ca3bdb7c001475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220'
self.assertEqual(str(our_commit_tx), ref_commit_tx_str)