electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

lnverifier.py (8570B)


      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
     27 import threading
     28 from typing import TYPE_CHECKING, Dict, Set
     29 
     30 import aiorpcx
     31 
     32 from . import bitcoin
     33 from . import ecc
     34 from . import constants
     35 from .util import bh2u, bfh, NetworkJobOnDefaultServer
     36 from .lnutil import funding_output_script_from_keys, ShortChannelID
     37 from .verifier import verify_tx_is_in_block, MerkleVerificationFailure
     38 from .transaction import Transaction
     39 from .interface import GracefulDisconnect
     40 from .crypto import sha256d
     41 from .lnmsg import decode_msg, encode_msg
     42 
     43 if TYPE_CHECKING:
     44     from .network import Network
     45     from .lnrouter import ChannelDB
     46 
     47 
     48 class LNChannelVerifier(NetworkJobOnDefaultServer):
     49     """ Verify channel announcements for the Channel DB """
     50 
     51     # FIXME the initial routing sync is bandwidth-heavy, and the electrum server
     52     # will start throttling us, making it even slower. one option would be to
     53     # spread it over multiple servers.
     54 
     55     def __init__(self, network: 'Network', channel_db: 'ChannelDB'):
     56         self.channel_db = channel_db
     57         self.lock = threading.Lock()
     58         self.unverified_channel_info = {}  # type: Dict[ShortChannelID, dict]  # scid -> msg_dict
     59         # channel announcements that seem to be invalid:
     60         self.blacklist = set()  # type: Set[ShortChannelID]
     61         NetworkJobOnDefaultServer.__init__(self, network)
     62 
     63     def _reset(self):
     64         super()._reset()
     65         self.started_verifying_channel = set()  # type: Set[ShortChannelID]
     66 
     67     # TODO make async; and rm self.lock completely
     68     def add_new_channel_info(self, short_channel_id: ShortChannelID, msg: dict) -> bool:
     69         if short_channel_id in self.unverified_channel_info:
     70             return False
     71         if short_channel_id in self.blacklist:
     72             return False
     73         with self.lock:
     74             self.unverified_channel_info[short_channel_id] = msg
     75             return True
     76 
     77     async def _run_tasks(self, *, taskgroup):
     78         await super()._run_tasks(taskgroup=taskgroup)
     79         async with taskgroup as group:
     80             await group.spawn(self.main)
     81 
     82     async def main(self):
     83         while True:
     84             await self._verify_some_channels()
     85             await asyncio.sleep(0.1)
     86 
     87     async def _verify_some_channels(self):
     88         blockchain = self.network.blockchain()
     89         local_height = blockchain.height()
     90 
     91         with self.lock:
     92             unverified_channel_info = list(self.unverified_channel_info)
     93 
     94         for short_channel_id in unverified_channel_info:
     95             if short_channel_id in self.started_verifying_channel:
     96                 continue
     97             block_height = short_channel_id.block_height
     98             # only resolve short_channel_id if headers are available.
     99             if block_height <= 0 or block_height > local_height:
    100                 continue
    101             header = blockchain.read_header(block_height)
    102             if header is None:
    103                 if block_height < constants.net.max_checkpoint():
    104                     await self.taskgroup.spawn(self.network.request_chunk(block_height, None, can_return_early=True))
    105                 continue
    106             self.started_verifying_channel.add(short_channel_id)
    107             await self.taskgroup.spawn(self.verify_channel(block_height, short_channel_id))
    108             #self.logger.info(f'requested short_channel_id {bh2u(short_channel_id)}')
    109 
    110     async def verify_channel(self, block_height: int, short_channel_id: ShortChannelID):
    111         # we are verifying channel announcements as they are from untrusted ln peers.
    112         # we use electrum servers to do this. however we don't trust electrum servers either...
    113         try:
    114             async with self._network_request_semaphore:
    115                 result = await self.network.get_txid_from_txpos(
    116                     block_height, short_channel_id.txpos, True)
    117         except aiorpcx.jsonrpc.RPCError:
    118             # the electrum server is complaining about the txpos for given block.
    119             # it is not clear what to do now, but let's believe the server.
    120             self._blacklist_short_channel_id(short_channel_id)
    121             return
    122         tx_hash = result['tx_hash']
    123         merkle_branch = result['merkle']
    124         # we need to wait if header sync/reorg is still ongoing, hence lock:
    125         async with self.network.bhi_lock:
    126             header = self.network.blockchain().read_header(block_height)
    127         try:
    128             verify_tx_is_in_block(tx_hash, merkle_branch, short_channel_id.txpos, header, block_height)
    129         except MerkleVerificationFailure as e:
    130             # the electrum server sent an incorrect proof. blame is on server, not the ln peer
    131             raise GracefulDisconnect(e) from e
    132         try:
    133             async with self._network_request_semaphore:
    134                 raw_tx = await self.network.get_transaction(tx_hash)
    135         except aiorpcx.jsonrpc.RPCError as e:
    136             # the electrum server can't find the tx; but it was the
    137             # one who told us about the txid!! blame is on server
    138             raise GracefulDisconnect(e) from e
    139         tx = Transaction(raw_tx)
    140         try:
    141             tx.deserialize()
    142         except Exception:
    143             # either bug in client, or electrum server is evil.
    144             # if we connect to a diff server at some point, let's try again.
    145             self.logger.warning(f"cannot deserialize transaction, skipping {tx_hash}")
    146             return
    147         if tx_hash != tx.txid():
    148             # either bug in client, or electrum server is evil.
    149             # if we connect to a diff server at some point, let's try again.
    150             self.logger.info(f"received tx does not match expected txid ({tx_hash} != {tx.txid()})")
    151             return
    152         # check funding output
    153         chan_ann_msg = self.unverified_channel_info[short_channel_id]
    154         redeem_script = funding_output_script_from_keys(chan_ann_msg['bitcoin_key_1'], chan_ann_msg['bitcoin_key_2'])
    155         expected_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script)
    156         try:
    157             actual_output = tx.outputs()[short_channel_id.output_index]
    158         except IndexError:
    159             self._blacklist_short_channel_id(short_channel_id)
    160             return
    161         if expected_address != actual_output.address:
    162             # FIXME what now? best would be to ban the originating ln peer.
    163             self.logger.info(f"funding output script mismatch for {short_channel_id}")
    164             self._remove_channel_from_unverified_db(short_channel_id)
    165             return
    166         # put channel into channel DB
    167         self.channel_db.add_verified_channel_info(chan_ann_msg, capacity_sat=actual_output.value)
    168         self._remove_channel_from_unverified_db(short_channel_id)
    169 
    170     def _remove_channel_from_unverified_db(self, short_channel_id: ShortChannelID):
    171         with self.lock:
    172             self.unverified_channel_info.pop(short_channel_id, None)
    173         self.started_verifying_channel.discard(short_channel_id)
    174 
    175     def _blacklist_short_channel_id(self, short_channel_id: ShortChannelID) -> None:
    176         self.blacklist.add(short_channel_id)
    177         with self.lock:
    178             self.unverified_channel_info.pop(short_channel_id, None)
    179 
    180 
    181 def verify_sig_for_channel_update(chan_upd: dict, node_id: bytes) -> bool:
    182     msg_bytes = chan_upd['raw']
    183     pre_hash = msg_bytes[2+64:]
    184     h = sha256d(pre_hash)
    185     sig = chan_upd['signature']
    186     if not ecc.verify_signature(node_id, sig, h):
    187         return False
    188     return True