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

spesmilo / electrum / 5707590002278400

25 Apr 2025 10:46AM UTC coverage: 60.343% (+0.05%) from 60.296%
5707590002278400

Pull #9751

CirrusCI

ecdsa
fixes
Pull Request #9751: Txbatcher without password in memory

42 of 67 new or added lines in 4 files covered. (62.69%)

1196 existing lines in 9 files now uncovered.

21659 of 35893 relevant lines covered (60.34%)

3.01 hits per line

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

53.13
/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 TYPE_CHECKING
5✔
6

7
from .util import TxMinedInfo, BelowDustLimit
5✔
8
from .util import EventListener, event_listener
5✔
9
from .transaction import Transaction, TxOutpoint
5✔
10
from .logging import Logger
5✔
11

12

13
if TYPE_CHECKING:
5✔
14
    from .network import Network
×
15
    from .lnsweep import SweepInfo
×
16
    from .lnworker import LNWallet
×
17
    from .lnchannel import AbstractChannel
×
18

19

20
class LNWatcher(Logger, EventListener):
5✔
21

22
    LOGGING_SHORTCUT = 'W'
5✔
23

24
    def __init__(self, lnworker: 'LNWallet'):
5✔
25
        self.lnworker = lnworker
5✔
26
        Logger.__init__(self)
5✔
27
        self.adb = lnworker.wallet.adb
5✔
28
        self.config = lnworker.config
5✔
29
        self.callbacks = {}  # address -> lambda function
5✔
30
        self.network = None
5✔
31
        self.register_callbacks()
5✔
32
        # status gets populated when we run
33
        self.channel_status = {}
5✔
34

35
    def start_network(self, network: 'Network'):
5✔
36
        self.network = network
×
37

38
    def stop(self):
5✔
39
        self.unregister_callbacks()
×
40

41
    def get_channel_status(self, outpoint):
5✔
42
        return self.channel_status.get(outpoint, 'unknown')
×
43

44
    def remove_callback(self, address):
5✔
45
        self.callbacks.pop(address, None)
5✔
46

47
    def add_callback(self, address, callback):
5✔
48
        self.adb.add_address(address)
5✔
49
        self.callbacks[address] = callback
5✔
50

51
    def trigger_callbacks(self):
5✔
52
        if not self.adb.synchronizer:
5✔
53
            self.logger.info("synchronizer not set yet")
5✔
54
            return
5✔
55
        for address, callback in list(self.callbacks.items()):
×
56
            callback()
×
57

58
    @event_listener
5✔
59
    async def on_event_blockchain_updated(self, *args):
5✔
60
        # we invalidate the cache on each new block because
61
        # some processes affect the list of sweep transactions
62
        # (hold invoice preimage revealed, MPP completed, etc)
63
        for chan in self.lnworker.channels.values():
×
64
            chan._sweep_info.clear()
×
65
        self.trigger_callbacks()
×
66

67
    @event_listener
5✔
68
    def on_event_wallet_updated(self, wallet):
5✔
69
        # called if we add local tx
70
        if wallet.adb != self.adb:
5✔
71
            return
5✔
72
        self.trigger_callbacks()
×
73

74
    @event_listener
5✔
75
    def on_event_adb_added_verified_tx(self, adb, tx_hash):
5✔
76
        if adb != self.adb:
5✔
77
            return
5✔
78
        self.trigger_callbacks()
5✔
79

80
    @event_listener
5✔
81
    def on_event_adb_set_up_to_date(self, adb):
5✔
82
        if adb != self.adb:
5✔
83
            return
5✔
84
        self.trigger_callbacks()
5✔
85

86
    def add_channel(self, chan: 'AbstractChannel') -> None:
5✔
87
        outpoint = chan.funding_outpoint.to_str()
5✔
88
        address = chan.get_funding_address()
5✔
89
        callback = lambda: self.check_onchain_situation(address, outpoint)
5✔
90
        callback()  # run once, for side effects
5✔
91
        if chan.need_to_subscribe():
5✔
92
            self.add_callback(address, callback)
5✔
93

94
    def unwatch_channel(self, address, funding_outpoint):
5✔
95
        self.logger.info(f'unwatching {funding_outpoint}')
5✔
96
        self.remove_callback(address)
5✔
97

98
    def check_onchain_situation(self, address, funding_outpoint):
5✔
99
        # early return if address has not been added yet
100
        if not self.adb.is_mine(address):
5✔
101
            return
×
102
        # inspect_tx_candidate might have added new addresses, in which case we return early
103
        funding_txid = funding_outpoint.split(':')[0]
5✔
104
        funding_height = self.adb.get_tx_height(funding_txid)
5✔
105
        closing_txid = self.adb.get_spender(funding_outpoint)
5✔
106
        closing_height = self.adb.get_tx_height(closing_txid)
5✔
107
        if closing_txid:
5✔
108
            closing_tx = self.adb.get_transaction(closing_txid)
5✔
109
            if closing_tx:
5✔
110
                keep_watching = self.sweep_commitment_transaction(funding_outpoint, closing_tx)
5✔
111
            else:
112
                self.logger.info(f"channel {funding_outpoint} closed by {closing_txid}. still waiting for tx itself...")
×
113
                keep_watching = True
×
114
        else:
115
            keep_watching = True
×
116
        self.update_channel_state(
5✔
117
            funding_outpoint=funding_outpoint,
118
            funding_txid=funding_txid,
119
            funding_height=funding_height,
120
            closing_txid=closing_txid,
121
            closing_height=closing_height,
122
            keep_watching=keep_watching)
123
        if not keep_watching:
5✔
124
            self.unwatch_channel(address, funding_outpoint)
5✔
125

126
    def diagnostic_name(self):
5✔
127
        return f"{self.lnworker.wallet.diagnostic_name()}-LNW"
5✔
128

129
    def update_channel_state(self, *, funding_outpoint: str, funding_txid: str,
5✔
130
                             funding_height: TxMinedInfo, closing_txid: str,
131
                             closing_height: TxMinedInfo, keep_watching: bool) -> None:
132
        chan = self.lnworker.channel_by_txo(funding_outpoint)
5✔
133
        if not chan:
5✔
134
            return
×
135
        chan.update_onchain_state(
5✔
136
            funding_txid=funding_txid,
137
            funding_height=funding_height,
138
            closing_txid=closing_txid,
139
            closing_height=closing_height,
140
            keep_watching=keep_watching)
141
        self.lnworker.handle_onchain_state(chan)
5✔
142

143
    def sweep_commitment_transaction(self, funding_outpoint, closing_tx) -> bool:
5✔
144
        """This function is called when a channel was closed. In this case
145
        we need to check for redeemable outputs of the commitment transaction
146
        or spenders down the line (HTLC-timeout/success transactions).
147

148
        Returns whether we should continue to monitor.
149

150
        Side-effécts:
151
          - sets defaults labels
152
          - populates wallet._accounting_addresses
153
        """
154
        chan = self.lnworker.channel_by_txo(funding_outpoint)
5✔
155
        if not chan:
5✔
156
            return False
×
157
        # detect who closed and get information about how to claim outputs
158
        sweep_info_dict = chan.sweep_ctx(closing_tx)
5✔
159
        keep_watching = False if sweep_info_dict else not self.adb.is_deeply_mined(closing_tx.txid())
5✔
160
        # create and broadcast transactions
161
        for prevout, sweep_info in sweep_info_dict.items():
5✔
162
            prev_txid, prev_index = prevout.split(':')
×
163
            name = sweep_info.name + ' ' + chan.get_id_for_log()
×
164
            self.lnworker.wallet.set_default_label(prevout, name)
×
165
            if not self.adb.get_transaction(prev_txid):
×
166
                # do not keep watching if prevout does not exist
167
                self.logger.info(f'prevout does not exist for {name}: {prevout}')
×
168
                continue
×
169
            spender_txid = self.adb.get_spender(prevout)
×
170
            spender_tx = self.adb.get_transaction(spender_txid) if spender_txid else None
×
171
            if spender_tx:
×
172
                # the spender might be the remote, revoked or not
173
                htlc_sweepinfo = chan.maybe_sweep_htlcs(closing_tx, spender_tx)
×
174
                for prevout2, htlc_sweep_info in htlc_sweepinfo.items():
×
175
                    htlc_tx_spender = self.adb.get_spender(prevout2)
×
176
                    self.lnworker.wallet.set_default_label(prevout2, htlc_sweep_info.name)
×
177
                    if htlc_tx_spender:
×
178
                        keep_watching |= not self.adb.is_deeply_mined(htlc_tx_spender)
×
179
                        self.maybe_add_accounting_address(htlc_tx_spender, htlc_sweep_info)
×
180
                    else:
181
                        keep_watching |= self.maybe_redeem(htlc_sweep_info)
×
182
                keep_watching |= not self.adb.is_deeply_mined(spender_txid)
×
183
                self.maybe_extract_preimage(chan, spender_tx, prevout)
×
184
                self.maybe_add_accounting_address(spender_txid, sweep_info)
×
185
            else:
186
                keep_watching |= self.maybe_redeem(sweep_info)
×
187
        return keep_watching
5✔
188

189
    def maybe_redeem(self, sweep_info: 'SweepInfo') -> bool:
5✔
190
        """ returns False if it was dust """
191
        try:
×
192
            self.lnworker.wallet.txbatcher.add_sweep_input('lnwatcher', sweep_info, self.config.FEE_POLICY_LIGHTNING)
×
193
        except BelowDustLimit:
×
194
            return False
×
195
        return True
×
196

197
    def maybe_extract_preimage(self, chan: 'AbstractChannel', spender_tx: Transaction, prevout: str):
5✔
NEW
198
        if not spender_tx.is_complete():
×
NEW
199
            self.logger.info('spender tx is unsigned')
×
NEW
200
            return
×
201
        txin_idx = spender_tx.get_input_idx_that_spent_prevout(TxOutpoint.from_str(prevout))
×
202
        assert txin_idx is not None
×
203
        spender_txin = spender_tx.inputs()[txin_idx]
×
204
        chan.extract_preimage_from_htlc_txin(
×
205
            spender_txin,
206
            is_deeply_mined=self.adb.is_deeply_mined(spender_tx.txid()),
207
        )
208

209
    def maybe_add_accounting_address(self, spender_txid: str, sweep_info: 'SweepInfo'):
5✔
210
        spender_tx = self.adb.get_transaction(spender_txid) if spender_txid else None
×
211
        if not spender_tx:
×
212
            return
×
213
        for i, txin in enumerate(spender_tx.inputs()):
×
214
            if txin.prevout == sweep_info.txin.prevout:
×
215
                break
×
216
        else:
217
            return
×
218
        if sweep_info.name in ['first-stage-htlc', 'first-stage-htlc-anchors']:
×
219
            # always consider ours
220
            pass
×
221
        else:
222
            privkey = sweep_info.txin.privkey
×
223
            witness = txin.witness_elements()
×
224
            for sig in witness:
×
225
                sig = witness[0]
×
226
                # fixme: verify sig is ours
227
                witness2 = sweep_info.txin.make_witness(sig)
×
228
                if txin.witness == witness2:
×
229
                    break
×
230
            else:
231
                self.logger.info(f"signature not found {sweep_info.name}, {txin.prevout.to_str()}")
×
232
                return
×
233
        self.logger.info(f'adding txin address {sweep_info.name}, {txin.prevout.to_str()}')
×
234
        prev_txid, prev_index = txin.prevout.to_str().split(':')
×
235
        prev_tx = self.adb.get_transaction(prev_txid)
×
236
        txout = prev_tx.outputs()[int(prev_index)]
×
237
        self.lnworker.wallet._accounting_addresses.add(txout.address)
×
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

© 2026 Coveralls, Inc