diff --git a/electrum/channel_db.py b/electrum/channel_db.py index c1ca88b0f..88e4a7d67 100644 --- a/electrum/channel_db.py +++ b/electrum/channel_db.py @@ -41,9 +41,11 @@ from . import constants, util from .util import bh2u, profiler, get_headers_dir, is_ip_address, json_normalize from .logging import Logger from .lnutil import (LNPeerAddr, format_short_channel_id, ShortChannelID, - validate_features, IncompatibleOrInsaneFeatures) + validate_features, IncompatibleOrInsaneFeatures, InvalidGossipMsg) from .lnverifier import LNChannelVerifier, verify_sig_for_channel_update from .lnmsg import decode_msg +from . import ecc +from .crypto import sha256d if TYPE_CHECKING: from .network import Network @@ -378,7 +380,7 @@ class ChannelDB(SqlDB): # even slower; especially as servers will start throttling us. # It would probably put significant strain on servers if all clients # verified the complete gossip. - def add_channel_announcement(self, msg_payloads, *, trusted=True): + def add_channel_announcements(self, msg_payloads, *, trusted=True): # note: signatures have already been verified. if type(msg_payloads) is dict: msg_payloads = [msg_payloads] @@ -452,7 +454,8 @@ class ChannelDB(SqlDB): self.logger.info(f'policy unchanged: {old_policy.timestamp} -> {new_policy.timestamp}') return changed - def add_channel_update(self, payload, max_age=None, verify=False, verbose=True): + def add_channel_update( + self, payload, *, max_age=None, verify=True, verbose=True) -> UpdateStatus: now = int(time.time()) short_channel_id = ShortChannelID(payload['short_channel_id']) timestamp = payload['timestamp'] @@ -468,8 +471,6 @@ class ChannelDB(SqlDB): start_node = channel_info.node1_id if direction == 0 else channel_info.node2_id payload['start_node'] = start_node # compare updates to existing database entries - timestamp = payload['timestamp'] - start_node = payload['start_node'] short_channel_id = ShortChannelID(payload['short_channel_id']) key = (start_node, short_channel_id) old_policy = self._policies.get(key) @@ -495,7 +496,7 @@ class ChannelDB(SqlDB): unchanged = [] good = [] for payload in payloads: - r = self.add_channel_update(payload, max_age=max_age, verbose=False) + r = self.add_channel_update(payload, max_age=max_age, verbose=False, verify=True) if r == UpdateStatus.ORPHANED: orphaned.append(payload) elif r == UpdateStatus.EXPIRED: @@ -567,15 +568,33 @@ class ChannelDB(SqlDB): if r == []: c.execute("INSERT INTO address (node_id, host, port, timestamp) VALUES (?,?,?,?)", (addr.pubkey, addr.host, addr.port, 0)) - def verify_channel_update(self, payload): + @classmethod + def verify_channel_update(cls, payload) -> None: short_channel_id = payload['short_channel_id'] short_channel_id = ShortChannelID(short_channel_id) if constants.net.rev_genesis_bytes() != payload['chain_hash']: - raise Exception('wrong chain hash') + raise InvalidGossipMsg('wrong chain hash') if not verify_sig_for_channel_update(payload, payload['start_node']): - raise Exception(f'failed verifying channel update for {short_channel_id}') - - def add_node_announcement(self, msg_payloads): + raise InvalidGossipMsg(f'failed verifying channel update for {short_channel_id}') + + @classmethod + def verify_channel_announcement(cls, payload) -> None: + h = sha256d(payload['raw'][2+256:]) + pubkeys = [payload['node_id_1'], payload['node_id_2'], payload['bitcoin_key_1'], payload['bitcoin_key_2']] + sigs = [payload['node_signature_1'], payload['node_signature_2'], payload['bitcoin_signature_1'], payload['bitcoin_signature_2']] + for pubkey, sig in zip(pubkeys, sigs): + if not ecc.verify_signature(pubkey, sig, h): + raise InvalidGossipMsg('signature failed') + + @classmethod + def verify_node_announcement(cls, payload) -> None: + pubkey = payload['node_id'] + signature = payload['signature'] + h = sha256d(payload['raw'][66:]) + if not ecc.verify_signature(pubkey, signature, h): + raise InvalidGossipMsg('signature failed') + + def add_node_announcements(self, msg_payloads): # note: signatures have already been verified. if type(msg_payloads) is dict: msg_payloads = [msg_payloads] diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index 02e303b16..ab072b812 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -342,29 +342,9 @@ class Peer(Logger): raise Exception('unknown message') if self.gossip_queue.empty(): break - # verify in peer's TaskGroup so that we fail the connection - self.verify_channel_announcements(chan_anns) - self.verify_node_announcements(node_anns) if self.network.lngossip: await self.network.lngossip.process_gossip(chan_anns, node_anns, chan_upds) - def verify_channel_announcements(self, chan_anns): - for payload in chan_anns: - h = sha256d(payload['raw'][2+256:]) - pubkeys = [payload['node_id_1'], payload['node_id_2'], payload['bitcoin_key_1'], payload['bitcoin_key_2']] - sigs = [payload['node_signature_1'], payload['node_signature_2'], payload['bitcoin_signature_1'], payload['bitcoin_signature_2']] - for pubkey, sig in zip(pubkeys, sigs): - if not ecc.verify_signature(pubkey, sig, h): - raise Exception('signature failed') - - def verify_node_announcements(self, node_anns): - for payload in node_anns: - pubkey = payload['node_id'] - signature = payload['signature'] - h = sha256d(payload['raw'][66:]) - if not ecc.verify_signature(pubkey, signature, h): - raise Exception('signature failed') - async def query_gossip(self): try: await asyncio.wait_for(self.initialized, LN_P2P_NETWORK_TIMEOUT) diff --git a/electrum/lnutil.py b/electrum/lnutil.py index d096100ca..685e92f30 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -288,6 +288,8 @@ class RemoteMisbehaving(LightningError): pass class UpfrontShutdownScriptViolation(RemoteMisbehaving): pass class NotFoundChanAnnouncementForUpdate(Exception): pass +class InvalidGossipMsg(Exception): + """e.g. signature check failed""" class PaymentFailure(UserFacingException): pass class NoPathFound(PaymentFailure): diff --git a/electrum/lnworker.py b/electrum/lnworker.py index c0260a839..888a3a361 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -58,7 +58,7 @@ from .lnutil import (Outpoint, LNPeerAddr, NUM_MAX_EDGES_IN_PAYMENT_PATH, SENT, RECEIVED, HTLCOwner, UpdateAddHtlc, Direction, LnFeatures, ShortChannelID, HtlcLog, derive_payment_secret_from_payment_preimage, - NoPathFound) + NoPathFound, InvalidGossipMsg) from .lnutil import ln_dummy_address, ln_compare_features, IncompatibleLightningFeatures from .lnrouter import TrampolineEdge from .transaction import PartialTxOutput, PartialTransaction, PartialTxInput @@ -552,15 +552,21 @@ class LNGossip(LNWorker): return current_est, total_est, progress_percent async def process_gossip(self, chan_anns, node_anns, chan_upds): + # note: we run in the originating peer's TaskGroup, so we can safely raise here + # and disconnect only from that peer await self.channel_db.data_loaded.wait() self.logger.debug(f'process_gossip {len(chan_anns)} {len(node_anns)} {len(chan_upds)}') # note: data processed in chunks to avoid taking sql lock for too long # channel announcements + for payload in chan_anns: + self.channel_db.verify_channel_announcement(payload) for chan_anns_chunk in chunks(chan_anns, 300): - self.channel_db.add_channel_announcement(chan_anns_chunk) + self.channel_db.add_channel_announcements(chan_anns_chunk) # node announcements + for payload in node_anns: + self.channel_db.verify_node_announcement(payload) for node_anns_chunk in chunks(node_anns, 100): - self.channel_db.add_node_announcement(node_anns_chunk) + self.channel_db.add_node_announcements(node_anns_chunk) # channel updates for chan_upds_chunk in chunks(chan_upds, 1000): categorized_chan_upds = self.channel_db.add_channel_updates( @@ -1269,7 +1275,10 @@ class LNWallet(LNWorker): def _handle_chanupd_from_failed_htlc(self, payload, *, route, sender_idx) -> Tuple[bool, bool]: blacklist = False update = False - r = self.channel_db.add_channel_update(payload) + try: + r = self.channel_db.add_channel_update(payload, verify=True) + except InvalidGossipMsg: + return True, False # blacklist short_channel_id = ShortChannelID(payload['short_channel_id']) if r == UpdateStatus.GOOD: self.logger.info(f"applied channel update to {short_channel_id}") diff --git a/electrum/tests/test_lnrouter.py b/electrum/tests/test_lnrouter.py index 50bcd6ec7..b1db724df 100644 --- a/electrum/tests/test_lnrouter.py +++ b/electrum/tests/test_lnrouter.py @@ -40,49 +40,51 @@ class Test_LNRouter(TestCaseForTestnet): cdb = fake_network.channel_db path_finder = lnrouter.LNPathFinder(cdb) self.assertEqual(cdb.num_channels, 0) - cdb.add_channel_announcement({'node_id_1': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', 'node_id_2': b'\x02cccccccccccccccccccccccccccccccc', + cdb.add_channel_announcements({'node_id_1': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', 'node_id_2': b'\x02cccccccccccccccccccccccccccccccc', 'bitcoin_key_1': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', 'bitcoin_key_2': b'\x02cccccccccccccccccccccccccccccccc', 'short_channel_id': bfh('0000000000000001'), 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'len': 0, 'features': b''}, trusted=True) self.assertEqual(cdb.num_channels, 1) - cdb.add_channel_announcement({'node_id_1': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', 'node_id_2': b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + cdb.add_channel_announcements({'node_id_1': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', 'node_id_2': b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', 'bitcoin_key_1': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', 'bitcoin_key_2': b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', 'short_channel_id': bfh('0000000000000002'), 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'len': 0, 'features': b''}, trusted=True) - cdb.add_channel_announcement({'node_id_1': b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'node_id_2': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + cdb.add_channel_announcements({'node_id_1': b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'node_id_2': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', 'bitcoin_key_1': b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'bitcoin_key_2': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', 'short_channel_id': bfh('0000000000000003'), 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'len': 0, 'features': b''}, trusted=True) - cdb.add_channel_announcement({'node_id_1': b'\x02cccccccccccccccccccccccccccccccc', 'node_id_2': b'\x02dddddddddddddddddddddddddddddddd', + cdb.add_channel_announcements({'node_id_1': b'\x02cccccccccccccccccccccccccccccccc', 'node_id_2': b'\x02dddddddddddddddddddddddddddddddd', 'bitcoin_key_1': b'\x02cccccccccccccccccccccccccccccccc', 'bitcoin_key_2': b'\x02dddddddddddddddddddddddddddddddd', 'short_channel_id': bfh('0000000000000004'), 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'len': 0, 'features': b''}, trusted=True) - cdb.add_channel_announcement({'node_id_1': b'\x02dddddddddddddddddddddddddddddddd', 'node_id_2': b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + cdb.add_channel_announcements({'node_id_1': b'\x02dddddddddddddddddddddddddddddddd', 'node_id_2': b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', 'bitcoin_key_1': b'\x02dddddddddddddddddddddddddddddddd', 'bitcoin_key_2': b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', 'short_channel_id': bfh('0000000000000005'), 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'len': 0, 'features': b''}, trusted=True) - cdb.add_channel_announcement({'node_id_1': b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'node_id_2': b'\x02dddddddddddddddddddddddddddddddd', + cdb.add_channel_announcements({'node_id_1': b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'node_id_2': b'\x02dddddddddddddddddddddddddddddddd', 'bitcoin_key_1': b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'bitcoin_key_2': b'\x02dddddddddddddddddddddddddddddddd', 'short_channel_id': bfh('0000000000000006'), 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'len': 0, 'features': b''}, trusted=True) - cdb.add_channel_update({'short_channel_id': bfh('0000000000000001'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0}) - cdb.add_channel_update({'short_channel_id': bfh('0000000000000001'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0}) - cdb.add_channel_update({'short_channel_id': bfh('0000000000000002'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': 99, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0}) - cdb.add_channel_update({'short_channel_id': bfh('0000000000000002'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0}) - cdb.add_channel_update({'short_channel_id': bfh('0000000000000003'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0}) - cdb.add_channel_update({'short_channel_id': bfh('0000000000000003'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0}) - cdb.add_channel_update({'short_channel_id': bfh('0000000000000004'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0}) - cdb.add_channel_update({'short_channel_id': bfh('0000000000000004'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0}) - cdb.add_channel_update({'short_channel_id': bfh('0000000000000005'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0}) - cdb.add_channel_update({'short_channel_id': bfh('0000000000000005'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 999, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0}) - cdb.add_channel_update({'short_channel_id': bfh('0000000000000006'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 99999999, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0}) - cdb.add_channel_update({'short_channel_id': bfh('0000000000000006'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0}) + def add_chan_upd(payload): + cdb.add_channel_update(payload, verify=False) + add_chan_upd({'short_channel_id': bfh('0000000000000001'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0}) + add_chan_upd({'short_channel_id': bfh('0000000000000001'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0}) + add_chan_upd({'short_channel_id': bfh('0000000000000002'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': 99, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0}) + add_chan_upd({'short_channel_id': bfh('0000000000000002'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0}) + add_chan_upd({'short_channel_id': bfh('0000000000000003'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0}) + add_chan_upd({'short_channel_id': bfh('0000000000000003'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0}) + add_chan_upd({'short_channel_id': bfh('0000000000000004'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0}) + add_chan_upd({'short_channel_id': bfh('0000000000000004'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0}) + add_chan_upd({'short_channel_id': bfh('0000000000000005'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0}) + add_chan_upd({'short_channel_id': bfh('0000000000000005'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 999, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0}) + add_chan_upd({'short_channel_id': bfh('0000000000000006'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 99999999, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0}) + add_chan_upd({'short_channel_id': bfh('0000000000000006'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0}) path = path_finder.find_path_for_payment( nodeA=b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', nodeB=b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',