Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Anchor commitments, new payment basepoint #7509

Closed
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7926219
prepare a channel to have anchors
bitromortac Sep 13, 2021
6426e3a
add static payment key
bitromortac Sep 22, 2021
a3cd1fd
lnutil: update ctx fee calculation for anchors
bitromortac Sep 13, 2021
85c6a83
lnchannel+lnutil: change htlc output, send new sig
bitromortac Sep 13, 2021
7816110
lnutil+lnchannel: add anchors, adapt to_remote
bitromortac Sep 13, 2021
3500246
tests: add anchor commitment test vectors from rfc
bitromortac Sep 13, 2021
81d7556
lnsweep: update sweeps to_remote and htlcs
bitromortac Sep 13, 2021
8a7ea74
lnwatcher: renaming and comments for clarity
bitromortac Sep 13, 2021
63d0a6a
lnwatcher: fix early cltv-locked claiming
bitromortac Sep 13, 2021
f456024
lnwatcher: add field for onchain htlc settlement control
bitromortac Sep 13, 2021
b88585b
backups: restore from closing tx, sweep to_remote
bitromortac Sep 15, 2021
1d89e4e
qt: add anchor channel icon
bitromortac Oct 11, 2021
205d6cb
enable anchor outputs via config option
bitromortac Sep 13, 2021
6326817
unit tests: test anchors in lnpeer and lnchannel
bitromortac Sep 13, 2021
cfb4a10
regtest: adapt to anchor channels
bitromortac Sep 15, 2021
421f32d
anchors: switch to zero-fee-htlcs
bitromortac Oct 15, 2021
3f9c530
tests: tests for both anchors and old ctx types
bitromortac Nov 8, 2021
f9210b4
sweep: rename sweep creation functions and reorder
bitromortac Nov 12, 2021
f4ebe6e
htlctx: deal with possible peer htlctx batching
bitromortac Nov 15, 2021
5cb97c0
watchtower: only send first-stage HTLC justice txs
bitromortac Nov 15, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 81 additions & 29 deletions electrum/lnchannel.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
from .invoices import PR_PAID
from .bitcoin import redeem_script_to_address
from .crypto import sha256, sha256d
from .transaction import Transaction, PartialTransaction, TxInput
from .transaction import Transaction, PartialTransaction, TxInput, Sighash
from .logging import Logger
from .lnonion import decode_onion_error, OnionFailureCode, OnionRoutingFailure
from . import lnutil
Expand All @@ -52,7 +52,7 @@
ScriptHtlc, PaymentFailure, calc_fees_for_commitment_tx, RemoteMisbehaving, make_htlc_output_witness_script,
ShortChannelID, map_htlcs_to_ctx_output_idxs, LNPeerAddr,
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)
from .lnsweep import create_sweeptxs_for_our_ctx, create_sweeptxs_for_their_ctx
from .lnsweep import create_sweeptx_for_their_revoked_htlc, SweepInfo
from .lnhtlc import HTLCManager
Expand Down Expand Up @@ -323,9 +323,11 @@ def update_closed_state(self, *, funding_txid: str, funding_height: TxMinedInfo,
def sweep_address(self) -> str:
# TODO: in case of unilateral close with pending HTLCs, this address will be reused
addr = None
if self.is_static_remotekey_enabled():
if self.has_anchors():
addr = self.lnworker.wallet.get_new_sweep_address_for_channel()
elif 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=False)
if addr is None:
addr = self._fallback_sweep_address
assert addr
Expand Down Expand Up @@ -399,6 +401,10 @@ def is_frozen_for_receiving(self) -> bool:
def is_static_remotekey_enabled(self) -> bool:
pass

@abstractmethod
def has_anchors(self) -> bool:
pass

@abstractmethod
def get_local_pubkey(self) -> bytes:
"""Returns our node ID."""
Expand Down Expand Up @@ -438,8 +444,13 @@ def init_config(self, cb):
self.config[LOCAL] = LocalConfig.from_seed(
channel_seed=cb.channel_seed,
to_self_delay=cb.local_delay,
# there are three cases of backups:
# 1. legacy: payment_basepoint will be derived
# 2. static_remotekey: to_remote sweep not necessary due to wallet address
# 3. anchor outputs: sweep to_remote by deriving the key from the funding pubkeys
# dummy values
static_remotekey=None,
static_payment_key=None,
dust_limit_sat=None,
max_htlc_value_in_flight_msat=None,
max_accepted_htlcs=None,
Expand Down Expand Up @@ -526,6 +537,9 @@ def is_static_remotekey_enabled(self) -> bool:
# their local config is not static)
return False

def has_anchors(self) -> Optional[bool]:
return None

def get_local_pubkey(self) -> bytes:
cb = self.cb
assert isinstance(cb, ChannelBackupStorage)
Expand Down Expand Up @@ -711,11 +725,14 @@ def construct_channel_announcement_without_sigs(self) -> bytes:
def is_static_remotekey_enabled(self) -> bool:
return bool(self.storage.get('static_remotekey_enabled'))

def has_anchors(self) -> bool:
return bool(self.storage.get('has_anchors'))

def get_wallet_addresses_channel_might_want_reserved(self) -> Sequence[str]:
ret = []
if 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())
ret.append(to_remote_address)
return ret

Expand Down Expand Up @@ -951,6 +968,10 @@ def sign_next_commitment(self) -> Tuple[bytes, Sequence[bytes]]:
commit=pending_remote_commitment,
ctx_output_idx=ctx_output_idx,
htlc=htlc)
if self.has_anchors():
# we send a signature with the following sighash flags
# for the peer to be able to replace inputs and outputs
htlc_tx.inputs()[0].sighash = Sighash.ANYONECANPAY | Sighash.SINGLE
sig = bfh(htlc_tx.sign_txin(0, their_remote_htlc_privkey))
htlc_sig = ecc.sig_string_from_der_sig(sig[:-1])
htlcsigs.append((ctx_output_idx, htlc_sig))
Expand Down Expand Up @@ -1012,6 +1033,9 @@ def _verify_htlc_sig(self, *, htlc: UpdateAddHtlc, htlc_sig: bytes, htlc_directi
commit=ctx,
ctx_output_idx=ctx_output_idx,
htlc=htlc)
if self.has_anchors():
# peer sent us a signature for our ctx using anchor sighash flags
htlc_tx.inputs()[0].sighash = Sighash.ANYONECANPAY | Sighash.SINGLE
pre_hash = sha256d(bfh(htlc_tx.serialize_preimage(0)))
remote_htlc_pubkey = derive_pubkey(self.config[REMOTE].htlc_basepoint.pubkey, pcp)
if not ecc.verify_signature(remote_htlc_pubkey, htlc_sig, pre_hash):
Expand All @@ -1021,7 +1045,8 @@ def get_remote_htlc_sig_for_htlc(self, *, htlc_relative_idx: int) -> bytes:
data = self.config[LOCAL].current_htlc_signatures
htlc_sigs = list(chunks(data, 64))
htlc_sig = htlc_sigs[htlc_relative_idx]
remote_htlc_sig = ecc.der_sig_from_sig_string(htlc_sig) + b'\x01'
remote_sighash = Sighash.ALL if not self.has_anchors() else Sighash.ANYONECANPAY | Sighash.SINGLE
remote_htlc_sig = ecc.der_sig_from_sig_string(htlc_sig) + remote_sighash.to_bytes(1, 'big')
return remote_htlc_sig

def revoke_current_commitment(self):
Expand Down Expand Up @@ -1142,7 +1167,7 @@ def balance_tied_up_in_htlcs_by_direction(self, ctx_owner: HTLCOwner = LOCAL, *,
return htlcsum(self.hm.htlcs_by_direction(ctx_owner, direction, ctn).values())

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
Expand All @@ -1163,28 +1188,47 @@ def consider_ctx(*, ctx_owner: HTLCOwner, is_htlc_dust: bool) -> int:
feerate=feerate,
is_local_initiator=self.constraints.is_initiator,
round_to_sat=False,
has_anchors=self.has_anchors()
)
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) * 1000
if sender == initiator == LOCAL: # see /~https://github.com/lightningnetwork/lightning-rfc/pull/740
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

# 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,
)[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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: needs to be multiplied by 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
if new_receiver_balance < 0:
return 0
return max_send_msat

Expand All @@ -1203,7 +1247,7 @@ def consider_ctx(*, ctx_owner: HTLCOwner, is_htlc_dust: bool) -> int:


def included_htlcs(self, subject: HTLCOwner, direction: Direction, ctn: int = None, *,
feerate: int = None) -> Sequence[UpdateAddHtlc]:
feerate: int = None) -> List[UpdateAddHtlc]:
"""Returns list of non-dust HTLCs for subject's commitment tx at ctn,
filtered by direction (of HTLCs).
"""
Expand All @@ -1215,9 +1259,9 @@ def included_htlcs(self, subject: HTLCOwner, direction: Direction, ctn: int = No
feerate = self.get_feerate(subject, ctn=ctn)
conf = self.config[subject]
if direction == RECEIVED:
threshold_sat = received_htlc_trim_threshold_sat(dust_limit_sat=conf.dust_limit_sat, feerate=feerate)
threshold_sat = received_htlc_trim_threshold_sat(dust_limit_sat=conf.dust_limit_sat, feerate=feerate, has_anchors=self.has_anchors())
else:
threshold_sat = offered_htlc_trim_threshold_sat(dust_limit_sat=conf.dust_limit_sat, feerate=feerate)
threshold_sat = offered_htlc_trim_threshold_sat(dust_limit_sat=conf.dust_limit_sat, feerate=feerate, has_anchors=self.has_anchors())
htlcs = self.hm.htlcs_by_direction(subject, direction, ctn=ctn).values()
return list(filter(lambda htlc: htlc.amount_msat // 1000 >= threshold_sat, htlcs))

Expand Down Expand Up @@ -1360,6 +1404,7 @@ def update_fee(self, feerate: int, from_us: bool) -> None:
num_htlcs=num_htlcs_in_ctx,
feerate=feerate,
is_local_initiator=self.constraints.is_initiator,
has_anchors=self.has_anchors()
)
remainder = sender_balance_msat - sender_reserve_msat - ctx_fees_msat[sender]
if remainder < 0:
Expand Down Expand Up @@ -1402,13 +1447,15 @@ def make_commitment(self, subject: HTLCOwner, this_point: bytes, ctn: int) -> Pa
remote_htlc_pubkey=other_htlc_pubkey,
local_htlc_pubkey=this_htlc_pubkey,
payment_hash=htlc.payment_hash,
cltv_expiry=htlc.cltv_expiry), htlc))
cltv_expiry=htlc.cltv_expiry,
has_anchors=self.has_anchors()), htlc))
# note: maybe flip initiator here for fee purposes, we want LOCAL and REMOTE
# in the resulting dict to correspond to the to_local and to_remote *outputs* of the ctx
onchain_fees = calc_fees_for_commitment_tx(
num_htlcs=len(htlcs),
feerate=feerate,
is_local_initiator=self.constraints.is_initiator == (subject == LOCAL),
has_anchors=self.has_anchors(),
)

if self.is_static_remotekey_enabled():
Expand All @@ -1434,22 +1481,27 @@ def make_commitment(self, subject: HTLCOwner, this_point: bytes, ctn: int) -> Pa
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=bh2u(local_script),
remote_script=bh2u(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=bh2u(local_script),
remote_script=bh2u(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,
Expand Down
22 changes: 17 additions & 5 deletions electrum/lnpeer.py
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,9 @@ def is_static_remotekey(self):
def is_upfront_shutdown_script(self):
return self.features.supports(LnFeatures.OPTION_UPFRONT_SHUTDOWN_SCRIPT_OPT)

def use_anchors(self) -> bool:
return self.features.supports(LnFeatures.OPTION_ANCHOR_OUTPUTS_OPT)

def upfront_shutdown_script_from_payload(self, payload, msg_identifier: str) -> Optional[bytes]:
if msg_identifier not in ['accept', 'open']:
raise ValueError("msg_identifier must be either 'accept' or 'open'")
Expand All @@ -534,12 +537,17 @@ def make_local_config(self, funding_sat: int, push_msat: int, initiator: HTLCOwn
# flexibility to decide an address at closing time
upfront_shutdown_script = b''

if self.is_static_remotekey():
wallet = self.lnworker.wallet
assert wallet.txin_type == 'p2wpkh'
addr = wallet.get_new_sweep_address_for_channel()
static_remotekey = bfh(wallet.get_public_key(addr))
if self.use_anchors():
static_payment_key = self.lnworker.static_payment_key
static_remotekey = None
elif self.is_static_remotekey():
wallet = self.lnworker.wallet
assert wallet.txin_type == 'p2wpkh'
addr = wallet.get_new_sweep_address_for_channel()
static_payment_key = None
static_remotekey = bfh(wallet.get_public_key(addr))
else:
static_payment_key = None
static_remotekey = None
dust_limit_sat = bitcoin.DUST_LIMIT_P2PKH
reserve_sat = max(funding_sat // 100, dust_limit_sat)
Expand All @@ -550,6 +558,7 @@ def make_local_config(self, funding_sat: int, push_msat: int, initiator: HTLCOwn
local_config = LocalConfig.from_seed(
channel_seed=channel_seed,
static_remotekey=static_remotekey,
static_payment_key=static_payment_key,
upfront_shutdown_script=upfront_shutdown_script,
to_self_delay=self.network.config.get('lightning_to_self_delay', 7 * 144),
dust_limit_sat=dust_limit_sat,
Expand Down Expand Up @@ -680,6 +689,7 @@ async def channel_establishment_flow(
funding_sat=funding_sat,
is_local_initiator=True,
initial_feerate_per_kw=feerate,
has_anchors=self.use_anchors(),
)

# -> funding created
Expand Down Expand Up @@ -770,6 +780,7 @@ def create_channel_storage(self, channel_id, outpoint, local_config, remote_conf
"unfulfilled_htlcs": {}, # htlc_id -> error_bytes, failure_message
"revocation_store": {},
"static_remotekey_enabled": self.is_static_remotekey(), # stored because it cannot be "downgraded", per BOLT2
"has_anchors": self.use_anchors(),
}
return StoredDict(chan_dict, self.lnworker.db if self.lnworker else None, [])

Expand Down Expand Up @@ -821,6 +832,7 @@ async def on_open_channel(self, payload):
funding_sat=funding_sat,
is_local_initiator=False,
initial_feerate_per_kw=feerate,
has_anchors=self.use_anchors(),
)

# note: we ignore payload['channel_flags'], which e.g. contains 'announce_channel'.
Expand Down
Loading