commit a8ce8109bea4210c821e1757f97d398a8c9103b4
parent 238f3c949ca2d23cbe4cc4e5a1ccd15371050663
Author: ThomasV <thomasv@electrum.org>
Date: Mon, 24 Jun 2019 11:13:18 +0200
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
Diffstat:
8 files changed, 280 insertions(+), 95 deletions(-)
diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py
@@ -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,
funding_output_script, SENT, RECEIVED, LOCAL, REMOTE, HTLCOwner, make_commitment_outputs,
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
@@ -165,7 +166,7 @@ class Channel(Logger):
self.set_state('DISCONNECTED')
self.local_commitment = None
self.remote_commitment = None
- self.sweep_info = None
+ self.sweep_info = {}
def get_payments(self):
out = {}
@@ -450,12 +451,6 @@ class Channel(Logger):
point = secret_to_pubkey(int.from_bytes(secret, 'big'))
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):
self.logger.info("receive_revocation")
@@ -469,16 +464,7 @@ class Channel(Logger):
# this might break
prev_remote_commitment = self.pending_commitment(REMOTE)
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
-
if self.pending_fee is not None:
if not self.constraints.is_initiator:
self.pending_fee[FUNDEE_SIGNED] = True
@@ -501,8 +487,6 @@ class Channel(Logger):
self.set_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):
"""
@@ -810,19 +794,23 @@ class Channel(Logger):
assert tx.is_complete()
return tx
- def get_sweep_info(self, ctx: Transaction):
- if self.sweep_info is None:
+ def sweep_ctx(self, ctx: Transaction):
+ txid = ctx.txid()
+ if self.sweep_info.get(txid) is None:
ctn = extract_ctn_from_tx_and_chan(ctx, self)
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)
- if our_sweep_info:
- self.sweep_info = our_sweep_info
+ if our_sweep_info is not None:
+ self.sweep_info[txid] = our_sweep_info
self.logger.info(f'we force closed.')
- elif their_sweep_info:
- self.sweep_info = their_sweep_info
+ elif their_sweep_info is not None:
+ self.sweep_info[txid] = their_sweep_info
self.logger.info(f'they force closed.')
else:
- self.sweep_info = {}
+ self.sweep_info[txid] = {}
self.logger.info(f'not sure who closed {ctx}.')
- self.logger.info(f'{repr(self.sweep_info)}')
- return self.sweep_info
+ return self.sweep_info[txid]
+
+ 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)
diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
@@ -40,6 +40,7 @@ from .lnutil import (Outpoint, LocalConfig, RECEIVED, UpdateAddHtlc,
LightningPeerConnectionClosed, HandshakeFailed, NotFoundChanAnnouncementForUpdate,
MINIMUM_MAX_HTLC_VALUE_IN_FLIGHT_ACCEPTED, MAXIMUM_HTLC_MINIMUM_MSAT_ACCEPTED,
MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED, RemoteMisbehaving, DEFAULT_TO_SELF_DELAY)
+from .lnsweep import create_sweeptxs_for_watchtower
from .lntransport import LNTransport, LNTransportBase
from .lnmsg import encode_msg, decode_msg
from .interface import GracefulDisconnect
@@ -1261,15 +1262,20 @@ class Peer(Logger):
self.logger.info("on_revoke_and_ack")
channel_id = payload["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].clear()
self.lnworker.save_channel(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()
+ sweeptxs = create_sweeptxs_for_watchtower(chan, ctx, per_commitment_secret, chan.sweep_address)
for tx in sweeptxs:
await self.lnwatcher.add_sweep_tx(outpoint, tx.prevout(0), str(tx))
diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py
@@ -27,8 +27,8 @@ _logger = get_logger(__name__)
-def create_sweeptxs_for_their_revoked_ctx(chan: 'Channel', ctx: Transaction, per_commitment_secret: bytes,
- sweep_address: str) -> Dict[str,Transaction]:
+def create_sweeptxs_for_watchtower(chan: 'Channel', ctx: Transaction, per_commitment_secret: bytes,
+ sweep_address: str) -> Dict[str,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).
@@ -75,8 +75,9 @@ def create_sweeptxs_for_their_revoked_ctx(chan: 'Channel', ctx: Transaction, per
sweep_address=sweep_address,
privkey=other_revocation_privkey,
is_revocation=True)
+
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 = chan.included_htlcs(REMOTE, RECEIVED, ctn)
for htlc in received_htlcs:
@@ -92,6 +93,68 @@ def create_sweeptxs_for_their_revoked_ctx(chan: 'Channel', ctx: Transaction, per
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,
sweep_address: str) -> Dict[str,Transaction]:
"""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),
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_per_commitment_secret = get_per_commitment_secret_from_seed(
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)
their_payment_pubkey = derive_pubkey(their_conf.payment_basepoint.pubkey, our_pcp)
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:
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 = {}
# to_local
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,
is_received_htlc=is_received_htlc)
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,
htlc_tx=htlc_tx,
htlctx_witness_script=htlctx_witness_script,
@@ -165,6 +235,7 @@ def create_sweeptxs_for_our_ctx(chan: 'Channel', ctx: Transaction, ctn: int,
# side effect
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)
+
# offered HTLCs, in our ctx --> "timeout"
# received HTLCs, in our ctx --> "success"
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)
return txs
-
-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)
+def analyze_ctx(chan, ctx):
# note: the remote sometimes has two valid non-revoked commitment transactions,
# 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
if ctn == their_conf.ctn:
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
their_pcp = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=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:
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
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)
@@ -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)
our_payment_pubkey = derive_pubkey(our_conf.payment_basepoint.pubkey, their_pcp)
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 \
and ctx.get_output_idx_from_address(to_remote_address) is None:
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
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)
@@ -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 = ecc.ECPrivkey.from_secret_scalar(our_payment_privkey)
assert our_payment_pubkey == our_payment_privkey.get_public_key_bytes(compressed=True)
- txs = {}
# to_local is handled by lnwatcher
# to_remote
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(),
is_revocation=is_revocation,
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)
# received HTLCs, in their ctx --> "timeout"
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
sweep_outputs = [TxOutput(TYPE_ADDRESS, sweep_address, outvalue)]
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!
, cltv_expiry=cltv_expiry, locktime=cltv_expiry)
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
if outvalue <= dust_threshold(): return None
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))
witness = construct_witness([sig, int(is_revocation), htlctx_witness_script])
diff --git a/electrum/lnutil.py b/electrum/lnutil.py
@@ -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
script, htlc_tx_output = make_htlc_tx_output(
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,
local_delayedpubkey=delayedpubkey,
success = is_htlc_success,
diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py
@@ -248,7 +248,7 @@ class LNWatcher(AddressSynchronizer):
self.network.trigger_callback('channel_closed', funding_outpoint, spenders,
funding_txid, funding_height, closing_txid,
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:
await self.unwatch_channel(address, funding_outpoint)
else:
@@ -289,8 +289,7 @@ class LNWatcher(AddressSynchronizer):
continue
sweep_txns = await self.sweepstore.get_sweep_tx(funding_outpoint, prevout)
for tx in sweep_txns:
- if not await self.broadcast_or_log(funding_outpoint, tx):
- self.logger.info(f'{tx.name} could not publish tx: {str(tx)}, prevout: {prevout}')
+ await self.broadcast_or_log(funding_outpoint, tx)
async def broadcast_or_log(self, funding_outpoint, tx):
height = self.get_tx_height(tx.txid()).height
@@ -299,9 +298,9 @@ class LNWatcher(AddressSynchronizer):
try:
txid = await self.network.broadcast_transaction(tx)
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:
- 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:
await self.tx_progress[funding_outpoint].tx_queue.put(tx)
return txid
diff --git a/electrum/lnworker.py b/electrum/lnworker.py
@@ -514,45 +514,66 @@ class LNWallet(LNWorker):
# remove from channel_db
if chan.short_channel_id is not None:
self.channel_db.remove_channel(chan.short_channel_id)
-
# 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
for prevout, e_tx in sweep_info.items():
name, csv_delay, cltv_expiry, gen_tx = e_tx
- if spenders.get(prevout) is not None:
- self.logger.info(f'outpoint already spent {prevout}')
- continue
- 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 ({} > {}), funding outpoint {} and tx {}'
- .format(name, local_height, cltv_expiry, funding_outpoint[:8], prev_txid[:8]))
- 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 ({} >= {}), 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}')
+ spender = spenders.get(prevout)
+ if spender is not None:
+ spender_tx = await self.network.get_transaction(spender)
+ spender_tx = Transaction(spender_tx)
+ e_htlc_tx = chan.sweep_htlc(closing_tx, spender_tx)
+ if e_htlc_tx:
+ spender2 = spenders.get(spender_tx.outputs()[0])
+ if spender2:
+ self.logger.info(f'htlc is already spent {name}: {prevout}')
+ else:
+ self.logger.info(f'trying to redeem htlc {name}: {prevout}')
+ await self.try_redeem(spender+':0', e_htlc_tx)
+ else:
+ self.logger.info(f'outpoint already spent {name}: {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)}')
+ self.logger.info(f'trying to redeem {name}: {prevout}')
+ await self.try_redeem(prevout, e_tx)
+
+ @log_exceptions
+ async def try_redeem(self, prevout, e_tx):
+ 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):
for x in chan.get_unfulfilled_htlcs():
diff --git a/electrum/tests/regtest/regtest.sh b/electrum/tests/regtest/regtest.sh
@@ -157,7 +157,7 @@ if [[ $1 == "redeem_htlcs" ]]; then
fi
-if [[ $1 == "breach_with_htlc" ]]; then
+if [[ $1 == "breach_with_unspent_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
@@ -218,3 +218,84 @@ if [[ $1 == "breach_with_htlc" ]]; then
exit 1
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
diff --git a/electrum/tests/test_regtest.py b/electrum/tests/test_regtest.py
@@ -33,5 +33,8 @@ class TestLightning(unittest.TestCase):
def test_redeem_htlcs(self):
self.run_shell(['redeem_htlcs'])
- def test_breach_with_htlc(self):
- self.run_shell(['breach_with_htlc'])
+ def test_breach_with_unspent_htlc(self):
+ self.run_shell(['breach_with_unspent_htlc'])
+
+ def test_breach_with_spent_htlc(self):
+ self.run_shell(['breach_with_spent_htlc'])