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

spesmilo / electrum / 6606673856430080

15 Jul 2025 10:35PM UTC coverage: 59.8% (-0.003%) from 59.803%
6606673856430080

push

CirrusCI

SomberNight
verifier: fix off-by-one for max_checkpoint

if a wallet had a tx mined in the max_checkpoint block, in certain cases
we would leave it forever in the "unverified" state and remain stuck in "synchronizing..."

0 of 3 new or added lines in 2 files covered. (0.0%)

10 existing lines in 5 files now uncovered.

21985 of 36764 relevant lines covered (59.8%)

2.99 hits per line

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

33.06
/electrum/lnverifier.py
1
# -*- coding: utf-8 -*-
2
#
3
# Electrum - lightweight Bitcoin client
4
# Copyright (C) 2018 The Electrum developers
5
#
6
# Permission is hereby granted, free of charge, to any person
7
# obtaining a copy of this software and associated documentation files
8
# (the "Software"), to deal in the Software without restriction,
9
# including without limitation the rights to use, copy, modify, merge,
10
# publish, distribute, sublicense, and/or sell copies of the Software,
11
# and to permit persons to whom the Software is furnished to do so,
12
# subject to the following conditions:
13
#
14
# The above copyright notice and this permission notice shall be
15
# included in all copies or substantial portions of the Software.
16
#
17
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
21
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
22
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
23
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
# SOFTWARE.
25

26
import asyncio
5✔
27
import threading
5✔
28
from typing import TYPE_CHECKING, Dict, Set
5✔
29

30
import aiorpcx
5✔
31
import electrum_ecc as ecc
5✔
32
from electrum_ecc import ECPubkey
5✔
33

34
from . import bitcoin
5✔
35
from . import constants
5✔
36
from .util import bfh, NetworkJobOnDefaultServer
5✔
37
from .lnutil import funding_output_script_from_keys, ShortChannelID
5✔
38
from .verifier import verify_tx_is_in_block, MerkleVerificationFailure
5✔
39
from .transaction import Transaction
5✔
40
from .interface import GracefulDisconnect
5✔
41
from .crypto import sha256d
5✔
42
from .lnmsg import decode_msg, encode_msg
5✔
43

44
if TYPE_CHECKING:
5✔
45
    from .network import Network
×
46
    from .lnrouter import ChannelDB
×
47

48

49
class LNChannelVerifier(NetworkJobOnDefaultServer):
5✔
50
    """ Verify channel announcements for the Channel DB """
51

52
    # FIXME the initial routing sync is bandwidth-heavy, and the electrum server
53
    # will start throttling us, making it even slower. one option would be to
54
    # spread it over multiple servers.
55

56
    def __init__(self, network: 'Network', channel_db: 'ChannelDB'):
5✔
57
        self.channel_db = channel_db
5✔
58
        self.lock = threading.Lock()
5✔
59
        self.unverified_channel_info = {}  # type: Dict[ShortChannelID, dict]  # scid -> msg_dict
5✔
60
        # channel announcements that seem to be invalid:
61
        self.blacklist = set()  # type: Set[ShortChannelID]
5✔
62
        NetworkJobOnDefaultServer.__init__(self, network)
5✔
63

64
    def _reset(self):
5✔
65
        super()._reset()
5✔
66
        self.started_verifying_channel = set()  # type: Set[ShortChannelID]
5✔
67

68
    # TODO make async; and rm self.lock completely
69
    def add_new_channel_info(self, short_channel_id: ShortChannelID, msg: dict) -> bool:
5✔
70
        if short_channel_id in self.unverified_channel_info:
×
71
            return False
×
72
        if short_channel_id in self.blacklist:
×
73
            return False
×
74
        with self.lock:
×
75
            self.unverified_channel_info[short_channel_id] = msg
×
76
            return True
×
77

78
    async def _run_tasks(self, *, taskgroup):
5✔
79
        await super()._run_tasks(taskgroup=taskgroup)
×
80
        async with taskgroup as group:
×
81
            await group.spawn(self.main)
×
82

83
    async def main(self):
5✔
84
        while True:
×
85
            await self._verify_some_channels()
×
86
            await asyncio.sleep(0.1)
×
87

88
    async def _verify_some_channels(self):
5✔
89
        blockchain = self.network.blockchain()
×
90
        local_height = blockchain.height()
×
91

92
        with self.lock:
×
93
            unverified_channel_info = list(self.unverified_channel_info)
×
94

95
        for short_channel_id in unverified_channel_info:
×
96
            if short_channel_id in self.started_verifying_channel:
×
97
                continue
×
98
            block_height = short_channel_id.block_height
×
99
            # only resolve short_channel_id if headers are available.
100
            if block_height <= 0 or block_height > local_height:
×
101
                continue
×
102
            header = blockchain.read_header(block_height)
×
103
            if header is None:
×
NEW
104
                if block_height <= constants.net.max_checkpoint():
×
NEW
105
                    await self.taskgroup.spawn(self.interface.request_chunk(block_height, can_return_early=True))
×
106
                continue
×
107
            self.started_verifying_channel.add(short_channel_id)
×
108
            await self.taskgroup.spawn(self.verify_channel(block_height, short_channel_id))
×
109
            #self.logger.info(f'requested short_channel_id {short_channel_id.hex()}')
110

111
    async def verify_channel(self, block_height: int, short_channel_id: ShortChannelID):
5✔
112
        # we are verifying channel announcements as they are from untrusted ln peers.
113
        # we use electrum servers to do this. however we don't trust electrum servers either...
114
        try:
×
115
            async with self._network_request_semaphore:
×
116
                result = await self.network.get_txid_from_txpos(
×
117
                    block_height, short_channel_id.txpos, True)
118
        except aiorpcx.jsonrpc.RPCError:
×
119
            # the electrum server is complaining about the txpos for given block.
120
            # it is not clear what to do now, but let's believe the server.
121
            self._blacklist_short_channel_id(short_channel_id)
×
122
            return
×
123
        tx_hash = result['tx_hash']
×
124
        merkle_branch = result['merkle']
×
125
        # we need to wait if header sync/reorg is still ongoing, hence lock:
126
        async with self.network.bhi_lock:
×
127
            header = self.network.blockchain().read_header(block_height)
×
128
        try:
×
129
            verify_tx_is_in_block(tx_hash, merkle_branch, short_channel_id.txpos, header, block_height)
×
130
        except MerkleVerificationFailure as e:
×
131
            # the electrum server sent an incorrect proof. blame is on server, not the ln peer
132
            raise GracefulDisconnect(e) from e
×
133
        try:
×
134
            async with self._network_request_semaphore:
×
135
                raw_tx = await self.network.get_transaction(tx_hash)
×
136
        except aiorpcx.jsonrpc.RPCError as e:
×
137
            # the electrum server can't find the tx; but it was the
138
            # one who told us about the txid!! blame is on server
139
            raise GracefulDisconnect(e) from e
×
140
        tx = Transaction(raw_tx)
×
141
        try:
×
142
            tx.deserialize()
×
143
        except Exception:
×
144
            # either bug in client, or electrum server is evil.
145
            # if we connect to a diff server at some point, let's try again.
146
            self.logger.warning(f"cannot deserialize transaction, skipping {tx_hash}")
×
147
            return
×
148
        if tx_hash != tx.txid():
×
149
            # either bug in client, or electrum server is evil.
150
            # if we connect to a diff server at some point, let's try again.
151
            self.logger.info(f"received tx does not match expected txid ({tx_hash} != {tx.txid()})")
×
152
            return
×
153
        # check funding output
154
        chan_ann_msg = self.unverified_channel_info[short_channel_id]
×
155
        redeem_script = funding_output_script_from_keys(chan_ann_msg['bitcoin_key_1'], chan_ann_msg['bitcoin_key_2'])
×
156
        expected_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script)
×
157
        try:
×
158
            actual_output = tx.outputs()[short_channel_id.output_index]
×
159
        except IndexError:
×
160
            self._blacklist_short_channel_id(short_channel_id)
×
161
            return
×
162
        if expected_address != actual_output.address:
×
163
            # FIXME what now? best would be to ban the originating ln peer.
164
            self.logger.info(f"funding output script mismatch for {short_channel_id}")
×
165
            self._remove_channel_from_unverified_db(short_channel_id)
×
166
            return
×
167
        # put channel into channel DB
168
        self.channel_db.add_verified_channel_info(chan_ann_msg, capacity_sat=actual_output.value)
×
169
        self._remove_channel_from_unverified_db(short_channel_id)
×
170

171
    def _remove_channel_from_unverified_db(self, short_channel_id: ShortChannelID):
5✔
172
        with self.lock:
×
173
            self.unverified_channel_info.pop(short_channel_id, None)
×
174
        self.started_verifying_channel.discard(short_channel_id)
×
175

176
    def _blacklist_short_channel_id(self, short_channel_id: ShortChannelID) -> None:
5✔
177
        self.blacklist.add(short_channel_id)
×
178
        with self.lock:
×
179
            self.unverified_channel_info.pop(short_channel_id, None)
×
180

181

182
def verify_sig_for_channel_update(chan_upd: dict, node_id: bytes) -> bool:
5✔
183
    msg_bytes = chan_upd['raw']
5✔
184
    pre_hash = msg_bytes[2+64:]
5✔
185
    h = sha256d(pre_hash)
5✔
186
    sig = chan_upd['signature']
5✔
187
    if not ECPubkey(node_id).ecdsa_verify(sig, h):
5✔
188
        return False
×
189
    return True
5✔
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