1
0

lnchannel: handle htlc-address collisions

We were previously generating an incorrect commitment_signed msg if there were
multiple htlcs sharing the same scriptPubKey.
This commit is contained in:
SomberNight
2019-09-07 07:37:13 +02:00
parent 00f15d491b
commit 83fcdbd561
4 changed files with 219 additions and 157 deletions

View File

@@ -2,7 +2,7 @@
# Distributed under the MIT software license, see the accompanying
# file LICENCE or http://www.opensource.org/licenses/mit-license.php
from typing import Optional, Dict, List, Tuple, TYPE_CHECKING, NamedTuple
from typing import Optional, Dict, List, Tuple, TYPE_CHECKING, NamedTuple, Callable
from enum import Enum, auto
from .util import bfh, bh2u
@@ -13,7 +13,8 @@ from .lnutil import (make_commitment_output_to_remote_address, make_commitment_o
make_htlc_tx_witness, make_htlc_tx_with_open_channel, UpdateAddHtlc,
LOCAL, REMOTE, make_htlc_output_witness_script, UnknownPaymentHash,
get_ordered_channel_configs, privkey_to_pubkey, get_per_commitment_secret_from_seed,
RevocationStore, extract_ctn_from_tx_and_chan, UnableToDeriveSecret, SENT, RECEIVED)
RevocationStore, extract_ctn_from_tx_and_chan, UnableToDeriveSecret, SENT, RECEIVED,
map_htlcs_to_ctx_output_idxs, Direction)
from .transaction import Transaction, TxOutput, construct_witness
from .simple_config import estimate_fee
from .logging import get_logger
@@ -28,7 +29,7 @@ _logger = get_logger(__name__)
def create_sweeptxs_for_watchtower(chan: 'Channel', ctx: Transaction, per_commitment_secret: bytes,
sweep_address: str) -> Dict[str,Transaction]:
sweep_address: str) -> List[Transaction]:
"""Presign sweeping transactions using the just received revoked pcs.
These will only be utilised if the remote breaches.
Sweep 'to_local', and all the HTLCs (two cases: directly from ctx, or from HTLC tx).
@@ -46,8 +47,9 @@ def create_sweeptxs_for_watchtower(chan: 'Channel', ctx: Transaction, per_commit
witness_script = bh2u(make_commitment_output_to_local_witness_script(
revocation_pubkey, to_self_delay, this_delayed_pubkey))
to_local_address = redeem_script_to_address('p2wsh', witness_script)
output_idx = ctx.get_output_idx_from_address(to_local_address)
if output_idx is not None:
output_idxs = ctx.get_output_idxs_from_address(to_local_address)
if output_idxs:
output_idx = output_idxs.pop()
sweep_tx = create_sweeptx_ctx_to_local(
sweep_address=sweep_address,
ctx=ctx,
@@ -58,15 +60,15 @@ def create_sweeptxs_for_watchtower(chan: 'Channel', ctx: Transaction, per_commit
if sweep_tx:
txs.append(sweep_tx)
# HTLCs
def create_sweeptx_for_htlc(htlc: 'UpdateAddHtlc', is_received_htlc: bool) -> Tuple[Optional[Transaction],
Optional[Transaction],
Transaction]:
def create_sweeptx_for_htlc(*, htlc: 'UpdateAddHtlc', htlc_direction: Direction,
ctx_output_idx: int) -> Optional[Transaction]:
htlc_tx_witness_script, htlc_tx = make_htlc_tx_with_open_channel(chan=chan,
pcp=pcp,
for_us=False,
we_receive=not is_received_htlc,
subject=REMOTE,
htlc_direction=htlc_direction,
commit=ctx,
htlc=htlc)
htlc=htlc,
ctx_output_idx=ctx_output_idx)
return create_sweeptx_that_spends_htlctx_that_spends_htlc_in_ctx(
'sweep_from_their_ctx_htlc_',
to_self_delay=0,
@@ -77,23 +79,22 @@ def create_sweeptxs_for_watchtower(chan: 'Channel', ctx: Transaction, per_commit
is_revocation=True)
ctn = extract_ctn_from_tx_and_chan(ctx, chan)
# received HTLCs, in their ctx
received_htlcs = chan.included_htlcs(REMOTE, RECEIVED, ctn)
for htlc in received_htlcs:
secondstage_sweep_tx = create_sweeptx_for_htlc(htlc, is_received_htlc=True)
if secondstage_sweep_tx:
txs.append(secondstage_sweep_tx)
# offered HTLCs, in their ctx
offered_htlcs = chan.included_htlcs(REMOTE, SENT, ctn)
for htlc in offered_htlcs:
secondstage_sweep_tx = create_sweeptx_for_htlc(htlc, is_received_htlc=False)
htlc_to_ctx_output_idx_map = map_htlcs_to_ctx_output_idxs(chan=chan,
ctx=ctx,
pcp=pcp,
subject=REMOTE,
ctn=ctn)
for (direction, htlc), (ctx_output_idx, htlc_relative_idx) in htlc_to_ctx_output_idx_map.items():
secondstage_sweep_tx = create_sweeptx_for_htlc(htlc=htlc,
htlc_direction=direction,
ctx_output_idx=ctx_output_idx)
if secondstage_sweep_tx:
txs.append(secondstage_sweep_tx)
return txs
def create_sweeptx_for_their_revoked_ctx(chan: 'Channel', ctx: Transaction, per_commitment_secret: bytes,
sweep_address: str) -> Dict[str,Transaction]:
sweep_address: str) -> Optional[Callable[[], Optional[Transaction]]]:
# prep
pcp = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True)
this_conf, other_conf = get_ordered_channel_configs(chan=chan, for_us=False)
@@ -107,8 +108,9 @@ def create_sweeptx_for_their_revoked_ctx(chan: 'Channel', ctx: Transaction, per_
witness_script = bh2u(make_commitment_output_to_local_witness_script(
revocation_pubkey, to_self_delay, this_delayed_pubkey))
to_local_address = redeem_script_to_address('p2wsh', witness_script)
output_idx = ctx.get_output_idx_from_address(to_local_address)
if output_idx is not None:
output_idxs = ctx.get_output_idxs_from_address(to_local_address)
if output_idxs:
output_idx = output_idxs.pop()
sweep_tx = lambda: create_sweeptx_ctx_to_local(
sweep_address=sweep_address,
ctx=ctx,
@@ -117,9 +119,10 @@ def create_sweeptx_for_their_revoked_ctx(chan: 'Channel', ctx: Transaction, per_
privkey=other_revocation_privkey,
is_revocation=True)
return sweep_tx
return None
def create_sweeptx_for_their_revoked_htlc(chan: 'Channel', ctx: Transaction, htlc_tx: Transaction,
sweep_address: str) -> Dict[str,Transaction]:
sweep_address: str) -> Optional[Tuple[str, int, int, Callable]]:
x = analyze_ctx(chan, ctx)
if not x:
return
@@ -154,8 +157,8 @@ def create_sweeptx_for_their_revoked_htlc(chan: 'Channel', ctx: Transaction, htl
def create_sweeptxs_for_our_ctx(chan: 'Channel', ctx: Transaction, ctn: int,
sweep_address: str) -> Dict[str,Transaction]:
def create_sweeptxs_for_our_ctx(*, chan: 'Channel', ctx: Transaction,
sweep_address: str) -> Optional[Dict[str, Tuple]]:
"""Handle the case where we force close unilaterally with our latest ctx.
Construct sweep txns for 'to_local', and for all HTLCs (2 txns each).
'to_local' can be swept even if this is a breach (by us),
@@ -181,8 +184,8 @@ def create_sweeptxs_for_our_ctx(chan: 'Channel', ctx: Transaction, ctn: int,
to_remote_address = make_commitment_output_to_remote_address(their_payment_pubkey)
# test ctx
_logger.debug(f'testing our ctx: {to_local_address} {to_remote_address}')
if ctx.get_output_idx_from_address(to_local_address) is None\
and ctx.get_output_idx_from_address(to_remote_address) is None:
if not ctx.get_output_idxs_from_address(to_local_address) \
and not ctx.get_output_idxs_from_address(to_remote_address):
return
# we have to_local, to_remote.
# other outputs are htlcs
@@ -193,8 +196,9 @@ def create_sweeptxs_for_our_ctx(chan: 'Channel', ctx: Transaction, ctn: int,
return {}
txs = {}
# to_local
output_idx = ctx.get_output_idx_from_address(to_local_address)
if output_idx is not None:
output_idxs = ctx.get_output_idxs_from_address(to_local_address)
if output_idxs:
output_idx = output_idxs.pop()
sweep_tx = lambda: create_sweeptx_ctx_to_local(
sweep_address=sweep_address,
ctx=ctx,
@@ -206,13 +210,14 @@ def create_sweeptxs_for_our_ctx(chan: 'Channel', ctx: Transaction, ctn: int,
prevout = ctx.txid() + ':%d'%output_idx
txs[prevout] = ('our_ctx_to_local', to_self_delay, 0, sweep_tx)
# HTLCs
def create_txns_for_htlc(htlc: 'UpdateAddHtlc', is_received_htlc: bool) -> Tuple[Optional[Transaction], Optional[Transaction]]:
if is_received_htlc:
def create_txns_for_htlc(*, htlc: 'UpdateAddHtlc', htlc_direction: Direction,
ctx_output_idx: int, htlc_relative_idx: int):
if htlc_direction == RECEIVED:
try:
preimage = chan.lnworker.get_preimage(htlc.payment_hash)
except UnknownPaymentHash as e:
_logger.info(f'trying to sweep htlc from our latest ctx but getting {repr(e)}')
return None, None
return
else:
preimage = None
htlctx_witness_script, htlc_tx = create_htlctx_that_spends_from_our_ctx(
@@ -222,7 +227,9 @@ def create_sweeptxs_for_our_ctx(chan: 'Channel', ctx: Transaction, ctn: int,
htlc=htlc,
local_htlc_privkey=our_htlc_privkey,
preimage=preimage,
is_received_htlc=is_received_htlc)
htlc_direction=htlc_direction,
ctx_output_idx=ctx_output_idx,
htlc_relative_idx=htlc_relative_idx)
sweep_tx = lambda: create_sweeptx_that_spends_htlctx_that_spends_htlc_in_ctx(
'our_ctx_htlc_',
to_self_delay=to_self_delay,
@@ -237,12 +244,16 @@ def create_sweeptxs_for_our_ctx(chan: 'Channel', ctx: Transaction, ctn: int,
# offered HTLCs, in our ctx --> "timeout"
# received HTLCs, in our ctx --> "success"
offered_htlcs = chan.included_htlcs(LOCAL, SENT, ctn) # type: List[UpdateAddHtlc]
received_htlcs = chan.included_htlcs(LOCAL, RECEIVED, ctn) # type: List[UpdateAddHtlc]
for htlc in offered_htlcs:
create_txns_for_htlc(htlc, is_received_htlc=False)
for htlc in received_htlcs:
create_txns_for_htlc(htlc, is_received_htlc=True)
htlc_to_ctx_output_idx_map = map_htlcs_to_ctx_output_idxs(chan=chan,
ctx=ctx,
pcp=our_pcp,
subject=LOCAL,
ctn=ctn)
for (direction, htlc), (ctx_output_idx, htlc_relative_idx) in htlc_to_ctx_output_idx_map.items():
create_txns_for_htlc(htlc=htlc,
htlc_direction=direction,
ctx_output_idx=ctx_output_idx,
htlc_relative_idx=htlc_relative_idx)
return txs
def analyze_ctx(chan: 'Channel', ctx: Transaction):
@@ -273,8 +284,8 @@ def analyze_ctx(chan: 'Channel', ctx: Transaction):
return
return ctn, their_pcp, is_revocation, per_commitment_secret
def create_sweeptxs_for_their_ctx(chan: 'Channel', ctx: Transaction, ctn: int,
sweep_address: str) -> Dict[str,Transaction]:
def create_sweeptxs_for_their_ctx(*, chan: 'Channel', ctx: Transaction,
sweep_address: str) -> Optional[Dict[str,Tuple]]:
"""Handle the case when the remote force-closes with their ctx.
Sweep outputs that do not have a CSV delay ('to_remote' and first-stage HTLCs).
Outputs with CSV delay ('to_local' and second-stage HTLCs) are redeemed by LNWatcher.
@@ -295,8 +306,8 @@ def create_sweeptxs_for_their_ctx(chan: 'Channel', ctx: Transaction, ctn: int,
to_remote_address = make_commitment_output_to_remote_address(our_payment_pubkey)
# test if this is their ctx
_logger.debug(f'testing their ctx: {to_local_address} {to_remote_address}')
if ctx.get_output_idx_from_address(to_local_address) is None \
and ctx.get_output_idx_from_address(to_remote_address) is None:
if not ctx.get_output_idxs_from_address(to_local_address) \
and not ctx.get_output_idxs_from_address(to_remote_address):
return
if is_revocation:
@@ -315,8 +326,9 @@ def create_sweeptxs_for_their_ctx(chan: 'Channel', ctx: Transaction, ctn: int,
assert our_payment_pubkey == our_payment_privkey.get_public_key_bytes(compressed=True)
# to_local is handled by lnwatcher
# to_remote
output_idx = ctx.get_output_idx_from_address(to_remote_address)
if output_idx is not None:
output_idxs = ctx.get_output_idxs_from_address(to_remote_address)
if output_idxs:
output_idx = output_idxs.pop()
prevout = ctx.txid() + ':%d'%output_idx
sweep_tx = lambda: create_sweeptx_their_ctx_to_remote(
sweep_address=sweep_address,
@@ -325,13 +337,14 @@ def create_sweeptxs_for_their_ctx(chan: 'Channel', ctx: Transaction, ctn: int,
our_payment_privkey=our_payment_privkey)
txs[prevout] = ('their_ctx_to_remote', 0, 0, sweep_tx)
# HTLCs
def create_sweeptx_for_htlc(htlc: 'UpdateAddHtlc', is_received_htlc: bool) -> Optional[Transaction]:
def create_sweeptx_for_htlc(htlc: 'UpdateAddHtlc', is_received_htlc: bool,
ctx_output_idx: int) -> None:
if not is_received_htlc and not is_revocation:
try:
preimage = chan.lnworker.get_preimage(htlc.payment_hash)
except UnknownPaymentHash as e:
_logger.info(f'trying to sweep htlc from their latest ctx but getting {repr(e)}')
return None
return
else:
preimage = None
htlc_output_witness_script = make_htlc_output_witness_script(
@@ -341,50 +354,51 @@ def create_sweeptxs_for_their_ctx(chan: 'Channel', ctx: Transaction, ctn: int,
local_htlc_pubkey=their_htlc_pubkey,
payment_hash=htlc.payment_hash,
cltv_expiry=htlc.cltv_expiry)
htlc_address = redeem_script_to_address('p2wsh', bh2u(htlc_output_witness_script))
# FIXME handle htlc_address collision
# also: https://github.com/lightningnetwork/lightning-rfc/issues/448
output_idx = ctx.get_output_idx_from_address(htlc_address)
if output_idx is not None:
cltv_expiry = htlc.cltv_expiry if is_received_htlc and not is_revocation else 0
prevout = ctx.txid() + ':%d'%output_idx
sweep_tx = lambda: create_sweeptx_their_ctx_htlc(
ctx=ctx,
witness_script=htlc_output_witness_script,
sweep_address=sweep_address,
preimage=preimage,
output_idx=output_idx,
privkey=our_revocation_privkey if is_revocation else our_htlc_privkey.get_secret_bytes(),
is_revocation=is_revocation,
cltv_expiry=cltv_expiry)
name = f'their_ctx_htlc_{output_idx}'
txs[prevout] = (name, 0, cltv_expiry, sweep_tx)
cltv_expiry = htlc.cltv_expiry if is_received_htlc and not is_revocation else 0
prevout = ctx.txid() + ':%d'%ctx_output_idx
sweep_tx = lambda: create_sweeptx_their_ctx_htlc(
ctx=ctx,
witness_script=htlc_output_witness_script,
sweep_address=sweep_address,
preimage=preimage,
output_idx=ctx_output_idx,
privkey=our_revocation_privkey if is_revocation else our_htlc_privkey.get_secret_bytes(),
is_revocation=is_revocation,
cltv_expiry=cltv_expiry)
name = f'their_ctx_htlc_{ctx_output_idx}'
txs[prevout] = (name, 0, cltv_expiry, sweep_tx)
# received HTLCs, in their ctx --> "timeout"
received_htlcs = chan.included_htlcs(REMOTE, RECEIVED, ctn=ctn) # type: List[UpdateAddHtlc]
for htlc in received_htlcs:
create_sweeptx_for_htlc(htlc, is_received_htlc=True)
# offered HTLCs, in their ctx --> "success"
offered_htlcs = chan.included_htlcs(REMOTE, SENT, ctn=ctn) # type: List[UpdateAddHtlc]
for htlc in offered_htlcs:
create_sweeptx_for_htlc(htlc, is_received_htlc=False)
htlc_to_ctx_output_idx_map = map_htlcs_to_ctx_output_idxs(chan=chan,
ctx=ctx,
pcp=their_pcp,
subject=REMOTE,
ctn=ctn)
for (direction, htlc), (ctx_output_idx, htlc_relative_idx) in htlc_to_ctx_output_idx_map.items():
create_sweeptx_for_htlc(htlc=htlc,
is_received_htlc=direction == RECEIVED,
ctx_output_idx=ctx_output_idx)
return txs
def create_htlctx_that_spends_from_our_ctx(chan: 'Channel', our_pcp: bytes,
ctx: Transaction, htlc: 'UpdateAddHtlc',
local_htlc_privkey: bytes, preimage: Optional[bytes],
is_received_htlc: bool) -> Tuple[bytes, Transaction]:
assert is_received_htlc == bool(preimage), 'preimage is required iff htlc is received'
htlc_direction: Direction, htlc_relative_idx: int,
ctx_output_idx: int) -> Tuple[bytes, Transaction]:
assert (htlc_direction == RECEIVED) == bool(preimage), 'preimage is required iff htlc is received'
preimage = preimage or b''
witness_script, htlc_tx = make_htlc_tx_with_open_channel(chan=chan,
pcp=our_pcp,
for_us=True,
we_receive=is_received_htlc,
subject=LOCAL,
htlc_direction=htlc_direction,
commit=ctx,
htlc=htlc,
name=f'our_ctx_htlc_tx_{bh2u(htlc.payment_hash)}',
cltv_expiry=0 if is_received_htlc else htlc.cltv_expiry)
remote_htlc_sig = chan.get_remote_htlc_sig_for_htlc(htlc, we_receive=is_received_htlc, ctx=ctx)
ctx_output_idx=ctx_output_idx,
name=f'our_ctx_{ctx_output_idx}_htlc_tx_{bh2u(htlc.payment_hash)}')
remote_htlc_sig = chan.get_remote_htlc_sig_for_htlc(htlc_relative_idx=htlc_relative_idx)
local_htlc_sig = bfh(htlc_tx.sign_txin(0, local_htlc_privkey))
txin = htlc_tx.inputs()[0]
witness_program = bfh(Transaction.get_preimage_script(txin))