Perform breach remedy without sweepstore:

- add functions to lnsweep
 - lnworker: analyze candidate ctx and htlc_tx
 - watchtower will be optional
 - add test for breach remedy with spent htlcs
 - save tx name as label
This commit is contained in:
ThomasV 2019-06-24 11:13:18 +02:00
parent 238f3c949c
commit a8ce8109be
8 changed files with 280 additions and 95 deletions

View file

@ -46,7 +46,8 @@ from .lnutil import (Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKey
HTLC_TIMEOUT_WEIGHT, HTLC_SUCCESS_WEIGHT, extract_ctn_from_tx_and_chan, UpdateAddHtlc, HTLC_TIMEOUT_WEIGHT, HTLC_SUCCESS_WEIGHT, extract_ctn_from_tx_and_chan, UpdateAddHtlc,
funding_output_script, SENT, RECEIVED, LOCAL, REMOTE, HTLCOwner, make_commitment_outputs, funding_output_script, SENT, RECEIVED, LOCAL, REMOTE, HTLCOwner, make_commitment_outputs,
ScriptHtlc, PaymentFailure, calc_onchain_fees, RemoteMisbehaving, make_htlc_output_witness_script) ScriptHtlc, PaymentFailure, calc_onchain_fees, RemoteMisbehaving, make_htlc_output_witness_script)
from .lnsweep import create_sweeptxs_for_their_revoked_ctx, create_sweeptxs_for_our_ctx, create_sweeptxs_for_their_ctx from .lnsweep import create_sweeptxs_for_our_ctx, create_sweeptxs_for_their_ctx
from .lnsweep import create_sweeptx_for_their_revoked_htlc
from .lnhtlc import HTLCManager from .lnhtlc import HTLCManager
@ -165,7 +166,7 @@ class Channel(Logger):
self.set_state('DISCONNECTED') self.set_state('DISCONNECTED')
self.local_commitment = None self.local_commitment = None
self.remote_commitment = None self.remote_commitment = None
self.sweep_info = None self.sweep_info = {}
def get_payments(self): def get_payments(self):
out = {} out = {}
@ -450,12 +451,6 @@ class Channel(Logger):
point = secret_to_pubkey(int.from_bytes(secret, 'big')) point = secret_to_pubkey(int.from_bytes(secret, 'big'))
return secret, point return secret, point
def process_new_revocation_secret(self, per_commitment_secret: bytes):
outpoint = self.funding_outpoint.to_str()
ctx = self.remote_commitment_to_be_revoked # FIXME can't we just reconstruct it?
sweeptxs = create_sweeptxs_for_their_revoked_ctx(self, ctx, per_commitment_secret, self.sweep_address)
return sweeptxs
def receive_revocation(self, revocation: RevokeAndAck): def receive_revocation(self, revocation: RevokeAndAck):
self.logger.info("receive_revocation") self.logger.info("receive_revocation")
@ -469,16 +464,7 @@ class Channel(Logger):
# this might break # this might break
prev_remote_commitment = self.pending_commitment(REMOTE) prev_remote_commitment = self.pending_commitment(REMOTE)
self.config[REMOTE].revocation_store.add_next_entry(revocation.per_commitment_secret) self.config[REMOTE].revocation_store.add_next_entry(revocation.per_commitment_secret)
# be robust to exceptions raised in lnwatcher
try:
sweeptxs = self.process_new_revocation_secret(revocation.per_commitment_secret)
except Exception as e:
self.logger.info("Could not process revocation secret: {}".format(repr(e)))
sweeptxs = []
##### start applying fee/htlc changes ##### start applying fee/htlc changes
if self.pending_fee is not None: if self.pending_fee is not None:
if not self.constraints.is_initiator: if not self.constraints.is_initiator:
self.pending_fee[FUNDEE_SIGNED] = True self.pending_fee[FUNDEE_SIGNED] = True
@ -501,8 +487,6 @@ class Channel(Logger):
self.set_remote_commitment() self.set_remote_commitment()
self.remote_commitment_to_be_revoked = prev_remote_commitment self.remote_commitment_to_be_revoked = prev_remote_commitment
# return sweep transactions for watchtower
return sweeptxs
def balance(self, whose, *, ctx_owner=HTLCOwner.LOCAL, ctn=None): def balance(self, whose, *, ctx_owner=HTLCOwner.LOCAL, ctn=None):
""" """
@ -810,19 +794,23 @@ class Channel(Logger):
assert tx.is_complete() assert tx.is_complete()
return tx return tx
def get_sweep_info(self, ctx: Transaction): def sweep_ctx(self, ctx: Transaction):
if self.sweep_info is None: txid = ctx.txid()
if self.sweep_info.get(txid) is None:
ctn = extract_ctn_from_tx_and_chan(ctx, self) ctn = extract_ctn_from_tx_and_chan(ctx, self)
our_sweep_info = create_sweeptxs_for_our_ctx(self, ctx, ctn, self.sweep_address) our_sweep_info = create_sweeptxs_for_our_ctx(self, ctx, ctn, self.sweep_address)
their_sweep_info = create_sweeptxs_for_their_ctx(self, ctx, ctn, self.sweep_address) their_sweep_info = create_sweeptxs_for_their_ctx(self, ctx, ctn, self.sweep_address)
if our_sweep_info: if our_sweep_info is not None:
self.sweep_info = our_sweep_info self.sweep_info[txid] = our_sweep_info
self.logger.info(f'we force closed.') self.logger.info(f'we force closed.')
elif their_sweep_info: elif their_sweep_info is not None:
self.sweep_info = their_sweep_info self.sweep_info[txid] = their_sweep_info
self.logger.info(f'they force closed.') self.logger.info(f'they force closed.')
else: else:
self.sweep_info = {} self.sweep_info[txid] = {}
self.logger.info(f'not sure who closed {ctx}.') self.logger.info(f'not sure who closed {ctx}.')
self.logger.info(f'{repr(self.sweep_info)}') return self.sweep_info[txid]
return self.sweep_info
def sweep_htlc(self, ctx:Transaction, htlc_tx: Transaction):
# look at the output address, check if it matches
return create_sweeptx_for_their_revoked_htlc(self, ctx, htlc_tx, self.sweep_address)

View file

@ -40,6 +40,7 @@ from .lnutil import (Outpoint, LocalConfig, RECEIVED, UpdateAddHtlc,
LightningPeerConnectionClosed, HandshakeFailed, NotFoundChanAnnouncementForUpdate, LightningPeerConnectionClosed, HandshakeFailed, NotFoundChanAnnouncementForUpdate,
MINIMUM_MAX_HTLC_VALUE_IN_FLIGHT_ACCEPTED, MAXIMUM_HTLC_MINIMUM_MSAT_ACCEPTED, MINIMUM_MAX_HTLC_VALUE_IN_FLIGHT_ACCEPTED, MAXIMUM_HTLC_MINIMUM_MSAT_ACCEPTED,
MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED, RemoteMisbehaving, DEFAULT_TO_SELF_DELAY) MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED, RemoteMisbehaving, DEFAULT_TO_SELF_DELAY)
from .lnsweep import create_sweeptxs_for_watchtower
from .lntransport import LNTransport, LNTransportBase from .lntransport import LNTransport, LNTransportBase
from .lnmsg import encode_msg, decode_msg from .lnmsg import encode_msg, decode_msg
from .interface import GracefulDisconnect from .interface import GracefulDisconnect
@ -1261,15 +1262,20 @@ class Peer(Logger):
self.logger.info("on_revoke_and_ack") self.logger.info("on_revoke_and_ack")
channel_id = payload["channel_id"] channel_id = payload["channel_id"]
chan = self.channels[channel_id] chan = self.channels[channel_id]
sweeptxs = chan.receive_revocation(RevokeAndAck(payload["per_commitment_secret"], payload["next_per_commitment_point"])) ctx = chan.remote_commitment_to_be_revoked # FIXME can't we just reconstruct it?
rev = RevokeAndAck(payload["per_commitment_secret"], payload["next_per_commitment_point"])
chan.receive_revocation(rev)
self._remote_changed_events[chan.channel_id].set() self._remote_changed_events[chan.channel_id].set()
self._remote_changed_events[chan.channel_id].clear() self._remote_changed_events[chan.channel_id].clear()
self.lnworker.save_channel(chan) self.lnworker.save_channel(chan)
self.maybe_send_commitment(chan) self.maybe_send_commitment(chan)
asyncio.ensure_future(self._on_revoke_and_ack(chan, sweeptxs)) asyncio.ensure_future(self._on_revoke_and_ack(chan, ctx, rev.per_commitment_secret))
async def _on_revoke_and_ack(self, chan, sweeptxs): @ignore_exceptions
@log_exceptions
async def _on_revoke_and_ack(self, chan, ctx, per_commitment_secret):
outpoint = chan.funding_outpoint.to_str() outpoint = chan.funding_outpoint.to_str()
sweeptxs = create_sweeptxs_for_watchtower(chan, ctx, per_commitment_secret, chan.sweep_address)
for tx in sweeptxs: for tx in sweeptxs:
await self.lnwatcher.add_sweep_tx(outpoint, tx.prevout(0), str(tx)) await self.lnwatcher.add_sweep_tx(outpoint, tx.prevout(0), str(tx))

View file

@ -27,8 +27,8 @@ _logger = get_logger(__name__)
def create_sweeptxs_for_their_revoked_ctx(chan: 'Channel', ctx: Transaction, per_commitment_secret: bytes, def create_sweeptxs_for_watchtower(chan: 'Channel', ctx: Transaction, per_commitment_secret: bytes,
sweep_address: str) -> Dict[str,Transaction]: sweep_address: str) -> Dict[str,Transaction]:
"""Presign sweeping transactions using the just received revoked pcs. """Presign sweeping transactions using the just received revoked pcs.
These will only be utilised if the remote breaches. 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). Sweep 'to_local', and all the HTLCs (two cases: directly from ctx, or from HTLC tx).
@ -75,8 +75,9 @@ def create_sweeptxs_for_their_revoked_ctx(chan: 'Channel', ctx: Transaction, per
sweep_address=sweep_address, sweep_address=sweep_address,
privkey=other_revocation_privkey, privkey=other_revocation_privkey,
is_revocation=True) is_revocation=True)
ctn = extract_ctn_from_tx_and_chan(ctx, chan) ctn = extract_ctn_from_tx_and_chan(ctx, chan)
assert ctn == chan.config[REMOTE].ctn assert ctn == chan.config[REMOTE].ctn - 1
# received HTLCs, in their ctx # received HTLCs, in their ctx
received_htlcs = chan.included_htlcs(REMOTE, RECEIVED, ctn) received_htlcs = chan.included_htlcs(REMOTE, RECEIVED, ctn)
for htlc in received_htlcs: for htlc in received_htlcs:
@ -92,6 +93,68 @@ def create_sweeptxs_for_their_revoked_ctx(chan: 'Channel', ctx: Transaction, per
return txs return txs
def create_sweeptx_for_their_revoked_ctx(chan: 'Channel', ctx: Transaction, per_commitment_secret: bytes,
sweep_address: str) -> Dict[str,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)
other_revocation_privkey = derive_blinded_privkey(other_conf.revocation_basepoint.privkey,
per_commitment_secret)
to_self_delay = other_conf.to_self_delay
this_delayed_pubkey = derive_pubkey(this_conf.delayed_basepoint.pubkey, pcp)
txs = []
# to_local
revocation_pubkey = ecc.ECPrivkey(other_revocation_privkey).get_public_key_bytes(compressed=True)
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:
sweep_tx = lambda: create_sweeptx_ctx_to_local(
sweep_address=sweep_address,
ctx=ctx,
output_idx=output_idx,
witness_script=witness_script,
privkey=other_revocation_privkey,
is_revocation=True)
return sweep_tx
def create_sweeptx_for_their_revoked_htlc(chan: 'Channel', ctx: Transaction, htlc_tx: Transaction,
sweep_address: str) -> Dict[str,Transaction]:
x = analyze_ctx(chan, ctx)
if not x:
return
ctn, their_pcp, is_revocation, per_commitment_secret = x
if not is_revocation:
return
# 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)
other_revocation_privkey = derive_blinded_privkey(other_conf.revocation_basepoint.privkey,
per_commitment_secret)
to_self_delay = other_conf.to_self_delay
this_delayed_pubkey = derive_pubkey(this_conf.delayed_basepoint.pubkey, pcp)
# same witness script as to_local
revocation_pubkey = ecc.ECPrivkey(other_revocation_privkey).get_public_key_bytes(compressed=True)
witness_script = bh2u(make_commitment_output_to_local_witness_script(
revocation_pubkey, to_self_delay, this_delayed_pubkey))
htlc_address = redeem_script_to_address('p2wsh', witness_script)
# check that htlc_tx is a htlc
if htlc_tx.outputs()[0].address != htlc_address:
return
gen_tx = lambda: create_sweeptx_ctx_to_local(
sweep_address=sweep_address,
ctx=htlc_tx,
output_idx=0,
witness_script=witness_script,
privkey=other_revocation_privkey,
is_revocation=True)
return 'redeem_htlc2', 0, 0, gen_tx
def create_sweeptxs_for_our_ctx(chan: 'Channel', ctx: Transaction, ctn: int, def create_sweeptxs_for_our_ctx(chan: 'Channel', ctx: Transaction, ctn: int,
sweep_address: str) -> Dict[str,Transaction]: sweep_address: str) -> Dict[str,Transaction]:
"""Handle the case where we force close unilaterally with our latest ctx. """Handle the case where we force close unilaterally with our latest ctx.
@ -99,6 +162,7 @@ def create_sweeptxs_for_our_ctx(chan: 'Channel', ctx: Transaction, ctn: int,
'to_local' can be swept even if this is a breach (by us), 'to_local' can be swept even if this is a breach (by us),
but HTLCs cannot (old HTLCs are no longer stored). but HTLCs cannot (old HTLCs are no longer stored).
""" """
ctn = extract_ctn_from_tx_and_chan(ctx, chan)
our_conf, their_conf = get_ordered_channel_configs(chan=chan, for_us=True) our_conf, their_conf = get_ordered_channel_configs(chan=chan, for_us=True)
our_per_commitment_secret = get_per_commitment_secret_from_seed( our_per_commitment_secret = get_per_commitment_secret_from_seed(
our_conf.per_commitment_secret_seed, RevocationStore.START_INDEX - ctn) our_conf.per_commitment_secret_seed, RevocationStore.START_INDEX - ctn)
@ -116,12 +180,18 @@ def create_sweeptxs_for_our_ctx(chan: 'Channel', ctx: Transaction, ctn: int,
to_local_address = redeem_script_to_address('p2wsh', to_local_witness_script) to_local_address = redeem_script_to_address('p2wsh', to_local_witness_script)
their_payment_pubkey = derive_pubkey(their_conf.payment_basepoint.pubkey, our_pcp) their_payment_pubkey = derive_pubkey(their_conf.payment_basepoint.pubkey, our_pcp)
to_remote_address = make_commitment_output_to_remote_address(their_payment_pubkey) to_remote_address = make_commitment_output_to_remote_address(their_payment_pubkey)
# test ctx # 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\ if ctx.get_output_idx_from_address(to_local_address) is None\
and ctx.get_output_idx_from_address(to_remote_address) is None: and ctx.get_output_idx_from_address(to_remote_address) is None:
return return
# we have to_local, to_remote.
# other outputs are htlcs
# if they are spent, we need to generate the script
# so, second-stage htlc sweep should not be returned here
if ctn != our_conf.ctn:
_logger.info("we breached.")
return {}
txs = {} txs = {}
# to_local # to_local
output_idx = ctx.get_output_idx_from_address(to_local_address) output_idx = ctx.get_output_idx_from_address(to_local_address)
@ -155,7 +225,7 @@ def create_sweeptxs_for_our_ctx(chan: 'Channel', ctx: Transaction, ctn: int,
preimage=preimage, preimage=preimage,
is_received_htlc=is_received_htlc) is_received_htlc=is_received_htlc)
sweep_tx = lambda: create_sweeptx_that_spends_htlctx_that_spends_htlc_in_ctx( sweep_tx = lambda: create_sweeptx_that_spends_htlctx_that_spends_htlc_in_ctx(
'sweep_from_our_ctx_htlc_', 'our_ctx_htlc_',
to_self_delay=to_self_delay, to_self_delay=to_self_delay,
htlc_tx=htlc_tx, htlc_tx=htlc_tx,
htlctx_witness_script=htlctx_witness_script, htlctx_witness_script=htlctx_witness_script,
@ -165,6 +235,7 @@ def create_sweeptxs_for_our_ctx(chan: 'Channel', ctx: Transaction, ctn: int,
# side effect # side effect
txs[htlc_tx.prevout(0)] = ('first-stage-htlc', 0, htlc_tx.cltv_expiry, lambda: htlc_tx) txs[htlc_tx.prevout(0)] = ('first-stage-htlc', 0, htlc_tx.cltv_expiry, lambda: htlc_tx)
txs[htlc_tx.txid() + ':0'] = ('second-stage-htlc', to_self_delay, 0, sweep_tx) txs[htlc_tx.txid() + ':0'] = ('second-stage-htlc', to_self_delay, 0, sweep_tx)
# offered HTLCs, in our ctx --> "timeout" # offered HTLCs, in our ctx --> "timeout"
# received HTLCs, in our ctx --> "success" # received HTLCs, in our ctx --> "success"
offered_htlcs = chan.included_htlcs(LOCAL, SENT, ctn) # type: List[UpdateAddHtlc] offered_htlcs = chan.included_htlcs(LOCAL, SENT, ctn) # type: List[UpdateAddHtlc]
@ -175,17 +246,11 @@ def create_sweeptxs_for_our_ctx(chan: 'Channel', ctx: Transaction, ctn: int,
create_txns_for_htlc(htlc, is_received_htlc=True) create_txns_for_htlc(htlc, is_received_htlc=True)
return txs return txs
def analyze_ctx(chan, ctx):
def create_sweeptxs_for_their_ctx(chan: 'Channel', ctx: Transaction, ctn: int,
sweep_address: str) -> Dict[str,Transaction]:
"""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.
"""
our_conf, their_conf = get_ordered_channel_configs(chan=chan, for_us=True)
ctn = extract_ctn_from_tx_and_chan(ctx, chan)
# note: the remote sometimes has two valid non-revoked commitment transactions, # note: the remote sometimes has two valid non-revoked commitment transactions,
# either of which could be broadcast (their_conf.ctn, their_conf.ctn+1) # either of which could be broadcast (their_conf.ctn, their_conf.ctn+1)
our_conf, their_conf = get_ordered_channel_configs(chan=chan, for_us=True)
ctn = extract_ctn_from_tx_and_chan(ctx, chan)
per_commitment_secret = None per_commitment_secret = None
if ctn == their_conf.ctn: if ctn == their_conf.ctn:
their_pcp = their_conf.current_per_commitment_point their_pcp = their_conf.current_per_commitment_point
@ -200,9 +265,23 @@ def create_sweeptxs_for_their_ctx(chan: 'Channel', ctx: Transaction, ctn: int,
return return
their_pcp = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True) their_pcp = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True)
is_revocation = True is_revocation = True
our_revocation_privkey = derive_blinded_privkey(our_conf.revocation_basepoint.privkey, per_commitment_secret) #_logger.info(f'tx for revoked: {list(txs.keys())}')
else: else:
return 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]:
"""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.
"""
txs = {}
our_conf, their_conf = get_ordered_channel_configs(chan=chan, for_us=True)
x = analyze_ctx(chan, ctx)
if not x:
return
ctn, their_pcp, is_revocation, per_commitment_secret = x
# to_local and to_remote addresses # to_local and to_remote addresses
our_revocation_pubkey = derive_blinded_pubkey(our_conf.revocation_basepoint.pubkey, their_pcp) our_revocation_pubkey = derive_blinded_pubkey(our_conf.revocation_basepoint.pubkey, their_pcp)
their_delayed_pubkey = derive_pubkey(their_conf.delayed_basepoint.pubkey, their_pcp) their_delayed_pubkey = derive_pubkey(their_conf.delayed_basepoint.pubkey, their_pcp)
@ -211,10 +290,18 @@ def create_sweeptxs_for_their_ctx(chan: 'Channel', ctx: Transaction, ctn: int,
to_local_address = redeem_script_to_address('p2wsh', witness_script) to_local_address = redeem_script_to_address('p2wsh', witness_script)
our_payment_pubkey = derive_pubkey(our_conf.payment_basepoint.pubkey, their_pcp) our_payment_pubkey = derive_pubkey(our_conf.payment_basepoint.pubkey, their_pcp)
to_remote_address = make_commitment_output_to_remote_address(our_payment_pubkey) to_remote_address = make_commitment_output_to_remote_address(our_payment_pubkey)
# test ctx # 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 \ if ctx.get_output_idx_from_address(to_local_address) is None \
and ctx.get_output_idx_from_address(to_remote_address) is None: and ctx.get_output_idx_from_address(to_remote_address) is None:
return return
if is_revocation:
our_revocation_privkey = derive_blinded_privkey(our_conf.revocation_basepoint.privkey, per_commitment_secret)
gen_tx = create_sweeptx_for_their_revoked_ctx(chan, ctx, per_commitment_secret, chan.sweep_address)
if gen_tx:
tx = gen_tx()
txs[tx.prevout(0)] = ('to_local_for_revoked_ctx', 0, 0, gen_tx)
# prep # prep
our_htlc_privkey = derive_privkey(secret=int.from_bytes(our_conf.htlc_basepoint.privkey, 'big'), per_commitment_point=their_pcp) our_htlc_privkey = derive_privkey(secret=int.from_bytes(our_conf.htlc_basepoint.privkey, 'big'), per_commitment_point=their_pcp)
our_htlc_privkey = ecc.ECPrivkey.from_secret_scalar(our_htlc_privkey) our_htlc_privkey = ecc.ECPrivkey.from_secret_scalar(our_htlc_privkey)
@ -223,7 +310,6 @@ def create_sweeptxs_for_their_ctx(chan: 'Channel', ctx: Transaction, ctn: int,
our_payment_privkey = derive_privkey(our_payment_bp_privkey.secret_scalar, their_pcp) our_payment_privkey = derive_privkey(our_payment_bp_privkey.secret_scalar, their_pcp)
our_payment_privkey = ecc.ECPrivkey.from_secret_scalar(our_payment_privkey) our_payment_privkey = ecc.ECPrivkey.from_secret_scalar(our_payment_privkey)
assert our_payment_pubkey == our_payment_privkey.get_public_key_bytes(compressed=True) assert our_payment_pubkey == our_payment_privkey.get_public_key_bytes(compressed=True)
txs = {}
# to_local is handled by lnwatcher # to_local is handled by lnwatcher
# to_remote # to_remote
output_idx = ctx.get_output_idx_from_address(to_remote_address) output_idx = ctx.get_output_idx_from_address(to_remote_address)
@ -268,7 +354,7 @@ def create_sweeptxs_for_their_ctx(chan: 'Channel', ctx: Transaction, ctn: int,
privkey=our_revocation_privkey if is_revocation else our_htlc_privkey.get_secret_bytes(), privkey=our_revocation_privkey if is_revocation else our_htlc_privkey.get_secret_bytes(),
is_revocation=is_revocation, is_revocation=is_revocation,
cltv_expiry=cltv_expiry) cltv_expiry=cltv_expiry)
name = f'their_ctx_sweep_htlc_{ctx.txid()[:8]}_{output_idx}' name = f'their_ctx_htlc_{output_idx}'
txs[prevout] = (name, 0, cltv_expiry, sweep_tx) txs[prevout] = (name, 0, cltv_expiry, sweep_tx)
# received HTLCs, in their ctx --> "timeout" # received HTLCs, in their ctx --> "timeout"
received_htlcs = chan.included_htlcs(REMOTE, RECEIVED, ctn=ctn) # type: List[UpdateAddHtlc] received_htlcs = chan.included_htlcs(REMOTE, RECEIVED, ctn=ctn) # type: List[UpdateAddHtlc]
@ -327,7 +413,7 @@ def create_sweeptx_their_ctx_htlc(ctx: Transaction, witness_script: bytes, sweep
if outvalue <= dust_threshold(): return None if outvalue <= dust_threshold(): return None
sweep_outputs = [TxOutput(TYPE_ADDRESS, sweep_address, outvalue)] sweep_outputs = [TxOutput(TYPE_ADDRESS, sweep_address, outvalue)]
tx = Transaction.from_io(sweep_inputs, sweep_outputs, version=2 tx = Transaction.from_io(sweep_inputs, sweep_outputs, version=2
, name=f'their_ctx_sweep_htlc_{ctx.txid()[:8]}_{output_idx}' , name=f'their_ctx_htlc_{output_idx}'
# note that cltv_expiry, and therefore also locktime will be zero when breach! # note that cltv_expiry, and therefore also locktime will be zero when breach!
, cltv_expiry=cltv_expiry, locktime=cltv_expiry) , cltv_expiry=cltv_expiry, locktime=cltv_expiry)
sig = bfh(tx.sign_txin(0, privkey)) sig = bfh(tx.sign_txin(0, privkey))
@ -431,7 +517,8 @@ def create_sweeptx_that_spends_htlctx_that_spends_htlc_in_ctx(
outvalue = val - fee outvalue = val - fee
if outvalue <= dust_threshold(): return None if outvalue <= dust_threshold(): return None
sweep_outputs = [TxOutput(TYPE_ADDRESS, sweep_address, outvalue)] sweep_outputs = [TxOutput(TYPE_ADDRESS, sweep_address, outvalue)]
tx = Transaction.from_io(sweep_inputs, sweep_outputs, version=2, name=name_prefix + htlc_tx.txid(), csv_delay=to_self_delay) name = name_prefix + htlc_tx.txid()[0:4]
tx = Transaction.from_io(sweep_inputs, sweep_outputs, version=2, name=name, csv_delay=to_self_delay)
sig = bfh(tx.sign_txin(0, privkey)) sig = bfh(tx.sign_txin(0, privkey))
witness = construct_witness([sig, int(is_revocation), htlctx_witness_script]) witness = construct_witness([sig, int(is_revocation), htlctx_witness_script])

View file

@ -359,7 +359,7 @@ def make_htlc_tx_with_open_channel(chan: 'Channel', pcp: bytes, for_us: bool,
is_htlc_success = for_us == we_receive is_htlc_success = for_us == we_receive
script, htlc_tx_output = make_htlc_tx_output( script, htlc_tx_output = make_htlc_tx_output(
amount_msat = amount_msat, amount_msat = amount_msat,
local_feerate = chan.pending_feerate(LOCAL if for_us else REMOTE), local_feerate = chan.pending_feerate(LOCAL if for_us else REMOTE), # uses pending feerate..
revocationpubkey=other_revocation_pubkey, revocationpubkey=other_revocation_pubkey,
local_delayedpubkey=delayedpubkey, local_delayedpubkey=delayedpubkey,
success = is_htlc_success, success = is_htlc_success,

View file

@ -248,7 +248,7 @@ class LNWatcher(AddressSynchronizer):
self.network.trigger_callback('channel_closed', funding_outpoint, spenders, self.network.trigger_callback('channel_closed', funding_outpoint, spenders,
funding_txid, funding_height, closing_txid, funding_txid, funding_height, closing_txid,
closing_height, closing_tx) # FIXME sooo many args.. closing_height, closing_tx) # FIXME sooo many args..
await self.do_breach_remedy(funding_outpoint, spenders) #await self.do_breach_remedy(funding_outpoint, spenders)
if not keep_watching: if not keep_watching:
await self.unwatch_channel(address, funding_outpoint) await self.unwatch_channel(address, funding_outpoint)
else: else:
@ -289,8 +289,7 @@ class LNWatcher(AddressSynchronizer):
continue continue
sweep_txns = await self.sweepstore.get_sweep_tx(funding_outpoint, prevout) sweep_txns = await self.sweepstore.get_sweep_tx(funding_outpoint, prevout)
for tx in sweep_txns: for tx in sweep_txns:
if not await self.broadcast_or_log(funding_outpoint, tx): await self.broadcast_or_log(funding_outpoint, tx)
self.logger.info(f'{tx.name} could not publish tx: {str(tx)}, prevout: {prevout}')
async def broadcast_or_log(self, funding_outpoint, tx): async def broadcast_or_log(self, funding_outpoint, tx):
height = self.get_tx_height(tx.txid()).height height = self.get_tx_height(tx.txid()).height
@ -299,9 +298,9 @@ class LNWatcher(AddressSynchronizer):
try: try:
txid = await self.network.broadcast_transaction(tx) txid = await self.network.broadcast_transaction(tx)
except Exception as e: except Exception as e:
self.logger.info(f'broadcast: {tx.name}: failure: {repr(e)}') self.logger.info(f'broadcast failure: {tx.name}: {repr(e)}')
else: else:
self.logger.info(f'broadcast: {tx.name}: success. txid: {txid}') self.logger.info(f'broadcast success: {tx.name}')
if funding_outpoint in self.tx_progress: if funding_outpoint in self.tx_progress:
await self.tx_progress[funding_outpoint].tx_queue.put(tx) await self.tx_progress[funding_outpoint].tx_queue.put(tx)
return txid return txid

View file

@ -514,45 +514,66 @@ class LNWallet(LNWorker):
# remove from channel_db # remove from channel_db
if chan.short_channel_id is not None: if chan.short_channel_id is not None:
self.channel_db.remove_channel(chan.short_channel_id) self.channel_db.remove_channel(chan.short_channel_id)
# detect who closed and set sweep_info # detect who closed and set sweep_info
sweep_info = chan.get_sweep_info(closing_tx) sweep_info = chan.sweep_ctx(closing_tx)
# create and broadcast transaction # create and broadcast transaction
for prevout, e_tx in sweep_info.items(): for prevout, e_tx in sweep_info.items():
name, csv_delay, cltv_expiry, gen_tx = e_tx name, csv_delay, cltv_expiry, gen_tx = e_tx
if spenders.get(prevout) is not None: spender = spenders.get(prevout)
self.logger.info(f'outpoint already spent {prevout}') if spender is not None:
continue spender_tx = await self.network.get_transaction(spender)
prev_txid, prev_index = prevout.split(':') spender_tx = Transaction(spender_tx)
broadcast = True e_htlc_tx = chan.sweep_htlc(closing_tx, spender_tx)
if cltv_expiry: if e_htlc_tx:
local_height = self.network.get_local_height() spender2 = spenders.get(spender_tx.outputs()[0])
remaining = cltv_expiry - local_height if spender2:
if remaining > 0: self.logger.info(f'htlc is already spent {name}: {prevout}')
self.logger.info('waiting for {}: CLTV ({} > {}), funding outpoint {} and tx {}' else:
.format(name, local_height, cltv_expiry, funding_outpoint[:8], prev_txid[:8])) self.logger.info(f'trying to redeem htlc {name}: {prevout}')
broadcast = False await self.try_redeem(spender+':0', e_htlc_tx)
if csv_delay: else:
prev_height = self.network.lnwatcher.get_tx_height(prev_txid) self.logger.info(f'outpoint already spent {name}: {prevout}')
remaining = csv_delay - prev_height.conf
if remaining > 0:
self.logger.info('waiting for {}: CSV ({} >= {}), funding outpoint {} and tx {}'
.format(name, prev_height.conf, csv_delay, funding_outpoint[:8], prev_txid[:8]))
broadcast = False
tx = gen_tx()
if tx is None:
self.logger.info(f'{name} could not claim output: {prevout}, dust')
if broadcast:
if not await self.network.lnwatcher.broadcast_or_log(funding_outpoint, tx):
self.logger.info(f'{name} could not publish encumbered tx: {str(tx)}, prevout: {prevout}')
else: else:
# it's OK to add local transaction, the fee will be recomputed self.logger.info(f'trying to redeem {name}: {prevout}')
try: await self.try_redeem(prevout, e_tx)
self.wallet.add_future_tx(tx, remaining)
self.logger.info(f'adding future tx: {name}. prevout: {prevout}') @log_exceptions
except Exception as e: async def try_redeem(self, prevout, e_tx):
self.logger.info(f'could not add future tx: {name}. prevout: {prevout} {str(e)}') name, csv_delay, cltv_expiry, gen_tx = e_tx
prev_txid, prev_index = prevout.split(':')
broadcast = True
if cltv_expiry:
local_height = self.network.get_local_height()
remaining = cltv_expiry - local_height
if remaining > 0:
self.logger.info('waiting for {}: CLTV ({} > {}), prevout {}'
.format(name, local_height, cltv_expiry, prevout))
broadcast = False
if csv_delay:
prev_height = self.network.lnwatcher.get_tx_height(prev_txid)
remaining = csv_delay - prev_height.conf
if remaining > 0:
self.logger.info('waiting for {}: CSV ({} >= {}), prevout: {}'
.format(name, prev_height.conf, csv_delay, prevout))
broadcast = False
tx = gen_tx()
self.wallet.set_label(tx.txid(), name)
if tx is None:
self.logger.info(f'{name} could not claim output: {prevout}, dust')
if broadcast:
try:
await self.network.broadcast_transaction(tx)
except Exception as e:
self.logger.info(f'could NOT publish {name} for prevout: {prevout}, {str(e)}')
else:
self.logger.info(f'success: broadcasting {name} for prevout: {prevout}')
else:
# it's OK to add local transaction, the fee will be recomputed
try:
self.wallet.add_future_tx(tx, remaining)
self.logger.info(f'adding future tx: {name}. prevout: {prevout}')
except Exception as e:
self.logger.info(f'could not add future tx: {name}. prevout: {prevout} {str(e)}')
def is_dangerous(self, chan): def is_dangerous(self, chan):
for x in chan.get_unfulfilled_htlcs(): for x in chan.get_unfulfilled_htlcs():

View file

@ -157,7 +157,7 @@ if [[ $1 == "redeem_htlcs" ]]; then
fi fi
if [[ $1 == "breach_with_htlc" ]]; then if [[ $1 == "breach_with_unspent_htlc" ]]; then
$bob daemon stop $bob daemon stop
ELECTRUM_DEBUG_LIGHTNING_SETTLE_DELAY=3 $bob daemon -s 127.0.0.1:51001:t start ELECTRUM_DEBUG_LIGHTNING_SETTLE_DELAY=3 $bob daemon -s 127.0.0.1:51001:t start
$bob daemon load_wallet $bob daemon load_wallet
@ -218,3 +218,84 @@ if [[ $1 == "breach_with_htlc" ]]; then
exit 1 exit 1
fi fi
fi fi
if [[ $1 == "breach_with_spent_htlc" ]]; then
$bob daemon stop
ELECTRUM_DEBUG_LIGHTNING_SETTLE_DELAY=3 $bob daemon -s 127.0.0.1:51001:t start
$bob daemon load_wallet
while alice_balance=$($alice getbalance | jq '.confirmed' | tr -d '"') && [ $alice_balance != "1" ]; do
echo "waiting for alice balance"
sleep 1
done
echo "alice opens channel"
bob_node=$($bob nodeid)
channel=$($alice open_channel $bob_node 0.15)
new_blocks 3
channel_state=""
while channel_state=$($alice list_channels | jq '.[] | .state' | tr -d '"') && [ $channel_state != "OPEN" ]; do
echo "waiting for channel open"
sleep 1
done
echo "alice pays bob"
invoice=$($bob addinvoice 0.05 "test")
$alice lnpay $invoice --timeout=1 || true
settled=$($alice list_channels | jq '.[] | .local_htlcs | .settles | length')
if [[ "$settled" != "0" ]]; then
echo "SETTLE_DELAY did not work, $settled != 0"
exit 1
fi
ctx=$($alice get_channel_ctx $channel | jq '.hex' | tr -d '"')
cp /tmp/alice/regtest/wallets/default_wallet /tmp/alice/regtest/wallets/toxic_wallet
sleep 5
settled=$($alice list_channels | jq '.[] | .local_htlcs | .settles | length')
if [[ "$settled" != "1" ]]; then
echo "SETTLE_DELAY did not work, $settled != 1"
exit 1
fi
echo $($bob getbalance)
echo "bob goes offline"
$bob daemon stop
ctx_id=$($bitcoin_cli sendrawtransaction $ctx)
echo "alice breaches with old ctx:" $ctx_id
new_blocks 1
if [[ $($bitcoin_cli gettxout $ctx_id 0 | jq '.confirmations') != "1" ]]; then
echo "breach tx not confirmed"
exit 1
fi
echo "wait for cltv_expiry blocks"
# note: this will let alice redeem to_local
# because cltv_delay is the same as csv_delay
new_blocks 144
echo "alice spends to_local and htlc outputs"
$alice daemon stop
cp /tmp/alice/regtest/wallets/toxic_wallet /tmp/alice/regtest/wallets/default_wallet
$alice daemon -s 127.0.0.1:51001:t start
$alice daemon load_wallet
# wait until alice has spent both ctx outputs
while [[ $($bitcoin_cli gettxout $ctx_id 0) ]]; do
echo "waiting until alice spends ctx outputs"
sleep 1
done
while [[ $($bitcoin_cli gettxout $ctx_id 1) ]]; do
echo "waiting until alice spends ctx outputs"
sleep 1
done
new_blocks 1
echo "bob comes back"
$bob daemon -s 127.0.0.1:51001:t start
$bob daemon load_wallet
while [[ $($bitcoin_cli getmempoolinfo | jq '.size') != "1" ]]; do
echo "waiting for bob's transaction"
sleep 1
done
echo "mempool has 1 tx"
new_blocks 1
sleep 5
balance=$($bob getbalance | jq '.confirmed')
if (( $(echo "$balance < 0.049" | bc -l) )); then
echo "htlc not redeemed."
exit 1
fi
echo "bob balance $balance"
fi

View file

@ -33,5 +33,8 @@ class TestLightning(unittest.TestCase):
def test_redeem_htlcs(self): def test_redeem_htlcs(self):
self.run_shell(['redeem_htlcs']) self.run_shell(['redeem_htlcs'])
def test_breach_with_htlc(self): def test_breach_with_unspent_htlc(self):
self.run_shell(['breach_with_htlc']) self.run_shell(['breach_with_unspent_htlc'])
def test_breach_with_spent_htlc(self):
self.run_shell(['breach_with_spent_htlc'])