commit a5b44d25b01be8aac2e6f661ba42f8e9930f97ff
parent c1d182601483be4829526a686036e09247a15099
Author: SomberNight <somber.night@protonmail.com>
Date: Mon, 23 Jul 2018 20:49:44 +0200
persist channel db on disk. verify channel gossip sigs.
Diffstat:
8 files changed, 396 insertions(+), 55 deletions(-)
diff --git a/electrum/constants.py b/electrum/constants.py
@@ -27,6 +27,7 @@ import os
import json
from .util import inv_dict
+from . import bitcoin
def read_json(filename, default):
@@ -49,6 +50,10 @@ class AbstractNet:
def max_checkpoint(cls) -> int:
return max(0, len(cls.CHECKPOINTS) * 2016 - 1)
+ @classmethod
+ def rev_genesis_bytes(cls) -> bytes:
+ return bytes.fromhex(bitcoin.rev_hex(cls.GENESIS))
+
class BitcoinMainnet(AbstractNet):
diff --git a/electrum/ecc.py b/electrum/ecc.py
@@ -313,7 +313,7 @@ def msg_magic(message: bytes) -> bytes:
return b"\x18Bitcoin Signed Message:\n" + length + message
-def verify_signature(pubkey, sig, h):
+def verify_signature(pubkey: bytes, sig: bytes, h: bytes) -> bool:
try:
ECPubkey(pubkey).verify_message_hash(sig, h)
except:
diff --git a/electrum/lnchanannverifier.py b/electrum/lnchanannverifier.py
@@ -0,0 +1,168 @@
+# -*- coding: utf-8 -*-
+#
+# Electrum - lightweight Bitcoin client
+# Copyright (C) 2018 The Electrum developers
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation files
+# (the "Software"), to deal in the Software without restriction,
+# including without limitation the rights to use, copy, modify, merge,
+# publish, distribute, sublicense, and/or sell copies of the Software,
+# and to permit persons to whom the Software is furnished to do so,
+# subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+import threading
+
+from . import lnbase
+from . import bitcoin
+from . import ecc
+from .util import ThreadJob, bh2u, bfh
+from .lnutil import invert_short_channel_id, funding_output_script_from_keys
+from .verifier import verify_tx_is_in_block, MerkleVerificationFailure
+from .transaction import Transaction
+
+
+class LNChanAnnVerifier(ThreadJob):
+ """ Verify channel announcements for the Channel DB """
+
+ def __init__(self, network, channel_db):
+ self.network = network
+ self.channel_db = channel_db
+ self.lock = threading.Lock()
+
+ # items only removed when whole verification succeeds for them.
+ # fixme: if it fails, it will never succeed
+ self.started_verifying_channel = set() # short_channel_id
+
+ self.unverified_channel_info = {} # short_channel_id -> channel_info
+
+ def add_new_channel_info(self, channel_info):
+ short_channel_id = channel_info.channel_id
+ if short_channel_id in self.unverified_channel_info:
+ return
+ if not verify_sigs_for_channel_announcement(channel_info.msg_payload):
+ return
+ with self.lock:
+ self.unverified_channel_info[short_channel_id] = channel_info
+
+ def get_pending_channel_info(self, short_channel_id):
+ return self.unverified_channel_info.get(short_channel_id, None)
+
+ def run(self):
+ interface = self.network.interface
+ if not interface:
+ return
+
+ blockchain = interface.blockchain
+ if not blockchain:
+ return
+
+ with self.lock:
+ unverified_channel_info = list(self.unverified_channel_info)
+
+ for short_channel_id in unverified_channel_info:
+ if short_channel_id in self.started_verifying_channel:
+ continue
+ block_height, tx_pos, output_idx = invert_short_channel_id(short_channel_id)
+ # only resolve short_channel_id if headers are available.
+ header = blockchain.read_header(block_height)
+ if header is None:
+ index = block_height // 2016
+ if index < len(blockchain.checkpoints):
+ self.network.request_chunk(interface, index)
+ continue
+ callback = lambda resp, short_channel_id=short_channel_id: self.on_txid_and_merkle(resp, short_channel_id)
+ self.network.get_txid_from_txpos(block_height, tx_pos, True,
+ callback=callback)
+ #self.print_error('requested short_channel_id', bh2u(short_channel_id))
+ with self.lock:
+ self.started_verifying_channel.add(short_channel_id)
+
+ def on_txid_and_merkle(self, response, short_channel_id):
+ if response.get('error'):
+ self.print_error('received an error:', response)
+ return
+ result = response['result']
+ tx_hash = result['tx_hash']
+ merkle_branch = result['merkle']
+ block_height, tx_pos, output_idx = invert_short_channel_id(short_channel_id)
+ header = self.network.blockchain().read_header(block_height)
+ try:
+ verify_tx_is_in_block(tx_hash, merkle_branch, tx_pos, header, block_height)
+ except MerkleVerificationFailure as e:
+ self.print_error(str(e))
+ return
+ callback = lambda resp, short_channel_id=short_channel_id: self.on_tx_response(resp, short_channel_id)
+ self.network.get_transaction(tx_hash, callback=callback)
+
+ def on_tx_response(self, response, short_channel_id):
+ if response.get('error'):
+ self.print_error('received an error:', response)
+ return
+ params = response['params']
+ result = response['result']
+ tx_hash = params[0]
+ tx = Transaction(result)
+ try:
+ tx.deserialize()
+ except Exception:
+ self.print_msg("cannot deserialize transaction, skipping", tx_hash)
+ return
+ if tx_hash != tx.txid():
+ self.print_error("received tx does not match expected txid ({} != {})"
+ .format(tx_hash, tx.txid()))
+ return
+ # check funding output
+ channel_info = self.unverified_channel_info[short_channel_id]
+ chan_ann = channel_info.msg_payload
+ redeem_script = funding_output_script_from_keys(chan_ann['bitcoin_key_1'], chan_ann['bitcoin_key_2'])
+ expected_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script)
+ output_idx = invert_short_channel_id(short_channel_id)[2]
+ try:
+ actual_output = tx.outputs()[output_idx]
+ except IndexError:
+ return
+ if expected_address != actual_output[1]:
+ return
+ # put channel into channel DB
+ channel_info.set_capacity(actual_output[2])
+ self.channel_db.add_verified_channel_info(short_channel_id, channel_info)
+ # remove channel from unverified
+ with self.lock:
+ self.unverified_channel_info.pop(short_channel_id, None)
+ try: self.started_verifying_channel.remove(short_channel_id)
+ except KeyError: pass
+
+
+def verify_sigs_for_channel_announcement(chan_ann: dict) -> bool:
+ msg_bytes = lnbase.gen_msg('channel_announcement', **chan_ann)
+ pre_hash = msg_bytes[2+256:]
+ h = bitcoin.Hash(pre_hash)
+ pubkeys = [chan_ann['node_id_1'], chan_ann['node_id_2'], chan_ann['bitcoin_key_1'], chan_ann['bitcoin_key_2']]
+ sigs = [chan_ann['node_signature_1'], chan_ann['node_signature_2'], chan_ann['bitcoin_signature_1'], chan_ann['bitcoin_signature_2']]
+ for pubkey, sig in zip(pubkeys, sigs):
+ if not ecc.verify_signature(pubkey, sig, h):
+ return False
+ return True
+
+
+def verify_sig_for_channel_update(chan_upd: dict, node_id: bytes) -> bool:
+ msg_bytes = lnbase.gen_msg('channel_update', **chan_upd)
+ pre_hash = msg_bytes[2+64:]
+ h = bitcoin.Hash(pre_hash)
+ sig = chan_upd['signature']
+ if not ecc.verify_signature(node_id, sig, h):
+ return False
+ return True
diff --git a/electrum/lnrouter.py b/electrum/lnrouter.py
@@ -30,8 +30,11 @@ import sys
import binascii
import hashlib
import hmac
+import os
+import json
+import threading
from collections import namedtuple, defaultdict
-from typing import Sequence, Union, Tuple
+from typing import Sequence, Union, Tuple, Optional
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
from cryptography.hazmat.backends import default_backend
@@ -39,9 +42,12 @@ from cryptography.hazmat.backends import default_backend
from . import bitcoin
from . import ecc
from . import crypto
+from . import constants
from .crypto import sha256
-from .util import PrintError, bh2u, profiler, xor_bytes
+from .util import PrintError, bh2u, profiler, xor_bytes, get_headers_dir, bfh
from .lnutil import get_ecdh
+from .storage import JsonDB
+from .lnchanannverifier import LNChanAnnVerifier, verify_sig_for_channel_update
class ChannelInfo(PrintError):
@@ -54,23 +60,71 @@ class ChannelInfo(PrintError):
assert type(self.node_id_2) is bytes
assert list(sorted([self.node_id_1, self.node_id_2])) == [self.node_id_1, self.node_id_2]
+ self.features_len = channel_announcement_payload['len']
+ self.features = channel_announcement_payload['features']
+ self.bitcoin_key_1 = channel_announcement_payload['bitcoin_key_1']
+ self.bitcoin_key_2 = channel_announcement_payload['bitcoin_key_2']
+
+ # this field does not get persisted
+ self.msg_payload = channel_announcement_payload
+
self.capacity_sat = None
self.policy_node1 = None
self.policy_node2 = None
+ def to_json(self) -> dict:
+ d = {}
+ d['short_channel_id'] = bh2u(self.channel_id)
+ d['node_id_1'] = bh2u(self.node_id_1)
+ d['node_id_2'] = bh2u(self.node_id_2)
+ d['len'] = bh2u(self.features_len)
+ d['features'] = bh2u(self.features)
+ d['bitcoin_key_1'] = bh2u(self.bitcoin_key_1)
+ d['bitcoin_key_2'] = bh2u(self.bitcoin_key_2)
+ d['policy_node1'] = self.policy_node1
+ d['policy_node2'] = self.policy_node2
+ d['capacity_sat'] = self.capacity_sat
+ return d
+
+ @classmethod
+ def from_json(cls, d: dict):
+ d2 = {}
+ d2['short_channel_id'] = bfh(d['short_channel_id'])
+ d2['node_id_1'] = bfh(d['node_id_1'])
+ d2['node_id_2'] = bfh(d['node_id_2'])
+ d2['len'] = bfh(d['len'])
+ d2['features'] = bfh(d['features'])
+ d2['bitcoin_key_1'] = bfh(d['bitcoin_key_1'])
+ d2['bitcoin_key_2'] = bfh(d['bitcoin_key_2'])
+ ci = ChannelInfo(d2)
+ ci.capacity_sat = d['capacity_sat']
+ ci.policy_node1 = ChannelInfoDirectedPolicy.from_json(d['policy_node1'])
+ ci.policy_node2 = ChannelInfoDirectedPolicy.from_json(d['policy_node2'])
+ return ci
+
def set_capacity(self, capacity):
- # TODO call this after looking up UTXO for funding txn on chain
self.capacity_sat = capacity
def on_channel_update(self, msg_payload):
assert self.channel_id == msg_payload['short_channel_id']
flags = int.from_bytes(msg_payload['flags'], 'big')
direction = flags & 1
+ new_policy = ChannelInfoDirectedPolicy(msg_payload)
+ if direction == 0:
+ old_policy = self.policy_node1
+ node_id = self.node_id_1
+ else:
+ old_policy = self.policy_node2
+ node_id = self.node_id_2
+ if old_policy and old_policy.timestamp >= new_policy.timestamp:
+ return # ignore
+ if not verify_sig_for_channel_update(msg_payload, node_id):
+ return # ignore
+ # save new policy
if direction == 0:
- self.policy_node1 = ChannelInfoDirectedPolicy(msg_payload)
+ self.policy_node1 = new_policy
else:
- self.policy_node2 = ChannelInfoDirectedPolicy(msg_payload)
- #self.print_error('channel update', binascii.hexlify(self.channel_id).decode("ascii"), flags)
+ self.policy_node2 = new_policy
def get_policy_for_node(self, node_id):
if node_id == self.node_id_1:
@@ -84,51 +138,121 @@ class ChannelInfo(PrintError):
class ChannelInfoDirectedPolicy:
def __init__(self, channel_update_payload):
- self.cltv_expiry_delta = channel_update_payload['cltv_expiry_delta']
- self.htlc_minimum_msat = channel_update_payload['htlc_minimum_msat']
- self.fee_base_msat = channel_update_payload['fee_base_msat']
- self.fee_proportional_millionths = channel_update_payload['fee_proportional_millionths']
- self.cltv_expiry_delta = int.from_bytes(self.cltv_expiry_delta, "big")
- self.htlc_minimum_msat = int.from_bytes(self.htlc_minimum_msat, "big")
- self.fee_base_msat = int.from_bytes(self.fee_base_msat, "big")
- self.fee_proportional_millionths = int.from_bytes(self.fee_proportional_millionths, "big")
+ cltv_expiry_delta = channel_update_payload['cltv_expiry_delta']
+ htlc_minimum_msat = channel_update_payload['htlc_minimum_msat']
+ fee_base_msat = channel_update_payload['fee_base_msat']
+ fee_proportional_millionths = channel_update_payload['fee_proportional_millionths']
+ flags = channel_update_payload['flags']
+ timestamp = channel_update_payload['timestamp']
+
+ self.cltv_expiry_delta = int.from_bytes(cltv_expiry_delta, "big")
+ self.htlc_minimum_msat = int.from_bytes(htlc_minimum_msat, "big")
+ self.fee_base_msat = int.from_bytes(fee_base_msat, "big")
+ self.fee_proportional_millionths = int.from_bytes(fee_proportional_millionths, "big")
+ self.flags = int.from_bytes(flags, "big")
+ self.timestamp = int.from_bytes(timestamp, "big")
+
+ def to_json(self) -> dict:
+ d = {}
+ d['cltv_expiry_delta'] = self.cltv_expiry_delta
+ d['htlc_minimum_msat'] = self.htlc_minimum_msat
+ d['fee_base_msat'] = self.fee_base_msat
+ d['fee_proportional_millionths'] = self.fee_proportional_millionths
+ d['flags'] = self.flags
+ d['timestamp'] = self.timestamp
+ return d
+ @classmethod
+ def from_json(cls, d: dict):
+ if d is None: return None
+ d2 = {}
+ d2['cltv_expiry_delta'] = d['cltv_expiry_delta'].to_bytes(2, "big")
+ d2['htlc_minimum_msat'] = d['htlc_minimum_msat'].to_bytes(8, "big")
+ d2['fee_base_msat'] = d['fee_base_msat'].to_bytes(4, "big")
+ d2['fee_proportional_millionths'] = d['fee_proportional_millionths'].to_bytes(4, "big")
+ d2['flags'] = d['flags'].to_bytes(2, "big")
+ d2['timestamp'] = d['timestamp'].to_bytes(4, "big")
+ return ChannelInfoDirectedPolicy(d2)
+
+
+class ChannelDB(JsonDB):
-class ChannelDB(PrintError):
+ def __init__(self, network):
+ self.network = network
- def __init__(self):
+ path = os.path.join(get_headers_dir(network.config), 'channel_db')
+ JsonDB.__init__(self, path)
+
+ self.lock = threading.Lock()
self._id_to_channel_info = {}
self._channels_for_node = defaultdict(set) # node -> set(short_channel_id)
+ self.ca_verifier = LNChanAnnVerifier(network, self)
+ self.network.add_jobs([self.ca_verifier])
+
+ self.load_data()
+
+ def load_data(self):
+ if os.path.exists(self.path):
+ with open(self.path, "r", encoding='utf-8') as f:
+ raw = f.read()
+ self.data = json.loads(raw)
+ channel_infos = self.get('channel_infos', {})
+ for short_channel_id, channel_info_d in channel_infos.items():
+ channel_info = ChannelInfo.from_json(channel_info_d)
+ short_channel_id = bfh(short_channel_id)
+ self.add_verified_channel_info(short_channel_id, channel_info)
+
+ def save_data(self):
+ with self.lock:
+ channel_infos = {}
+ for short_channel_id, channel_info in self._id_to_channel_info.items():
+ channel_infos[bh2u(short_channel_id)] = channel_info
+ self.put('channel_infos', channel_infos)
+ self.write()
+
def __len__(self):
return len(self._id_to_channel_info)
- def get_channel_info(self, channel_id):
+ def get_channel_info(self, channel_id) -> Optional[ChannelInfo]:
return self._id_to_channel_info.get(channel_id, None)
def get_channels_for_node(self, node_id):
"""Returns the set of channels that have node_id as one of the endpoints."""
return self._channels_for_node[node_id]
+ def add_verified_channel_info(self, short_channel_id: bytes, channel_info: ChannelInfo):
+ with self.lock:
+ self._id_to_channel_info[short_channel_id] = channel_info
+ self._channels_for_node[channel_info.node_id_1].add(short_channel_id)
+ self._channels_for_node[channel_info.node_id_2].add(short_channel_id)
+
def on_channel_announcement(self, msg_payload):
short_channel_id = msg_payload['short_channel_id']
- #self.print_error('channel announcement', binascii.hexlify(short_channel_id).decode("ascii"))
- channel_info = ChannelInfo(msg_payload)
if short_channel_id in self._id_to_channel_info:
- self.print_error("IGNORING CHANNEL ANNOUNCEMENT, WE ALREADY KNOW THIS CHANNEL")
return
- self._id_to_channel_info[short_channel_id] = channel_info
- self._channels_for_node[channel_info.node_id_1].add(short_channel_id)
- self._channels_for_node[channel_info.node_id_2].add(short_channel_id)
+ if constants.net.rev_genesis_bytes() != msg_payload['chain_hash']:
+ return
+ channel_info = ChannelInfo(msg_payload)
+ self.ca_verifier.add_new_channel_info(channel_info)
def on_channel_update(self, msg_payload):
short_channel_id = msg_payload['short_channel_id']
- try:
- channel_info = self._id_to_channel_info[short_channel_id]
- except KeyError:
+ if constants.net.rev_genesis_bytes() != msg_payload['chain_hash']:
+ return
+ # try finding channel in verified db
+ channel_info = self._id_to_channel_info.get(short_channel_id, None)
+ if channel_info is None:
+ # try finding channel in pending db
+ channel_info = self.ca_verifier.get_pending_channel_info(short_channel_id)
+ if channel_info is None:
+ # try finding channel in verified db, again
+ # (maybe this is redundant but this should prevent a race..)
+ channel_info = self._id_to_channel_info.get(short_channel_id, None)
+ if channel_info is None:
self.print_error("could not find", short_channel_id)
- else:
- channel_info.on_channel_update(msg_payload)
+ return
+ channel_info.on_channel_update(msg_payload)
def remove_channel(self, short_channel_id):
try:
diff --git a/electrum/lnutil.py b/electrum/lnutil.py
@@ -344,8 +344,11 @@ def sign_and_get_sig_string(tx, local_config, remote_config):
sig_64 = sig_string_from_der_sig(sig[:-1])
return sig_64
-def funding_output_script(local_config, remote_config):
- pubkeys = sorted([bh2u(local_config.multisig_key.pubkey), bh2u(remote_config.multisig_key.pubkey)])
+def funding_output_script(local_config, remote_config) -> str:
+ return funding_output_script_from_keys(local_config.multisig_key.pubkey, remote_config.multisig_key.pubkey)
+
+def funding_output_script_from_keys(pubkey1: bytes, pubkey2: bytes) -> str:
+ pubkeys = sorted([bh2u(pubkey1), bh2u(pubkey2)])
return transaction.multisig_script(pubkeys, 2)
def calc_short_channel_id(block_height: int, tx_pos_in_block: int, output_index: int) -> bytes:
@@ -354,6 +357,12 @@ def calc_short_channel_id(block_height: int, tx_pos_in_block: int, output_index:
oi = output_index.to_bytes(2, byteorder='big')
return bh + tpos + oi
+def invert_short_channel_id(short_channel_id: bytes) -> (int, int, int):
+ bh = int.from_bytes(short_channel_id[:3], byteorder='big')
+ tpos = int.from_bytes(short_channel_id[3:6], byteorder='big')
+ oi = int.from_bytes(short_channel_id[6:8], byteorder='big')
+ return bh, tpos, oi
+
def get_obscured_ctn(ctn, local, remote):
mask = int.from_bytes(sha256(local + remote)[-6:], 'big')
return ctn ^ mask
diff --git a/electrum/network.py b/electrum/network.py
@@ -301,7 +301,7 @@ class Network(Logger):
# lightning network
self.lightning_nodes = {}
- self.channel_db = lnrouter.ChannelDB()
+ self.channel_db = lnrouter.ChannelDB(self)
self.path_finder = lnrouter.LNPathFinder(self.channel_db)
self.lnwatcher = lnwatcher.LNWatcher(self)
@@ -1183,6 +1183,7 @@ class Network(Logger):
def stop(self):
assert self._loop_thread != threading.current_thread(), 'must not be called from network thread'
fut = asyncio.run_coroutine_threadsafe(self._stop(full_shutdown=True), self.asyncio_loop)
+ self.channel_db.save_data()
try:
fut.result(timeout=2)
except (asyncio.TimeoutError, asyncio.CancelledError): pass
diff --git a/electrum/tests/test_lnrouter.py b/electrum/tests/test_lnrouter.py
@@ -1,11 +1,17 @@
import unittest
+import tempfile
+import shutil
from electrum.util import bh2u, bfh
from electrum.lnbase import Peer
from electrum.lnrouter import OnionHopsDataSingle, new_onion_packet, OnionPerHop
from electrum import bitcoin, lnrouter
+from electrum.simple_config import SimpleConfig
+from electrum import lnchanannverifier
-class Test_LNRouter(unittest.TestCase):
+from . import TestCaseForTestnet
+
+class Test_LNRouter(TestCaseForTestnet):
#@staticmethod
#def parse_witness_list(witness_bytes):
@@ -21,12 +27,26 @@ class Test_LNRouter(unittest.TestCase):
# assert witness_bytes == b"", witness_bytes
# return res
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.electrum_path = tempfile.mkdtemp()
+ cls.config = SimpleConfig({'electrum_path': cls.electrum_path})
+ @classmethod
+ def tearDownClass(cls):
+ super().tearDownClass()
+ shutil.rmtree(cls.electrum_path)
def test_find_path_for_payment(self):
class fake_network:
- channel_db = lnrouter.ChannelDB()
+ config = self.config
trigger_callback = lambda x: None
+ add_jobs = lambda *args: None
+ fake_network.channel_db = lnrouter.ChannelDB(fake_network())
+ def no_verify_add_new_channel_info(channel_info):
+ fake_network.channel_db.add_verified_channel_info(channel_info.channel_id, channel_info)
+ fake_network.channel_db.ca_verifier.add_new_channel_info = no_verify_add_new_channel_info
class fake_ln_worker:
path_finder = lnrouter.LNPathFinder(fake_network.channel_db)
privkey = bitcoin.sha256('privkeyseed')
@@ -34,27 +54,39 @@ class Test_LNRouter(unittest.TestCase):
channels = []
invoices = {}
channels_for_peer = lambda x: []
- p = Peer(fake_ln_worker, '', 0, 'a')
- p.on_channel_announcement({'node_id_1': b'b', 'node_id_2': b'c', 'short_channel_id': bfh('0000000000000001')})
- p.on_channel_announcement({'node_id_1': b'b', 'node_id_2': b'e', 'short_channel_id': bfh('0000000000000002')})
- p.on_channel_announcement({'node_id_1': b'a', 'node_id_2': b'b', 'short_channel_id': bfh('0000000000000003')})
- p.on_channel_announcement({'node_id_1': b'c', 'node_id_2': b'd', 'short_channel_id': bfh('0000000000000004')})
- p.on_channel_announcement({'node_id_1': b'd', 'node_id_2': b'e', 'short_channel_id': bfh('0000000000000005')})
- p.on_channel_announcement({'node_id_1': b'a', 'node_id_2': b'd', 'short_channel_id': bfh('0000000000000006')})
+ p = Peer(fake_ln_worker, '', 0, b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
+ p.on_channel_announcement({'node_id_1': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
+ 'node_id_2': b'\x02cccccccccccccccccccccccccccccccc',
+ 'short_channel_id': bfh('0000000000000001'), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
+ p.on_channel_announcement({'node_id_1': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
+ 'node_id_2': b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
+ 'short_channel_id': bfh('0000000000000002'), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
+ p.on_channel_announcement({'node_id_1': b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
+ 'node_id_2': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
+ 'short_channel_id': bfh('0000000000000003'), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
+ p.on_channel_announcement({'node_id_1': b'\x02cccccccccccccccccccccccccccccccc',
+ 'node_id_2': b'\x02dddddddddddddddddddddddddddddddd',
+ 'short_channel_id': bfh('0000000000000004'), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
+ p.on_channel_announcement({'node_id_1': b'\x02dddddddddddddddddddddddddddddddd',
+ 'node_id_2': b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
+ 'short_channel_id': bfh('0000000000000005'), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
+ p.on_channel_announcement({'node_id_1': b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
+ 'node_id_2': b'\x02dddddddddddddddddddddddddddddddd',
+ 'short_channel_id': bfh('0000000000000006'), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
o = lambda i: i.to_bytes(8, "big")
- p.on_channel_update({'short_channel_id': bfh('0000000000000001'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)})
- p.on_channel_update({'short_channel_id': bfh('0000000000000001'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)})
- p.on_channel_update({'short_channel_id': bfh('0000000000000002'), 'flags': b'\x00', 'cltv_expiry_delta': o(99), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)})
- p.on_channel_update({'short_channel_id': bfh('0000000000000002'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)})
- p.on_channel_update({'short_channel_id': bfh('0000000000000003'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)})
- p.on_channel_update({'short_channel_id': bfh('0000000000000003'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)})
- p.on_channel_update({'short_channel_id': bfh('0000000000000004'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)})
- p.on_channel_update({'short_channel_id': bfh('0000000000000004'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)})
- p.on_channel_update({'short_channel_id': bfh('0000000000000005'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)})
- p.on_channel_update({'short_channel_id': bfh('0000000000000005'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(999)})
- p.on_channel_update({'short_channel_id': bfh('0000000000000006'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(99999999)})
- p.on_channel_update({'short_channel_id': bfh('0000000000000006'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)})
- self.assertNotEqual(None, fake_ln_worker.path_finder.find_path_for_payment(b'a', b'e', 100000))
+ p.on_channel_update({'short_channel_id': bfh('0000000000000001'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
+ p.on_channel_update({'short_channel_id': bfh('0000000000000001'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
+ p.on_channel_update({'short_channel_id': bfh('0000000000000002'), 'flags': b'\x00', 'cltv_expiry_delta': o(99), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
+ p.on_channel_update({'short_channel_id': bfh('0000000000000002'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
+ p.on_channel_update({'short_channel_id': bfh('0000000000000003'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
+ p.on_channel_update({'short_channel_id': bfh('0000000000000003'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
+ p.on_channel_update({'short_channel_id': bfh('0000000000000004'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
+ p.on_channel_update({'short_channel_id': bfh('0000000000000004'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
+ p.on_channel_update({'short_channel_id': bfh('0000000000000005'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
+ p.on_channel_update({'short_channel_id': bfh('0000000000000005'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(999), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
+ p.on_channel_update({'short_channel_id': bfh('0000000000000006'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(99999999), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
+ p.on_channel_update({'short_channel_id': bfh('0000000000000006'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
+ self.assertNotEqual(None, fake_ln_worker.path_finder.find_path_for_payment(b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', 100000))
diff --git a/electrum/util.py b/electrum/util.py
@@ -217,7 +217,9 @@ class MyEncoder(json.JSONEncoder):
return obj.isoformat(' ')[:-3]
if isinstance(obj, set):
return list(obj)
- return super().default(obj)
+ if hasattr(obj, 'to_json') and callable(obj.to_json):
+ return obj.to_json()
+ return super(MyEncoder, self).default(obj)
class ThreadJob(Logger):