• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

spesmilo / electrum / 6542897949966336

11 Dec 2024 03:12PM UTC coverage: 60.418% (+0.05%) from 60.37%
6542897949966336

push

CirrusCI

ecdsa
Refactor lnsweep:
 - PartialTxInput has an optional make_witness method
 - SweepInfo has a txin enriched with make_witness
 - SweepInfo has an optional txout, for 1st stage HTLCs
 - sweep transactions are created in lnwatcher

The purpose of this change is to allow combining several
inputs in the same sweep transaction.

29 of 128 new or added lines in 5 files covered. (22.66%)

4 existing lines in 2 files now uncovered.

20124 of 33308 relevant lines covered (60.42%)

3.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

23.57
/electrum/lnwatcher.py
1
# Copyright (C) 2018 The Electrum developers
2
# Distributed under the MIT software license, see the accompanying
3
# file LICENCE or http://www.opensource.org/licenses/mit-license.php
4

5
from typing import NamedTuple, Iterable, TYPE_CHECKING
6✔
6
import os
6✔
7
import asyncio
6✔
8
from enum import IntEnum, auto
6✔
9
from typing import NamedTuple, Dict
6✔
10

11
from . import util
6✔
12
from .sql_db import SqlDB, sql
6✔
13
from .wallet_db import WalletDB
6✔
14
from .util import bfh, log_exceptions, ignore_exceptions, TxMinedInfo, random_shuffled_copy
6✔
15
from .address_synchronizer import AddressSynchronizer, TX_HEIGHT_LOCAL, TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE
6✔
16
from .transaction import Transaction, TxOutpoint
6✔
17
from .transaction import match_script_against_template
6✔
18
from .lnutil import WITNESS_TEMPLATE_RECEIVED_HTLC, WITNESS_TEMPLATE_OFFERED_HTLC
6✔
19
from .logging import Logger
6✔
20

21

22
if TYPE_CHECKING:
6✔
23
    from .network import Network
×
24
    from .lnsweep import SweepInfo
×
25
    from .lnworker import LNWallet
×
26

27
class ListenerItem(NamedTuple):
6✔
28
    # this is triggered when the lnwatcher is all done with the outpoint used as index in LNWatcher.tx_progress
29
    all_done : asyncio.Event
6✔
30
    # txs we broadcast are put on this queue so that the test can wait for them to get mined
31
    tx_queue : asyncio.Queue
6✔
32

33
class TxMinedDepth(IntEnum):
6✔
34
    """ IntEnum because we call min() in get_deepest_tx_mined_depth_for_txids """
35
    DEEP = auto()
6✔
36
    SHALLOW = auto()
6✔
37
    MEMPOOL = auto()
6✔
38
    FREE = auto()
6✔
39

40

41
create_sweep_txs="""
6✔
42
CREATE TABLE IF NOT EXISTS sweep_txs (
43
funding_outpoint VARCHAR(34) NOT NULL,
44
ctn INTEGER NOT NULL,
45
prevout VARCHAR(34),
46
tx VARCHAR
47
)"""
48

49
create_channel_info="""
6✔
50
CREATE TABLE IF NOT EXISTS channel_info (
51
outpoint VARCHAR(34) NOT NULL,
52
address VARCHAR(32),
53
PRIMARY KEY(outpoint)
54
)"""
55

56

57
class SweepStore(SqlDB):
6✔
58

59
    def __init__(self, path, network):
6✔
60
        super().__init__(network.asyncio_loop, path)
×
61

62
    def create_database(self):
6✔
63
        c = self.conn.cursor()
×
64
        c.execute(create_channel_info)
×
65
        c.execute(create_sweep_txs)
×
66
        self.conn.commit()
×
67

68
    @sql
6✔
69
    def get_sweep_tx(self, funding_outpoint, prevout):
6✔
70
        c = self.conn.cursor()
×
71
        c.execute("SELECT tx FROM sweep_txs WHERE funding_outpoint=? AND prevout=?", (funding_outpoint, prevout))
×
72
        return [Transaction(r[0].hex()) for r in c.fetchall()]
×
73

74
    @sql
6✔
75
    def list_sweep_tx(self):
6✔
76
        c = self.conn.cursor()
×
77
        c.execute("SELECT funding_outpoint FROM sweep_txs")
×
78
        return set([r[0] for r in c.fetchall()])
×
79

80
    @sql
6✔
81
    def add_sweep_tx(self, funding_outpoint, ctn, prevout, raw_tx):
6✔
82
        c = self.conn.cursor()
×
83
        assert Transaction(raw_tx).is_complete()
×
84
        c.execute("""INSERT INTO sweep_txs (funding_outpoint, ctn, prevout, tx) VALUES (?,?,?,?)""", (funding_outpoint, ctn, prevout, bfh(raw_tx)))
×
85
        self.conn.commit()
×
86

87
    @sql
6✔
88
    def get_num_tx(self, funding_outpoint):
6✔
89
        c = self.conn.cursor()
×
90
        c.execute("SELECT count(*) FROM sweep_txs WHERE funding_outpoint=?", (funding_outpoint,))
×
91
        return int(c.fetchone()[0])
×
92

93
    @sql
6✔
94
    def get_ctn(self, outpoint, addr):
6✔
95
        if not self._has_channel(outpoint):
×
96
            self._add_channel(outpoint, addr)
×
97
        c = self.conn.cursor()
×
98
        c.execute("SELECT max(ctn) FROM sweep_txs WHERE funding_outpoint=?", (outpoint,))
×
99
        return int(c.fetchone()[0] or 0)
×
100

101
    @sql
6✔
102
    def remove_sweep_tx(self, funding_outpoint):
6✔
103
        c = self.conn.cursor()
×
104
        c.execute("DELETE FROM sweep_txs WHERE funding_outpoint=?", (funding_outpoint,))
×
105
        self.conn.commit()
×
106

107
    def _add_channel(self, outpoint, address):
6✔
108
        c = self.conn.cursor()
×
109
        c.execute("INSERT INTO channel_info (address, outpoint) VALUES (?,?)", (address, outpoint))
×
110
        self.conn.commit()
×
111

112
    @sql
6✔
113
    def remove_channel(self, outpoint):
6✔
114
        c = self.conn.cursor()
×
115
        c.execute("DELETE FROM channel_info WHERE outpoint=?", (outpoint,))
×
116
        self.conn.commit()
×
117

118
    def _has_channel(self, outpoint):
6✔
119
        c = self.conn.cursor()
×
120
        c.execute("SELECT * FROM channel_info WHERE outpoint=?", (outpoint,))
×
121
        r = c.fetchone()
×
122
        return r is not None
×
123

124
    @sql
6✔
125
    def get_address(self, outpoint):
6✔
126
        c = self.conn.cursor()
×
127
        c.execute("SELECT address FROM channel_info WHERE outpoint=?", (outpoint,))
×
128
        r = c.fetchone()
×
129
        return r[0] if r else None
×
130

131
    @sql
6✔
132
    def list_channels(self):
6✔
133
        c = self.conn.cursor()
×
134
        c.execute("SELECT outpoint, address FROM channel_info")
×
135
        return [(r[0], r[1]) for r in c.fetchall()]
×
136

137

138
from .util import EventListener, event_listener
6✔
139

140
class LNWatcher(Logger, EventListener):
6✔
141

142
    LOGGING_SHORTCUT = 'W'
6✔
143

144
    def __init__(self, adb: 'AddressSynchronizer', network: 'Network'):
6✔
145

146
        Logger.__init__(self)
×
147
        self.adb = adb
×
148
        self.config = network.config
×
149
        self.callbacks = {} # address -> lambda: coroutine
×
150
        self.network = network
×
151
        self.register_callbacks()
×
152
        # status gets populated when we run
153
        self.channel_status = {}
×
154

155
    async def stop(self):
6✔
156
        self.unregister_callbacks()
×
157

158
    def get_channel_status(self, outpoint):
6✔
159
        return self.channel_status.get(outpoint, 'unknown')
×
160

161
    def add_channel(self, outpoint: str, address: str) -> None:
6✔
162
        assert isinstance(outpoint, str)
×
163
        assert isinstance(address, str)
×
164
        cb = lambda: self.check_onchain_situation(address, outpoint)
×
165
        self.add_callback(address, cb)
×
166

167
    async def unwatch_channel(self, address, funding_outpoint):
6✔
168
        self.logger.info(f'unwatching {funding_outpoint}')
×
169
        self.remove_callback(address)
×
170

171
    def remove_callback(self, address):
6✔
172
        self.callbacks.pop(address, None)
×
173

174
    def add_callback(self, address, callback):
6✔
175
        self.adb.add_address(address)
×
176
        self.callbacks[address] = callback
×
177

178
    @event_listener
6✔
179
    async def on_event_blockchain_updated(self, *args):
6✔
180
        await self.trigger_callbacks()
×
181

182
    @event_listener
6✔
183
    async def on_event_adb_added_verified_tx(self, adb, tx_hash):
6✔
184
        if adb != self.adb:
×
185
            return
×
186
        await self.trigger_callbacks()
×
187

188
    @event_listener
6✔
189
    async def on_event_adb_set_up_to_date(self, adb):
6✔
190
        if adb != self.adb:
×
191
            return
×
192
        await self.trigger_callbacks()
×
193

194
    @log_exceptions
6✔
195
    async def trigger_callbacks(self):
6✔
196
        if not self.adb.synchronizer:
×
197
            self.logger.info("synchronizer not set yet")
×
198
            return
×
199
        for address, callback in list(self.callbacks.items()):
×
200
            await callback()
×
201

202
    async def check_onchain_situation(self, address, funding_outpoint):
6✔
203
        # early return if address has not been added yet
204
        if not self.adb.is_mine(address):
×
205
            return
×
206
        # inspect_tx_candidate might have added new addresses, in which case we return early
207
        if not self.adb.is_up_to_date():
×
208
            return
×
209
        funding_txid = funding_outpoint.split(':')[0]
×
210
        funding_height = self.adb.get_tx_height(funding_txid)
×
211
        closing_txid = self.get_spender(funding_outpoint)
×
212
        closing_height = self.adb.get_tx_height(closing_txid)
×
213
        if closing_txid:
×
214
            closing_tx = self.adb.get_transaction(closing_txid)
×
215
            if closing_tx:
×
216
                keep_watching = await self.sweep_commitment_transaction(funding_outpoint, closing_tx)
×
217
            else:
218
                self.logger.info(f"channel {funding_outpoint} closed by {closing_txid}. still waiting for tx itself...")
×
219
                keep_watching = True
×
220
        else:
221
            keep_watching = True
×
222
        await self.update_channel_state(
×
223
            funding_outpoint=funding_outpoint,
224
            funding_txid=funding_txid,
225
            funding_height=funding_height,
226
            closing_txid=closing_txid,
227
            closing_height=closing_height,
228
            keep_watching=keep_watching)
229
        if not keep_watching:
×
230
            await self.unwatch_channel(address, funding_outpoint)
×
231

232
    async def sweep_commitment_transaction(self, funding_outpoint, closing_tx) -> bool:
6✔
233
        raise NotImplementedError()  # implemented by subclasses
×
234

235
    async def update_channel_state(self, *, funding_outpoint: str, funding_txid: str,
6✔
236
                                   funding_height: TxMinedInfo, closing_txid: str,
237
                                   closing_height: TxMinedInfo, keep_watching: bool) -> None:
238
        raise NotImplementedError()  # implemented by subclasses
×
239

240

241
    def get_spender(self, outpoint) -> str:
6✔
242
        """
243
        returns txid spending outpoint.
244
        subscribes to addresses as a side effect.
245
        """
246
        prev_txid, index = outpoint.split(':')
×
247
        spender_txid = self.adb.db.get_spent_outpoint(prev_txid, int(index))
×
248
        if not spender_txid:
×
249
            return
×
250
        spender_tx = self.adb.get_transaction(spender_txid)
×
251
        for i, o in enumerate(spender_tx.outputs()):
×
252
            if o.address is None:
×
253
                continue
×
254
            if not self.adb.is_mine(o.address):
×
255
                self.adb.add_address(o.address)
×
256
        return spender_txid
×
257

258
    def inspect_tx_candidate(self, outpoint, n: int) -> Dict[str, str]:
6✔
259
        """
260
        returns a dict of spenders for a transaction of interest.
261
        subscribes to addresses as a side effect.
262
        n==0 => outpoint is a channel funding.
263
        n==1 => outpoint is a commitment or close output: to_local, to_remote or first-stage htlc
264
        n==2 => outpoint is a second-stage htlc
265
        """
266
        prev_txid, index = outpoint.split(':')
×
267
        spender_txid = self.adb.db.get_spent_outpoint(prev_txid, int(index))
×
268
        result = {outpoint:spender_txid}
×
269
        if n == 0:
×
270
            if spender_txid is None:
×
271
                self.channel_status[outpoint] = 'open'
×
272
            elif not self.is_deeply_mined(spender_txid):
×
273
                self.channel_status[outpoint] = 'closed (%d)' % self.adb.get_tx_height(spender_txid).conf
×
274
            else:
275
                self.channel_status[outpoint] = 'closed (deep)'
×
276
        if spender_txid is None:
×
277
            return result
×
278
        spender_tx = self.adb.get_transaction(spender_txid)
×
279
        if n == 1:
×
280
            # if tx input is not a first-stage HTLC, we can stop recursion
281
            # FIXME: this is not true for anchor channels
282
            if len(spender_tx.inputs()) != 1:
×
283
                return result
×
284
            o = spender_tx.inputs()[0]
×
285
            witness = o.witness_elements()
×
286
            if not witness:
×
287
                # This can happen if spender_tx is a local unsigned tx in the wallet history, e.g.:
288
                # channel is coop-closed, outpoint is for our coop-close output, and spender_tx is an
289
                # arbitrary wallet-spend.
290
                return result
×
291
            redeem_script = witness[-1]
×
292
            if match_script_against_template(redeem_script, WITNESS_TEMPLATE_OFFERED_HTLC):
×
293
                #self.logger.info(f"input script matches offered htlc {redeem_script.hex()}")
294
                pass
×
295
            elif match_script_against_template(redeem_script, WITNESS_TEMPLATE_RECEIVED_HTLC):
×
296
                #self.logger.info(f"input script matches received htlc {redeem_script.hex()}")
297
                pass
×
298
            else:
299
                return result
×
300
        for i, o in enumerate(spender_tx.outputs()):
×
301
            if o.address is None:
×
302
                continue
×
303
            if not self.adb.is_mine(o.address):
×
304
                self.adb.add_address(o.address)
×
305
            elif n < 2:
×
306
                r = self.inspect_tx_candidate(spender_txid+':%d'%i, n+1)
×
307
                result.update(r)
×
308
        return result
×
309

310
    def get_tx_mined_depth(self, txid: str):
6✔
311
        if not txid:
×
312
            return TxMinedDepth.FREE
×
313
        tx_mined_depth = self.adb.get_tx_height(txid)
×
314
        height, conf = tx_mined_depth.height, tx_mined_depth.conf
×
315
        if conf > 100:
×
316
            return TxMinedDepth.DEEP
×
317
        elif conf > 0:
×
318
            return TxMinedDepth.SHALLOW
×
319
        elif height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT):
×
320
            return TxMinedDepth.MEMPOOL
×
321
        elif height in (TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE):
×
322
            return TxMinedDepth.FREE
×
323
        elif height > 0 and conf == 0:
×
324
            # unverified but claimed to be mined
325
            return TxMinedDepth.MEMPOOL
×
326
        else:
327
            raise NotImplementedError()
×
328

329
    def is_deeply_mined(self, txid):
6✔
330
        return self.get_tx_mined_depth(txid) == TxMinedDepth.DEEP
×
331

332

333
class WatchTower(LNWatcher):
6✔
334

335
    LOGGING_SHORTCUT = 'W'
6✔
336

337
    def __init__(self, network: 'Network'):
6✔
338
        adb = AddressSynchronizer(WalletDB('', storage=None, upgrade=True), network.config, name=self.diagnostic_name())
×
339
        adb.start_network(network)
×
340
        LNWatcher.__init__(self, adb, network)
×
341
        self.network = network
×
342
        self.sweepstore = SweepStore(os.path.join(self.network.config.path, "watchtower_db"), network)
×
343
        # this maps funding_outpoints to ListenerItems, which have an event for when the watcher is done,
344
        # and a queue for seeing which txs are being published
345
        self.tx_progress = {} # type: Dict[str, ListenerItem]
×
346

347
    async def stop(self):
6✔
348
        await super().stop()
×
349
        await self.adb.stop()
×
350

351
    def diagnostic_name(self):
6✔
352
        return "local_tower"
×
353

354
    async def start_watching(self):
6✔
355
        # I need to watch the addresses from sweepstore
356
        lst = await self.sweepstore.list_channels()
×
357
        for outpoint, address in random_shuffled_copy(lst):
×
358
            self.add_channel(outpoint, address)
×
359

360
    async def sweep_commitment_transaction(self, funding_outpoint, closing_tx):
6✔
361
        spenders = self.inspect_tx_candidate(funding_outpoint, 0)
×
362
        keep_watching = False
×
363
        for prevout, spender in spenders.items():
×
364
            if spender is not None:
×
365
                keep_watching |= not self.is_deeply_mined(spender)
×
366
                continue
×
367
            sweep_txns = await self.sweepstore.get_sweep_tx(funding_outpoint, prevout)
×
368
            for tx in sweep_txns:
×
369
                await self.broadcast_or_log(funding_outpoint, tx)
×
370
                keep_watching = True
×
371
        return keep_watching
×
372

373
    async def broadcast_or_log(self, funding_outpoint: str, tx: Transaction):
6✔
374
        height = self.adb.get_tx_height(tx.txid()).height
×
375
        if height != TX_HEIGHT_LOCAL:
×
376
            return
×
377
        try:
×
378
            txid = await self.network.broadcast_transaction(tx)
×
379
        except Exception as e:
×
380
            self.logger.info(f'broadcast failure: txid={tx.txid()}, funding_outpoint={funding_outpoint}: {repr(e)}')
×
381
        else:
382
            self.logger.info(f'broadcast success: txid={tx.txid()}, funding_outpoint={funding_outpoint}')
×
383
            if funding_outpoint in self.tx_progress:
×
384
                await self.tx_progress[funding_outpoint].tx_queue.put(tx)
×
385
            return txid
×
386

387
    async def get_ctn(self, outpoint, addr):
6✔
388
        if addr not in self.callbacks.keys():
×
389
            self.logger.info(f'watching new channel: {outpoint} {addr}')
×
390
            self.add_channel(outpoint, addr)
×
391
        return await self.sweepstore.get_ctn(outpoint, addr)
×
392

393
    def get_num_tx(self, outpoint):
6✔
394
        async def f():
×
395
            return await self.sweepstore.get_num_tx(outpoint)
×
396
        return self.network.run_from_another_thread(f())
×
397

398
    def list_sweep_tx(self):
6✔
399
        async def f():
×
400
            return await self.sweepstore.list_sweep_tx()
×
401
        return self.network.run_from_another_thread(f())
×
402

403
    def list_channels(self):
6✔
404
        async def f():
×
405
            return await self.sweepstore.list_channels()
×
406
        return self.network.run_from_another_thread(f())
×
407

408
    async def unwatch_channel(self, address, funding_outpoint):
6✔
409
        await super().unwatch_channel(address, funding_outpoint)
×
410
        await self.sweepstore.remove_sweep_tx(funding_outpoint)
×
411
        await self.sweepstore.remove_channel(funding_outpoint)
×
412
        if funding_outpoint in self.tx_progress:
×
413
            self.tx_progress[funding_outpoint].all_done.set()
×
414

415
    async def update_channel_state(self, *args, **kwargs):
6✔
416
        pass
×
417

418

419

420

421
class LNWalletWatcher(LNWatcher):
6✔
422

423
    def __init__(self, lnworker: 'LNWallet', network: 'Network'):
6✔
424
        self.network = network
×
425
        self.lnworker = lnworker
×
426
        LNWatcher.__init__(self, lnworker.wallet.adb, network)
×
427

428
    @event_listener
6✔
429
    async def on_event_blockchain_updated(self, *args):
6✔
430
        # overload parent method with cache invalidation
431
        # we invalidate the cache on each new block because
432
        # some processes affect the list of sweep transactions
433
        # (hold invoice preimage revealed, MPP completed, etc)
434
        for chan in self.lnworker.channels.values():
×
435
            chan._sweep_info.clear()
×
436
        await self.trigger_callbacks()
×
437

438
    def diagnostic_name(self):
6✔
439
        return f"{self.lnworker.wallet.diagnostic_name()}-LNW"
×
440

441
    @ignore_exceptions
6✔
442
    @log_exceptions
6✔
443
    async def update_channel_state(self, *, funding_outpoint: str, funding_txid: str,
6✔
444
                                   funding_height: TxMinedInfo, closing_txid: str,
445
                                   closing_height: TxMinedInfo, keep_watching: bool) -> None:
446
        chan = self.lnworker.channel_by_txo(funding_outpoint)
×
447
        if not chan:
×
448
            return
×
449
        chan.update_onchain_state(
×
450
            funding_txid=funding_txid,
451
            funding_height=funding_height,
452
            closing_txid=closing_txid,
453
            closing_height=closing_height,
454
            keep_watching=keep_watching)
455
        await self.lnworker.handle_onchain_state(chan)
×
456

457
    @log_exceptions
6✔
458
    async def sweep_commitment_transaction(self, funding_outpoint, closing_tx) -> bool:
6✔
459
        """This function is called when a channel was closed. In this case
460
        we need to check for redeemable outputs of the commitment transaction
461
        or spenders down the line (HTLC-timeout/success transactions).
462

463
        Returns whether we should continue to monitor."""
464
        chan = self.lnworker.channel_by_txo(funding_outpoint)
×
465
        if not chan:
×
466
            return False
×
467
        chan_id_for_log = chan.get_id_for_log()
×
468
        # detect who closed and get information about how to claim outputs
469
        sweep_info_dict = chan.sweep_ctx(closing_tx)
×
470
        self.logger.info(f"do_breach_remedy: {[x.name for x in sweep_info_dict.values()]}")
×
471
        keep_watching = False if sweep_info_dict else not self.is_deeply_mined(closing_tx.txid())
×
472

473
        # create and broadcast transactions
474
        for prevout, sweep_info in sweep_info_dict.items():
×
475
            prev_txid, prev_index = prevout.split(':')
×
476
            name = sweep_info.name + ' ' + chan.get_id_for_log()
×
477
            if not self.adb.get_transaction(prev_txid):
×
478
                # do not keep watching if prevout does not exist
479
                self.logger.info(f'prevout does not exist for {name}: {prev_txid}')
×
480
                continue
×
481
            spender_txid = self.get_spender(prevout)
×
482
            spender_tx = self.adb.get_transaction(spender_txid) if spender_txid else None
×
483
            if spender_tx:
×
484
                # the spender might be the remote, revoked or not
NEW
485
                htlc_idx_to_sweepinfo = chan.maybe_sweep_htlcs(closing_tx, spender_tx)
×
NEW
486
                for prevout2, htlc_sweep_info in htlc_idx_to_sweepinfo.items():
×
NEW
487
                    htlc_tx_spender = self.get_spender(prevout2)
×
488
                    if htlc_tx_spender:
×
489
                        keep_watching |= not self.is_deeply_mined(htlc_tx_spender)
×
490
                    else:
491
                        keep_watching = True
×
NEW
492
                    await self.maybe_redeem(prevout2, htlc_sweep_info, name)
×
493
                else:
494
                    keep_watching |= not self.is_deeply_mined(spender_txid)
×
495
                    txin_idx = spender_tx.get_input_idx_that_spent_prevout(TxOutpoint.from_str(prevout))
×
496
                    assert txin_idx is not None
×
497
                    spender_txin = spender_tx.inputs()[txin_idx]
×
498
                    chan.extract_preimage_from_htlc_txin(spender_txin)
×
499
            else:
500
                keep_watching = True
×
501
            # broadcast or maybe update our own tx
502
            await self.maybe_redeem(prevout, sweep_info, name)
×
503

504
        return keep_watching
×
505

506
    def get_redeem_tx(self, prevout: str, sweep_info: 'SweepInfo', name: str):
6✔
507
        # check if redeem tx needs to be updated
508
        # if it is in the mempool, we need to check fee rise
509
        txid = self.get_spender(prevout)
×
510
        old_tx = self.adb.get_transaction(txid)
×
511
        assert old_tx is not None or txid is None
×
512
        tx_depth = self.get_tx_mined_depth(txid) if txid else None
×
513
        if txid and tx_depth not in [TxMinedDepth.FREE, TxMinedDepth.MEMPOOL]:
×
514
            assert old_tx is not None
×
515
            return old_tx, None
×
NEW
516
        inputs = [sweep_info.txin]
×
NEW
517
        outputs = [sweep_info.txout] if sweep_info.txout else []
×
518
        # password is needed for 1st stage htlc tx with anchors
NEW
519
        password = self.lnworker.wallet.get_unlocked_password()
×
NEW
520
        new_tx = self.lnworker.wallet.create_transaction(
×
521
            inputs = inputs,
522
            outputs = outputs,
523
            password = password,
524
            locktime = sweep_info.cltv_abs,
525
            BIP69_sort=False,
526
        )
527
        if new_tx is None:
×
528
            self.logger.info(f'{name} could not claim output: {prevout}, dust')
×
529
            assert old_tx is not None
×
530
            return old_tx, None
×
531
        if txid is None:
×
532
            return None, new_tx
×
533
        elif tx_depth == TxMinedDepth.MEMPOOL:
×
534
            delta = new_tx.get_fee() - self.adb.get_tx_fee(txid)
×
535
            if delta > 1:
×
536
                self.logger.info(f'increasing fee of mempool tx {name}: {prevout}')
×
537
                return old_tx, new_tx
×
538
            else:
539
                assert old_tx is not None
×
540
                return old_tx, None
×
541
        elif tx_depth == TxMinedDepth.FREE:
×
542
            # return new tx, even if it is equal to old_tx,
543
            # because we need to test if it can be broadcast
544
            return old_tx, new_tx
×
545
        else:
546
            assert old_tx is not None
×
547
            return old_tx, None
×
548

549
    async def maybe_redeem(self, prevout, sweep_info: 'SweepInfo', name: str) -> None:
6✔
550
        old_tx, new_tx = self.get_redeem_tx(prevout, sweep_info, name)
×
551
        if new_tx is None:
×
552
            return
×
553
        prev_txid, prev_index = prevout.split(':')
×
554
        can_broadcast = True
×
555
        local_height = self.network.get_local_height()
×
556
        if sweep_info.cltv_abs:
×
557
            wanted_height = sweep_info.cltv_abs
×
558
            if wanted_height - local_height > 0:
×
559
                can_broadcast = False
×
560
                # self.logger.debug(f"pending redeem for {prevout}. waiting for {name}: CLTV ({local_height=}, {wanted_height=})")
561
        if sweep_info.csv_delay:
×
562
            prev_height = self.adb.get_tx_height(prev_txid)
×
563
            if prev_height.height > 0:
×
564
                wanted_height = prev_height.height + sweep_info.csv_delay - 1
×
565
            else:
566
                wanted_height = local_height + sweep_info.csv_delay
×
567
            if wanted_height - local_height > 0:
×
568
                can_broadcast = False
×
569
                # self.logger.debug(
570
                #     f"pending redeem for {prevout}. waiting for {name}: CSV "
571
                #     f"({local_height=}, {wanted_height=}, {prev_height.height=}, {sweep_info.csv_delay=})")
572
        if not (sweep_info.cltv_abs or sweep_info.csv_delay):
×
573
            # used to control settling of htlcs onchain for testing purposes
574
            # careful, this prevents revocation as well
575
            if not self.lnworker.enable_htlc_settle_onchain:
×
576
                return
×
577
        if can_broadcast:
×
578
            self.logger.info(f'we can broadcast: {name}')
×
579
            if await self.network.try_broadcasting(new_tx, name):
×
580
                tx_was_added = self.adb.add_transaction(new_tx, is_new=(old_tx is None))
×
581
            else:
582
                tx_was_added = False
×
583
        else:
584
            # we may have a tx with a different fee, in which case it will be replaced
585
            if not old_tx or (old_tx and old_tx.txid() != new_tx.txid()):
×
586
                try:
×
587
                    tx_was_added = self.adb.add_transaction(new_tx, is_new=(old_tx is None))
×
588
                except Exception as e:
×
589
                    self.logger.info(f'could not add future tx: {name}. prevout: {prevout} {str(e)}')
×
590
                    tx_was_added = False
×
591
                if tx_was_added:
×
592
                    self.logger.info(f'added redeem tx: {name}. prevout: {prevout}')
×
593
            else:
594
                tx_was_added = False
×
595
            # set future tx regardless of tx_was_added, because it is not persisted
596
            # (and wanted_height can change if input of CSV was not mined before)
597
            self.adb.set_future_tx(new_tx.txid(), wanted_height=wanted_height)
×
598
        if tx_was_added:
×
599
            self.lnworker.wallet.set_label(new_tx.txid(), name)
×
600
            if old_tx and old_tx.txid() != new_tx.txid():
×
601
                self.lnworker.wallet.set_label(old_tx.txid(), None)
×
602
            util.trigger_callback('wallet_updated', self.lnworker.wallet)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc