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,
|
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)
|
||||||
|
|
|
@ -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))
|
||||||
|
|
||||||
|
|
|
@ -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])
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'])
|
||||||
|
|
Loading…
Add table
Reference in a new issue