tredeem htlcs: - fix bug in lnsweep: lnwatcher transactions were indexed by prev_txid - add test for breach remedy with unsettled htlcs - add timeout option to lnpay, and replace DO_NOT_SETTLE with SETTLE_DELAY so that we can read intermediate commitment tx in regtest - electrum - Electrum Bitcoin wallet (HTM) git clone https://git.parazyd.org/electrum (DIR) Log (DIR) Files (DIR) Refs (DIR) Submodules --- (DIR) commit 7be4cdaf18dd3bc0aab41e2c0d09b4d29bf992d6 (DIR) parent 7418bd45526a3c9bd2cfb883fa33269814acc42d (HTM) Author: ThomasV <thomasv@electrum.org> Date: Wed, 29 May 2019 17:34:12 +0200 redeem htlcs: - fix bug in lnsweep: lnwatcher transactions were indexed by prev_txid - add test for breach remedy with unsettled htlcs - add timeout option to lnpay, and replace DO_NOT_SETTLE with SETTLE_DELAY so that we can read intermediate commitment tx in regtest Diffstat: M electrum/commands.py | 4 ++-- M electrum/lnchannel.py | 5 ++--- M electrum/lnpeer.py | 3 +-- M electrum/lnsweep.py | 12 ++++++------ M electrum/lnwatcher.py | 25 ++++++++++++------------- M electrum/simple_config.py | 4 +--- M electrum/tests/regtest/regtest.sh | 49 +++++++++++++++++++++++++++++-- M electrum/tests/test_regtest.py | 3 +++ 8 files changed, 73 insertions(+), 32 deletions(-) --- (DIR) diff --git a/electrum/commands.py b/electrum/commands.py t@@ -780,8 +780,8 @@ class Commands: self.lnworker.reestablish_channel() @command('wn') - def lnpay(self, invoice): - return self.lnworker.pay(invoice, timeout=10) + def lnpay(self, invoice, timeout=10): + return self.lnworker.pay(invoice, timeout=timeout) @command('wn') def addinvoice(self, requested_amount, message): (DIR) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py t@@ -457,9 +457,8 @@ class Channel(Logger): 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) - for prev_txid, tx in sweeptxs.items(): - if tx is not None: - self.lnwatcher.add_sweep_tx(outpoint, prev_txid, str(tx)) + for tx in sweeptxs: + self.lnwatcher.add_sweep_tx(outpoint, tx.prevout(0), str(tx)) def receive_revocation(self, revocation: RevokeAndAck): self.logger.info("receive_revocation") (DIR) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py t@@ -1237,8 +1237,7 @@ class Peer(Logger): await self.fail_htlc(chan, htlc.htlc_id, onion_packet, reason) return self.network.trigger_callback('htlc_added', htlc, invoice, RECEIVED) - if self.network.config.debug_lightning_do_not_settle: - return + await asyncio.sleep(self.network.config.lightning_settle_delay) await self._fulfill_htlc(chan, htlc.htlc_id, preimage) async def _fulfill_htlc(self, chan: Channel, htlc_id: int, preimage: bytes): (DIR) diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py t@@ -40,7 +40,7 @@ def create_sweeptxs_for_their_revoked_ctx(chan: 'Channel', ctx: Transaction, per per_commitment_secret) to_self_delay = other_conf.to_self_delay this_delayed_pubkey = derive_pubkey(this_conf.delayed_basepoint.pubkey, pcp) - txs = {} + 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( t@@ -55,7 +55,7 @@ def create_sweeptxs_for_their_revoked_ctx(chan: 'Channel', ctx: Transaction, per witness_script=witness_script, privkey=other_revocation_privkey, is_revocation=True) - txs[ctx.txid()] = sweep_tx + txs.append(sweep_tx) # HTLCs def create_sweeptx_for_htlc(htlc: 'UpdateAddHtlc', is_received_htlc: bool) -> Tuple[Optional[Transaction], Optional[Transaction], t@@ -93,17 +93,17 @@ def create_sweeptxs_for_their_revoked_ctx(chan: 'Channel', ctx: Transaction, per for htlc in received_htlcs: direct_sweep_tx, secondstage_sweep_tx, htlc_tx = create_sweeptx_for_htlc(htlc, is_received_htlc=True) if direct_sweep_tx: - txs[ctx.txid()] = direct_sweep_tx + txs.append(direct_sweep_tx) if secondstage_sweep_tx: - txs[htlc_tx.txid()] = secondstage_sweep_tx + txs.append(secondstage_sweep_tx) # offered HTLCs, in their ctx offered_htlcs = chan.included_htlcs(REMOTE, SENT, ctn) for htlc in offered_htlcs: direct_sweep_tx, secondstage_sweep_tx, htlc_tx = create_sweeptx_for_htlc(htlc, is_received_htlc=False) if direct_sweep_tx: - txs[ctx.txid()] = direct_sweep_tx + txs.append(direct_sweep_tx) if secondstage_sweep_tx: - txs[htlc_tx.txid()] = secondstage_sweep_tx + txs.append(secondstage_sweep_tx) return txs (DIR) diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py t@@ -48,7 +48,7 @@ class SweepTx(Base): __tablename__ = 'sweep_txs' funding_outpoint = Column(String(34), primary_key=True) index = Column(Integer(), primary_key=True) - prev_txid = Column(String(32)) + prevout = Column(String(34)) tx = Column(String()) class ChannelInfo(Base): t@@ -64,22 +64,22 @@ class SweepStore(SqlDB): super().__init__(network, path, Base) @sql - def get_sweep_tx(self, funding_outpoint, prev_txid): - return [Transaction(bh2u(r.tx)) for r in self.DBSession.query(SweepTx).filter(SweepTx.funding_outpoint==funding_outpoint, SweepTx.prev_txid==prev_txid).all()] + def get_sweep_tx(self, funding_outpoint, prevout): + return [Transaction(bh2u(r.tx)) for r in self.DBSession.query(SweepTx).filter(SweepTx.funding_outpoint==funding_outpoint, SweepTx.prevout==prevout).all()] @sql def get_tx_by_index(self, funding_outpoint, index): r = self.DBSession.query(SweepTx).filter(SweepTx.funding_outpoint==funding_outpoint, SweepTx.index==index).one_or_none() - return r.prev_txid, bh2u(r.tx) + return r.prevout, bh2u(r.tx) @sql def list_sweep_tx(self): return set(r.funding_outpoint for r in self.DBSession.query(SweepTx).all()) @sql - def add_sweep_tx(self, funding_outpoint, prev_txid, tx): + def add_sweep_tx(self, funding_outpoint, prevout, tx): n = self.DBSession.query(SweepTx).filter(funding_outpoint==funding_outpoint).count() - self.DBSession.add(SweepTx(funding_outpoint=funding_outpoint, index=n, prev_txid=prev_txid, tx=bfh(tx))) + self.DBSession.add(SweepTx(funding_outpoint=funding_outpoint, index=n, prevout=prevout, tx=bfh(tx))) self.DBSession.commit() @sql t@@ -172,8 +172,8 @@ class LNWatcher(AddressSynchronizer): self.watchtower.add_channel(outpoint, address) self.logger.info("sending %d transactions to watchtower"%(local_n - n)) for index in range(n, local_n): - prev_txid, tx = self.sweepstore.get_tx_by_index(outpoint, index) - self.watchtower.add_sweep_tx(outpoint, prev_txid, tx) + prevout, tx = self.sweepstore.get_tx_by_index(outpoint, index) + self.watchtower.add_sweep_tx(outpoint, prevout, tx) except ConnectionRefusedError: self.logger.info('could not reach watchtower, will retry in 5s') await asyncio.sleep(5) t@@ -260,11 +260,10 @@ class LNWatcher(AddressSynchronizer): for prevout, spender in spenders.items(): if spender is not None: continue - prev_txid, prev_n = prevout.split(':') - sweep_txns = self.sweepstore.get_sweep_tx(funding_outpoint, prev_txid) + sweep_txns = 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)}, prev_txid: {prev_txid}') + self.logger.info(f'{tx.name} could not publish tx: {str(tx)}, prevout: {prevout}') async def broadcast_or_log(self, funding_outpoint, tx): height = self.get_tx_height(tx.txid()).height t@@ -280,8 +279,8 @@ class LNWatcher(AddressSynchronizer): await self.tx_progress[funding_outpoint].tx_queue.put(tx) return txid - def add_sweep_tx(self, funding_outpoint: str, prev_txid: str, tx: str): - self.sweepstore.add_sweep_tx(funding_outpoint, prev_txid, tx) + def add_sweep_tx(self, funding_outpoint: str, prevout: str, tx: str): + self.sweepstore.add_sweep_tx(funding_outpoint, prevout, tx) if self.watchtower: self.watchtower_queue.put_nowait(funding_outpoint) (DIR) diff --git a/electrum/simple_config.py b/electrum/simple_config.py t@@ -66,9 +66,7 @@ class SimpleConfig(Logger): options = {} Logger.__init__(self) - - self.debug_lightning = 'ELECTRUM_DEBUG_LIGHTNING_DANGEROUS' in os.environ - self.debug_lightning_do_not_settle = 'ELECTRUM_DEBUG_LIGHTNING_DO_NOT_SETTLE' in os.environ + self.lightning_settle_delay = int(os.environ.get('ELECTRUM_DEBUG_LIGHTNING_SETTLE_DELAY', 0)) # This lock needs to be acquired for updating and reading the config in # a thread-safe way. (DIR) diff --git a/electrum/tests/regtest/regtest.sh b/electrum/tests/regtest/regtest.sh t@@ -107,7 +107,7 @@ fi if [[ $1 == "redeem_htlcs" ]]; then $bob daemon stop - ELECTRUM_DEBUG_LIGHTNING_DO_NOT_SETTLE=1 $bob daemon -s 127.0.0.1:51001:t start + ELECTRUM_DEBUG_LIGHTNING_SETTLE_DELAY=10 $bob daemon -s 127.0.0.1:51001:t start $bob daemon load_wallet sleep 1 # alice opens channel t@@ -117,11 +117,11 @@ if [[ $1 == "redeem_htlcs" ]]; then sleep 10 # alice pays bob invoice=$($bob addinvoice 0.05 "test") - $alice lnpay $invoice || true + $alice lnpay $invoice --timeout=1 || true sleep 1 settled=$($alice list_channels | jq '.[] | .local_htlcs | .settles | length') if [[ "$settled" != "0" ]]; then - echo 'DO_NOT_SETTLE did not work' + echo 'SETTLE_DELAY did not work' exit 1 fi # bob goes away t@@ -145,3 +145,46 @@ if [[ $1 == "redeem_htlcs" ]]; then exit 1 fi fi + + +if [[ $1 == "breach_with_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 + sleep 1 + # alice opens channel + bob_node=$($bob nodeid) + channel=$($alice open_channel $bob_node 0.15) + new_blocks 6 + sleep 5 + # alice pays bob + invoice=$($bob addinvoice 0.05 "test") + echo "alice pays bob" + $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' + exit 1 + fi + ctx=$($alice get_channel_ctx $channel | jq '.hex' | tr -d '"') + sleep 3 + settled=$($alice list_channels | jq '.[] | .local_htlcs | .settles | length') + if [[ "$settled" != "1" ]]; then + echo 'SETTLE_DELAY did not work' + exit 1 + fi + echo $($bob getbalance) + echo "alice breaches with old ctx" + echo $ctx + $bitcoin_cli sendrawtransaction $ctx + new_blocks 1 + sleep 10 + new_blocks 1 + sleep 1 + echo $($bob getbalance) + balance_after=$($bob getbalance | jq '[.confirmed, .unconfirmed] | to_entries | map(select(.value != null).value) | map(tonumber) | add ') + if (( $(echo "$balance_after < 0.14" | bc -l) )); then + echo "htlc not redeemed." + exit 1 + fi +fi (DIR) diff --git a/electrum/tests/test_regtest.py b/electrum/tests/test_regtest.py t@@ -32,3 +32,6 @@ 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'])