mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-23 17:47:31 +00:00
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:
parent
238f3c949c
commit
a8ce8109be
8 changed files with 280 additions and 95 deletions
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ _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]:
|
||||
"""Presign sweeping transactions using the just received revoked pcs.
|
||||
These will only be utilised if the remote breaches.
|
||||
|
@ -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])
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -514,38 +514,59 @@ 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
|
||||
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:
|
||||
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 ({} > {}), funding outpoint {} and tx {}'
|
||||
.format(name, local_height, cltv_expiry, funding_outpoint[:8], prev_txid[:8]))
|
||||
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 ({} >= {}), funding outpoint {} and tx {}'
|
||||
.format(name, prev_height.conf, csv_delay, funding_outpoint[:8], prev_txid[:8]))
|
||||
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:
|
||||
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}')
|
||||
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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'])
|
||||
|
|
Loading…
Add table
Reference in a new issue